From 69eb9484ac28077f4671eaa169bc360540c6af1d Mon Sep 17 00:00:00 2001 From: jneilliii Date: Sat, 16 Apr 2022 20:02:48 -0400 Subject: [PATCH] 1.0.2 (#304) * resolves issue related to shutdown monitoring not powering off devices due to SIGTERM, #274 * fix for #281, timelapse race condition * add JSON response to on/off API commands * add `Always on Upload` option to not require start print from slicer upload, #190, #267 * replace deprecated getBoolen with get_boolean * remove duplicated bundling of fontawesome 5 if running on OctoPrint 1.5.0+ --- README.md | 6 +- octoprint_tplinksmartplug/__init__.py | 153 +++++++++++------- .../static/css/tplinksmartplug.css | 2 +- .../templates/tplinksmartplug_settings.jinja2 | 3 + setup.py | 2 +- 5 files changed, 104 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 6f6b043..13971e7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # OctoPrint-TPLinkSmartplug -***Warning***: Recent firmware version 1.1.0 for the HS100, HS110 have been reported to break the capability of this plugin to communicate with kasa devices. So far it seems to be only effecting UK version plugs, but could spread to other firmware variants. +***Warning***: Recent firmware version 1.1.0 for the HS100, HS110 have been reported to break the capability of this plugin to communicate with kasa devices. So far it seems to be only effecting UK version plugs, but could spread to other firmware variants. ***Warning***: Recent firmware updates for the HS103 breaks the use of this plugin with those devices. TP-Link may push the same firmware to other devices, but be warned that updating your devices firmware may break the use of this plugin. It appears this can be resolved by never connecting the plug to the cloud by following the steps outlined [here](https://www.tp-link.com/us/support/faq/2707/). -Work inspired by [OctoPrint-PSUControl](https://github.com/kantlivelong/OctoPrint-PSUControl) and [TP-Link WiFi SmartPlug Client](https://github.com/softScheck/tplink-smartplug), this plugin controls a TP-Link Smartplug via OctoPrint's nav bar. Currently known compatible models are the HS100, HS107, HS110, HS300, KP115. Other Kasa app based devices may work. Tapo series devices will not work with this plugin, and probably never will because of their closed communication. +Work inspired by [OctoPrint-PSUControl](https://github.com/kantlivelong/OctoPrint-PSUControl) and [TP-Link WiFi SmartPlug Client](https://github.com/softScheck/tplink-smartplug), this plugin controls a TP-Link Smartplug via OctoPrint's nav bar. Currently known compatible models are the HS100, HS103, HS105, HS107, HS110, HS300, KP303, KP115. Other Kasa app based devices may work. Tapo series devices will not work with this plugin, and probably never will because of their closed communication. ## Screenshots ![screenshot](screenshot.png) @@ -55,7 +55,7 @@ Once installed go into settings and enter the ip address for your TP-Link Smartp - When checked will run system command configured in **System Command On** setting after a delay in seconds configured in **System Command On Delay**. - **Run System Command Before Off** - When checked will run system command configured in **System Command Off** setting after a delay in seconds configured in **System Command Off Delay**. - + ## Most recent changelog **[1.0.1](https://github.com/jneilliii/OctoPrint-TPLinkSmartplug/releases/tag/1.0.0)** (04/15/2021) diff --git a/octoprint_tplinksmartplug/__init__.py b/octoprint_tplinksmartplug/__init__.py index 96b4fd6..886273d 100644 --- a/octoprint_tplinksmartplug/__init__.py +++ b/octoprint_tplinksmartplug/__init__.py @@ -15,6 +15,8 @@ import threading import time import sqlite3 + +from octoprint.util.version import is_octoprint_compatible from uptime import uptime from datetime import datetime from struct import unpack @@ -79,8 +81,7 @@ class tplinksmartplugPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.ProgressPlugin, - octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.ShutdownPlugin): + octoprint.plugin.EventHandlerPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.tplinksmartplug") @@ -158,15 +159,6 @@ def on_after_startup(self): self._tplinksmartplug_logger.debug("powering on %s during startup failed." % (plug["ip"])) self._reset_idle_timer() - ##~~ ShutdownPlugin mixin - - def on_shutdown(self): - if self._settings.getBoolean(["event_on_shutdown_monitoring"]): - for plug in self._settings.get(['arrSmartplugs']): - if plug["event_on_shutdown"] is True: - self._tplinksmartplug_logger.debug("powering off %s due to shutdown event." % plug["ip"]) - self.turn_off(plug["ip"]) - ##~~ SettingsPlugin mixin def get_settings_defaults(self): @@ -181,6 +173,7 @@ def get_settings_defaults(self): event_on_error_monitoring=False, event_on_disconnect_monitoring=False, event_on_upload_monitoring=False, + event_on_upload_monitoring_always=False, event_on_startup_monitoring=False, event_on_shutdown_monitoring=False, cost_rate=0, @@ -313,7 +306,7 @@ def on_settings_migrate(self, target, current=None): for plug in self._settings.get(['arrSmartplugs']): if "/" in plug["ip"]: plug_ip, plug_num = plug["ip"].split("/") - plug["ip"] = "{}/{}".format(plug_ip, int(plug_num)+1) + plug["ip"] = "{}/{}".format(plug_ip, int(plug_num) + 1) arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"], arrSmartplugs_new) @@ -337,20 +330,25 @@ def on_settings_migrate(self, target, current=None): ##~~ AssetPlugin mixin def get_assets(self): - return dict( - js=["js/jquery-ui.min.js", - "js/knockout-sortable.1.2.0.js", - "js/fontawesome-iconpicker.js", - "js/ko.iconpicker.js", - "js/tplinksmartplug.js", - "js/knockout-bootstrap.min.js", - "js/ko.observableDictionary.js", - "js/plotly-latest.min.js"], - css=["css/font-awesome.min.css", - "css/font-awesome-v4-shims.min.css", - "css/fontawesome-iconpicker.css", - "css/tplinksmartplug.css"] - ) + css = ["css/fontawesome-iconpicker.css", + "css/tplinksmartplug.css", + ] + + if not is_octoprint_compatible(">=1.5.0"): + css += [ + "css/font-awesome.min.css", + "css/font-awesome-v4-shims.min.css", + ] + + return {'js': ["js/jquery-ui.min.js", + "js/knockout-sortable.1.2.0.js", + "js/fontawesome-iconpicker.js", + "js/ko.iconpicker.js", + "js/tplinksmartplug.js", + "js/knockout-bootstrap.min.js", + "js/ko.observableDictionary.js", + "js/plotly-latest.min.js"], + 'css': css} ##~~ TemplatePlugin mixin @@ -393,15 +391,19 @@ def turn_on(self, plugip): plug_num = 0 if plug["useCountdownRules"] and int(plug["countdownOnDelay"]) > 0: self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'), plug_ip, plug_num) - chk = self.lookup(self.sendCommand(json.loads('{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":1,"name":"turn on"}}}' % plug["countdownOnDelay"]), plug_ip, plug_num), *["count_down", "add_rule", "err_code"]) + chk = self.lookup(self.sendCommand(json.loads( + '{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":1,"name":"turn on"}}}' % plug[ + "countdownOnDelay"]), plug_ip, plug_num), *["count_down", "add_rule", "err_code"]) if chk == 0: self._countdown_active = True - c = threading.Timer(int(plug["countdownOnDelay"]) + 3, self._plugin_manager.send_plugin_message, [self._identifier, dict(check_status=True, ip=plugip)]) + c = threading.Timer(int(plug["countdownOnDelay"]) + 3, self._plugin_manager.send_plugin_message, + [self._identifier, dict(check_status=True, ip=plugip)]) c.daemon = True c.start() else: turn_on_cmnd = dict(system=dict(set_relay_state=dict(state=1))) - chk = self.lookup(self.sendCommand(turn_on_cmnd, plug_ip, plug_num), *["system", "set_relay_state", "err_code"]) + chk = self.lookup(self.sendCommand(turn_on_cmnd, plug_ip, plug_num), + *["system", "set_relay_state", "err_code"]) self._tplinksmartplug_logger.debug(chk) if chk == 0: @@ -424,7 +426,7 @@ def turn_on(self, plugip): self._waitForHeaters = False self._reset_idle_timer() - return self.check_status(plugip) + return self.check_status(plugip) def turn_off(self, plugip): timenow = datetime.now() @@ -439,10 +441,13 @@ def turn_off(self, plugip): plug_num = 0 if plug["useCountdownRules"] and int(plug["countdownOffDelay"]) > 0: self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'), plug_ip, plug_num) - chk = self.lookup(self.sendCommand(json.loads('{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":0,"name":"turn off"}}}' % plug["countdownOffDelay"]), plug_ip, plug_num), *["count_down", "add_rule", "err_code"]) + chk = self.lookup(self.sendCommand(json.loads( + '{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":0,"name":"turn off"}}}' % plug[ + "countdownOffDelay"]), plug_ip, plug_num), *["count_down", "add_rule", "err_code"]) if chk == 0: self._countdown_active = True - c = threading.Timer(int(plug["countdownOffDelay"]) + 3, self._plugin_manager.send_plugin_message, [self._identifier, dict(check_status=True, ip=plugip)]) + c = threading.Timer(int(plug["countdownOffDelay"]) + 3, self._plugin_manager.send_plugin_message, + [self._identifier, dict(check_status=True, ip=plugip)]) c.start() if plug["gcodeCmdOff"] and plug["gcodeRunCmdOff"] != "": self._tplinksmartplug_logger.debug("sending gcode commands to printer.") @@ -457,11 +462,12 @@ def turn_off(self, plugip): if not plug["useCountdownRules"]: turn_off_cmnd = dict(system=dict(set_relay_state=dict(state=0))) - chk = self.lookup(self.sendCommand(turn_off_cmnd, plug_ip, plug_num), *["system", "set_relay_state", "err_code"]) + chk = self.lookup(self.sendCommand(turn_off_cmnd, plug_ip, plug_num), + *["system", "set_relay_state", "err_code"]) self._tplinksmartplug_logger.debug(chk) - if chk == 0: - return self.check_status(plugip) + + return self.check_status(plugip) def check_statuses(self): for plug in self._settings.get(["arrSmartplugs"]): @@ -478,7 +484,8 @@ def check_status(self, plugip): self._tplinksmartplug_logger.debug(check_status_cmnd) if len(plug_ip) == 2: response = self.sendCommand(check_status_cmnd, plug_ip[0], plug_ip[1]) - timer_chk = self.lookup(response, *["system", "get_sysinfo", "children"])[int(plug_ip[1])-1]["on_time"] + timer_chk = self.lookup(response, *["system", "get_sysinfo", "children"])[int(plug_ip[1]) - 1][ + "on_time"] else: response = self.sendCommand(check_status_cmnd, plug_ip[0]) timer_chk = self.deep_get(response, ["system", "get_sysinfo", "on_time"], default=0) @@ -525,14 +532,16 @@ def check_status(self, plugip): if self.db_path is not None: db = sqlite3.connect(self.db_path) cursor = db.cursor() - cursor.execute('''INSERT INTO energy_data(ip, timestamp, current, power, total, voltage) VALUES(?,?,?,?,?,?)''',[plugip, today.isoformat(' '), c, p, t, v]) + cursor.execute( + '''INSERT INTO energy_data(ip, timestamp, current, power, total, voltage) VALUES(?,?,?,?,?,?)''', + [plugip, today.isoformat(' '), c, p, t, v]) db.commit() db.close() if len(plug_ip) == 2: chk = self.lookup(response, *["system", "get_sysinfo", "children"]) if chk: - chk = chk[int(plug_ip[1])-1]["state"] + chk = chk[int(plug_ip[1]) - 1]["state"] else: chk = self.lookup(response, *["system", "get_sysinfo", "relay_state"]) @@ -576,7 +585,9 @@ def on_api_command(self, command, data): elif command == 'getEnergyData': db = sqlite3.connect(self.db_path) cursor = db.cursor() - cursor.execute('''SELECT timestamp, current, power, total, voltage FROM energy_data WHERE ip=? ORDER BY timestamp DESC LIMIT ?,?''', (data["ip"], data["record_offset"], data["record_limit"])) + cursor.execute( + '''SELECT timestamp, current, power, total, voltage FROM energy_data WHERE ip=? ORDER BY timestamp DESC LIMIT ?,?''', + (data["ip"], data["record_offset"], data["record_limit"])) response = {'energy_data': cursor.fetchall()} db.close() self._tplinksmartplug_logger.debug(response) @@ -618,7 +629,9 @@ def on_api_command(self, command, data): self._settings.save() # eventManager().fire(Events.SETTINGS_UPDATED) if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown" or command == "abortAutomaticShutdown": - self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) + self._plugin_manager.send_plugin_message(self._identifier, + dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", + timeout_value=self._timeout_value)) else: return flask.jsonify(response) @@ -635,7 +648,7 @@ def on_event(self, event, payload): if response["currentState"] == "on": self._plugin_manager.send_plugin_message(self._identifier, response) # Error Event - if event == Events.ERROR and self._settings.getBoolean(["event_on_error_monitoring"]) is True: + if event == Events.ERROR and self._settings.get_boolean(["event_on_error_monitoring"]) is True: self._tplinksmartplug_logger.debug("powering off due to %s event." % event) for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_error"] is True: @@ -648,7 +661,9 @@ def on_event(self, event, payload): if self._settings.get_boolean(["powerOffWhenIdle"]): self._tplinksmartplug_logger.debug("resetting idle timer due to %s event." % event) self._reset_idle_timer() - self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) + self._plugin_manager.send_plugin_message(self._identifier, + dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", + timeout_value=self._timeout_value)) return # Cancelled Print Interpreted Event if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error(): @@ -658,7 +673,7 @@ def on_event(self, event, payload): self._autostart_file = None return # Print Started Event - if event == Events.PRINT_STARTED and self._settings.getFloat(["cost_rate"]) > 0: + if event == Events.PRINT_STARTED and self._settings.get_float(["cost_rate"]) > 0: self.print_job_started = True self._tplinksmartplug_logger.debug(payload.get("path", None)) for plug in self._settings.get(["arrSmartplugs"]): @@ -676,7 +691,9 @@ def on_event(self, event, payload): if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None - self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) + self._plugin_manager.send_plugin_message(self._identifier, + dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", + timeout_value=self._timeout_value)) if event == Events.PRINT_STARTED and self._countdown_active: for plug in self._settings.get(["arrSmartplugs"]): @@ -703,11 +720,12 @@ def on_event(self, event, payload): self._tplinksmartplug_logger.debug("hours: %s" % hours) power_used = self.print_job_power * hours self._tplinksmartplug_logger.debug("power used: %s" % power_used) - power_cost = power_used * self._settings.getFloat(["cost_rate"]) + power_cost = power_used * self._settings.get_float(["cost_rate"]) self._tplinksmartplug_logger.debug("power total cost: %s" % power_cost) self._storage_interface = self._file_manager._storage(payload.get("origin", "local")) - self._storage_interface.set_additional_metadata(payload.get("path"), "statistics", dict(lastPowerCost=dict(_default=float('{:.4f}'.format(power_cost)))), merge=True) + self._storage_interface.set_additional_metadata(payload.get("path"), "statistics", dict( + lastPowerCost=dict(_default=float('{:.4f}'.format(power_cost)))), merge=True) self.print_job_power = 0.0 self.print_job_started = False @@ -741,9 +759,10 @@ def on_event(self, event, payload): self._printer.select_file(self._autostart_file, False, printAfterSelect=True) self._autostart_file = None # File Uploaded Event - if event == Events.UPLOAD and self._settings.getBoolean(["event_on_upload_monitoring"]): - if payload.get("print", False): # implemented in OctoPrint version 1.4.1 - self._tplinksmartplug_logger.debug("File uploaded: %s. Turning enabled plugs on." % payload.get("name", "")) + if event == Events.UPLOAD and self._settings.get_boolean(["event_on_upload_monitoring"]): + if payload.get("print", False) or self._settings.get_boolean(["event_on_upload_monitoring_always"]): # implemented in OctoPrint version 1.4.1 + self._tplinksmartplug_logger.debug( + "File uploaded: %s. Turning enabled plugs on." % payload.get("name", "")) self._tplinksmartplug_logger.debug(payload) for plug in self._settings.get(['arrSmartplugs']): self._tplinksmartplug_logger.debug(plug) @@ -751,10 +770,18 @@ def on_event(self, event, payload): self._tplinksmartplug_logger.debug("powering on %s due to %s event." % (plug["ip"], event)) response = self.turn_on(plug["ip"]) if response["currentState"] == "on": - self._tplinksmartplug_logger.debug("power on successful for %s attempting connection in %s seconds" % (plug["ip"], plug.get("autoConnectDelay", "0"))) + self._tplinksmartplug_logger.debug( + "power on successful for %s attempting connection in %s seconds" % ( + plug["ip"], plug.get("autoConnectDelay", "0"))) self._plugin_manager.send_plugin_message(self._identifier, response) if payload.get("path", False) and payload.get("target") == "local": self._autostart_file = payload.get("path") + # Shutdown Event + if event == Events.SHUTDOWN and self._settings.get_boolean(["event_on_shutdown_monitoring"]): + for plug in self._settings.get(['arrSmartplugs']): + if plug["event_on_shutdown"] is True: + self._tplinksmartplug_logger.debug("powering off %s due to shutdown event." % plug["ip"]) + self.turn_off(plug["ip"]) ##~~ Idle Timeout @@ -795,14 +822,20 @@ def _idle_poweroff(self): if (uptime() / 60) <= (self._settings.get_int(["idleTimeout"])): self._tplinksmartplug_logger.debug("Just booted so wait for time sync.") - self._tplinksmartplug_logger.debug("uptime: {}, comparison: {}".format((uptime() / 60), (self._settings.get_int(["idleTimeout"])))) + self._tplinksmartplug_logger.debug( + "uptime: {}, comparison: {}".format((uptime() / 60), (self._settings.get_int(["idleTimeout"])))) self._reset_idle_timer() return - self._tplinksmartplug_logger.debug("Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) + self._tplinksmartplug_logger.debug( + "Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._tplinksmartplug_logger.debug("Heaters below temperature.") + self._tplinksmartplug_logger.debug("Checking for timelapse running.") if self._wait_for_timelapse(): + if self._printer.is_printing() or self._printer.is_paused(): + self._tplinksmartplug_logger.debug("Aborted power off due to print activity.") + return self._timer_start() else: self._tplinksmartplug_logger.debug("Aborted power off due to activity.") @@ -884,7 +917,8 @@ def _wait_for_heaters(self): self._waitForHeaters = False return True - self._tplinksmartplug_logger.debug("Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) + self._tplinksmartplug_logger.debug( + "Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer @@ -904,7 +938,9 @@ def _timer_task(self): return self._timeout_value -= 1 - self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) + self._plugin_manager.send_plugin_message(self._identifier, + dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", + timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() @@ -931,7 +967,7 @@ def _get_device_id(self, plugip): if len(plug_ip) == 2: response = self.deep_get(plug_data, ["system", "get_sysinfo", "children"], default=False) if response: - response = response[int(plug_ip[1])-1]["id"] + response = response[int(plug_ip[1]) - 1]["id"] else: response = self.deep_get(response, ["system", "get_sysinfo", "deviceId"]) if response: @@ -1038,7 +1074,8 @@ def sendCommand(self, cmd, plugip, plug_num=0): def gcode_turn_off(self, plug): if self._printer.is_printing() and plug["warnPrinting"] is True: - self._tplinksmartplug_logger.debug("Not powering off %s immediately because printer is printing." % plug["label"]) + self._tplinksmartplug_logger.debug( + "Not powering off %s immediately because printer is printing." % plug["label"]) self.power_off_queue.append(plug) else: chk = self.turn_off(plug["ip"]) @@ -1109,7 +1146,9 @@ def processAtCommand(self, comm_instance, phase, command, parameters, tags=None, self._abort_timer = None self._timeout_value = None if command in ["TPLINKIDLEON", "TPLINKIDLEOFF"]: - self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) + self._plugin_manager.send_plugin_message(self._identifier, + dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", + timeout_value=self._timeout_value)) ##~~ Temperatures received hook diff --git a/octoprint_tplinksmartplug/static/css/tplinksmartplug.css b/octoprint_tplinksmartplug/static/css/tplinksmartplug.css index f429c8c..2ac477a 100644 --- a/octoprint_tplinksmartplug/static/css/tplinksmartplug.css +++ b/octoprint_tplinksmartplug/static/css/tplinksmartplug.css @@ -40,7 +40,7 @@ div#tplinksmartplugs { } #sidebar_plugin_tplinksmartplug > div.accordion-inner > #tplinksmartplugs > div > div.on { - color: #000; + color: inherit; } #sidebar_plugin_tplinksmartplug > div.accordion-inner > #tplinksmartplugs > div > div.off { diff --git a/octoprint_tplinksmartplug/templates/tplinksmartplug_settings.jinja2 b/octoprint_tplinksmartplug/templates/tplinksmartplug_settings.jinja2 index 694f4d5..79257aa 100644 --- a/octoprint_tplinksmartplug/templates/tplinksmartplug_settings.jinja2 +++ b/octoprint_tplinksmartplug/templates/tplinksmartplug_settings.jinja2 @@ -78,6 +78,9 @@ + diff --git a/setup.py b/setup.py index 9f5496e..37e9f34 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "OctoPrint-TPLinkSmartplug" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "1.0.1" +plugin_version = "1.0.2" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module