first commit

This commit is contained in:
Mattia Mascarello 2023-04-17 02:49:07 +02:00
commit d42dbf7b6d
11 changed files with 399 additions and 0 deletions

51
README.md Normal file
View File

@ -0,0 +1,51 @@
# SyncThru Telegram Bot
![Example](example.png)
Receive alerts on your ink, tray, and drum levels of SyncThru-enabled Samsung printers on Telegram, and check their status.
## Installation
Simply clone the repository or download the files, the install requirements with
```
pip install -r requirements.txt
```
## Usage
Create a bot with [@BotFather](https://t.me/BotFather) and copy its token into the config.
You need to run `main.py` for information polling and level messages
and `telegram.py` for the checking feature
A printer may be configured as a static-ip or a dynamic-ip printer, in which case the software will attempt to find its local ip given its serial number
Do not edit the `store.json` file if you do not know what you are doing, or you may crash the whole thing.
## Configuration
Here is an explainer
```json
{
"printers": [
{
"ip": "192.168.1.2", // initial (or static) iè
"serial_num": "08HRB8GJ5E01R8W", // serial number
"dynamic_ip": true, // whether the ip is dynamic
"toner": true, // enables toner alerts
"drum": true,// enables drum alerts
"tray": true, // enables tray alerts
"alert_levels": [50, 20, 5] // the critical levels to which the alerts must be dispatched
}
],
"update_interval": 600000, // how many seconds between updates
"telegram_bot_token": "123443:AAFG0kwfF92fds32sp92d", // the telegram token
"telegram_user_id": 153325233 // your telegram id, you can get it at https://t.me/username_to_id_bot
}
```

6
alert.py Normal file
View File

@ -0,0 +1,6 @@
class Alert:
def __init__(self, severity: int, code: str, desc: str, uptime: int):
self.severity = severity
self.code = code
self.desc = desc
self.uptime = uptime

16
config.json Normal file
View File

@ -0,0 +1,16 @@
{
"printers": [
{
"ip": "192.168.1.2",
"serial_num": "08HRB8GJ5E01R8W",
"dynamic_ip": true,
"toner": true,
"drum": true,
"tray": true,
"alert_levels": [50, 20, 5]
}
],
"update_interval": 600000,
"telegram_bot_token": "",
"telegram_user_id": 135
}

5
drum.py Normal file
View File

@ -0,0 +1,5 @@
class Drum:
def __init__(self, color: str, remaining_percent: int):
self.color = color
self.remaining_percent = remaining_percent
self.used_percent = 100 - remaining_percent

BIN
example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

71
main.py Normal file
View File

@ -0,0 +1,71 @@
import requests
import json
import demjson
import datetime
import telebot
from printer import Printer
import time
def store_update(store: list, data: dict):
for idx, printer in enumerate(store):
if printer["serial_num"] == data["serial_num"]:
printer[int(idx)] = data
return
store.append(data)
def store_get_printer_by_serial_num(store: list, serial_num: str):
for printer in store:
if printer["serial_num"] == serial_num:
return printer
return None
def store_get():
try:
with open("store.json", "r") as store_file:
return json.loads(store_file.read())["data"]
except Exception as e:
return []
def store_set(data: list):
with open("store.json", "w+") as store_file:
store_file.write(json.dumps(
{"data": data, "last_update": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}))
if __name__ == "__main__":
with open("config.json", "r") as config_file:
config = json.loads(config_file.read())
token = config["telegram_bot_token"]
bot = telebot.TeleBot(token, parse_mode="markdown")
while True:
store_data = store_get()
for printer in config["printers"]:
ip = printer["ip"]
# pull persistent data from store
p = store_get_printer_by_serial_num(
store_data, printer["serial_num"])
reachedTonLevels = {}
reachedDrumLevels = {}
reachedTrayLevels = {}
if p is not None and printer["dynamic_ip"]:
if p["ip"] is not None:
ip = p["ip"]
if "reachedTonLevels" in p:
reachedTonLevels = p["reachedTonLevels"]
if "reachedDrumLevels" in p:
reachedDrumLevels = p["reachedDrumLevels"]
if "reachedTrayLevels" in p:
reachedTrayLevels = p["reachedTrayLevels"]
# build and update printer
p = Printer(config,
ip, printer["serial_num"], printer["dynamic_ip"], bot,
printer["toner"], printer["drum"], printer["tray"],
printer["alert_levels"], reachedTonLevels, reachedDrumLevels, reachedTrayLevels)
p.update()
store_update(store_data, p.to_dict()) # replace printer data in store
store_set(store_data) # persist store
time.sleep(config["update_interval"])

188
printer.py Normal file
View File

@ -0,0 +1,188 @@
import requests
import demjson
import ipaddress
from toner import Toner
from drum import Drum
from tray import Tray
from alert import Alert
class SerialNumException(Exception):
pass
class Printer:
def __init__(self, config: dict, ip: str, serial_num: str, dynamic_ip: bool, bot,
toner_alerts: bool, drum_alerts: bool, tray_alerts: bool, alert_levels: list[int],
reachedTonLevels: dict, reachedDrumLevels: dict, reachedTrayLevels: dict):
self.config = config
self.ip = ip
self.serial_num = serial_num
self.dynamic_ip = dynamic_ip
self.model_name = "????"
self.toners = []
self.drums = []
self.trays = []
self.alerts = []
self.uptime = 0
self.bot = bot
self.toner_alerts = toner_alerts
self.drum_alerts = drum_alerts
self.tray_alerts = tray_alerts
self.alert_levels = alert_levels
self.reachedTonLevels = reachedTonLevels
self.reachedDrumLevels = reachedDrumLevels
self.reachedTrayLevels = reachedTrayLevels
def __update_data(self, custom_ip=None) -> bool:
# this is pretty much a reverse-engineered version of the web api
ip = self.ip
if custom_ip is not None:
ip = custom_ip
home_url = 'http://'+ip+'/sws/app/information/home/home.json'
try:
r = requests.get(home_url)
text = r.content.decode("utf8")
home_data = demjson.decode(text)
self.model_name = home_data["identity"]["model_name"]
if home_data["identity"]["serial_num"] != self.serial_num:
raise SerialNumException()
for component in home_data.keys():
if "drum_" in component:
color = component.replace("drum_", "")
drum = home_data[component]
if not drum["opt"]:
continue
self.drums.append(Drum(color, drum["remaining"]))
if self.drum_alerts: # check if alerts are enabled
drumLevels = []
for level in self.alert_levels:
if drum["remaining"] <= level: # check alert levels reached
drumLevels.append(level)
if color not in self.reachedDrumLevels.keys():
self.reachedDrumLevels[color] = [] # add color key to dict if not present
if len(drumLevels) and min(drumLevels) not in self.reachedDrumLevels["color"]: # check if alert level reached is not already in dict
dyn = ""
if self.dynamic_ip:
dyn = "(dynamic)"
rem = drum["remaining"]
# send telegram message
self.bot.send_message(
self.config["telegram_user_id"], f"Drum {color} of printer {self.serial_num} @ {self.ip} {dyn} is at {rem}%")
# add alert level to dict
self.reachedDrumLevels["color"] = drumLevels
if "tray" in component:
tray_no = int(component.replace("tray", ""))
tray = home_data[component]
if not tray["opt"]:
continue
self.trays.append(
Tray(tray_no, tray["capa"], tray["paper_level"]))
if self.tray_alerts:
trayLevels = []
for level in self.alert_levels:
if tray["paper_level"] <= level:
trayLevels.append(level)
tray_key = f"t{tray_no}"
if tray_key not in self.reachedTrayLevels.keys():
self.reachedTrayLevels[tray_key] = []
if len(trayLevels) and min(trayLevels) not in self.reachedTrayLevels[tray_key]:
dyn = ""
if self.dynamic_ip:
dyn = "(dynamic)"
lev = tray["paper_level"]
self.bot.send_message(
self.config["telegram_user_id"], f"Tray {tray_no} of printer {self.serial_num} @ {self.ip} {dyn} is at {lev} pages")
self.reachedTrayLevels[tray_key] = trayLevels
if "toner_" in component:
color = component.replace("toner_", "")
ton = home_data[component]
if not ton["opt"]:
continue
self.toners.append(
Toner(color, ton["cnt"], ton["remaining"]))
if color not in self.reachedTonLevels.keys():
self.reachedTonLevels[color] = []
if self.toner_alerts:
tonLevels = []
for level in self.alert_levels:
if ton["remaining"] <= level:
tonLevels.append(level)
if len(tonLevels) and min(tonLevels) not in self.reachedTonLevels[color]:
dyn = ""
if self.dynamic_ip:
dyn = "(dynamic)"
rem = ton["remaining"]
self.bot.send_message(
self.config["telegram_user_id"], f"Toner {color} of printer {self.serial_num} @ {self.ip} {dyn} is at {rem}%")
self.reachedTonLevels[color] = tonLevels
alert_url = 'http://'+ip+'/sws/app/information/activealert/activealert.json'
r = requests.get(alert_url)
text = r.content.decode("utf8")
alert_data = demjson.decode(text)
self.uptime = alert_data["sysuptime"]
for alert in alert_data["recordData"]:
self.alerts.append(
Alert(alert["severity"], alert["code"], alert["desc"], alert["sysuptime"]))
return True
except requests.exceptions.HTTPError as errh:
print("Http Error:", errh)
except requests.exceptions.ConnectionError as errc:
print("Error Connecting:", errc)
except requests.exceptions.Timeout as errt:
print("Timeout Error:", errt)
except requests.exceptions.RequestException as err:
print("OOps: Something Else", err)
except SerialNumException:
print("Serial Num mismatch")
except demjson.JSONError:
print("JSON error")
return False
def ip_scan_lan(self):
# Scan the local network for the printer
if self.dynamic_ip:
network = ipaddress.ip_network('192.168.1.0/24')
for ip in network:
# Ignore e.g. 192.168.1.0 and 192.168.1.255
if ip == network.broadcast_address or ip == network.network_address:
continue
print(ip)
if self.__update_data(str(ip)):
print("OK")
self.ip = str(ip)
return True
return False
def to_dict(self) -> dict:
# Convert the printer object to a dict
di = {}
di["ip"] = self.ip
di["serial_num"] = self.serial_num
di["dynamic_ip"] = self.dynamic_ip
di["model_name"] = self.model_name
di["uptime"] = self.uptime
di["reachedDrumLevels"] = self.reachedDrumLevels
di["reachedTrayLevels"] = self.reachedTrayLevels
di["reachedTonLevels"] = self.reachedTonLevels
di["toners"] = []
di["drums"] = []
di["trays"] = []
di["alerts"] = []
for toner in self.toners:
di["toners"].append(dict(toner.__dict__))
for drum in self.drums:
di["drums"].append(dict(drum.__dict__))
for tray in self.trays:
di["trays"].append(dict(tray.__dict__))
for alert in self.alerts:
di["alerts"].append(dict(alert.__dict__))
return di
def update(self):
if not self.__update_data(str(self.ip)):
return self.ip_scan_lan()
else:
return True

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
telebot==0.0.5
demjson==2.2.4

49
telegram.py Normal file
View File

@ -0,0 +1,49 @@
import telebot
import json
# load config
with open("config.json", "r") as jsonconf:
conf = json.loads(jsonconf.read())
TOKEN = conf["telegram_bot_token"]
USER_ID = conf["telegram_user_id"]
# printer data in understandable form
def readout(store_data: dict):
text = ""
for printer in store_data["data"]:
text += "Printer *"+printer["model_name"]+"* _" + \
printer["serial_num"]+"_ @ "+printer["ip"]
if printer["dynamic_ip"]:
text += " (dynamic)"
text += ":\n\nTONERS\n"
for toner in printer["toners"]:
text += "*"+toner["color"]+"* "+str(toner["remaining_percent"])+"% ("+str(
toner["pages_total"])+" pages printed so far)\n"
text += "\nDRUMS\n"
for drum in printer["drums"]:
text += "*"+drum["color"]+"* "+str(drum["remaining_percent"])+"% \n"
text += "\nTRAYS\n"
for tray in printer["trays"]:
text += "*"+str(tray["tray_no"])+"* "+str(("???" if tray["paper_level"]
== 0 else tray["paper_level"]))+"/"+str(tray["capacity"])+"\n"
if len(printer["alerts"]):
text += "\nALERTS\n"
for alert in printer["alerts"]:
text += "*"+alert["code"]+"* "+alert["desc"]+"\n"
text+="\n\n\n"
text += "Last update: "+store_data["last_update"]+"\n"
return text
bot = telebot.TeleBot(TOKEN, parse_mode="markdown")
# just an echo bot
@bot.message_handler(func=lambda m: True)
def echo_all(message):
if message.chat.id != USER_ID:
return
with open("store.json", "r") as store_file:
store_data = json.loads(store_file.read())
t = readout(store_data)
bot.reply_to(message, t)
bot.polling()

6
toner.py Normal file
View File

@ -0,0 +1,6 @@
class Toner:
def __init__(self, color: str, pages_total: int, remaining_percent: int) -> None:
self.color = color
self.pages_total = pages_total
self.remaining_percent = remaining_percent
self.used_percent = 100 - remaining_percent

5
tray.py Normal file
View File

@ -0,0 +1,5 @@
class Tray:
def __init__(self, tray_no: int, capacity: int, paper_level: int) -> None:
self.tray_no = tray_no
self.capacity = capacity
self.paper_level = paper_level