diff --git a/packages/modules/smarthome/shelly/off.py b/packages/modules/smarthome/shelly/off.py old mode 100644 new mode 100755 index e9ce1b4f46..bce4a2d76b --- a/packages/modules/smarthome/shelly/off.py +++ b/packages/modules/smarthome/shelly/off.py @@ -1,11 +1,12 @@ #!/usr/bin/python3 import sys -import time -import urllib.request +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth import os import json -named_tuple = time.localtime() # getstruct_time -time_string = time.strftime("%m/%d/%Y, %H:%M:%S shelly off.py", named_tuple) +import logging + +log = logging.getLogger(__name__) devicenumber = str(sys.argv[1]) ipadr = str(sys.argv[2]) uberschuss = int(sys.argv[3]) @@ -18,40 +19,44 @@ shaut = int(sys.argv[5]) user = str(sys.argv[6]) pw = str(sys.argv[7]) + fbase = '/var/www/html/openWB/ramdisk/smarthome_device_ret.' -fnameg = fbase + str(ipadr) + '_shelly_infogv1' -if os.path.isfile(fnameg): - with open(fnameg, 'r') as f: - jsonin = json.loads(f.read()) - gen = str(jsonin['gen']) - model = str(jsonin['model']) -else: - gen = "1" -if (gen == "1"): - if (chan == 0): - url = "http://" + str(ipadr) + "/relay/0?turn=off" - # urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=off", - # timeout=3) +fnameg = fbase + str(ipadr) + '_shelly_infogv2' +log_pfx = "Device " + str(devicenumber) + " IP " + ipadr + ": " + +try: + if os.path.isfile(fnameg): + with open(fnameg, 'r') as f: + jsonin = json.loads(f.read()) + gen = str(jsonin['gen']) + model = str(jsonin['model']) + else: + gen = "1" + + if gen == "1": + if chan == 0: + url = f"http://{ipadr}/relay/0?turn=off" + else: + chan = chan - 1 + url = f"http://{ipadr}/relay/{chan}?turn=off" + else: + if chan > 0: + chan = chan - 1 + if "SPEM-003CE" in model: + chan = 100 + url = f"http://{ipadr}/rpc/Switch.Set?id={chan}&on=false" + + if shaut == 1: + if gen == "1": + # HTTP Basic Auth für Gen 1 + auth = HTTPBasicAuth(user, pw) + else: + # HTTP Digest Auth für Gen 2 oder SPEM-003CE + auth = HTTPDigestAuth("admin", pw) + response = requests.get(url, auth=auth, timeout=3) else: - chan = chan - 1 - url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=off" - # urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + - # "?turn=off", timeout=3) -else: - if (chan > 0): - chan = chan - 1 - # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater - # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann - if ("SPEM-003CE" in model): - chan = 100 - # gen 2 will das als off cmd IPderPro3EM/rpc/Switch.Set?id=100&on=false - url = "http://" + str(ipadr) + "/rpc/Switch.Set?id=" + str(chan) + "&on=false" -if (shaut == 1): - # print("Shelly off" + str(shaut) + user + pw) - passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() - passman.add_password(None, url, user, pw) - authhandler = urllib.request.HTTPBasicAuthHandler(passman) - opener = urllib.request.build_opener(authhandler) - urllib.request.install_opener(opener) -with urllib.request.urlopen(url) as response: - response.read().decode("utf-8") + response = requests.get(url, timeout=3) + + response.raise_for_status() # Fehler, wenn Statuscode nicht 2xx ist +except Exception as e: + log.error(f"{log_pfx}Error on changing switch: {str(e)}") diff --git a/packages/modules/smarthome/shelly/on.py b/packages/modules/smarthome/shelly/on.py index 85320a6444..11946788a5 100644 --- a/packages/modules/smarthome/shelly/on.py +++ b/packages/modules/smarthome/shelly/on.py @@ -1,8 +1,12 @@ #!/usr/bin/python3 import sys -import urllib.request +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth import os import json +import logging + +log = logging.getLogger(__name__) devicenumber = str(sys.argv[1]) ipadr = str(sys.argv[2]) uberschuss = int(sys.argv[3]) @@ -15,40 +19,44 @@ shaut = int(sys.argv[5]) user = str(sys.argv[6]) pw = str(sys.argv[7]) + fbase = '/var/www/html/openWB/ramdisk/smarthome_device_ret.' -fnameg = fbase + str(ipadr) + '_shelly_infogv1' -if os.path.isfile(fnameg): - with open(fnameg, 'r') as f: - jsonin = json.loads(f.read()) - gen = str(jsonin['gen']) - model = str(jsonin['model']) -else: - gen = "1" -if (gen == "1"): - if (chan == 0): - url = "http://" + str(ipadr) + "/relay/0?turn=on" - # urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=on", - # timeout=3) +fnameg = fbase + str(ipadr) + '_shelly_infogv2' +log_pfx = "Device " + str(devicenumber) + " IP " + ipadr + ": " + +try: + if os.path.isfile(fnameg): + with open(fnameg, 'r') as f: + jsonin = json.loads(f.read()) + gen = str(jsonin['gen']) + model = str(jsonin['model']) + else: + gen = "1" + + if gen == "1": + if chan == 0: + url = f"http://{ipadr}/relay/0?turn=on" + else: + chan = chan - 1 + url = f"http://{ipadr}/relay/{chan}?turn=on" + else: + if chan > 0: + chan = chan - 1 + if "SPEM-003CE" in model: + chan = 100 + url = f"http://{ipadr}/rpc/Switch.Set?id={chan}&on=true" + + if shaut == 1: + if gen == "1": + # HTTP Basic Auth für Gen 1 + auth = HTTPBasicAuth(user, pw) + else: + # HTTP Digest Auth für Gen 2 oder SPEM-003CE + auth = HTTPDigestAuth("admin", pw) + response = requests.get(url, auth=auth, timeout=3) else: - chan = chan - 1 - url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=on" - # urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + - # "?turn=on", timeout=3) -else: - if (chan > 0): - chan = chan - 1 - # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater - # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann - if ("SPEM-003CE" in model): - chan = 100 - # gen 2 will das als on cmd /rpc/Switch.Set?id=100&on=true - url = "http://" + str(ipadr) + "/rpc/Switch.Set?id=" + str(chan) + "&on=true" -if (shaut == 1): - # print("Shelly on" + str(shaut) + user + pw) - passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() - passman.add_password(None, url, user, pw) - authhandler = urllib.request.HTTPBasicAuthHandler(passman) - opener = urllib.request.build_opener(authhandler) - urllib.request.install_opener(opener) -with urllib.request.urlopen(url) as response: - response.read().decode("utf-8") + response = requests.get(url, timeout=3) + + response.raise_for_status() # Fehler, wenn Statuscode nicht 2xx ist +except Exception as e: + log.error(f"{log_pfx}Error on changing switch: {str(e)}") diff --git a/packages/modules/smarthome/shelly/watt.py b/packages/modules/smarthome/shelly/watt.py index 166e57ff0e..aa80846eab 100644 --- a/packages/modules/smarthome/shelly/watt.py +++ b/packages/modules/smarthome/shelly/watt.py @@ -3,7 +3,8 @@ import os import time import json -import urllib.request +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth from typing import Any from smarthome.smartret import writeret import logging @@ -11,23 +12,11 @@ log = logging.getLogger(__name__) -def totalPowerFromShellyJson(answer: Any, workchan: int) -> int: - if (workchan == 0): - if 'meters' in answer: - meters = answer['meters'] # shelly - else: - meters = answer['emeters'] # shellyEM & shelly3EM - total = 0 - # shellyEM has one meter, shelly3EM has three meters: - for meter in meters: - total = total + meter['power'] - return int(total) - workchan = workchan - 1 - try: - total = int(answer['meters'][workchan]['power']) # Abfrage shelly - except Exception: - total = int(answer['emeters'][workchan]['power']) # Abfrage shellyEM - return int(total) +def totalPowerFromShellyJson(answer: Any, workchan: int, component: str) -> int: + if workchan > 0: + return int(answer[component][workchan - 1]['power']) + power_sum = sum(emeter['power'] for emeter in answer[component] if isinstance(emeter, dict) and 'power' in emeter) + return int(power_sum) named_tuple = time.localtime() # getstruct_time @@ -46,134 +35,199 @@ def totalPowerFromShellyJson(answer: Any, workchan: int) -> int: user = str(sys.argv[6]) pw = str(sys.argv[7]) # Setze Default-Werte, andernfalls wird der letzte Wert ewig fortgeschrieben. -# Insbesondere wichtig für aktuelle Leistung -# Zähler wird beim Neustart auf 0 gesetzt, darf daher nicht übergeben werden. powerc = 0 -temp0 = '0.0' -temp1 = '0.0' -temp2 = '0.0' +temp = ['0.0', '0.0', '0.0'] aktpower = 0 relais = 0 gen = '1' model = '???' +profile = '???' +components = {} # lesen endpoint, gen bestimmem. gen 1 hat unter Umstaenden keinen Eintrag +write_info = False +delete_info = False +device_info = {} +power_field = ['total_act_power', 'a_act_power', 'b_act_power', 'c_act_power'] + fbase = '/var/www/html/openWB/ramdisk/smarthome_device_ret.' -fname = fbase + str(ipadr) + '_shelly_info' -fnameg = fbase + str(ipadr) + '_shelly_infogv1' -if os.path.isfile(fnameg): - with open(fnameg, 'r') as f: - jsonin = json.loads(f.read()) - gen = str(jsonin['gen']) - model = str(jsonin['model']) -else: - aread = urllib.request.urlopen("http://" + str(ipadr) + "/shelly", - timeout=3).read().decode("utf-8") - agen = json.loads(str(aread)) - with open(fname, 'w') as f: - json.dump(agen, f) - if 'gen' in agen: - gen = str(int(agen['gen'])) - if 'model' in agen: - model = str(agen['model']) - elif 'type' in agen: - model = str(agen['type']) - jsontype = {"gen": str(gen), "model": str(model)} - with open(fnameg, 'w') as f: - f.write(json.dumps(jsontype)) -# Versuche Daten von Shelly abzurufen. +# Response of "/shelly"-url: +fname_shellyinfo = fbase + ipadr + '_shelly_info' +# Response of "/rpc/Shelly.ListProfiles": +fname_profiles = fbase + ipadr + '_shelly_infoc' +# Internal cache for gathered device info: +fname_devcache = fbase + ipadr + '_shelly_infogv2' +# Response for "/status" or "/rpc/Shelly.GetStatus": +fname_statusrsp = fbase + ipadr + '_shelly_res' + +log_pfx = "Device " + str(devicenumber) + " IP " + ipadr + ": " + +# Do we have a cache of the device features? try: - # print("Shelly " + str(shaut) + user + pw) - if (gen == "1"): - url = "http://" + str(ipadr) + "/status" - if (shaut == 1): - passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() - passman.add_password(None, url, user, pw) - authhandler = urllib.request.HTTPBasicAuthHandler(passman) - opener = urllib.request.build_opener(authhandler) - urllib.request.install_opener(opener) - with urllib.request.urlopen(url, timeout=3) as response: - aread = response.read().decode("utf-8") - answer = json.loads(str(aread)) + if os.path.isfile(fname_devcache): + try: + with open(fname_devcache, 'r') as f: + device_info = json.loads(f.read()) + gen = str(device_info['gen']) + model = str(device_info['model']) + profile = str(device_info['profile']) + components = device_info['components'] # Kein str(), da dict + except Exception: + # Delete this cache file - it seems broken + delete_info = True + pass else: - aread = urllib.request.urlopen("http://"+str(ipadr) + - "/rpc/Shelly.GetStatus", - timeout=3).read().decode("utf-8") - answer = json.loads(str(aread)) - with open('/var/www/html/openWB/ramdisk/smarthome_device_ret.' + - str(ipadr) + '_shelly', 'w') as f: - f.write(str(answer)) -except Exception: - log.debug("failed to connect to device on " + - ipadr + ", setting all values to 0") -# answer.update(a_dictionary) -# Versuche Werte aus der Antwort zu extrahieren. + # New device analysis: Start with /shelly URL + url = f'http://{ipadr}/shelly' + response = requests.get(url, timeout=3) + response.raise_for_status() + aread = response.text + log.warning(log_pfx + "/shelly response " + aread) + device_info = json.loads(aread) + agen = json.loads(aread) + with open(fname_shellyinfo, 'w') as f: + json.dump(agen, f) + if 'gen' in agen: + gen = str(int(agen['gen'])) + device_info['gen'] = gen + if 'model' in agen: + model = str(agen['model']) + elif 'type' in agen: + model = str(agen['type']) + device_info['model'] = model + if 'profile' in agen: + # Shelly with multiple profiles (z.B. 3EM, 2PM) + profile = str(agen['profile']) + device_info['profile'] = profile + if gen != "1": + url = f'http://{ipadr}/rpc/Shelly.ListProfiles' + if shaut == 1: + response = requests.get(url, timeout=3, + auth=HTTPDigestAuth("admin", pw)) + else: + response = requests.get(url, timeout=3) + response.raise_for_status() + aread = response.text + log.warning(log_pfx + " /rpc/Shelly.ListProfiles response " + aread) + agen = json.loads(aread) + with open(fname_profiles, 'w') as f: + json.dump(agen, f) + for item in agen['profiles'][profile]['components']: + components[item['type']] = item['count'] + device_info['components'] = components + # We have a new device analysis, store it: + write_info = True +except Exception as e: + log.error(log_pfx + 'Error on device analysis ' + str(e)) + pass + +# Pre-Analysis done / loaded, now get the data: try: - if (gen == "1"): - aktpower = totalPowerFromShellyJson(answer, chan) + # For future use: Caching of response: + # Check the last response is < 5 seconds old + if (os.path.exists(fname_statusrsp) and + os.path.getmtime(fname_statusrsp) + 4 > time.time()): + # We will use a cached Status-page + with open(fname_statusrsp, 'r') as f: + answer = json.loads(f.read()) else: - if (chan > 0): - workchan = chan - 1 - else: - workchan = chan - sw = 'switch:' + str(workchan) - if ("SPEM-003CE" in model): - if (workchan == 1): - aktpower = int(answer['em:0']['a_act_power']) - elif (workchan == 2): - aktpower = int(answer['em:0']['b_act_power']) - elif (workchan == 3): - aktpower = int(answer['em:0']['c_act_power']) + # No (valid) cache: We have to fetch the data: + url = f'http://{ipadr}/{"status" if gen == "1" else "rpc/Shelly.GetStatus"}' + if shaut == 1: + if gen == "1": + response = requests.get(url, timeout=3, + auth=HTTPBasicAuth(user, pw)) else: - aktpower = int(answer['em:0']['total_act_power']) - elif ("PM-001PCEU16" in model): - # "SNPM-001PCEU16" (gen 2) und "S3PM-001PCEU16" (gen 3) - aktpower = int(answer['pm1:0']['apower']) + response = requests.get(url, timeout=3, + auth=HTTPDigestAuth("admin", pw)) else: - aktpower = int(answer[sw]['apower']) -except Exception: + response = requests.get(url, timeout=3) + + response.raise_for_status() + aread = response.text + answer = json.loads(aread) + with open(fname_statusrsp, 'w') as f: + json.dump(answer, f) + + if not components: + # Late device analysis, based on the first response: + prefixes = ['switch:', 'em:', 'emdata:', 'pm1:', 'em1:', 'em1data:', 'temperature:'] + components = { + prefix[:-1]: count + for prefix in prefixes + if (count := sum(key.startswith(prefix) for key in answer.keys())) > 0 + } + # Gen1 - Komponenten: + prefixes = ['relays', 'emeters', 'meters', 'ext_temperature'] + for prefix in prefixes: + if prefix in answer: + components[prefix] = len(answer.get(prefix)) + device_info['components'] = components + write_info = True + +except Exception as e: + log.error(log_pfx + 'Error on data fetch ' + str(e)) pass +# We have the response: Start parsing: +workchan = chan - 1 if chan > 0 else chan + try: - if (chan > 0): - workchan = chan - 1 - else: - workchan = chan - if (gen == "1"): - relais = int(answer['relays'][workchan]['ison']) - else: - # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater - # Leistunsmessung erfasst werden, da die Leistung auf drei verschieden Kanäle angeliefert werden kann - if ("SPEM-003CE" in model): - workchan = 100 - sw = 'switch:' + str(workchan) + if 'switch' in components: + # Beim Shelly Pro 3EM mit AddOn ist die Switch-ID 100, sonst ab 0: + sw = 'switch:' + str(workchan) if 'SPEM-003CE' not in model else 'switch:100' + if sw not in answer: + # Typisch, wenn der Messwert auf einem höheren Kanal geholt werden soll + sw = 'switch:0' relais = int(answer[sw]['output']) -except Exception: - pass + aktpower = int(answer[sw]['apower']) if 'apower' in answer[sw] else 0 + if 'relays' in components: + relais = int(answer['relays'][workchan if (workchan < len(answer['relays'])) else 0]['ison']) + if 'meters' in components: + aktpower = totalPowerFromShellyJson(answer, chan, 'meters') + if 'pm1' in components: + if chan == 0: + aktpower = int(sum(answer['pm1:' + str(em)]['apower'] for em in range(components['pm1']))) + else: + sw = 'pm1:' + str(workchan) + aktpower = int(answer[sw]['apower']) + if 'em1' in components: + if chan == 0: + aktpower = int(sum(answer['em1:' + str(em)]['act_power'] for em in range(components['em1']))) + else: + sw = 'em1:' + str(workchan) + aktpower = int(answer[sw]['act_power']) + if 'em' in components: + aktpower = int(answer['em:0'][power_field[chan]]) + if 'emeters' in components: + aktpower = totalPowerFromShellyJson(answer, chan, 'emeters') + if 'ext_temperature' in components: + for i in range(len(answer['ext_temperature'])): + temp[i] = str(answer['ext_temperature'][str(i)]['tC']) + if 'temperature' in components: + for i in range(components['temperature']): + field = 'temperature:' + str(i + 100) + if field in answer: + temp[i] = str(answer[field]['tC']) +except Exception as e: + exc_type, exc_value, exc_traceback = sys.exc_info() + filename = exc_traceback.tb_frame.f_code.co_filename + line_number = exc_traceback.tb_lineno + function_name = exc_traceback.tb_frame.f_code.co_name + log.error( + f"{log_pfx}Error on data parsing: {str(e)} (File: {filename}, Line: {line_number}, Function: {function_name})") -try: - if gen == "1": - temp0 = str(answer['ext_temperature']['0']['tC']) - else: - temp0 = str(answer['temperature:100']['tC']) -except Exception: - pass +if write_info: + with open(fname_devcache, 'w') as f: + f.write(json.dumps(device_info)) + log.warning(log_pfx + " cached info " + json.dumps(device_info)) -try: - if gen == "1": - temp1 = str(answer['ext_temperature']['1']['tC']) - else: - temp1 = str(answer['temperature:101']['tC']) -except Exception: - pass +if delete_info: + try: + os.remove(fname_shellyinfo) + except Exception: + pass -try: - if gen == "1": - temp2 = str(answer['ext_temperature']['2']['tC']) - else: - temp2 = str(answer['temperature:102']['tC']) -except Exception: - pass answer = '{"power":' + str(aktpower) + ',"powerc":' + str(powerc) -answer += ',"on":' + str(relais) + ',"temp0":' + str(temp0) -answer += ',"temp1":' + str(temp1) + ',"temp2":' + str(temp2) + '}' +answer += ',"on":' + str(relais) + ',"temp0":' + str(temp[0]) +answer += ',"temp1":' + str(temp[1]) + ',"temp2":' + str(temp[2]) + '}' writeret(answer, devicenumber)