@@ -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 }
0 commit comments