first commit
This commit is contained in:
commit
d42dbf7b6d
51
README.md
Normal file
51
README.md
Normal 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
6
alert.py
Normal 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
16
config.json
Normal 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
5
drum.py
Normal 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
BIN
example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
71
main.py
Normal file
71
main.py
Normal 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
188
printer.py
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
telebot==0.0.5
|
||||||
|
demjson==2.2.4
|
49
telegram.py
Normal file
49
telegram.py
Normal 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
6
toner.py
Normal 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
|
Loading…
Reference in New Issue
Block a user