Skip to content

Commit

Permalink
1.0.0rc1 (#119)
Browse files Browse the repository at this point in the history
* add additional logging during check status, to aid in debugging #101
* set _autostart_file None if print canceled or completed to prevent restarting file on connect, #118
* add baudrate to printer connection command
* fix invalid state in confirmation prompt, #116
* switch to using params in requests.get
* move check statuses to onAllBound to avoid potential lock up of UI load, #90
* fix date format for graphing data, #92
* add css error highlighting and disable update button on graph tab when inputs are invalid
* remove redundant css properties
* add cost to settings and graphing, #87
* add connect event monitoring, #72
  • Loading branch information
jneilliii authored Jan 24, 2021
1 parent dbfda1a commit 290fc5e
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 63 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Check out my other plugins [here](https://plugins.octoprint.org/by_author/#jneil
- [SimplyPrint](https://simplyprint.dk/)
- [Andrew Beeman](https://github.com/Kiendeleo)
- [Calanish](https://github.com/calanish)
- [Will O](https://github.com/4wrxb)

### Support My Efforts
I, jneilliii, programmed this plugin for fun and do my best effort to support those that have issues with it, please return the favor and leave me a tip or become a Patron if you find this plugin helpful and want me to continue future development.
Expand Down
135 changes: 96 additions & 39 deletions octoprint_tasmota/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class tasmotaPlugin(octoprint.plugin.SettingsPlugin,
octoprint.plugin.EventHandlerPlugin):

def __init__(self):
self.print_job_power = 0.0
self._logger = logging.getLogger("octoprint.plugins.tasmota")
self._tasmota_logger = logging.getLogger("octoprint.plugins.tasmota.debug")
self.thermal_runaway_triggered = False
Expand All @@ -92,6 +93,8 @@ def __init__(self):
self.powerOffWhenIdle = False
self._idleTimer = None
self._autostart_file = None
self.print_job_started = False
self._storage_interface = None

##~~ StartupPlugin mixin

Expand Down Expand Up @@ -160,13 +163,15 @@ def get_settings_defaults(self):
thermal_runaway_max_extruder=300,
event_on_error_monitoring=False,
event_on_disconnect_monitoring=False,
event_on_connecting_monitoring=False,
arrSmartplugs=[],
abortTimeout=30,
powerOffWhenIdle=False,
idleTimeout=30,
idleIgnoreCommands='M105',
idleTimeoutWaitTemp=50,
event_on_upload_monitoring=False
event_on_upload_monitoring=False,
cost_rate=0
)

def on_settings_save(self, data):
Expand Down Expand Up @@ -219,7 +224,7 @@ def on_settings_save(self, data):
self.poll_status.start()

def get_settings_version(self):
return 9
return 10

def on_settings_migrate(self, target, current=None):
if current is None or current < 6:
Expand Down Expand Up @@ -248,6 +253,13 @@ def on_settings_migrate(self, target, current=None):
plug["event_on_upload"] = False
arrSmartplugs_new.append(plug)
self._settings.set(["arrSmartplugs"], arrSmartplugs_new)
if current < 10:
# Add new fields
arrSmartplugs_new = []
for plug in self._settings.get(['arrSmartplugs']):
plug["event_on_connecting"] = False
arrSmartplugs_new.append(plug)
self._settings.set(["arrSmartplugs"], arrSmartplugs_new)

##~~ AssetPlugin mixin

Expand Down Expand Up @@ -290,25 +302,36 @@ def on_event(self, event, payload):
if plug["event_on_error"] == True:
self._tasmota_logger.debug("powering off %s:%s due to %s event." % (plug["ip"], plug["idx"], event))
self.turn_off(plug["ip"], plug["idx"])

if event == Events.CONNECTING and self._settings.get_boolean(["event_on_connecting_monitoring"]) and self._printer.is_closed_or_error:
self._tasmota_logger.debug("powering on due to %s event." % event)
for plug in self._settings.get(['arrSmartplugs']):
if plug["event_on_connecting"] == True:
self._tasmota_logger.debug("powering on %s:%s due to %s event." % (plug["ip"], plug["idx"], event))
self.turn_on(plug["ip"], plug["idx"])

# Disconnected Event
if event == Events.DISCONNECTED and self._settings.get_boolean(["event_on_disconnect_monitoring"]):
self._tasmota_logger.debug("powering off due to %s event." % event)
for plug in self._settings.get(['arrSmartplugs']):
if plug["event_on_disconnect"] == True:
self._tasmota_logger.debug("powering off %s:%s due to %s event." % (plug["ip"], plug["idx"], event))
self.turn_off(plug["ip"], plug["idx"])

# Client Opened Event
if event == Events.CLIENT_OPENED:
self._plugin_manager.send_plugin_message(self._identifier,
dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout",
timeout_value=self._timeout_value))
return

# Printer Connected Event
if event == Events.CONNECTED:
if self._autostart_file:
self._tasmota_logger.debug("printer connected starting print of %s" % self._autostart_file)
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
Expand All @@ -329,17 +352,54 @@ def on_event(self, event, payload):
self._autostart_file = payload.get("path")

# Print Started Event
if event == Events.PRINT_STARTED and self.powerOffWhenIdle == True:
if event == Events.PRINT_STARTED and self._settings.getFloat(["cost_rate"]) > 0:
self.print_job_started = True
self._tasmota_logger.debug(payload.get("path", None))
for plug in self._settings.get(["arrSmartplugs"]):
status = self.check_status(plug["ip"], plug["idx"])
self.print_job_power -= float(self.deep_get(status, ["energy_data", "Total"], default=0))
self._tasmota_logger.debug(self.print_job_power)

if event == Events.PRINT_STARTED and self.powerOffWhenIdle:
if self._abort_timer is not None:
self._abort_timer.cancel()
self._abort_timer = None
self._tasmota_logger.debug("Power off aborted because starting new print.")
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))

# Print Cancelled/Done Events
if event == Events.PRINT_DONE and self.print_job_started:
self._tasmota_logger.debug(payload)

for plug in self._settings.get(["arrSmartplugs"]):
status = self.check_status(plug["ip"], plug["idx"])
self.print_job_power += float(self.deep_get(status, ["energy_data", "Total"], default=0))
self._tasmota_logger.debug(self.print_job_power)

hours = (payload.get("time", 0) / 60) / 60
self._tasmota_logger.debug("hours: %s" % hours)
power_used = self.print_job_power * hours
self._tasmota_logger.debug("power used: %s" % power_used)
power_cost = power_used * self._settings.getFloat(["cost_rate"])
self._tasmota_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._autostart_file = None
self.print_job_power = 0.0
self.print_job_started = False

if event == Events.PRINT_CANCELLED:
self._autostart_file = None
self.print_job_power = 0.0
self.print_job_started = False

# Timelapse events
if self.powerOffWhenIdle == True and event == Events.MOVIE_RENDERING:
self._tasmota_logger.debug("Timelapse generation started: %s" % payload.get("movie_basename", ""))
self._timelapse_active = True
Expand All @@ -355,16 +415,12 @@ def turn_on(self, plugip, plugidx):
plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip, "idx", plugidx)
try:
if plug["use_backlog"] and int(plug["backlog_on_delay"]) > 0:
webresponse = requests.get(
"http://" + plug["ip"] + "/cm?user=" + plug["username"] + "&password=" + requests.utils.quote(
plug["password"]) + "&cmnd=backlog%20delay%20" + str(
int(plug["backlog_on_delay"]) * 10) + "%3BPower" + str(plug["idx"]) + "%20on%3B")
backlog_command = "backlog delay {};Power{} on;".format(int(plug["backlog_on_delay"]) * 10, plug["idx"])
requests.get("http://{}/cm".format(plugip), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": backlog_command}, timeout=3)
response = dict()
response["POWER%s" % plug["idx"]] = "ON"
else:
webresponse = requests.get(
"http://" + plug["ip"] + "/cm?user=" + plug["username"] + "&password=" + requests.utils.quote(
plug["password"]) + "&cmnd=Power" + str(plug["idx"]) + "%20on")
webresponse = requests.get("http://{}/cm".format(plugip), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": "Power{} on".format(plug["idx"])}, timeout=3)
response = webresponse.json()
chk = response["POWER%s" % plug["idx"]]
except:
Expand All @@ -378,7 +434,7 @@ def turn_on(self, plugip, plugidx):
if plug["autoConnect"] and self._printer.is_closed_or_error():
self._logger.info(self._settings.global_get(['serial']))
c = threading.Timer(int(plug["autoConnectDelay"]), self._printer.connect,
kwargs=dict(port=self._settings.global_get(['serial', 'port'])))
kwargs=dict(port=self._settings.global_get(['serial', 'port']), baudrate=self._settings.global_get(['serial', 'baudrate'])))
c.daemon = True
c.start()
if plug["sysCmdOn"]:
Expand All @@ -399,12 +455,8 @@ def turn_off(self, plugip, plugidx):
if plug["use_backlog"] and int(plug["backlog_off_delay"]) > 0:
self._tasmota_logger.debug(
"Using backlog commands with a delay value of %s" % str(int(plug["backlog_off_delay"]) * 10))
backlog_url = "http://" + plug["ip"] + "/cm?user=" + plug[
"username"] + "&password=" + requests.utils.quote(
plug["password"]) + "&cmnd=backlog%20delay%20" + str(
int(plug["backlog_off_delay"]) * 10) + "%3BPower" + str(plug["idx"]) + "%20off%3B"
self._tasmota_logger.debug("Sending command %s" % backlog_url)
webresponse = requests.get(backlog_url)
backlog_command = "backlog delay {};Power{} off;".format(int(plug["backlog_on_delay"]) * 10, plug["idx"])
requests.get("http://{}/cm".format(plugip), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": backlog_command}, timeout=3)
response = dict()
response["POWER%s" % plug["idx"]] = "OFF"
if plug["sysCmdOff"]:
Expand All @@ -419,9 +471,7 @@ def turn_off(self, plugip, plugidx):
time.sleep(int(plug["autoDisconnectDelay"]))
if not plug["use_backlog"]:
self._tasmota_logger.debug("Not using backlog commands")
webresponse = requests.get(
"http://" + plug["ip"] + "/cm?user=" + plug["username"] + "&password=" + requests.utils.quote(
plug["password"]) + "&cmnd=Power" + str(plug["idx"]) + "%20off")
webresponse = requests.get("http://{}/cm".format(plugip), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": "Power{} off".format(plug["idx"])}, timeout=3)
response = webresponse.json()
chk = response["POWER%s" % plug["idx"]]
if chk.upper() == "OFF":
Expand All @@ -442,9 +492,9 @@ def check_status(self, plugip, plugidx):
try:
plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip, "idx", plugidx)
self._tasmota_logger.debug(plug)
webresponse = requests.get(
"http://" + plugip + "/cm?user=" + plug["username"] + "&password=" + requests.utils.quote(
plug["password"]) + "&cmnd=Status%200")
webresponse = requests.get("http://{}/cm".format(plugip), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": "Status 0"}, timeout=3)
self._tasmota_logger.debug("check status code: {}".format(webresponse.status_code))
self._tasmota_logger.debug("check status text: {}".format(webresponse.text))
response = webresponse.json()
self._tasmota_logger.debug("%s index %s response: %s" % (plugip, plugidx, response))
# chk = response["POWER%s" % plugidx]
Expand Down Expand Up @@ -486,6 +536,8 @@ def check_status(self, plugip, plugidx):
self._tasmota_logger.error('Invalid ip or unknown error connecting to %s.' % plugip, exc_info=True)
response = "unknown error with %s." % plugip
chk = "UNKNOWN"
energy_data = None
sensor_data = None

self._tasmota_logger.debug("%s index %s is %s" % (plugip, plugidx, chk))
if chk.upper() == "ON":
Expand All @@ -500,15 +552,13 @@ def check_status(self, plugip, plugidx):
return response

def checkSetOption26(self, plugip, username, password):
webresponse = requests.get("http://" + plugip + "/cm?user=" + username + "&password=" + requests.utils.quote(
password) + "&cmnd=SetOption26")
webresponse = requests.get("http://{}/cm".format(plugip), params={"user": username, "password": requests.utils.quote(password), "cmnd": "SetOption26"}, timeout=3)
response = webresponse.json()
self._tasmota_logger.debug(response)
return response

def setSetOption26(self, plugip, username, password):
webresponse = requests.get("http://" + plugip + "/cm?user=" + username + "&password=" + requests.utils.quote(
password) + "&cmnd=SetOption26%20ON")
webresponse = requests.get("http://{}/cm".format(plugip), params={"user": username, "password": requests.utils.quote(password), "cmnd": "SetOption26 ON"}, timeout=3)
response = webresponse.json()
self._tasmota_logger.debug(response)
return response
Expand Down Expand Up @@ -557,9 +607,7 @@ def on_api_command(self, command, data):
self._timeout_value = None
for plug in self._settings.get(["arrSmartplugs"]):
if plug["use_backlog"] and int(plug["backlog_off_delay"]) > 0:
backlog_url = "http://" + plug["ip"] + "/cm?user=" + plug[
"username"] + "&password=" + requests.utils.quote(plug["password"]) + "&cmnd=backlog"
webresponse = requests.get(backlog_url)
webresponse = requests.get("http://{}/cm".format(plug["ip"]), params={"user": plug["username"], "password": requests.utils.quote(plug["password"]), "cmnd": "backlog"}, timeout=3)
self._tasmota_logger.debug("Cleared countdown rules for %s" % plug["ip"])
self._tasmota_logger.debug(webresponse)
self._tasmota_logger.debug("Power off aborted.")
Expand Down Expand Up @@ -838,6 +886,21 @@ def plug_search(self, list, key1, value1, key2, value2):
if item[key1] == value1 and item[key2] == value2:
return item

def deep_get(self, d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
deep_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
assert type(keys) is list
if d is None:
return default
if not keys:
return d
return self.deep_get(d.get(keys[0]), keys[1:], default)

##~~ Access Permissions Hook

def get_additional_permissions(self, *args, **kwargs):
Expand All @@ -853,9 +916,6 @@ def get_additional_permissions(self, *args, **kwargs):
##~~ Softwareupdate hook

def get_update_information(self):
# Define the configuration for your plugin to use with the Software Update
# Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update
# for details.
return dict(
tasmota=dict(
displayName="Tasmota",
Expand Down Expand Up @@ -883,9 +943,6 @@ def get_update_information(self):
)


# If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py
# ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that
# can be overwritten via __plugin_xyz__ control properties. See the documentation for that.
__plugin_name__ = "Tasmota"
__plugin_pythoncompat__ = ">=2.7,<4"

Expand Down
10 changes: 7 additions & 3 deletions octoprint_tasmota/static/css/tasmota.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#navbar_plugin_tasmota > a > i.unknown::after {
content: " ?";
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
} }
}

/* TouchUI - Show Label */
#touch #navbar_plugin_tasmota > a > div.tasmota_label {
Expand All @@ -27,8 +27,6 @@
}

#settings_plugin_tasmota > table > thead > tr > th, #settings_plugin_tasmota > table > tbody > tr > td {
border-bottom-width: 1px;
border-top-width: 1px;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
Expand Down Expand Up @@ -68,3 +66,9 @@
height: 25px;
font-size: 12px;
}

input[type="datetime-local"].alert-error {
color: #b94a48;
background-color: #f2dede;
border-color: #b94a48;
}
Loading

0 comments on commit 290fc5e

Please sign in to comment.