Skip to content

Commit 17d16bd

Browse files
authored
1.0.4
2 parents 1641da6 + 5117a89 commit 17d16bd

File tree

4 files changed

+126
-27
lines changed

4 files changed

+126
-27
lines changed

octoprint_tplinksmartplug/__init__.py

Lines changed: 116 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ def __init__(self):
103103
self.poll_status = None
104104
self.power_off_queue = []
105105
self._gcode_queued = False
106+
self.active_timers = {"on": {}, "off": {}}
107+
self.total_correction = 0
108+
self.last_row = [0,0,0,0,0,0,0]
109+
self.last_row_entered = False
106110

107111
##~~ StartupPlugin mixin
108112

@@ -124,9 +128,38 @@ def on_startup(self, host, port):
124128
db = sqlite3.connect(self.db_path)
125129
cursor = db.cursor()
126130
cursor.execute(
127-
'''CREATE TABLE energy_data(id INTEGER PRIMARY KEY, ip TEXT, timestamp TEXT, current REAL, power REAL, total REAL, voltage REAL)''')
131+
'''CREATE TABLE energy_data(id INTEGER PRIMARY KEY, ip TEXT, timestamp DATETIME, voltage REAL, current REAL, power REAL, total REAL, grandtotal REAL)''')
132+
else:
133+
db = sqlite3.connect(self.db_path)
134+
cursor = db.cursor()
135+
136+
#Update 'energy_data' table schema if 'grandtotal' column not present
137+
cursor.execute('''SELECT * FROM energy_data''')
138+
if 'grandtotal' not in next(zip(*cursor.description)):
139+
#Change type of 'timestamp' to 'DATETIME' (from 'TEXT'), add new 'grandtotal' column
140+
cursor.execute('''
141+
ALTER TABLE energy_data RENAME TO _energy_data''')
142+
cursor.execute('''
143+
CREATE TABLE energy_data (id INTEGER PRIMARY KEY, ip TEXT, timestamp DATETIME, voltage REAL, current REAL, power REAL, total REAL, grandtotal REAL)''')
144+
#Copy over table, skipping non-changed values and calculating running grandtotal
145+
cursor.execute('''
146+
INSERT INTO energy_data (ip, timestamp, voltage, current, power, total, grandtotal) SELECT ip, timestamp, voltage, current, power, total, grandtotal FROM
147+
(WITH temptable AS (SELECT *, total - LAG(total,1) OVER(ORDER BY id) AS delta, LAG(power,1) OVER(ORDER BY id) AS power1 FROM _energy_data)
148+
SELECT *, ROUND(SUM(MAX(delta,0)) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),6)
149+
AS grandtotal FROM temptable WHERE power > 0 OR power1 > 0 OR delta <> 0 OR delta IS NULL)''')
150+
151+
cursor.execute('''DROP TABLE _energy_data''')
152+
#Compact database
128153
db.commit()
129-
db.close()
154+
cursor.execute('''VACUUM''')
155+
156+
self.last_row = list(cursor.execute('''SELECT id, timestamp, voltage, current, power, total, grandtotal
157+
FROM energy_data ORDER BY ROWID DESC LIMIT 1''').fetchone() or [0,0,0,0,0,0,0]) #Round to remove floating point imprecision
158+
self.last_row = self.last_row[:2] + [round(x,6) for x in self.last_row[2:]] #Round to correct floating point imprecision in sqlite
159+
self.last_row_entered = True
160+
self.total_correction = self.last_row[6] - self.last_row[5] #grandtotal - total
161+
db.commit()
162+
db.close()
130163

131164
def on_after_startup(self):
132165
self._logger.info("TPLinkSmartplug loaded!")
@@ -143,7 +176,7 @@ def on_after_startup(self):
143176
self.idleTimeout = self._settings.get_int(["idleTimeout"])
144177
self._tplinksmartplug_logger.debug("idleTimeout: %s" % self.idleTimeout)
145178
self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"])
146-
self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',')
179+
self._idleIgnoreCommandsArray = self.idleIgnoreCommands.replace(" ", "").split(',')
147180
self._tplinksmartplug_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands)
148181
self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"])
149182
self._tplinksmartplug_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp)
@@ -158,6 +191,22 @@ def on_after_startup(self):
158191
else:
159192
self._tplinksmartplug_logger.debug("powering on %s during startup failed." % (plug["ip"]))
160193
self._reset_idle_timer()
194+
self.loaded = True
195+
196+
def on_connect(self, *args, **kwargs): #Power up on connect
197+
if hasattr(self, 'loaded') is False: return None
198+
if self._settings.get_boolean(["connect_on_connect_request"]) is True:
199+
self._tplinksmartplug_logger.debug("powering on due to 'Connect' request.")
200+
for plug in self._settings.get(['arrSmartplugs']):
201+
if plug["connect_on_connect"] is True and self._printer.is_closed_or_error():
202+
self._tplinksmartplug_logger.debug("powering on %s due to 'Connect' request." % (plug["ip"]))
203+
response = self.turn_on(plug["ip"])
204+
if response.get("currentState", False) == "on":
205+
self._tplinksmartplug_logger.debug("powering on %s during 'Connect' succeeded." % (plug["ip"]))
206+
self._plugin_manager.send_plugin_message(self._identifier, response)
207+
else:
208+
self._tplinksmartplug_logger.debug("powering on %s during 'Connect' failed." % (plug["ip"]))
209+
return None
161210

162211
##~~ SettingsPlugin mixin
163212

@@ -168,7 +217,7 @@ def get_settings_defaults(self):
168217
'event_on_upload_monitoring': False, 'event_on_upload_monitoring_always': False,
169218
'event_on_startup_monitoring': False, 'event_on_shutdown_monitoring': False, 'cost_rate': 0,
170219
'abortTimeout': 30, 'powerOffWhenIdle': False, 'idleTimeout': 30, 'idleIgnoreCommands': 'M105',
171-
'idleTimeoutWaitTemp': 50, 'progress_polling': False, 'useDropDown': False}
220+
'idleIgnoreHeaters': '', 'idleTimeoutWaitTemp': 50, 'progress_polling': False, 'useDropDown': False}
172221

173222
def on_settings_save(self, data):
174223
old_debug_logging = self._settings.get_boolean(["debug_logging"])
@@ -186,7 +235,7 @@ def on_settings_save(self, data):
186235

187236
self.idleTimeout = self._settings.get_int(["idleTimeout"])
188237
self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"])
189-
self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',')
238+
self._idleIgnoreCommandsArray = self.idleIgnoreCommands.replace(" ", "").split(',')
190239
self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"])
191240

192241
if self.powerOffWhenIdle != old_powerOffWhenIdle:
@@ -511,18 +560,43 @@ def check_status(self, plugip):
511560
p = ""
512561
if "total_wh" in emeter_data["get_realtime"]:
513562
t = emeter_data["get_realtime"]["total_wh"] / 1000.0
563+
emeter_data["get_realtime"]["total_wh"] += self.total_correction * 1000.0 #Add back total correction factor, so becomes grandtotal
514564
elif "total" in emeter_data["get_realtime"]:
515565
t = emeter_data["get_realtime"]["total"]
566+
emeter_data["get_realtime"]["total"] += self.total_correction #Add back total correction factor, so becomes grandtotal
516567
else:
517568
t = ""
518569
if self.db_path is not None:
519-
db = sqlite3.connect(self.db_path)
520-
cursor = db.cursor()
521-
cursor.execute(
522-
'''INSERT INTO energy_data(ip, timestamp, current, power, total, voltage) VALUES(?,?,?,?,?,?)''',
523-
[plugip, today.isoformat(' '), c, p, t, v])
524-
db.commit()
525-
db.close()
570+
last_p = self.last_row[4]
571+
last_t = self.last_row[5]
572+
573+
if last_t is not None and t < last_t: #total has reset since last measurement
574+
self.total_correction += last_t
575+
gt = round(t + self.total_correction, 6) #Prevent accumulated floating-point rounding errors
576+
current_row = [plugip, today.isoformat(' '), v, c, p, t, gt]
577+
578+
if self.last_row_entered is False and last_p == 0 and p > 0: #Go back & enter last_row on power return (if not entered already)
579+
db = sqlite3.connect(self.db_path)
580+
cursor = db.cursor()
581+
cursor.execute(
582+
'''INSERT INTO energy_data(ip, timestamp, voltage, current, power, total, grandtotal) VALUES(?,?,?,?,?,?,?)''',
583+
self.last_row)
584+
db.commit()
585+
db.close()
586+
self.last_row_entered = True
587+
else:
588+
self.last_row_entered = False
589+
590+
if t != last_t or p > 0 or last_p > 0: #Enter current_row if change in total or power is on or just turned off
591+
db = sqlite3.connect(self.db_path)
592+
cursor = db.cursor()
593+
cursor.execute(
594+
'''INSERT INTO energy_data(ip, timestamp, voltage, current, power, total, grandtotal) VALUES(?,?,?,?,?,?,?)''',
595+
current_row)
596+
db.commit()
597+
db.close()
598+
599+
self.last_row = current_row
526600

527601
if len(plug_ip) == 2:
528602
chk = self.lookup(response, *["system", "get_sysinfo", "children"])
@@ -572,7 +646,7 @@ def on_api_command(self, command, data):
572646
db = sqlite3.connect(self.db_path)
573647
cursor = db.cursor()
574648
cursor.execute(
575-
'''SELECT timestamp, current, power, total, voltage FROM energy_data WHERE ip=? ORDER BY timestamp DESC LIMIT ?,?''',
649+
'''SELECT timestamp, current, power, grandtotal, voltage FROM energy_data WHERE ip=? ORDER BY timestamp DESC LIMIT ?,?''',
576650
(data["ip"], data["record_offset"], data["record_limit"]))
577651
response = {'energy_data': cursor.fetchall()}
578652
db.close()
@@ -704,7 +778,7 @@ def on_event(self, event, payload):
704778

705779
hours = (payload.get("time", 0) / 60) / 60
706780
self._tplinksmartplug_logger.debug("hours: %s" % hours)
707-
power_used = self.print_job_power * hours
781+
power_used = self.print_job_power
708782
self._tplinksmartplug_logger.debug("power used: %s" % power_used)
709783
power_cost = power_used * self._settings.get_float(["cost_rate"])
710784
self._tplinksmartplug_logger.debug("power total cost: %s" % power_cost)
@@ -747,7 +821,7 @@ def on_event(self, event, payload):
747821
# File Uploaded Event
748822
if event == Events.UPLOAD and self._settings.get_boolean(["event_on_upload_monitoring"]):
749823
if payload.get("print", False) or self._settings.get_boolean(
750-
["event_on_upload_monitoring_always"]): # implemented in OctoPrint version 1.4.1
824+
["event_on_upload_monitoring_always"]): # implemented in OctoPrint version 1.4.1
751825
self._tplinksmartplug_logger.debug(
752826
"File uploaded: %s. Turning enabled plugs on." % payload.get("name", ""))
753827
self._tplinksmartplug_logger.debug(payload)
@@ -849,11 +923,12 @@ def _wait_for_timelapse(self):
849923
def _wait_for_heaters(self):
850924
self._waitForHeaters = True
851925
heaters = self._printer.get_current_temperatures()
926+
ignored_heaters = self._settings.get(["idleIgnoreHeaters"]).replace(" ", "").split(',')
852927

853928
for heater, entry in heaters.items():
854929
target = entry.get("target")
855-
if target is None:
856-
# heater doesn't exist in fw
930+
if target is None or heater in ignored_heaters:
931+
# heater doesn't exist in fw or set to be ignored
857932
continue
858933

859934
try:
@@ -879,7 +954,7 @@ def _wait_for_heaters(self):
879954
highest_temp = 0
880955
heaters_above_waittemp = []
881956
for heater, entry in heaters.items():
882-
if not heater.startswith("tool"):
957+
if not heater.startswith("tool") or heater in ignored_heaters:
883958
continue
884959

885960
actual = entry.get("actual")
@@ -1060,6 +1135,10 @@ def sendCommand(self, cmd, plugip, plug_num=0):
10601135
##~~ Gcode processing hook
10611136

10621137
def gcode_turn_off(self, plug):
1138+
if plug["ip"] in self.active_timers["off"]:
1139+
self.active_timers["off"][plug["ip"]].cancel()
1140+
del self.active_timers["off"][plug["ip"]]
1141+
10631142
if self._printer.is_printing() and plug["warnPrinting"] is True:
10641143
self._tplinksmartplug_logger.debug(
10651144
"Not powering off %s immediately because printer is printing." % plug["label"])
@@ -1068,7 +1147,12 @@ def gcode_turn_off(self, plug):
10681147
chk = self.turn_off(plug["ip"])
10691148
self._plugin_manager.send_plugin_message(self._identifier, chk)
10701149

1150+
10711151
def gcode_turn_on(self, plug):
1152+
if plug["ip"] in self.active_timers["on"]:
1153+
self.active_timers["on"][plug["ip"]].cancel()
1154+
del self.active_timers["on"][plug["ip"]]
1155+
10721156
chk = self.turn_on(plug["ip"])
10731157
self._plugin_manager.send_plugin_message(self._identifier, chk)
10741158

@@ -1108,19 +1192,25 @@ def processAtCommand(self, comm_instance, phase, command, parameters, tags=None,
11081192
plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip)
11091193
self._tplinksmartplug_logger.debug(plug)
11101194
if plug and plug["gcodeEnabled"]:
1111-
t = threading.Timer(int(plug["gcodeOnDelay"]), self.gcode_turn_on, [plug])
1112-
t.daemon = True
1113-
t.start()
1195+
if plugip in self.active_timers["off"]:
1196+
self.active_timers["off"][plugip].cancel()
1197+
del self.active_timers["off"][plugip]
1198+
self.active_timers["on"][plugip] = threading.Timer(int(plug["gcodeOnDelay"]), self.gcode_turn_on, [plug])
1199+
self.active_timers["on"][plugip].daemon = True
1200+
self.active_timers["on"][plugip].start()
11141201
return None
11151202
if command == "TPLINKOFF":
11161203
plugip = parameters
11171204
self._tplinksmartplug_logger.debug("Received TPLINKOFF command, attempting power off of %s." % plugip)
11181205
plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip)
11191206
self._tplinksmartplug_logger.debug(plug)
11201207
if plug and plug["gcodeEnabled"]:
1121-
t = threading.Timer(int(plug["gcodeOffDelay"]), self.gcode_turn_off, [plug])
1122-
t.daemon = True
1123-
t.start()
1208+
if plugip in self.active_timers["on"]:
1209+
self.active_timers["on"][plugip].cancel()
1210+
del self.active_timers["on"][plugip]
1211+
self.active_timers["off"][plugip] = threading.Timer(int(plug["gcodeOffDelay"]), self.gcode_turn_off, [plug])
1212+
self.active_timers["off"][plugip].daemon = True
1213+
self.active_timers["off"][plugip].start()
11241214
return None
11251215
if command == 'TPLINKIDLEON':
11261216
self.powerOffWhenIdle = True
@@ -1215,5 +1305,6 @@ def __plugin_load__():
12151305
"octoprint.comm.protocol.atcommand.sending": __plugin_implementation__.processAtCommand,
12161306
"octoprint.comm.protocol.temperatures.received": __plugin_implementation__.monitor_temperatures,
12171307
"octoprint.access.permissions": __plugin_implementation__.get_additional_permissions,
1218-
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information
1308+
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
1309+
"octoprint.printer.handle_connect": __plugin_implementation__.on_connect
12191310
}

octoprint_tplinksmartplug/static/js/tplinksmartplug.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ $(function() {
396396
trace_total.x.push(row[0]);
397397
trace_total.y.push(row[3]);
398398
trace_cost.x.push(row[0]);
399-
trace_cost.y.push(row[3]*self.settings.settings.plugins.tplinksmartplug.cost_rate());
399+
trace_cost.y.push((row[3]*self.settings.settings.plugins.tplinksmartplug.cost_rate()).toFixed(3));
400400
});
401401
var layout = {title:'TP-Link Smartplug Energy Data',
402402
grid: {rows: 2, columns: 1, pattern: 'independent'},

octoprint_tplinksmartplug/templates/tplinksmartplug_settings.jinja2

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@
183183
</div>
184184
</div>
185185
</div>
186+
<div class="row-fluid">
187+
<div class="control-group">
188+
<label class="control-label">{{ _('Heaters to Ignore for Idle') }}</label>
189+
<div class="controls" data-toggle="tooltip" data-bind="tooltip: {}" title="{{ _('Comma separated list of heaters to ignore for idle temperature. ie, for Prusa MK4 include X.') }}">
190+
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.tplinksmartplug.idleIgnoreHeaters, enable: settings.settings.plugins.tplinksmartplug.powerOffWhenIdle() && settings.settings.plugins.tplinksmartplug.powerOffWhenIdle()" disabled />
191+
</div>
192+
</div>
193+
</div>
186194
<div class="row-fluid">
187195
<div class="control-group">
188196
<label class="control-label">{{ _('GCode Commands to Ignore for Idle') }}</label>

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
plugin_name = "OctoPrint-TPLinkSmartplug"
1515

1616
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
17-
plugin_version = "1.0.3"
17+
plugin_version = "1.0.4"
1818

1919
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
2020
# module

0 commit comments

Comments
 (0)