From 17693e06c224ebc3ee924f8fa72b9b51c1bc2366 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 25 Aug 2018 07:57:45 +0200 Subject: [PATCH 001/134] Improve version detection Also fix url --- envoy_reader/envoy_reader.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 5604aa2..0136f1e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,12 +4,12 @@ envoy_model = "S" def call_api(ip_address): - url = "http://envoy/production.json".format(ip_address) - response = requests.get(url, timeout=5) + url = "http://{}/production.json".format(ip_address) + response = requests.get(url, timeout=5, allow_redirects=False) if response.status_code == 301: global envoy_model envoy_model = "Original" - url = "http://envoy/api/v1/production" + url = "http://{}/api/v1/production".format(ip_address) response = requests.get(url, timeout=5) return response.json() @@ -147,12 +147,12 @@ def lifetime_consumption(ip_address): if __name__ == "__main__": - url = "" - print("production {}".format(production(url))) - print("consumption {}".format(consumption(url))) - print("daily_production {}".format(daily_production(url))) - print("daily_consumption {}".format(daily_consumption(url))) - print("seven_days_production {}".format(seven_days_production(url))) - print("seven_days_consumption {}".format(seven_days_consumption(url))) - print("lifetime_production {}".format(lifetime_production(url))) - print("lifetime_consumption {}".format(lifetime_consumption(url))) + host = "envoy" + print("production {}".format(production(host))) + print("consumption {}".format(consumption(host))) + print("daily_production {}".format(daily_production(host))) + print("daily_consumption {}".format(daily_consumption(host))) + print("seven_days_production {}".format(seven_days_production(host))) + print("seven_days_consumption {}".format(seven_days_consumption(host))) + print("lifetime_production {}".format(lifetime_production(host))) + print("lifetime_consumption {}".format(lifetime_consumption(host))) From cff6e6f3490249a871bb7b6dab781ecc04dc1511 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sat, 25 Aug 2018 15:55:21 -0500 Subject: [PATCH 002/134] Improve detection of envoy model --- envoy_reader/envoy_reader.py | 310 ++++++++++++++++++----------------- 1 file changed, 163 insertions(+), 147 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 0136f1e..b0d0a3a 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -2,157 +2,173 @@ import sys import json -envoy_model = "S" -def call_api(ip_address): - url = "http://{}/production.json".format(ip_address) - response = requests.get(url, timeout=5, allow_redirects=False) - if response.status_code == 301: - global envoy_model - envoy_model = "Original" - url = "http://{}/api/v1/production".format(ip_address) - response = requests.get(url, timeout=5) - return response.json() - - return response.json() +class EnvoyReader(): + def __init__(self, host, envoy_model): + self.host = host + self.envoy_model = envoy_model -def production(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - production = raw_json["production"][1]["wNow"] - else: - production = raw_json["wattsNow"] - return int(production) - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def consumption(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - consumption = raw_json["consumption"][0]["wNow"] - return int(consumption) - else: - return "Unavailable" - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def daily_production(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - daily_production = raw_json["production"][1]["whToday"] - else: - daily_production = raw_json["wattHoursToday"] - return int(daily_production) - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def daily_consumption(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - daily_consumption = raw_json["consumption"][0]["whToday"] - return int(daily_consumption) + def call_api(self): + if self.envoy_model == "original": + url = "http://{}/api/v1/production".format(self.host) + response = requests.get(url, timeout=5) else: - return "Unavailable" - - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def seven_days_production(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - seven_days_production = raw_json["production"][1][ - "whLastSevenDays" - ] - else: - seven_days_production = raw_json["wattHoursSevenDays"] - return int(seven_days_production) - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def seven_days_consumption(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - seven_days_consumption = raw_json["consumption"][0][ - "whLastSevenDays" - ] - return int(seven_days_consumption) - else: - return "Unavailable" - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def lifetime_production(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - lifetime_production = raw_json["production"][1][ - "whLifetime" - ] - else: - lifetime_production = raw_json["wattHoursLifetime"] - return int(lifetime_production) - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") - - -def lifetime_consumption(ip_address): - try: - raw_json = call_api(ip_address) - if envoy_model == "S": - lifetime_consumption = raw_json["consumption"][0][ - "whLifetime" - ] - return int(lifetime_consumption) - else: - return "Unavailable" + url = "http://{}/production.json".format(self.host) + response = requests.get(url, timeout=5, allow_redirects=False) + if response.status_code == 200: + self.envoy_model = "s" + elif response.status_code == 301: + self.envoy_model = "original" + url = "http://{}/api/v1/production".format(self.host) + response = requests.get(url, timeout=5) + return response.json() - except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address") + def production(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model == "s": + production = raw_json["production"][1]["wNow"] + else: + production = raw_json["wattsNow"] + return int(production) + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def consumption(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + consumption = raw_json["consumption"][0]["wNow"] + return int(consumption) + else: + return "Unavailable" + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def daily_production(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + daily_production = raw_json["production"][1]["whToday"] + else: + daily_production = raw_json["wattHoursToday"] + return int(daily_production) + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def daily_consumption(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + daily_consumption = raw_json["consumption"][0]["whToday"] + return int(daily_consumption) + else: + return "Unavailable" + + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def seven_days_production(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + seven_days_production = raw_json["production"][1][ + "whLastSevenDays" + ] + else: + seven_days_production = raw_json["wattHoursSevenDays"] + return int(seven_days_production) + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def seven_days_consumption(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + seven_days_consumption = raw_json["consumption"][0][ + "whLastSevenDays" + ] + return int(seven_days_consumption) + else: + return "Unavailable" + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def lifetime_production(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + lifetime_production = raw_json["production"][1][ + "whLifetime" + ] + else: + lifetime_production = raw_json["wattHoursLifetime"] + return int(lifetime_production) + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") + + def lifetime_consumption(self): + try: + raw_json = EnvoyReader.call_api(self) + if self.envoy_model.lower() == "s": + lifetime_consumption = raw_json["consumption"][0][ + "whLifetime" + ] + return int(lifetime_consumption) + else: + return "Unavailable" + + except requests.exceptions.ConnectionError: + return "Unable to connect to Envoy. Check the IP address" + except (json.decoder.JSONDecodeError, KeyError): + return ("Got a response, but it doesn't look right. " + + "Check the IP address") if __name__ == "__main__": - host = "envoy" - print("production {}".format(production(host))) - print("consumption {}".format(consumption(host))) - print("daily_production {}".format(daily_production(host))) - print("daily_consumption {}".format(daily_consumption(host))) - print("seven_days_production {}".format(seven_days_production(host))) - print("seven_days_consumption {}".format(seven_days_consumption(host))) - print("lifetime_production {}".format(lifetime_production(host))) - print("lifetime_consumption {}".format(lifetime_consumption(host))) + host = input("Enter the Envoy IP address, " + + "or press enter to search for it.") + if host == "": + host = "envoy" + envoy_model = input("Enter the model of the Envoy " + + "('Original' or 'S'), or press enter.") + if envoy_model == "": + envoy_model = "unknown" + + print("production {}".format(EnvoyReader(host, envoy_model) + .production())) + print("consumption {}".format(EnvoyReader(host, envoy_model) + .consumption())) + print("daily_production {}".format(EnvoyReader(host, envoy_model) + .daily_production())) + print("daily_consumption {}".format(EnvoyReader(host, envoy_model) + .daily_consumption())) + print("seven_days_production {}".format(EnvoyReader(host, envoy_model) + .seven_days_production())) + print("seven_days_consumption {}".format(EnvoyReader(host, envoy_model) + .seven_days_consumption())) + print("lifetime_production {}".format(EnvoyReader(host, envoy_model) + .lifetime_production())) + print("lifetime_consumption {}".format(EnvoyReader(host, envoy_model) + .lifetime_consumption())) From 58fd8f1297c90ea7280c47ae26924813818a5ad1 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 13:16:01 -0500 Subject: [PATCH 003/134] Bump request timeout to 10 seconds --- envoy_reader/envoy_reader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index b0d0a3a..3c19daf 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -5,22 +5,22 @@ class EnvoyReader(): def __init__(self, host, envoy_model): - self.host = host - self.envoy_model = envoy_model + self.host = host.lower() + self.envoy_model = envoy_model.lower() def call_api(self): if self.envoy_model == "original": url = "http://{}/api/v1/production".format(self.host) - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=10) else: url = "http://{}/production.json".format(self.host) - response = requests.get(url, timeout=5, allow_redirects=False) + response = requests.get(url, timeout=10, allow_redirects=False) if response.status_code == 200: self.envoy_model = "s" elif response.status_code == 301: self.envoy_model = "original" url = "http://{}/api/v1/production".format(self.host) - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=10) return response.json() def production(self): From c7b6c8942cb1652c14e1006114b61771d65ab7ab Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 13:16:24 -0500 Subject: [PATCH 004/134] import EnvoyReader class instead of individual functions --- envoy_reader/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/envoy_reader/__init__.py b/envoy_reader/__init__.py index 6e413ce..d4f8526 100644 --- a/envoy_reader/__init__.py +++ b/envoy_reader/__init__.py @@ -1,8 +1 @@ -from envoy_reader.envoy_reader import production -from envoy_reader.envoy_reader import consumption -from envoy_reader.envoy_reader import daily_production -from envoy_reader.envoy_reader import daily_consumption -from envoy_reader.envoy_reader import seven_days_production -from envoy_reader.envoy_reader import seven_days_consumption -from envoy_reader.envoy_reader import lifetime_production -from envoy_reader.envoy_reader import lifetime_consumption +from envoy_reader.envoy_reader import EnvoyReader From 5926a81f1a406c55e219feec589695326ec1eee2 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 13:22:06 -0500 Subject: [PATCH 005/134] Skip call_api if we know the Envoy model doesn't support it --- envoy_reader/envoy_reader.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 3c19daf..f1af29c 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -39,8 +39,10 @@ def production(self): def consumption(self): try: + if self.envoy_model == "original": + return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": consumption = raw_json["consumption"][0]["wNow"] return int(consumption) else: @@ -54,7 +56,7 @@ def consumption(self): def daily_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": daily_production = raw_json["production"][1]["whToday"] else: daily_production = raw_json["wattHoursToday"] @@ -67,8 +69,10 @@ def daily_production(self): def daily_consumption(self): try: + if self.envoy_model == "original": + return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) else: @@ -83,7 +87,7 @@ def daily_consumption(self): def seven_days_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": seven_days_production = raw_json["production"][1][ "whLastSevenDays" ] @@ -98,8 +102,10 @@ def seven_days_production(self): def seven_days_consumption(self): try: + if self.envoy_model == "original": + return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": seven_days_consumption = raw_json["consumption"][0][ "whLastSevenDays" ] @@ -115,7 +121,7 @@ def seven_days_consumption(self): def lifetime_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": lifetime_production = raw_json["production"][1][ "whLifetime" ] @@ -130,8 +136,10 @@ def lifetime_production(self): def lifetime_consumption(self): try: + if self.envoy_model == "original": + return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model.lower() == "s": + if self.envoy_model == "s": lifetime_consumption = raw_json["consumption"][0][ "whLifetime" ] From 685310526aafcdb15a16ea508b2d89d6028b95a3 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 13:36:19 -0500 Subject: [PATCH 006/134] Add support for Envoy IQ --- envoy_reader/envoy_reader.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f1af29c..033e9a4 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -7,12 +7,18 @@ class EnvoyReader(): def __init__(self, host, envoy_model): self.host = host.lower() self.envoy_model = envoy_model.lower() + # The Envoy IQ and the S supply the same json, so it's easier to + # just set them both to be the 's' + if self.envoy_model == "iq": + self.envoy_model = "s" def call_api(self): if self.envoy_model == "original": url = "http://{}/api/v1/production".format(self.host) response = requests.get(url, timeout=10) else: + # If it responds at this url we know it is the Envoy S + # If it gives us a 301 error then it is an original url = "http://{}/production.json".format(self.host) response = requests.get(url, timeout=10, allow_redirects=False) if response.status_code == 200: @@ -160,7 +166,7 @@ def lifetime_consumption(self): if host == "": host = "envoy" envoy_model = input("Enter the model of the Envoy " + - "('Original' or 'S'), or press enter.") + "('Original', 'S', or 'IQ'), or press enter.") if envoy_model == "": envoy_model = "unknown" From a6d7e5b2e00953222b231ebb20415f744a158180 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 14:35:16 -0500 Subject: [PATCH 007/134] Test for capabilities rather than state model name. --- envoy_reader/envoy_reader.py | 140 ++++++++++++++++------------------- 1 file changed, 62 insertions(+), 78 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 033e9a4..293f441 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,160 +4,152 @@ class EnvoyReader(): - def __init__(self, host, envoy_model): + def __init__(self, host): self.host = host.lower() - self.envoy_model = envoy_model.lower() - # The Envoy IQ and the S supply the same json, so it's easier to - # just set them both to be the 's' - if self.envoy_model == "iq": - self.envoy_model = "s" def call_api(self): - if self.envoy_model == "original": - url = "http://{}/api/v1/production".format(self.host) - response = requests.get(url, timeout=10) + url = "http://{}/production.json".format(self.host) + response = requests.get(url, timeout=10) + if response.status_code == 200 and len(response.json()) == 3: + return response.json() else: - # If it responds at this url we know it is the Envoy S - # If it gives us a 301 error then it is an original - url = "http://{}/production.json".format(self.host) + url = "http://{}/api/v1/production".format(self.host) response = requests.get(url, timeout=10, allow_redirects=False) - if response.status_code == 200: - self.envoy_model = "s" - elif response.status_code == 301: - self.envoy_model = "original" - url = "http://{}/api/v1/production".format(self.host) - response = requests.get(url, timeout=10) - return response.json() + return response.json() def production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: production = raw_json["production"][1]["wNow"] - else: + except (IndexError, KeyError): production = raw_json["wattsNow"] return int(production) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + "Check the IP address") def consumption(self): try: - if self.envoy_model == "original": - return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: consumption = raw_json["consumption"][0]["wNow"] - return int(consumption) - else: + except (IndexError, KeyError): return "Unavailable" + return int(consumption) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") def daily_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: daily_production = raw_json["production"][1]["whToday"] - else: + except (KeyError, IndexError): daily_production = raw_json["wattHoursToday"] return int(daily_production) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") def daily_consumption(self): try: - if self.envoy_model == "original": - return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: daily_consumption = raw_json["consumption"][0]["whToday"] - return int(daily_consumption) - else: + except (KeyError, IndexError): return "Unavailable" + return int(daily_consumption) except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") def seven_days_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: seven_days_production = raw_json["production"][1][ "whLastSevenDays" ] - else: + except (KeyError, IndexError): seven_days_production = raw_json["wattHoursSevenDays"] return int(seven_days_production) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") def seven_days_consumption(self): try: - if self.envoy_model == "original": - return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: seven_days_consumption = raw_json["consumption"][0][ "whLastSevenDays" ] - return int(seven_days_consumption) - else: + except (KeyError, IndexError): return "Unavailable" + return int(seven_days_consumption) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") def lifetime_production(self): try: raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: lifetime_production = raw_json["production"][1][ "whLifetime" ] - else: + except (KeyError, IndexError): lifetime_production = raw_json["wattHoursLifetime"] return int(lifetime_production) + except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + "Check the IP address") def lifetime_consumption(self): try: - if self.envoy_model == "original": - return "Unavailable" raw_json = EnvoyReader.call_api(self) - if self.envoy_model == "s": + try: lifetime_consumption = raw_json["consumption"][0][ "whLifetime" ] - return int(lifetime_consumption) - else: + except (KeyError, IndexError): return "Unavailable" + return int(lifetime_consumption) except requests.exceptions.ConnectionError: return "Unable to connect to Envoy. Check the IP address" - except (json.decoder.JSONDecodeError, KeyError): + except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address, or maybe your model of Envoy " + + "doesn't support this") if __name__ == "__main__": @@ -165,24 +157,16 @@ def lifetime_consumption(self): "or press enter to search for it.") if host == "": host = "envoy" - envoy_model = input("Enter the model of the Envoy " + - "('Original', 'S', or 'IQ'), or press enter.") - if envoy_model == "": - envoy_model = "unknown" - - print("production {}".format(EnvoyReader(host, envoy_model) - .production())) - print("consumption {}".format(EnvoyReader(host, envoy_model) - .consumption())) - print("daily_production {}".format(EnvoyReader(host, envoy_model) - .daily_production())) - print("daily_consumption {}".format(EnvoyReader(host, envoy_model) - .daily_consumption())) - print("seven_days_production {}".format(EnvoyReader(host, envoy_model) + + print("production {}".format(EnvoyReader(host).production())) + print("consumption {}".format(EnvoyReader(host).consumption())) + print("daily_production {}".format(EnvoyReader(host).daily_production())) + print("daily_consumption {}".format(EnvoyReader(host).daily_consumption())) + print("seven_days_production {}".format(EnvoyReader(host) .seven_days_production())) - print("seven_days_consumption {}".format(EnvoyReader(host, envoy_model) + print("seven_days_consumption {}".format(EnvoyReader(host) .seven_days_consumption())) - print("lifetime_production {}".format(EnvoyReader(host, envoy_model) + print("lifetime_production {}".format(EnvoyReader(host) .lifetime_production())) - print("lifetime_consumption {}".format(EnvoyReader(host, envoy_model) + print("lifetime_consumption {}".format(EnvoyReader(host) .lifetime_consumption())) From f3d21cb7b876141faf9cf7d433b240f75d029ed3 Mon Sep 17 00:00:00 2001 From: jesserizzo Date: Sun, 26 Aug 2018 14:49:28 -0500 Subject: [PATCH 008/134] Bump version to 0.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f2d5ff2..f07cf8d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.1", + version="0.2", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 2f69beed828e81b324f48f83b5e9ac8c44030a20 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:03:41 +0200 Subject: [PATCH 009/134] Fix detection of original Envoy model --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 293f441..39009db 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -10,7 +10,7 @@ def __init__(self, host): def call_api(self): url = "http://{}/production.json".format(self.host) response = requests.get(url, timeout=10) - if response.status_code == 200 and len(response.json()) == 3: + if response.status_code == 200 and response.url == url and len(response.json()) == 3: return response.json() else: url = "http://{}/api/v1/production".format(self.host) @@ -154,7 +154,7 @@ def lifetime_consumption(self): if __name__ == "__main__": host = input("Enter the Envoy IP address, " + - "or press enter to search for it.") + "or press enter to search for it: ") if host == "": host = "envoy" From 169e6a89e885f33abc0308ba394e92f86b8b56f5 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:09:19 +0200 Subject: [PATCH 010/134] Cleanup error messages --- envoy_reader/envoy_reader.py | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 39009db..62fed7b 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -27,10 +27,10 @@ def production(self): return int(production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address '" + self.host + "'.") def consumption(self): try: @@ -42,11 +42,11 @@ def consumption(self): return int(consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") def daily_production(self): try: @@ -58,11 +58,11 @@ def daily_production(self): return int(daily_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") def daily_consumption(self): try: @@ -74,11 +74,11 @@ def daily_consumption(self): return int(daily_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") def seven_days_production(self): try: @@ -92,11 +92,11 @@ def seven_days_production(self): return int(seven_days_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") def seven_days_consumption(self): try: @@ -110,11 +110,11 @@ def seven_days_consumption(self): return int(seven_days_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") def lifetime_production(self): try: @@ -128,10 +128,10 @@ def lifetime_production(self): return int(lifetime_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address") + "Check the IP address '" + self.host + "'.") def lifetime_consumption(self): try: @@ -145,11 +145,11 @@ def lifetime_consumption(self): return int(lifetime_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address" + return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." except (json.decoder.JSONDecodeError, KeyError, IndexError): return ("Got a response, but it doesn't look right. " + - "Check the IP address, or maybe your model of Envoy " + - "doesn't support this") + "Check the IP address '" + self.host + "', or maybe your model of Envoy " + + "doesn't support this.") if __name__ == "__main__": From fe592a8f8b62bb82bcb51a60a3642bedf9e206fd Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:12:10 +0200 Subject: [PATCH 011/134] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f28d67..25ac559 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ A program to read from an Enphase Envoy on the local network. Reads electricity consumed and produced instantaneously, that day, the last seven days, and the lifetime of the envoy. -Only tested on the Envoy S +Tested on the original Envoy (production data only) and the Envoy S (production and consumption data). + +Original Envoy: http://envoy/api/v1/production + +Envoy S: http://envoy/production.json From 223ddf8c76a0450da5160b6059493d0cfc7e3b74 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:14:02 +0200 Subject: [PATCH 012/134] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25ac559..32b79be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -A program to read from an Enphase Envoy on the local network. Reads electricity consumed and produced instantaneously, that day, the last seven days, and the lifetime of the envoy. +A program to read from an Enphase Envoy on the local network. Reads electricity production and consumption (if available) for the current moment, current day, the last seven days, and the lifetime of the Envoy. Tested on the original Envoy (production data only) and the Envoy S (production and consumption data). -Original Envoy: http://envoy/api/v1/production - -Envoy S: http://envoy/production.json +This reader uses a JSON endpoint on the Envoy gateway: +- Original Envoy: http://envoy/api/v1/production +- Envoy S: http://envoy/production.json From adebb4d837315af719e8626c9dbad50298978a1a Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:45:20 +0200 Subject: [PATCH 013/134] Make call more efficient for Envoy-C --- envoy_reader/envoy_reader.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 62fed7b..8b5b296 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,18 +4,38 @@ class EnvoyReader(): + # C for Envoy model C - S for Envoy model S + model = "" + url = "" + def __init__(self, host): self.host = host.lower() + def detect_model(self): + self.url = "http://{}/production.json".format(self.host) + response = requests.get(self.url, timeout=10, allow_redirects=False) + if response.status_code == 200 and len(response.json()) == 3: + self.model = "S" + else: + self.url = "http://{}/api/v1/production".format(self.host) + response = requests.get(self.url, timeout=10, allow_redirects=False) + if response.status_code == 200: + self.model = "C" + def call_api(self): - url = "http://{}/production.json".format(self.host) - response = requests.get(url, timeout=10) - if response.status_code == 200 and response.url == url and len(response.json()) == 3: - return response.json() + # detection + if self.model == "": + EnvoyReader.detect_model(self) + + if self.model == "S" or self.model == "C": + return EnvoyReader.call_api_get(self) else: - url = "http://{}/api/v1/production".format(self.host) - response = requests.get(url, timeout=10, allow_redirects=False) - return response.json() + # TODO throw exception + return "Could not connect or determine Envoy model. Check the IP address '" + self.host + "'." + + def call_api_get(self): + response = requests.get(self.url, timeout=10, allow_redirects=False) + return response.json() def production(self): try: From 30473933671aa2aacbc887131eea85c9a37e72b6 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:47:31 +0200 Subject: [PATCH 014/134] text --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 8b5b296..de5be95 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -173,8 +173,8 @@ def lifetime_consumption(self): if __name__ == "__main__": - host = input("Enter the Envoy IP address, " + - "or press enter to search for it: ") + host = input("Enter the Envoy IP address or host name, " + + "or press enter to use 'envoy' as default: ") if host == "": host = "envoy" From cdb22ad71366737cf5a90cccdea6f8d76856a215 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 10:56:35 +0200 Subject: [PATCH 015/134] throw runtime error --- envoy_reader/envoy_reader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index de5be95..a223d89 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -21,7 +21,7 @@ def detect_model(self): response = requests.get(self.url, timeout=10, allow_redirects=False) if response.status_code == 200: self.model = "C" - + def call_api(self): # detection if self.model == "": @@ -30,8 +30,7 @@ def call_api(self): if self.model == "S" or self.model == "C": return EnvoyReader.call_api_get(self) else: - # TODO throw exception - return "Could not connect or determine Envoy model. Check the IP address '" + self.host + "'." + raise RuntimeError("Could not connect or determine Envoy model. Check the IP address '" + self.host + "'.") def call_api_get(self): response = requests.get(self.url, timeout=10, allow_redirects=False) From f47e58e998946416feca1631a125836ce4b721c6 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 11:05:38 +0200 Subject: [PATCH 016/134] single test instance --- envoy_reader/envoy_reader.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index a223d89..993707e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -177,15 +177,16 @@ def lifetime_consumption(self): if host == "": host = "envoy" - print("production {}".format(EnvoyReader(host).production())) - print("consumption {}".format(EnvoyReader(host).consumption())) - print("daily_production {}".format(EnvoyReader(host).daily_production())) - print("daily_consumption {}".format(EnvoyReader(host).daily_consumption())) - print("seven_days_production {}".format(EnvoyReader(host) + testreader = EnvoyReader(host) + print("production {}".format(testreader.production())) + print("consumption {}".format(testreader.consumption())) + print("daily_production {}".format(testreader.daily_production())) + print("daily_consumption {}".format(testreader.daily_consumption())) + print("seven_days_production {}".format(testreader .seven_days_production())) - print("seven_days_consumption {}".format(EnvoyReader(host) + print("seven_days_consumption {}".format(testreader .seven_days_consumption())) - print("lifetime_production {}".format(EnvoyReader(host) + print("lifetime_production {}".format(testreader .lifetime_production())) - print("lifetime_consumption {}".format(EnvoyReader(host) + print("lifetime_consumption {}".format(testreader .lifetime_consumption())) From 58405d1f6c1eb96419969f8f4dceacb56d98c0bb Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 11:14:26 +0200 Subject: [PATCH 017/134] test output formatting --- envoy_reader/envoy_reader.py | 60 +++++++++++++++--------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 993707e..c8647cc 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -52,12 +52,12 @@ def production(self): "Check the IP address '" + self.host + "'.") def consumption(self): + if self.model == "C": + return "Unavailable on model " + self.model + try: raw_json = EnvoyReader.call_api(self) - try: - consumption = raw_json["consumption"][0]["wNow"] - except (IndexError, KeyError): - return "Unavailable" + consumption = raw_json["consumption"][0]["wNow"] return int(consumption) except requests.exceptions.ConnectionError: @@ -84,12 +84,12 @@ def daily_production(self): "doesn't support this.") def daily_consumption(self): + if self.model == "C": + return "Unavailable on model " + self.model + try: raw_json = EnvoyReader.call_api(self) - try: - daily_consumption = raw_json["consumption"][0]["whToday"] - except (KeyError, IndexError): - return "Unavailable" + daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) except requests.exceptions.ConnectionError: @@ -118,14 +118,12 @@ def seven_days_production(self): "doesn't support this.") def seven_days_consumption(self): + if self.model == "C": + return "Unavailable on model " + self.model + try: raw_json = EnvoyReader.call_api(self) - try: - seven_days_consumption = raw_json["consumption"][0][ - "whLastSevenDays" - ] - except (KeyError, IndexError): - return "Unavailable" + seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: @@ -139,9 +137,7 @@ def lifetime_production(self): try: raw_json = EnvoyReader.call_api(self) try: - lifetime_production = raw_json["production"][1][ - "whLifetime" - ] + lifetime_production = raw_json["production"][1]["whLifetime"] except (KeyError, IndexError): lifetime_production = raw_json["wattHoursLifetime"] return int(lifetime_production) @@ -153,14 +149,12 @@ def lifetime_production(self): "Check the IP address '" + self.host + "'.") def lifetime_consumption(self): + if self.model == "C": + return "Unavailable on model " + self.model + try: raw_json = EnvoyReader.call_api(self) - try: - lifetime_consumption = raw_json["consumption"][0][ - "whLifetime" - ] - except (KeyError, IndexError): - return "Unavailable" + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] return int(lifetime_consumption) except requests.exceptions.ConnectionError: @@ -178,15 +172,11 @@ def lifetime_consumption(self): host = "envoy" testreader = EnvoyReader(host) - print("production {}".format(testreader.production())) - print("consumption {}".format(testreader.consumption())) - print("daily_production {}".format(testreader.daily_production())) - print("daily_consumption {}".format(testreader.daily_consumption())) - print("seven_days_production {}".format(testreader - .seven_days_production())) - print("seven_days_consumption {}".format(testreader - .seven_days_consumption())) - print("lifetime_production {}".format(testreader - .lifetime_production())) - print("lifetime_consumption {}".format(testreader - .lifetime_consumption())) + print("production: {}".format(testreader.production())) + print("consumption: {}".format(testreader.consumption())) + print("daily_production: {}".format(testreader.daily_production())) + print("daily_consumption: {}".format(testreader.daily_consumption())) + print("seven_days_production: {}".format(testreader.seven_days_production())) + print("seven_days_consumption: {}".format(testreader.seven_days_consumption())) + print("lifetime_production: {}".format(testreader.lifetime_production())) + print("lifetime_consumption: {}".format(testreader.lifetime_consumption())) From 5346430a17bd4c9ec8e52f1769ceca13d767f652 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 16:48:17 +0200 Subject: [PATCH 018/134] Refactor endpoint check + error messages --- .gitignore | 132 +++++++++++++++++++++++++++++++++++ envoy_reader/envoy_reader.py | 129 ++++++++++++++++------------------ 2 files changed, 194 insertions(+), 67 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5a6c7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +### Python Patch ### +.venv/ + +### Python.VirtualEnv Stack ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + + +# End of https://www.gitignore.io/api/python \ No newline at end of file diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c8647cc..6787638 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,56 +4,66 @@ class EnvoyReader(): - # C for Envoy model C - S for Envoy model S - model = "" - url = "" + # P for prooduction data only (ie. Envoy model C) + # PC for production and consumption data (ie. Envoy model S) + endpoint_type = "" + endpoint_url = "" + + message_consumption_not_available = "Consumption data not available for your Envoy device." def __init__(self, host): self.host = host.lower() def detect_model(self): - self.url = "http://{}/production.json".format(self.host) - response = requests.get(self.url, timeout=10, allow_redirects=False) + self.endpoint_url = "http://{}/production.json".format(self.host) + response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) if response.status_code == 200 and len(response.json()) == 3: - self.model = "S" + self.endpoint_type = "PC" + return else: - self.url = "http://{}/api/v1/production".format(self.host) - response = requests.get(self.url, timeout=10, allow_redirects=False) + self.endpoint_url = "http://{}/api/v1/production".format(self.host) + response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) if response.status_code == 200: - self.model = "C" + self.endpoint_type = "P" + return + + self.endpoint_url = "" + raise RuntimeError( + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") def call_api(self): - # detection - if self.model == "": + # detection of endpoint + if self.endpoint_type == "": EnvoyReader.detect_model(self) - if self.model == "S" or self.model == "C": - return EnvoyReader.call_api_get(self) - else: - raise RuntimeError("Could not connect or determine Envoy model. Check the IP address '" + self.host + "'.") - - def call_api_get(self): - response = requests.get(self.url, timeout=10, allow_redirects=False) + response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) return response.json() + def create_json_errormessage(self): + return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") + + def create_connect_errormessage(self): + return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + + "Maybe your model of Envoy doesn't support the requested metric.") + def production(self): try: raw_json = EnvoyReader.call_api(self) - try: + if self.endpoint_type == "PC": production = raw_json["production"][1]["wNow"] - except (IndexError, KeyError): + else: production = raw_json["wattsNow"] return int(production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "'.") + return EnvoyReader.create_json_errormessage(self) def consumption(self): - if self.model == "C": - return "Unavailable on model " + self.model + if self.endpoint_type == "P": + return self.message_consumption_not_available try: raw_json = EnvoyReader.call_api(self) @@ -61,31 +71,27 @@ def consumption(self): return int(consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) def daily_production(self): try: raw_json = EnvoyReader.call_api(self) - try: + if self.endpoint_type == "PC": daily_production = raw_json["production"][1]["whToday"] - except (KeyError, IndexError): + else: daily_production = raw_json["wattHoursToday"] return int(daily_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) def daily_consumption(self): - if self.model == "C": - return "Unavailable on model " + self.model + if self.endpoint_type == "P": + return self.message_consumption_not_available try: raw_json = EnvoyReader.call_api(self) @@ -93,33 +99,27 @@ def daily_consumption(self): return int(daily_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) def seven_days_production(self): try: raw_json = EnvoyReader.call_api(self) - try: - seven_days_production = raw_json["production"][1][ - "whLastSevenDays" - ] - except (KeyError, IndexError): + if self.endpoint_type == "PC": + seven_days_production = raw_json["production"][1]["whLastSevenDays"] + else: seven_days_production = raw_json["wattHoursSevenDays"] return int(seven_days_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) def seven_days_consumption(self): - if self.model == "C": - return "Unavailable on model " + self.model + if self.endpoint_type == "P": + return self.message_consumption_not_available try: raw_json = EnvoyReader.call_api(self) @@ -127,30 +127,27 @@ def seven_days_consumption(self): return int(seven_days_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) def lifetime_production(self): try: raw_json = EnvoyReader.call_api(self) - try: + if self.endpoint_type == "PC": lifetime_production = raw_json["production"][1]["whLifetime"] - except (KeyError, IndexError): + else: lifetime_production = raw_json["wattHoursLifetime"] return int(lifetime_production) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "'.") + return EnvoyReader.create_json_errormessage(self) def lifetime_consumption(self): - if self.model == "C": - return "Unavailable on model " + self.model + if self.endpoint_type == "P": + return self.message_consumption_not_available try: raw_json = EnvoyReader.call_api(self) @@ -158,11 +155,9 @@ def lifetime_consumption(self): return int(lifetime_consumption) except requests.exceptions.ConnectionError: - return "Unable to connect to Envoy. Check the IP address '" + self.host + "'." + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return ("Got a response, but it doesn't look right. " + - "Check the IP address '" + self.host + "', or maybe your model of Envoy " + - "doesn't support this.") + return EnvoyReader.create_json_errormessage(self) if __name__ == "__main__": From 1531ab650e542f4b83056484a3ba5e31a93f9834 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 29 Sep 2018 16:52:53 +0200 Subject: [PATCH 019/134] Fix tyop --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6787638..a49e90e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,7 +4,7 @@ class EnvoyReader(): - # P for prooduction data only (ie. Envoy model C) + # P for production data only (ie. Envoy model C) # PC for production and consumption data (ie. Envoy model S) endpoint_type = "" endpoint_url = "" From 9ab6d21a7d1a6584abe8694d985b260a45abc28d Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Mon, 27 May 2019 11:12:08 -0500 Subject: [PATCH 020/134] Fix error messages, increase network timeout --- envoy_reader/envoy_reader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index a49e90e..c1ed6f9 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -16,13 +16,13 @@ def __init__(self, host): def detect_model(self): self.endpoint_url = "http://{}/production.json".format(self.host) - response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200 and len(response.json()) == 3: self.endpoint_type = "PC" return else: self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P" return @@ -37,13 +37,13 @@ def call_api(self): if self.endpoint_type == "": EnvoyReader.detect_model(self) - response = requests.get(self.endpoint_url, timeout=10, allow_redirects=False) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) return response.json() - def create_json_errormessage(self): + def create_connect_errormessage(self): return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") - def create_connect_errormessage(self): + def create_json_errormessage (self): return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + "Maybe your model of Envoy doesn't support the requested metric.") From 243f90abc7ca781bc93348739742b8cedaa16d6d Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Mon, 27 May 2019 11:29:14 -0500 Subject: [PATCH 021/134] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f07cf8d..fe6f2b3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.2", + version="0.4", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 3c0d45db4032f1f04a383e56fa98d8937e4bb5ec Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Sun, 9 Jun 2019 18:00:33 -0500 Subject: [PATCH 022/134] Get production from individual inverters --- envoy_reader/envoy_reader.py | 106 +++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c1ed6f9..6699c36 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,6 +1,7 @@ import requests import sys import json +from requests.auth import HTTPDigestAuth class EnvoyReader(): @@ -8,6 +9,7 @@ class EnvoyReader(): # PC for production and consumption data (ie. Envoy model S) endpoint_type = "" endpoint_url = "" + serial_number_last_six = "" message_consumption_not_available = "Consumption data not available for your Envoy device." @@ -16,40 +18,53 @@ def __init__(self, host): def detect_model(self): self.endpoint_url = "http://{}/production.json".format(self.host) - response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200 and len(response.json()) == 3: self.endpoint_type = "PC" return else: self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P" return self.endpoint_url = "" raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") - + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") + + def get_serial_number(self): + try: + response = requests.get( + "http://{}/info.xml".format(self.host), timeout=30, allow_redirects=False) + sn = response.text.split("")[1].split("")[0][-6:] + self.serial_number_last_six = sn + except: + print( + "Unable to find device serial number, this is needed to read inverter production.") + def call_api(self): # detection of endpoint if self.endpoint_type == "": - EnvoyReader.detect_model(self) + self.detect_model() - response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) return response.json() def create_connect_errormessage(self): return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") - def create_json_errormessage (self): + def create_json_errormessage(self): return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + "Maybe your model of Envoy doesn't support the requested metric.") def production(self): try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() if self.endpoint_type == "PC": production = raw_json["production"][1]["wNow"] else: @@ -57,27 +72,27 @@ def production(self): return int(production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def consumption(self): if self.endpoint_type == "P": return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() consumption = raw_json["consumption"][0]["wNow"] return int(consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def daily_production(self): try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() if self.endpoint_type == "PC": daily_production = raw_json["production"][1]["whToday"] else: @@ -85,27 +100,27 @@ def daily_production(self): return int(daily_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def daily_consumption(self): if self.endpoint_type == "P": return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def seven_days_production(self): try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() if self.endpoint_type == "PC": seven_days_production = raw_json["production"][1]["whLastSevenDays"] else: @@ -113,27 +128,27 @@ def seven_days_production(self): return int(seven_days_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def seven_days_consumption(self): if self.endpoint_type == "P": return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def lifetime_production(self): try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() if self.endpoint_type == "PC": lifetime_production = raw_json["production"][1]["whLifetime"] else: @@ -141,23 +156,36 @@ def lifetime_production(self): return int(lifetime_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def lifetime_consumption(self): if self.endpoint_type == "P": return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() lifetime_consumption = raw_json["consumption"][0]["whLifetime"] return int(lifetime_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + def inverters_production(self): + if self.serial_number_last_six == "": + self.get_serial_number() + + try: + response = requests.get("http://{}/api/v1/production/inverters".format(self.host), + auth=HTTPDigestAuth("envoy", self.serial_number_last_six)) + return response.json() + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() if __name__ == "__main__": @@ -171,7 +199,13 @@ def lifetime_consumption(self): print("consumption: {}".format(testreader.consumption())) print("daily_production: {}".format(testreader.daily_production())) print("daily_consumption: {}".format(testreader.daily_consumption())) - print("seven_days_production: {}".format(testreader.seven_days_production())) - print("seven_days_consumption: {}".format(testreader.seven_days_consumption())) - print("lifetime_production: {}".format(testreader.lifetime_production())) - print("lifetime_consumption: {}".format(testreader.lifetime_consumption())) + print("seven_days_production: {}".format( + testreader.seven_days_production())) + print("seven_days_consumption: {}".format( + testreader.seven_days_consumption())) + print("lifetime_production: {}".format( + testreader.lifetime_production())) + print("lifetime_consumption: {}".format( + testreader.lifetime_consumption())) + print("inverters_production: {}".format( + testreader.inverters_production())) From bbd5eb03a31d142522c16aac08a1ce30b2de44b9 Mon Sep 17 00:00:00 2001 From: Jesse Rizzo Date: Sun, 9 Jun 2019 18:31:39 -0500 Subject: [PATCH 023/134] fix inverter production dictionary --- envoy_reader/envoy_reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6699c36..9c88c91 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -181,7 +181,10 @@ def inverters_production(self): try: response = requests.get("http://{}/api/v1/production/inverters".format(self.host), auth=HTTPDigestAuth("envoy", self.serial_number_last_six)) - return response.json() + response_dict = {} + for item in response.json(): + response_dict[item["serialNumber"]] = item["lastReportWatts"] + return response_dict except requests.exceptions.ConnectionError: return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): From f03d296ec346519a015ca72600ebaeb4cb036dcb Mon Sep 17 00:00:00 2001 From: Jesse Rizzo Date: Fri, 14 Jun 2019 09:22:19 -0500 Subject: [PATCH 024/134] Make reader async --- envoy_reader/envoy_reader.py | 435 ++++++++++++++++++----------------- 1 file changed, 221 insertions(+), 214 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 9c88c91..ea25c02 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,214 +1,221 @@ -import requests -import sys -import json -from requests.auth import HTTPDigestAuth - - -class EnvoyReader(): - # P for production data only (ie. Envoy model C) - # PC for production and consumption data (ie. Envoy model S) - endpoint_type = "" - endpoint_url = "" - serial_number_last_six = "" - - message_consumption_not_available = "Consumption data not available for your Envoy device." - - def __init__(self, host): - self.host = host.lower() - - def detect_model(self): - self.endpoint_url = "http://{}/production.json".format(self.host) - response = requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and len(response.json()) == 3: - self.endpoint_type = "PC" - return - else: - self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: - self.endpoint_type = "P" - return - - self.endpoint_url = "" - raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") - - def get_serial_number(self): - try: - response = requests.get( - "http://{}/info.xml".format(self.host), timeout=30, allow_redirects=False) - sn = response.text.split("")[1].split("")[0][-6:] - self.serial_number_last_six = sn - except: - print( - "Unable to find device serial number, this is needed to read inverter production.") - - def call_api(self): - # detection of endpoint - if self.endpoint_type == "": - self.detect_model() - - response = requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - return response.json() - - def create_connect_errormessage(self): - return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") - - def create_json_errormessage(self): - return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + - "Maybe your model of Envoy doesn't support the requested metric.") - - def production(self): - try: - raw_json = self.call_api() - if self.endpoint_type == "PC": - production = raw_json["production"][1]["wNow"] - else: - production = raw_json["wattsNow"] - return int(production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def consumption(self): - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = self.call_api() - consumption = raw_json["consumption"][0]["wNow"] - return int(consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def daily_production(self): - try: - raw_json = self.call_api() - if self.endpoint_type == "PC": - daily_production = raw_json["production"][1]["whToday"] - else: - daily_production = raw_json["wattHoursToday"] - return int(daily_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def daily_consumption(self): - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = self.call_api() - daily_consumption = raw_json["consumption"][0]["whToday"] - return int(daily_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def seven_days_production(self): - try: - raw_json = self.call_api() - if self.endpoint_type == "PC": - seven_days_production = raw_json["production"][1]["whLastSevenDays"] - else: - seven_days_production = raw_json["wattHoursSevenDays"] - return int(seven_days_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def seven_days_consumption(self): - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = self.call_api() - seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] - return int(seven_days_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def lifetime_production(self): - try: - raw_json = self.call_api() - if self.endpoint_type == "PC": - lifetime_production = raw_json["production"][1]["whLifetime"] - else: - lifetime_production = raw_json["wattHoursLifetime"] - return int(lifetime_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def lifetime_consumption(self): - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = self.call_api() - lifetime_consumption = raw_json["consumption"][0]["whLifetime"] - return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - def inverters_production(self): - if self.serial_number_last_six == "": - self.get_serial_number() - - try: - response = requests.get("http://{}/api/v1/production/inverters".format(self.host), - auth=HTTPDigestAuth("envoy", self.serial_number_last_six)) - response_dict = {} - for item in response.json(): - response_dict[item["serialNumber"]] = item["lastReportWatts"] - return response_dict - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - -if __name__ == "__main__": - host = input("Enter the Envoy IP address or host name, " + - "or press enter to use 'envoy' as default: ") - if host == "": - host = "envoy" - - testreader = EnvoyReader(host) - print("production: {}".format(testreader.production())) - print("consumption: {}".format(testreader.consumption())) - print("daily_production: {}".format(testreader.daily_production())) - print("daily_consumption: {}".format(testreader.daily_consumption())) - print("seven_days_production: {}".format( - testreader.seven_days_production())) - print("seven_days_consumption: {}".format( - testreader.seven_days_consumption())) - print("lifetime_production: {}".format( - testreader.lifetime_production())) - print("lifetime_consumption: {}".format( - testreader.lifetime_consumption())) - print("inverters_production: {}".format( - testreader.inverters_production())) +import requests_async as requests +import requests as requests_sync +import asyncio +import sys +import json +from requests.auth import HTTPDigestAuth + + +class EnvoyReader(): + # P for production data only (ie. Envoy model C) + # PC for production and consumption data (ie. Envoy model S) + + message_consumption_not_available = "Consumption data not available for your Envoy device." + + def __init__(self, host): + self.host = host.lower() + self.endpoint_type = "" + self.endpoint_url = "" + self.serial_number_last_six = "" + + async def detect_model(self): + self.endpoint_url = "http://{}/production.json".format(self.host) + response = await requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200 and len(response.json()) == 3: + self.endpoint_type = "PC" + return + else: + self.endpoint_url = "http://{}/api/v1/production".format(self.host) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P" + return + + self.endpoint_url = "" + raise RuntimeError( + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") + + async def get_serial_number(self): + try: + response = await requests.get( + "http://{}/info.xml".format(self.host), timeout=10, allow_redirects=False) + sn = response.text.split("")[1].split("")[0][-6:] + self.serial_number_last_six = sn + except: + print( + "Unable to find device serial number, this is needed to read inverter production.") + + async def call_api(self): + # detection of endpoint + if self.endpoint_type == "": + await self.detect_model() + + response = await requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + return response.json() + + def create_connect_errormessage(self): + return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") + + def create_json_errormessage(self): + return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + + "Maybe your model of Envoy doesn't support the requested metric.") + + async def production(self): + try: + raw_json = await self.call_api() + if self.endpoint_type == "PC": + production = raw_json["production"][1]["wNow"] + else: + production = raw_json["wattsNow"] + return int(production) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def consumption(self): + if self.endpoint_type == "P": + return self.message_consumption_not_available + + try: + raw_json = await self.call_api() + consumption = raw_json["consumption"][0]["wNow"] + return int(consumption) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def daily_production(self): + try: + raw_json = await self.call_api() + if self.endpoint_type == "PC": + daily_production = raw_json["production"][1]["whToday"] + else: + daily_production = raw_json["wattHoursToday"] + return int(daily_production) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def daily_consumption(self): + if self.endpoint_type == "P": + return self.message_consumption_not_available + + try: + raw_json = await self.call_api() + daily_consumption = raw_json["consumption"][0]["whToday"] + return int(daily_consumption) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def seven_days_production(self): + try: + raw_json = await self.call_api() + if self.endpoint_type == "PC": + seven_days_production = raw_json["production"][1]["whLastSevenDays"] + else: + seven_days_production = raw_json["wattHoursSevenDays"] + return int(seven_days_production) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def seven_days_consumption(self): + if self.endpoint_type == "P": + return self.message_consumption_not_available + + try: + raw_json = await self.call_api() + seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] + return int(seven_days_consumption) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def lifetime_production(self): + try: + raw_json = await self.call_api() + if self.endpoint_type == "PC": + lifetime_production = raw_json["production"][1]["whLifetime"] + else: + lifetime_production = raw_json["wattHoursLifetime"] + return int(lifetime_production) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def lifetime_consumption(self): + if self.endpoint_type == "P": + return self.message_consumption_not_available + + try: + raw_json = await self.call_api() + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] + return int(lifetime_consumption) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + + async def inverters_production(self): + if self.serial_number_last_six == "": + await self.get_serial_number() + try: + response = requests_sync.get("http://{}/api/v1/production/inverters".format(self.host), + auth=HTTPDigestAuth("envoy", self.serial_number_last_six)) + response_dict = {} + for item in response.json(): + response_dict[item["serialNumber"]] = item["lastReportWatts"] + return response_dict + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): + return self.create_json_errormessage() + + def run_in_console(self): + print("Reading...") + loop = asyncio.get_event_loop() + results = loop.run_until_complete(asyncio.gather( + self.production(), self.consumption(), self.daily_production(), self.daily_consumption(), + self.seven_days_production(), self.seven_days_consumption(), self.lifetime_production(), self.lifetime_consumption(), self.inverters_production())) + + print("production: {}".format(results[0])) + print("consumption: {}".format(results[1])) + print("daily_production: {}".format(results[2])) + print("daily_consumption: {}".format(results[3])) + print("seven_days_production: {}".format(results[4])) + print("seven_days_consumption: {}".format(results[5])) + print("lifetime_production: {}".format(results[6])) + print("lifetime_consumption: {}".format(results[7])) + print("inverters_production: {}".format(results[8])) + + +if __name__ == "__main__": + host = input("Enter the Envoy IP address or host name, " + + "or press enter to use 'envoy' as default: ") + if host == "": + host = "envoy" + + testreader = EnvoyReader(host) + testreader.run_in_console() + #loop = asyncio.get_event_loop() + #loop.run_until_complete(asyncio.ensure_future(testreader.run_in_console())) From f89ae9b57bea94b1015c2aae4f004658c0cc28b8 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Fri, 14 Jun 2019 10:56:51 -0500 Subject: [PATCH 025/134] Fix formatting --- README.md | 2 + envoy_reader/envoy_reader.py | 102 ++++++++++++++++++++++++----------- setup.py | 2 +- 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 32b79be..460a50b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ A program to read from an Enphase Envoy on the local network. Reads electricity production and consumption (if available) for the current moment, current day, the last seven days, and the lifetime of the Envoy. +Also reads production from individual inverters if supported. Tested on the original Envoy (production data only) and the Envoy S (production and consumption data). This reader uses a JSON endpoint on the Envoy gateway: + - Original Envoy: http://envoy/api/v1/production - Envoy S: http://envoy/production.json diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ea25c02..b646c57 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,16 +1,20 @@ -import requests_async as requests -import requests as requests_sync +"""Module to read production and consumption values from an Enphase Envoy on + the local network""" import asyncio import sys import json from requests.auth import HTTPDigestAuth +import requests as requests_sync +import requests_async as requests class EnvoyReader(): + """Instance of EnvoyReader""" # P for production data only (ie. Envoy model C) # PC for production and consumption data (ie. Envoy model S) - message_consumption_not_available = "Consumption data not available for your Envoy device." + message_consumption_not_available = ("Consumption data not available for " + "your Envoy device.") def __init__(self, host): self.host = host.lower() @@ -19,19 +23,21 @@ def __init__(self, host): self.serial_number_last_six = "" async def detect_model(self): + """Method to determine if the Envoy supports consumption values or + only production""" self.endpoint_url = "http://{}/production.json".format(self.host) response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200 and len(response.json()) == 3: self.endpoint_type = "PC" return - else: - self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: - self.endpoint_type = "P" - return + + self.endpoint_url = "http://{}/api/v1/production".format(self.host) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P" + return self.endpoint_url = "" raise RuntimeError( @@ -39,16 +45,22 @@ async def detect_model(self): "Check that the device is up at 'http://" + self.host + "'.") async def get_serial_number(self): + """Method to get last six digits of Envoy serial number for auth""" try: response = await requests.get( - "http://{}/info.xml".format(self.host), timeout=10, allow_redirects=False) + "http://{}/info.xml".format(self.host), + timeout=10, allow_redirects=False) sn = response.text.split("")[1].split("")[0][-6:] self.serial_number_last_six = sn - except: - print( - "Unable to find device serial number, this is needed to read inverter production.") + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + # except + # print( + # "Unable to find device serial number, " + + # "this is needed to read inverter production.") async def call_api(self): + """Method to call the Envoy API""" # detection of endpoint if self.endpoint_type == "": await self.detect_model() @@ -58,13 +70,20 @@ async def call_api(self): return response.json() def create_connect_errormessage(self): - return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") + """Create error message if unable to connect to Envoy""" + return ("Unable to connect to Envoy. " + + "Check that the device is up at 'http://" + + self.host + "'.") def create_json_errormessage(self): - return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + - "Maybe your model of Envoy doesn't support the requested metric.") + """Create error message if unable to parse JSON response""" + return ("Got a response from '" + self.endpoint_url + + "', but metric could not be found. " + + "Maybe your model of Envoy doesn't " + + "support the requested metric.") async def production(self): + """Call API and parse production values from response""" try: raw_json = await self.call_api() if self.endpoint_type == "PC": @@ -79,6 +98,7 @@ async def production(self): return self.create_json_errormessage() async def consumption(self): + """Call API and parse consumption values from response""" if self.endpoint_type == "P": return self.message_consumption_not_available @@ -93,6 +113,7 @@ async def consumption(self): return self.create_json_errormessage() async def daily_production(self): + """Call API and parse todays production values from response""" try: raw_json = await self.call_api() if self.endpoint_type == "PC": @@ -107,6 +128,7 @@ async def daily_production(self): return self.create_json_errormessage() async def daily_consumption(self): + """Call API and parse todays consumption values from response""" if self.endpoint_type == "P": return self.message_consumption_not_available @@ -121,10 +143,13 @@ async def daily_consumption(self): return self.create_json_errormessage() async def seven_days_production(self): + """Call API and parse the past seven days production values from the + response""" try: raw_json = await self.call_api() if self.endpoint_type == "PC": - seven_days_production = raw_json["production"][1]["whLastSevenDays"] + seven_days_production = raw_json["production"][1] + ["whLastSevenDays"] else: seven_days_production = raw_json["wattHoursSevenDays"] return int(seven_days_production) @@ -135,12 +160,15 @@ async def seven_days_production(self): return self.create_json_errormessage() async def seven_days_consumption(self): + """Call API and parse the past seven days consumption values from + the response""" if self.endpoint_type == "P": return self.message_consumption_not_available try: raw_json = await self.call_api() - seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] + seven_days_consumption = raw_json["consumption"][0] + ["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: @@ -149,6 +177,7 @@ async def seven_days_consumption(self): return self.create_json_errormessage() async def lifetime_production(self): + """Call API and parse the lifetime of production from response""" try: raw_json = await self.call_api() if self.endpoint_type == "PC": @@ -163,6 +192,7 @@ async def lifetime_production(self): return self.create_json_errormessage() async def lifetime_consumption(self): + """Call API and parse the lifetime of consumption from response""" if self.endpoint_type == "P": return self.message_consumption_not_available @@ -177,11 +207,16 @@ async def lifetime_consumption(self): return self.create_json_errormessage() async def inverters_production(self): + """Hit a different Envoy endpoint and get the production values for + individual inverters""" if self.serial_number_last_six == "": await self.get_serial_number() try: - response = requests_sync.get("http://{}/api/v1/production/inverters".format(self.host), - auth=HTTPDigestAuth("envoy", self.serial_number_last_six)) + response = requests_sync.get( + "http://{}/api/v1/production/inverters" + .format(self.host), + auth=HTTPDigestAuth("envoy", + self.serial_number_last_six)) response_dict = {} for item in response.json(): response_dict[item["serialNumber"]] = item["lastReportWatts"] @@ -192,11 +227,20 @@ async def inverters_production(self): return self.create_json_errormessage() def run_in_console(self): + """If running this module directly, print all the values in the + console.""" print("Reading...") loop = asyncio.get_event_loop() results = loop.run_until_complete(asyncio.gather( - self.production(), self.consumption(), self.daily_production(), self.daily_consumption(), - self.seven_days_production(), self.seven_days_consumption(), self.lifetime_production(), self.lifetime_consumption(), self.inverters_production())) + self.production(), + self.consumption(), + self.daily_production(), + self.daily_consumption(), + self.seven_days_production(), + self.seven_days_consumption(), + self.lifetime_production(), + self.lifetime_consumption(), + self.inverters_production())) print("production: {}".format(results[0])) print("consumption: {}".format(results[1])) @@ -210,12 +254,10 @@ def run_in_console(self): if __name__ == "__main__": - host = input("Enter the Envoy IP address or host name, " + + HOST = input("Enter the Envoy IP address or host name, " + "or press enter to use 'envoy' as default: ") - if host == "": - host = "envoy" + if HOST == "": + HOST = "envoy" - testreader = EnvoyReader(host) - testreader.run_in_console() - #loop = asyncio.get_event_loop() - #loop.run_until_complete(asyncio.ensure_future(testreader.run_in_console())) + TESTREADER = EnvoyReader(HOST) + TESTREADER.run_in_console() diff --git a/setup.py b/setup.py index fe6f2b3..b8956d7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.4", + version="0.6", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From fa2b2db2c05ce451a156dbca3a4d7587f3fb6820 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Fri, 14 Jun 2019 11:17:01 -0500 Subject: [PATCH 026/134] something --- envoy_reader/envoy_reader.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index b646c57..22d2784 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -148,8 +148,7 @@ async def seven_days_production(self): try: raw_json = await self.call_api() if self.endpoint_type == "PC": - seven_days_production = raw_json["production"][1] - ["whLastSevenDays"] + seven_days_production = raw_json["production"][1]["whLastSevenDays"] else: seven_days_production = raw_json["wattHoursSevenDays"] return int(seven_days_production) @@ -167,8 +166,7 @@ async def seven_days_consumption(self): try: raw_json = await self.call_api() - seven_days_consumption = raw_json["consumption"][0] - ["whLastSevenDays"] + seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: From 77a267592bcd442ad774a11fd79d94b20f8153b6 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 14 Dec 2018 02:33:55 -0800 Subject: [PATCH 027/134] Improve detection of Envoy-S with consumption metering --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 22d2784..572d556 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -28,7 +28,7 @@ async def detect_model(self): self.endpoint_url = "http://{}/production.json".format(self.host) response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and len(response.json()) == 3: + if response.status_code == 200 and len(response.json()) >= 2: self.endpoint_type = "PC" return From 1e1b7f99c994df81b95fddd9f1829bb033fbd0b2 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Tue, 18 Jun 2019 23:08:08 -0700 Subject: [PATCH 028/134] Added await for get method Get method was missing await --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 22d2784..ea53b51 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -33,7 +33,7 @@ async def detect_model(self): return self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get( + response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P" From c0a1ccde012ef16a10d9be9eb222e550d3bba2f5 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Tue, 18 Jun 2019 23:21:21 -0700 Subject: [PATCH 029/134] Added empty string check On the Envoy Model C there is no webpage of http://IP_ADDR/info.xml, so the response will be blank. This check only runs if the response in not blank --- envoy_reader/envoy_reader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ea53b51..63ebbca 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -50,8 +50,9 @@ async def get_serial_number(self): response = await requests.get( "http://{}/info.xml".format(self.host), timeout=10, allow_redirects=False) - sn = response.text.split("")[1].split("")[0][-6:] - self.serial_number_last_six = sn + if len(response.text) > 0: + sn = response.text.split("")[1].split("")[0][-6:] + self.serial_number_last_six = sn except requests.exceptions.ConnectionError: return self.create_connect_errormessage() # except From 2ef7453e5067e953f60cbbf93e187c09b88635cd Mon Sep 17 00:00:00 2001 From: dalklein <49771139+dalklein@users.noreply.github.com> Date: Thu, 20 Jun 2019 11:54:49 -0400 Subject: [PATCH 030/134] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 460a50b..b34867d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Also reads production from individual inverters if supported. Tested on the original Envoy (production data only) and the Envoy S (production and consumption data). This reader uses a JSON endpoint on the Envoy gateway: - -- Original Envoy: http://envoy/api/v1/production +- Original Envoy: http://envoy/api/v1/production (available on software level R3.9 and later) - Envoy S: http://envoy/production.json + +For older Envoy with software before R3.9, data is collected from html at http://envoy/production From 7942180e152e35bee64e567c9ccc10a5ab51cde3 Mon Sep 17 00:00:00 2001 From: dalklein <49771139+dalklein@users.noreply.github.com> Date: Thu, 20 Jun 2019 16:24:04 -0400 Subject: [PATCH 031/134] Add files via upload --- envoy_reader/envoy_reader.py | 529 ++++++++++++++++++----------------- 1 file changed, 268 insertions(+), 261 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 22d2784..d98d75e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,261 +1,268 @@ -"""Module to read production and consumption values from an Enphase Envoy on - the local network""" -import asyncio -import sys -import json -from requests.auth import HTTPDigestAuth -import requests as requests_sync -import requests_async as requests - - -class EnvoyReader(): - """Instance of EnvoyReader""" - # P for production data only (ie. Envoy model C) - # PC for production and consumption data (ie. Envoy model S) - - message_consumption_not_available = ("Consumption data not available for " - "your Envoy device.") - - def __init__(self, host): - self.host = host.lower() - self.endpoint_type = "" - self.endpoint_url = "" - self.serial_number_last_six = "" - - async def detect_model(self): - """Method to determine if the Envoy supports consumption values or - only production""" - self.endpoint_url = "http://{}/production.json".format(self.host) - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and len(response.json()) == 3: - self.endpoint_type = "PC" - return - - self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: - self.endpoint_type = "P" - return - - self.endpoint_url = "" - raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") - - async def get_serial_number(self): - """Method to get last six digits of Envoy serial number for auth""" - try: - response = await requests.get( - "http://{}/info.xml".format(self.host), - timeout=10, allow_redirects=False) - sn = response.text.split("")[1].split("")[0][-6:] - self.serial_number_last_six = sn - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - # except - # print( - # "Unable to find device serial number, " + - # "this is needed to read inverter production.") - - async def call_api(self): - """Method to call the Envoy API""" - # detection of endpoint - if self.endpoint_type == "": - await self.detect_model() - - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - return response.json() - - def create_connect_errormessage(self): - """Create error message if unable to connect to Envoy""" - return ("Unable to connect to Envoy. " + - "Check that the device is up at 'http://" - + self.host + "'.") - - def create_json_errormessage(self): - """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.endpoint_url + - "', but metric could not be found. " + - "Maybe your model of Envoy doesn't " + - "support the requested metric.") - - async def production(self): - """Call API and parse production values from response""" - try: - raw_json = await self.call_api() - if self.endpoint_type == "PC": - production = raw_json["production"][1]["wNow"] - else: - production = raw_json["wattsNow"] - return int(production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def consumption(self): - """Call API and parse consumption values from response""" - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = await self.call_api() - consumption = raw_json["consumption"][0]["wNow"] - return int(consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def daily_production(self): - """Call API and parse todays production values from response""" - try: - raw_json = await self.call_api() - if self.endpoint_type == "PC": - daily_production = raw_json["production"][1]["whToday"] - else: - daily_production = raw_json["wattHoursToday"] - return int(daily_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def daily_consumption(self): - """Call API and parse todays consumption values from response""" - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = await self.call_api() - daily_consumption = raw_json["consumption"][0]["whToday"] - return int(daily_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def seven_days_production(self): - """Call API and parse the past seven days production values from the - response""" - try: - raw_json = await self.call_api() - if self.endpoint_type == "PC": - seven_days_production = raw_json["production"][1]["whLastSevenDays"] - else: - seven_days_production = raw_json["wattHoursSevenDays"] - return int(seven_days_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def seven_days_consumption(self): - """Call API and parse the past seven days consumption values from - the response""" - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = await self.call_api() - seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] - return int(seven_days_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def lifetime_production(self): - """Call API and parse the lifetime of production from response""" - try: - raw_json = await self.call_api() - if self.endpoint_type == "PC": - lifetime_production = raw_json["production"][1]["whLifetime"] - else: - lifetime_production = raw_json["wattHoursLifetime"] - return int(lifetime_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def lifetime_consumption(self): - """Call API and parse the lifetime of consumption from response""" - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = await self.call_api() - lifetime_consumption = raw_json["consumption"][0]["whLifetime"] - return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def inverters_production(self): - """Hit a different Envoy endpoint and get the production values for - individual inverters""" - if self.serial_number_last_six == "": - await self.get_serial_number() - try: - response = requests_sync.get( - "http://{}/api/v1/production/inverters" - .format(self.host), - auth=HTTPDigestAuth("envoy", - self.serial_number_last_six)) - response_dict = {} - for item in response.json(): - response_dict[item["serialNumber"]] = item["lastReportWatts"] - return response_dict - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): - return self.create_json_errormessage() - - def run_in_console(self): - """If running this module directly, print all the values in the - console.""" - print("Reading...") - loop = asyncio.get_event_loop() - results = loop.run_until_complete(asyncio.gather( - self.production(), - self.consumption(), - self.daily_production(), - self.daily_consumption(), - self.seven_days_production(), - self.seven_days_consumption(), - self.lifetime_production(), - self.lifetime_consumption(), - self.inverters_production())) - - print("production: {}".format(results[0])) - print("consumption: {}".format(results[1])) - print("daily_production: {}".format(results[2])) - print("daily_consumption: {}".format(results[3])) - print("seven_days_production: {}".format(results[4])) - print("seven_days_consumption: {}".format(results[5])) - print("lifetime_production: {}".format(results[6])) - print("lifetime_consumption: {}".format(results[7])) - print("inverters_production: {}".format(results[8])) - - -if __name__ == "__main__": - HOST = input("Enter the Envoy IP address or host name, " + - "or press enter to use 'envoy' as default: ") - if HOST == "": - HOST = "envoy" - - TESTREADER = EnvoyReader(HOST) - TESTREADER.run_in_console() +import requests +import sys +import json +import re + +PRODUCTION_REGEX = r'Currentl.*\s+\s*(\d+|\d+\.\d+)\s*(W|kW|MW)' +DAY_PRODUCTION_REGEX = r'Today\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +WEEK_PRODUCTION_REGEX = r'Past Week\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +LIFE_PRODUCTION_REGEX = r'Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' + +class EnvoyReader(): + # P0 for older Envoy model C, firmware before R3.9.xx, without local api json pages + # P for production data only (ie. Envoy model C, firmware R3.9.xx and later) + # PC for production and consumption data (ie. Envoy model S) + endpoint_type = "" + endpoint_url = "" + + message_consumption_not_available = "Consumption data not available for your Envoy device." + + def __init__(self, host): + self.host = host.lower() + + def detect_model(self): + self.endpoint_url = "http://{}/production.json".format(self.host) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200 and len(response.json()) == 3: + self.endpoint_type = "PC" # Envoy-S, Production and Consumption monitoring + return + else: + self.endpoint_url = "http://{}/api/v1/production".format(self.host) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P" # newer Envoy-C, production only + return + else: + self.endpoint_url = "http://{}/production".format(self.host) + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P0" # older firmware Envoy-C, production only + return + + self.endpoint_url = "" + raise RuntimeError( + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") + + def call_api(self): + # detection of endpoint if not already known + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + + response = requests.get(self.endpoint_url, timeout=30, allow_redirects=False) + if self.endpoint_type == "P" or self.endpoint_type == "PC": + return response.json() # these little Envoys have .json + if self.endpoint_type == "P0": + return response.text # these little Envoys have none... + + def create_connect_errormessage(self): + return ("Unable to connect to Envoy. Check that the device is up at 'http://" + self.host + "'.") + + def create_json_errormessage(self): + return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + + "Maybe your model of Envoy doesn't support the requested metric.") + + def production(self): + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + + try: + if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) + production = raw_json["production"][1]["wNow"] + else: + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + production = raw_json["wattsNow"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search(PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kW": + production = float(match.group(1))*1000 + else: + if match.group(2) == "mW": + production = float(match.group(1))*1000000 + else: + production = float(match.group(1)) + else: + raise RuntimeError("No match for production, check REGEX " + text) + return int(production) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def consumption(self): + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = EnvoyReader.call_api(self) + consumption = raw_json["consumption"][0]["wNow"] + return int(consumption) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def daily_production(self): + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + + try: + if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) + daily_production = raw_json["production"][1]["whToday"] + else: + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + daily_production = raw_json["wattHoursToday"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search(DAY_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + daily_production = float(match.group(1))*1000 + else: + if match.group(2) == "MWh": + daily_production = float(match.group(1))*1000000 + else: + daily_production = float(match.group(1)) + else: + raise RuntimeError("No match for Day production, check REGEX " + text) + return int(daily_production) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def daily_consumption(self): + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = EnvoyReader.call_api(self) + daily_consumption = raw_json["consumption"][0]["whToday"] + return int(daily_consumption) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def seven_days_production(self): + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + + try: + if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) + seven_days_production = raw_json["production"][1]["whLastSevenDays"] + else: + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + seven_days_production = raw_json["wattHoursSevenDays"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search(WEEK_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + seven_days_production = float(match.group(1))*1000 + else: + if match.group(2) == "MWh": + seven_days_production = float(match.group(1))*1000000 + else: + seven_days_production = float(match.group(1)) + else: + raise RuntimeError("No match for 7 Day production, check REGEX " + text) + return int(seven_days_production) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def seven_days_consumption(self): + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = EnvoyReader.call_api(self) + seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] + return int(seven_days_consumption) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def lifetime_production(self): + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + + try: + if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) + lifetime_production = raw_json["production"][1]["whLifetime"] + else: + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + lifetime_production = raw_json["wattHoursLifetime"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search(LIFE_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + lifetime_production = float(match.group(1))*1000 + else: + if match.group(2) == "MWh": + lifetime_production = float(match.group(1))*1000000 + else: + lifetime_production = float(match.group(1)) + else: + raise RuntimeError("No match for Lifetime production, check REGEX " + text) + return int(lifetime_production) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + def lifetime_consumption(self): + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = EnvoyReader.call_api(self) + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] + return int(lifetime_consumption) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + + +if __name__ == "__main__": + host = input("Enter the Envoy IP address or host name, " + + "or press enter to use 'envoy' as default: ") + if host == "": + host = "envoy" + + testreader = EnvoyReader(host) + print("production: {}".format(testreader.production())) + print("consumption: {}".format(testreader.consumption())) + print("daily_production: {}".format(testreader.daily_production())) + print("daily_consumption: {}".format(testreader.daily_consumption())) + print("seven_days_production: {}".format(testreader.seven_days_production())) + print("seven_days_consumption: {}".format(testreader.seven_days_consumption())) + print("lifetime_production: {}".format(testreader.lifetime_production())) + print("lifetime_consumption: {}".format(testreader.lifetime_consumption())) From a9f8d76ba36cd0dd453a1d42477f61d4e6184d44 Mon Sep 17 00:00:00 2001 From: dalklein Date: Fri, 21 Jun 2019 03:14:40 +0100 Subject: [PATCH 032/134] add support for older Envoy C, s/w =R3.9 which sport .json data reporting --- envoy_reader/envoy_reader.py | 273 +++++++++++++++++++++++++---------- 1 file changed, 197 insertions(+), 76 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 22d2784..818ddf7 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,13 +4,24 @@ import sys import json from requests.auth import HTTPDigestAuth -import requests as requests_sync -import requests_async as requests - +import requests +#import requests as requests_sync +#import requests_async as requests +import re + +PRODUCTION_REGEX = \ + r'Currentl.*\s+\s*(\d+|\d+\.\d+)\s*(W|kW|MW)' +DAY_PRODUCTION_REGEX = \ + r'Today\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +WEEK_PRODUCTION_REGEX = \ + r'Past Week\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +LIFE_PRODUCTION_REGEX = \ + r'Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' class EnvoyReader(): """Instance of EnvoyReader""" - # P for production data only (ie. Envoy model C) + # P0 for older Envoy model C, s/w < R3.9 no json pages + # P for production data only (ie. Envoy model C, s/w >= R3.9) # PC for production and consumption data (ie. Envoy model S) message_consumption_not_available = ("Consumption data not available for " @@ -22,27 +33,34 @@ def __init__(self, host): self.endpoint_url = "" self.serial_number_last_six = "" - async def detect_model(self): + def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" self.endpoint_url = "http://{}/production.json".format(self.host) - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and len(response.json()) == 3: - self.endpoint_type = "PC" - return - - self.endpoint_url = "http://{}/api/v1/production".format(self.host) response = requests.get( self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: - self.endpoint_type = "P" + if response.status_code == 200 and len(response.json()) == 3: + self.endpoint_type = "PC" # Envoy-S with consumption return + else: + self.endpoint_url = "http://{}/api/v1/production".format(self.host) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P" # Envoy-C, production only + return + else: + self.endpoint_url = "http://{}/production".format(self.host) + response = requests.get( + self.endpoint_url, timeout=30, allow_redirects=False) + if response.status_code == 200: + self.endpoint_type = "P0" # older Envoy-C + return self.endpoint_url = "" raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") async def get_serial_number(self): """Method to get last six digits of Envoy serial number for auth""" @@ -59,156 +77,243 @@ async def get_serial_number(self): # "Unable to find device serial number, " + # "this is needed to read inverter production.") - async def call_api(self): + def call_api(self): """Method to call the Envoy API""" - # detection of endpoint + # detection of endpoint if not already known if self.endpoint_type == "": - await self.detect_model() + self.detect_model() - response = await requests.get( + response = requests.get( self.endpoint_url, timeout=30, allow_redirects=False) - return response.json() + if self.endpoint_type == "P" or self.endpoint_type == "PC": + return response.json() # these Envoys have .json + if self.endpoint_type == "P0": + return response.text # these Envoys have .html def create_connect_errormessage(self): """Create error message if unable to connect to Envoy""" return ("Unable to connect to Envoy. " + - "Check that the device is up at 'http://" + "Check that the device is up at 'http://" + self.host + "'.") def create_json_errormessage(self): """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.endpoint_url + + return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + "Maybe your model of Envoy doesn't " + "support the requested metric.") - async def production(self): + def production(self): """Call API and parse production values from response""" + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + try: - raw_json = await self.call_api() if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) production = raw_json["production"][1]["wNow"] else: - production = raw_json["wattsNow"] + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + production = raw_json["wattsNow"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search( + PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kW": + production = float(match.group(1))*1000 + else: + if match.group(2) == "mW": + production = float( + match.group(1))*1000000 + else: + production = float(match.group(1)) + else: + raise RuntimeError( + "No match for production, check REGEX " + + text) return int(production) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def consumption(self): + def consumption(self): """Call API and parse consumption values from response""" - if self.endpoint_type == "P": + if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = EnvoyReader.call_api(self) consumption = raw_json["consumption"][0]["wNow"] return int(consumption) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def daily_production(self): + def daily_production(self): """Call API and parse todays production values from response""" + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + try: - raw_json = await self.call_api() if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) daily_production = raw_json["production"][1]["whToday"] else: - daily_production = raw_json["wattHoursToday"] + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + daily_production = raw_json["wattHoursToday"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search( + DAY_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + daily_production = float( + match.group(1))*1000 + else: + if match.group(2) == "MWh": + daily_production = float( + match.group(1))*1000000 + else: + daily_production = float( + match.group(1)) + else: + raise RuntimeError( + "No match for Day production, " + "check REGEX " + + text) return int(daily_production) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def daily_consumption(self): + def daily_consumption(self): """Call API and parse todays consumption values from response""" - if self.endpoint_type == "P": + if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = EnvoyReader.call_api(self) daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def seven_days_production(self): + def seven_days_production(self): """Call API and parse the past seven days production values from the response""" + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + try: - raw_json = await self.call_api() if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) seven_days_production = raw_json["production"][1]["whLastSevenDays"] else: - seven_days_production = raw_json["wattHoursSevenDays"] + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + seven_days_production = raw_json["wattHoursSevenDays"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search( + WEEK_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + seven_days_production = float( + match.group(1))*1000 + else: + if match.group(2) == "MWh": + seven_days_production = float( + match.group(1))*1000000 + else: + seven_days_production = float( + match.group(1)) + else: + raise RuntimeError("No match for 7 Day production, " + "check REGEX " + text) return int(seven_days_production) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def seven_days_consumption(self): + def seven_days_consumption(self): """Call API and parse the past seven days consumption values from the response""" - if self.endpoint_type == "P": + if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = EnvoyReader.call_api(self) seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def lifetime_production(self): + def lifetime_production(self): """Call API and parse the lifetime of production from response""" + if self.endpoint_type == "": + EnvoyReader.detect_model(self) + try: - raw_json = await self.call_api() if self.endpoint_type == "PC": + raw_json = EnvoyReader.call_api(self) lifetime_production = raw_json["production"][1]["whLifetime"] else: - lifetime_production = raw_json["wattHoursLifetime"] + if self.endpoint_type == "P": + raw_json = EnvoyReader.call_api(self) + lifetime_production = raw_json["wattHoursLifetime"] + else: + if self.endpoint_type == "P0": + text = EnvoyReader.call_api(self) + match = re.search( + LIFE_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + lifetime_production = float( + match.group(1))*1000 + else: + if match.group(2) == "MWh": + lifetime_production = float( + match.group(1))*1000000 + else: + lifetime_production = float( + match.group(1)) + else: + raise RuntimeError( + "No match for Lifetime production, " + "check REGEX " + text) return int(lifetime_production) except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - - async def lifetime_consumption(self): - """Call API and parse the lifetime of consumption from response""" - if self.endpoint_type == "P": - return self.message_consumption_not_available - - try: - raw_json = await self.call_api() - lifetime_consumption = raw_json["consumption"][0]["whLifetime"] - return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + return EnvoyReader.create_connect_errormessage(self) except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + return EnvoyReader.create_json_errormessage(self) - async def inverters_production(self): + def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" if self.serial_number_last_six == "": - await self.get_serial_number() +# await self.get_serial_number() + self.get_serial_number() try: response = requests_sync.get( "http://{}/api/v1/production/inverters" @@ -224,6 +329,21 @@ async def inverters_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): return self.create_json_errormessage() + def lifetime_consumption(self): + """Call API and parse the lifetime of consumption from response""" + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = EnvoyReader.call_api(self) + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] + return int(lifetime_consumption) + + except requests.exceptions.ConnectionError: + return EnvoyReader.create_connect_errormessage(self) + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return EnvoyReader.create_json_errormessage(self) + def run_in_console(self): """If running this module directly, print all the values in the console.""" @@ -248,7 +368,8 @@ def run_in_console(self): print("seven_days_consumption: {}".format(results[5])) print("lifetime_production: {}".format(results[6])) print("lifetime_consumption: {}".format(results[7])) - print("inverters_production: {}".format(results[8])) + print("inverters_production: {}".format(results[8])) + if __name__ == "__main__": From 3e2cdade11c3bb7c838715aa1f18b91bff3700f6 Mon Sep 17 00:00:00 2001 From: dalklein Date: Fri, 21 Jun 2019 03:42:51 +0100 Subject: [PATCH 033/134] corrected calls like EnvoyReader.whatever(self) to self.whatever() --- envoy_reader/__init__.py | 1 - envoy_reader/envoy_reader.py | 68 ++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 35 deletions(-) delete mode 100644 envoy_reader/__init__.py diff --git a/envoy_reader/__init__.py b/envoy_reader/__init__.py deleted file mode 100644 index d4f8526..0000000 --- a/envoy_reader/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from envoy_reader.envoy_reader import EnvoyReader diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 818ddf7..c7a7687 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -59,8 +59,8 @@ def detect_model(self): self.endpoint_url = "" raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + self.host + "'.") async def get_serial_number(self): """Method to get last six digits of Envoy serial number for auth""" @@ -93,12 +93,12 @@ def call_api(self): def create_connect_errormessage(self): """Create error message if unable to connect to Envoy""" return ("Unable to connect to Envoy. " + - "Check that the device is up at 'http://" + "Check that the device is up at 'http://" + self.host + "'.") def create_json_errormessage(self): """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.endpoint_url + + return ("Got a response from '" + self.endpoint_url + "', but metric could not be found. " + "Maybe your model of Envoy doesn't " + "support the requested metric.") @@ -137,9 +137,9 @@ def production(self): return int(production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def consumption(self): """Call API and parse consumption values from response""" @@ -152,26 +152,26 @@ def consumption(self): return int(consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def daily_production(self): """Call API and parse todays production values from response""" if self.endpoint_type == "": - EnvoyReader.detect_model(self) + self.detect_model() try: if self.endpoint_type == "PC": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() daily_production = raw_json["production"][1]["whToday"] else: if self.endpoint_type == "P": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P0": - text = EnvoyReader.call_api(self) + text = self.call_api() match = re.search( DAY_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -193,9 +193,9 @@ def daily_production(self): return int(daily_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def daily_consumption(self): """Call API and parse todays consumption values from response""" @@ -203,32 +203,32 @@ def daily_consumption(self): return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def seven_days_production(self): """Call API and parse the past seven days production values from the response""" if self.endpoint_type == "": - EnvoyReader.detect_model(self) + self.detect_model() try: if self.endpoint_type == "PC": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() seven_days_production = raw_json["production"][1]["whLastSevenDays"] else: if self.endpoint_type == "P": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P0": - text = EnvoyReader.call_api(self) + text = self.call_api() match = re.search( WEEK_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -248,9 +248,9 @@ def seven_days_production(self): return int(seven_days_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def seven_days_consumption(self): """Call API and parse the past seven days consumption values from @@ -259,31 +259,31 @@ def seven_days_consumption(self): return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def lifetime_production(self): """Call API and parse the lifetime of production from response""" if self.endpoint_type == "": - EnvoyReader.detect_model(self) + self.detect_model() try: if self.endpoint_type == "PC": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() lifetime_production = raw_json["production"][1]["whLifetime"] else: if self.endpoint_type == "P": - raw_json = EnvoyReader.call_api(self) + raw_json = self.call_api() lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P0": - text = EnvoyReader.call_api(self) + text = self.call_api() match = re.search( LIFE_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -304,9 +304,9 @@ def lifetime_production(self): return int(lifetime_production) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def inverters_production(self): """Hit a different Envoy endpoint and get the production values for @@ -340,9 +340,9 @@ def lifetime_consumption(self): return int(lifetime_consumption) except requests.exceptions.ConnectionError: - return EnvoyReader.create_connect_errormessage(self) + return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return EnvoyReader.create_json_errormessage(self) + return self.create_json_errormessage() def run_in_console(self): """If running this module directly, print all the values in the From 14ea31387bd7c0a077960f52c67424fc131ce495 Mon Sep 17 00:00:00 2001 From: dalklein <49771139+dalklein@users.noreply.github.com> Date: Thu, 20 Jun 2019 22:54:59 -0400 Subject: [PATCH 034/134] Update envoy_reader.py This works with 0.94.4 homeassistant and older Envoy C, with R3.7 s/w (no .json data). I'm not sure how to use the async requests working, so the import and await statements need fixing. Otherwise this should still work with newer Envoys, but please test it. the envoy_reader.py file is changed obviously, and the README.md file is also updated. Thanks! --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c7a7687..98b838f 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -368,7 +368,7 @@ def run_in_console(self): print("seven_days_consumption: {}".format(results[5])) print("lifetime_production: {}".format(results[6])) print("lifetime_consumption: {}".format(results[7])) - print("inverters_production: {}".format(results[8])) + print("inverters_production: {}".format(results[8])) From e3d4a02844bbf551b6a1843c0e2f9827ee8ea3bb Mon Sep 17 00:00:00 2001 From: dalklein Date: Fri, 21 Jun 2019 14:01:09 +0100 Subject: [PATCH 035/134] coorected my local install issue for requests_async, and updated code accordingly todo: have an issue with asyncio somehow not returning data yet. --- envoy_reader/envoy_reader.py | 69 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 98b838f..d2cec87 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,9 +4,9 @@ import sys import json from requests.auth import HTTPDigestAuth -import requests -#import requests as requests_sync -#import requests_async as requests +#import requests +import requests as requests_sync +import requests_async as requests import re PRODUCTION_REGEX = \ @@ -33,25 +33,25 @@ def __init__(self, host): self.endpoint_url = "" self.serial_number_last_six = "" - def detect_model(self): + async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" self.endpoint_url = "http://{}/production.json".format(self.host) - response = requests.get( + response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200 and len(response.json()) == 3: self.endpoint_type = "PC" # Envoy-S with consumption return else: self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = requests.get( + response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P" # Envoy-C, production only return else: self.endpoint_url = "http://{}/production".format(self.host) - response = requests.get( + response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P0" # older Envoy-C @@ -77,13 +77,13 @@ async def get_serial_number(self): # "Unable to find device serial number, " + # "this is needed to read inverter production.") - def call_api(self): + async def call_api(self): """Method to call the Envoy API""" # detection of endpoint if not already known if self.endpoint_type == "": - self.detect_model() + await self.detect_model() - response = requests.get( + response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) if self.endpoint_type == "P" or self.endpoint_type == "PC": return response.json() # these Envoys have .json @@ -103,22 +103,22 @@ def create_json_errormessage(self): "Maybe your model of Envoy doesn't " + "support the requested metric.") - def production(self): + async def production(self): """Call API and parse production values from response""" if self.endpoint_type == "": EnvoyReader.detect_model(self) try: if self.endpoint_type == "PC": - raw_json = EnvoyReader.call_api(self) + raw_json = await self.call_api() production = raw_json["production"][1]["wNow"] else: if self.endpoint_type == "P": - raw_json = EnvoyReader.call_api(self) + raw_json = await self.call_api() production = raw_json["wattsNow"] else: if self.endpoint_type == "P0": - text = EnvoyReader.call_api(self) + text = await self.call_api() match = re.search( PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -141,13 +141,13 @@ def production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def consumption(self): + async def consumption(self): """Call API and parse consumption values from response""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = EnvoyReader.call_api(self) + raw_json = await self.call_api() consumption = raw_json["consumption"][0]["wNow"] return int(consumption) @@ -156,22 +156,22 @@ def consumption(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def daily_production(self): + async def daily_production(self): """Call API and parse todays production values from response""" if self.endpoint_type == "": self.detect_model() try: if self.endpoint_type == "PC": - raw_json = self.call_api() + raw_json = await self.call_api() daily_production = raw_json["production"][1]["whToday"] else: if self.endpoint_type == "P": - raw_json = self.call_api() + raw_json = await self.call_api() daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P0": - text = self.call_api() + text = await self.call_api() match = re.search( DAY_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -197,13 +197,13 @@ def daily_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def daily_consumption(self): + async def daily_consumption(self): """Call API and parse todays consumption values from response""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = self.call_api() + raw_json = await self.call_api() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) @@ -212,7 +212,7 @@ def daily_consumption(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def seven_days_production(self): + async def seven_days_production(self): """Call API and parse the past seven days production values from the response""" if self.endpoint_type == "": @@ -220,15 +220,15 @@ def seven_days_production(self): try: if self.endpoint_type == "PC": - raw_json = self.call_api() + raw_json = await self.call_api() seven_days_production = raw_json["production"][1]["whLastSevenDays"] else: if self.endpoint_type == "P": - raw_json = self.call_api() + raw_json = await self.call_api() seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P0": - text = self.call_api() + text = await self.call_api() match = re.search( WEEK_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -252,14 +252,14 @@ def seven_days_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def seven_days_consumption(self): + async def seven_days_consumption(self): """Call API and parse the past seven days consumption values from the response""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = self.call_api() + raw_json = await self.call_api() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) @@ -268,22 +268,22 @@ def seven_days_consumption(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def lifetime_production(self): + async def lifetime_production(self): """Call API and parse the lifetime of production from response""" if self.endpoint_type == "": self.detect_model() try: if self.endpoint_type == "PC": - raw_json = self.call_api() + raw_json = await self.call_api() lifetime_production = raw_json["production"][1]["whLifetime"] else: if self.endpoint_type == "P": - raw_json = self.call_api() + raw_json = await self.call_api() lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P0": - text = self.call_api() + text = await self.call_api() match = re.search( LIFE_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -308,12 +308,11 @@ def lifetime_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() - def inverters_production(self): + async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" if self.serial_number_last_six == "": -# await self.get_serial_number() - self.get_serial_number() + await self.get_serial_number() try: response = requests_sync.get( "http://{}/api/v1/production/inverters" From efccb3bff731d47f8d71410cd6df26e07b7999f7 Mon Sep 17 00:00:00 2001 From: dalklein Date: Fri, 21 Jun 2019 14:15:20 +0100 Subject: [PATCH 036/134] cleanup, still asyncio issue, may need fixing elsewhere in my installation --- envoy_reader/envoy_reader.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index d2cec87..0c88436 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,7 +4,6 @@ import sys import json from requests.auth import HTTPDigestAuth -#import requests import requests as requests_sync import requests_async as requests import re @@ -308,6 +307,21 @@ async def lifetime_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError): return self.create_json_errormessage() + async def lifetime_consumption(self): + """Call API and parse the lifetime of consumption from response""" + if self.endpoint_type == "P" or self.endpoint_type == "P0": + return self.message_consumption_not_available + + try: + raw_json = await self.call_api() + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] + return int(lifetime_consumption) + + except requests.exceptions.ConnectionError: + return self.create_connect_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError): + return self.create_json_errormessage() + async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" @@ -328,21 +342,6 @@ async def inverters_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): return self.create_json_errormessage() - def lifetime_consumption(self): - """Call API and parse the lifetime of consumption from response""" - if self.endpoint_type == "P" or self.endpoint_type == "P0": - return self.message_consumption_not_available - - try: - raw_json = EnvoyReader.call_api(self) - lifetime_consumption = raw_json["consumption"][0]["whLifetime"] - return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() - def run_in_console(self): """If running this module directly, print all the values in the console.""" @@ -370,7 +369,6 @@ def run_in_console(self): print("inverters_production: {}".format(results[8])) - if __name__ == "__main__": HOST = input("Enter the Envoy IP address or host name, " + "or press enter to use 'envoy' as default: ") From 894542d1d12142c00986990494b1eaa1f3c48151 Mon Sep 17 00:00:00 2001 From: dalklein Date: Fri, 21 Jun 2019 14:28:53 +0100 Subject: [PATCH 037/134] added gtdiehl's empty string check in pulling serial numbers for Envoy C --- envoy_reader/envoy_reader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 0c88436..e4854e2 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -67,8 +67,9 @@ async def get_serial_number(self): response = await requests.get( "http://{}/info.xml".format(self.host), timeout=10, allow_redirects=False) - sn = response.text.split("")[1].split("")[0][-6:] - self.serial_number_last_six = sn + if len(response.text) > 0: + sn = response.text.split("")[1].split("")[0][-6:] + self.serial_number_last_six = sn except requests.exceptions.ConnectionError: return self.create_connect_errormessage() # except From 2fd9116df092815e0f5463c6edb45e28b3b361cd Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Fri, 21 Jun 2019 15:03:58 -0700 Subject: [PATCH 038/134] Username and Pass for testing --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 63ebbca..e33f764 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -214,8 +214,8 @@ async def inverters_production(self): response = requests_sync.get( "http://{}/api/v1/production/inverters" .format(self.host), - auth=HTTPDigestAuth("envoy", - self.serial_number_last_six)) + auth=HTTPDigestAuth("installer", + "j53f34e9")) response_dict = {} for item in response.json(): response_dict[item["serialNumber"]] = item["lastReportWatts"] From a2795d57719cb167c1f7e6c8d4cd1e373f7f1bd0 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Fri, 21 Jun 2019 15:06:16 -0700 Subject: [PATCH 039/134] Revert "Username and Pass for testing" This reverts commit 2fd9116df092815e0f5463c6edb45e28b3b361cd. --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index e33f764..63ebbca 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -214,8 +214,8 @@ async def inverters_production(self): response = requests_sync.get( "http://{}/api/v1/production/inverters" .format(self.host), - auth=HTTPDigestAuth("installer", - "j53f34e9")) + auth=HTTPDigestAuth("envoy", + self.serial_number_last_six)) response_dict = {} for item in response.json(): response_dict[item["serialNumber"]] = item["lastReportWatts"] From 9832ba81ca80172fbc641fb9371488f9645466c1 Mon Sep 17 00:00:00 2001 From: dalklein Date: Sat, 22 Jun 2019 11:07:17 +0100 Subject: [PATCH 040/134] added awaits, solved issue with async requests in added code for reading older Envoy --- envoy_reader/envoy_reader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index e4854e2..1482aa0 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -106,7 +106,7 @@ def create_json_errormessage(self): async def production(self): """Call API and parse production values from response""" if self.endpoint_type == "": - EnvoyReader.detect_model(self) + await self.detect_model() try: if self.endpoint_type == "PC": @@ -159,7 +159,7 @@ async def consumption(self): async def daily_production(self): """Call API and parse todays production values from response""" if self.endpoint_type == "": - self.detect_model() + await self.detect_model() try: if self.endpoint_type == "PC": @@ -216,7 +216,7 @@ async def seven_days_production(self): """Call API and parse the past seven days production values from the response""" if self.endpoint_type == "": - self.detect_model() + await self.detect_model() try: if self.endpoint_type == "PC": @@ -271,7 +271,7 @@ async def seven_days_consumption(self): async def lifetime_production(self): """Call API and parse the lifetime of production from response""" if self.endpoint_type == "": - self.detect_model() + await self.detect_model() try: if self.endpoint_type == "PC": From 4650abfd84ac219d94b2d6dd35a5a41e748044c6 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Thu, 18 Jul 2019 13:33:17 -0700 Subject: [PATCH 041/134] User configurable Usename and Password Changed the inverter authentication to be user configurable. Or if not used will use the default of 'envoy' for the username and the last six digits of the Envoy's serial number for the password. --- envoy_reader/envoy_reader.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 5dff81e..b616b2e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -26,8 +26,10 @@ class EnvoyReader(): message_consumption_not_available = ("Consumption data not available for " "your Envoy device.") - def __init__(self, host): + def __init__(self, host, username="envoy", password=""): self.host = host.lower() + self.username = username + self.password = password self.endpoint_type = "" self.endpoint_url = "" self.serial_number_last_six = "" @@ -326,14 +328,23 @@ async def lifetime_consumption(self): async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" - if self.serial_number_last_six == "": - await self.get_serial_number() + + """If a password was not given as an argument when instantiating + the EnvoyReader object than use the last six numbers of the serial + number as the password. Otherwise use the password argument value.""" + if self.password == "": + if self.serial_number_last_six == "": + await self.get_serial_number() + self.password = self.serial_number_last_six + else: + self.serial_number_last_six = self.password + try: response = requests_sync.get( "http://{}/api/v1/production/inverters" .format(self.host), - auth=HTTPDigestAuth("envoy", - self.serial_number_last_six)) + auth=HTTPDigestAuth(self.username, + self.password)) response_dict = {} for item in response.json(): response_dict[item["serialNumber"]] = item["lastReportWatts"] From 5625c1e432b1f3ce56bdf255ca353585957900fb Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Thu, 18 Jul 2019 17:57:55 -0500 Subject: [PATCH 042/134] move doc string below imports --- .gitignore | 3 +++ envoy_reader/__init__.py | 0 envoy_reader/envoy_reader.py | 5 +++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 envoy_reader/__init__.py diff --git a/.gitignore b/.gitignore index d5a6c7a..181282c 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,7 @@ pyvenv.cfg pip-selfcheck.json +.vscode + + # End of https://www.gitignore.io/api/python \ No newline at end of file diff --git a/envoy_reader/__init__.py b/envoy_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 5dff81e..be259db 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,5 +1,3 @@ -"""Module to read production and consumption values from an Enphase Envoy on - the local network""" import asyncio import sys import json @@ -8,6 +6,9 @@ import requests_async as requests import re +"""Module to read production and consumption values from an Enphase Envoy on + the local network""" + PRODUCTION_REGEX = \ r'Currentl.*\s+\s*(\d+|\d+\.\d+)\s*(W|kW|MW)' DAY_PRODUCTION_REGEX = \ diff --git a/setup.py b/setup.py index b8956d7..b4046fa 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.6", + version="0.8.1", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 55800899275062dbf2e18cf357d745a3784139e7 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Thu, 18 Jul 2019 19:44:37 -0500 Subject: [PATCH 043/134] add install_requires to setup.py --- setup.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index b4046fa..2fb6e85 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.8.1", + version="0.8.3", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", @@ -13,9 +13,14 @@ long_description_content_type="text/markdown", url="https://github.com/jesserizzo/envoy_reader", packages=setuptools.find_packages(), - classifiers=( + install_requires=[ + "asyncio == 3.4.3", + "requests == 2.22.0", + "requests_async == 0.6.2" + ], + classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - ), + ], ) From 79391e87016b41f1bd4ef70af3eb965cb6d01346 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Sat, 20 Jul 2019 12:13:04 -0500 Subject: [PATCH 044/134] tweak requirements versions --- envoy_reader/envoy_reader.py | 9 +++++---- setup.py | 7 +++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index be259db..8c7dc4c 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -18,6 +18,7 @@ LIFE_PRODUCTION_REGEX = \ r'Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' + class EnvoyReader(): """Instance of EnvoyReader""" # P0 for older Envoy model C, s/w < R3.9 no json pages @@ -67,7 +68,7 @@ async def get_serial_number(self): try: response = await requests.get( "http://{}/info.xml".format(self.host), - timeout=10, allow_redirects=False) + timeout=30, allow_redirects=False) if len(response.text) > 0: sn = response.text.split("")[1].split("")[0][-6:] self.serial_number_last_six = sn @@ -134,7 +135,7 @@ async def production(self): else: raise RuntimeError( "No match for production, check REGEX " - + text) + + text) return int(production) except requests.exceptions.ConnectionError: @@ -245,7 +246,7 @@ async def seven_days_production(self): match.group(1)) else: raise RuntimeError("No match for 7 Day production, " - "check REGEX " + text) + "check REGEX " + text) return int(seven_days_production) except requests.exceptions.ConnectionError: @@ -301,7 +302,7 @@ async def lifetime_production(self): else: raise RuntimeError( "No match for Lifetime production, " - "check REGEX " + text) + "check REGEX " + text) return int(lifetime_production) except requests.exceptions.ConnectionError: diff --git a/setup.py b/setup.py index 2fb6e85..cf2d14d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.8.3", + version="0.8.6", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", @@ -14,9 +14,8 @@ url="https://github.com/jesserizzo/envoy_reader", packages=setuptools.find_packages(), install_requires=[ - "asyncio == 3.4.3", - "requests == 2.22.0", - "requests_async == 0.6.2" + "requests >= 2.22.0", + "requests_async >= 0.6.0" ], classifiers=[ "Programming Language :: Python :: 3", From 37a7ea794c969a9184401d7a74d807b22b68341b Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Mon, 19 Aug 2019 17:18:20 -0700 Subject: [PATCH 045/134] Updated API and fixed logic error Added user input for authentication when running from console. Removed unused import sys Exposed the Last Reported date from each inverter --- .idea/.gitignore | 3 +++ .idea/envoy_reader.iml | 12 +++++++++ .../inspectionProfiles/profiles_settings.xml | 6 +++++ .idea/misc.xml | 4 +++ .idea/modules.xml | 9 +++++++ .idea/vcs.xml | 6 +++++ envoy_reader/envoy_reader.py | 26 ++++++++++++++----- 7 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/envoy_reader.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..0e40fe8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ + +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/envoy_reader.iml b/.idea/envoy_reader.iml new file mode 100644 index 0000000..a5182be --- /dev/null +++ b/.idea/envoy_reader.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a2e120d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..05d471b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index b616b2e..f67bee2 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,8 +1,9 @@ """Module to read production and consumption values from an Enphase Envoy on the local network""" import asyncio -import sys import json +import time + from requests.auth import HTTPDigestAuth import requests as requests_sync import requests_async as requests @@ -336,9 +337,7 @@ async def inverters_production(self): if self.serial_number_last_six == "": await self.get_serial_number() self.password = self.serial_number_last_six - else: - self.serial_number_last_six = self.password - + try: response = requests_sync.get( "http://{}/api/v1/production/inverters" @@ -347,7 +346,8 @@ async def inverters_production(self): self.password)) response_dict = {} for item in response.json(): - response_dict[item["serialNumber"]] = item["lastReportWatts"] + response_dict[item["serialNumber"]] = [item["lastReportWatts"], + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] return response_dict except requests.exceptions.ConnectionError: return self.create_connect_errormessage() @@ -384,8 +384,22 @@ def run_in_console(self): if __name__ == "__main__": HOST = input("Enter the Envoy IP address or host name, " + "or press enter to use 'envoy' as default: ") + + USERNAME = input("Enter the Username for Inverter data authentication, " + + "or press enter to use 'envoy' as default: ") + + PASSWORD = input("Enter the Password for Inverter data authentication, " + + "or press enter to use the default password: ") + if HOST == "": HOST = "envoy" - TESTREADER = EnvoyReader(HOST) + if USERNAME == "": + USERNAME = "envoy" + + if PASSWORD == "": + TESTREADER = EnvoyReader(HOST, USERNAME) + else: + TESTREADER = EnvoyReader(HOST, USERNAME, PASSWORD) + TESTREADER.run_in_console() From 8fd3f94e8826d9eb15806e5661311be7b08242b1 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:05:48 -0700 Subject: [PATCH 046/134] Delete .gitignore --- .idea/.gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 0e40fe8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# Default ignored files -/workspace.xml \ No newline at end of file From 264c21fd53be453ecdf0876775b5676cc3d73f3e Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:06:03 -0700 Subject: [PATCH 047/134] Delete profiles_settings.xml --- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From 2b1c2a81b075e978a2c7a28cde370b38efad121d Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:06:12 -0700 Subject: [PATCH 048/134] Delete envoy_reader.iml --- .idea/envoy_reader.iml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .idea/envoy_reader.iml diff --git a/.idea/envoy_reader.iml b/.idea/envoy_reader.iml deleted file mode 100644 index a5182be..0000000 --- a/.idea/envoy_reader.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From 050304401230b7183d6a017d457eb41756aafc14 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:06:20 -0700 Subject: [PATCH 049/134] Delete misc.xml --- .idea/misc.xml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .idea/misc.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index a2e120d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file From fdfed94f24213ee7891c27ecbc1dbe86f066ba85 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:06:38 -0700 Subject: [PATCH 050/134] Delete modules.xml --- .idea/modules.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .idea/modules.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 05d471b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From 509bcce3813dd4faf6937f761af27b1f3480cb72 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Wed, 28 Aug 2019 19:06:47 -0700 Subject: [PATCH 051/134] Delete vcs.xml --- .idea/vcs.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 4c829cd1b7e18cc98a5fcaae9ff8deef872ee279 Mon Sep 17 00:00:00 2001 From: Dennis de Greef Date: Fri, 8 Nov 2019 18:20:15 +0100 Subject: [PATCH 052/134] Fix support for Envoy 'P' models that do not have consumption capabilities --- envoy_reader/envoy_reader.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 8c7dc4c..03ec967 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -145,6 +145,8 @@ async def production(self): async def consumption(self): """Call API and parse consumption values from response""" + if self.endpoint_type == "": + await self.detect_model() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -201,6 +203,8 @@ async def daily_production(self): async def daily_consumption(self): """Call API and parse todays consumption values from response""" + if self.endpoint_type == "": + await self.detect_model() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -257,6 +261,8 @@ async def seven_days_production(self): async def seven_days_consumption(self): """Call API and parse the past seven days consumption values from the response""" + if self.endpoint_type == "": + await self.detect_model() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -312,6 +318,8 @@ async def lifetime_production(self): async def lifetime_consumption(self): """Call API and parse the lifetime of consumption from response""" + if self.endpoint_type == "": + await self.detect_model() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available From ea1e81805589587182b42a266bf8d150faa68f96 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Sun, 17 Nov 2019 07:31:42 -0600 Subject: [PATCH 053/134] increment minor version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cf2d14d..81fac21 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.8.6", + version="0.10.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From c339923a2f6ca5878114d14397626f0a1cc53378 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Mon, 16 Dec 2019 15:07:06 -0800 Subject: [PATCH 054/134] Log message when login fails --- envoy_reader/envoy_reader.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 2f26bbc..08b5a90 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -354,11 +354,14 @@ async def inverters_production(self): .format(self.host), auth=HTTPDigestAuth(self.username, self.password)) - response_dict = {} - for item in response.json(): - response_dict[item["serialNumber"]] = [item["lastReportWatts"], - time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] - return response_dict + if response is not None and response.status_code != 401: + response_dict = {} + for item in response.json(): + response_dict[item["serialNumber"]] = [item["lastReportWatts"], + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] + return response_dict + else: + raise Exception(f'Authentication failed for Enphase Envoy: {self.host}') except requests.exceptions.ConnectionError: return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): From 8a61300c8314c4e077afe7fa1a141f960ff405fd Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Mon, 16 Dec 2019 15:48:00 -0800 Subject: [PATCH 055/134] Changed exception to requests raise_for_status() --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 08b5a90..e465ffe 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -361,7 +361,7 @@ async def inverters_production(self): time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] return response_dict else: - raise Exception(f'Authentication failed for Enphase Envoy: {self.host}') + response.raise_for_status() except requests.exceptions.ConnectionError: return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): From a0d17dcf018c774d9ea13618c17836f0e424bae2 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Thu, 19 Dec 2019 14:24:37 -0800 Subject: [PATCH 056/134] Change production retrieval Since there are two different Envoy-S models, one that has only inverters and one with inverters and CTs (eim). The production method should retrieve watts from the inverters as all Envoy-S will have inverters. Closes #14 --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 2f26bbc..45535de 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -116,7 +116,7 @@ async def production(self): try: if self.endpoint_type == "PC": raw_json = await self.call_api() - production = raw_json["production"][1]["wNow"] + production = raw_json["production"][0]["wNow"] else: if self.endpoint_type == "P": raw_json = await self.call_api() From b8c8b02f2f36be6af4924d2dbfbb70f86df22329 Mon Sep 17 00:00:00 2001 From: Jesse Rizzo Date: Sat, 21 Dec 2019 16:20:57 -0600 Subject: [PATCH 057/134] increment version number for pypi upload --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81fac21..b77bf52 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.10.0", + version="0.11.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From da6b247c7e9b3ede79b16eb48c59328aeb1ee588 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Mon, 23 Dec 2019 14:53:20 -0800 Subject: [PATCH 058/134] Fallback to inverter wNow if eim not available --- envoy_reader/envoy_reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 45535de..3cf5551 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -116,7 +116,10 @@ async def production(self): try: if self.endpoint_type == "PC": raw_json = await self.call_api() - production = raw_json["production"][0]["wNow"] + try: + production = raw_json["production"][1]["wNow"] + except IndexError: + production = raw_json["production"][0]["wNow"] else: if self.endpoint_type == "P": raw_json = await self.call_api() From 2e49fcc9d9c2a7dd473a0381b148196a88e90c83 Mon Sep 17 00:00:00 2001 From: "jesse.rizzo" Date: Wed, 25 Dec 2019 10:15:04 -0600 Subject: [PATCH 059/134] increment version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b77bf52..d925d90 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.11.0", + version="0.12.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 191253cc8ab0cad0a9a72722ffea80caba7f4143 Mon Sep 17 00:00:00 2001 From: Dennis de Greef Date: Thu, 26 Dec 2019 13:41:09 +0100 Subject: [PATCH 060/134] Use better detection for PC type --- envoy_reader/envoy_reader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c14f398..63cf138 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -37,13 +37,17 @@ def __init__(self, host, username="envoy", password=""): self.endpoint_url = "" self.serial_number_last_six = "" + def hasProductionAndConsumption(self, json): + """Check if json has keys for both production and consumption""" + return "production" in json and "consumption" in json + async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" self.endpoint_url = "http://{}/production.json".format(self.host) response = await requests.get( self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and len(response.json()) >= 2: + if response.status_code == 200 and self.hasProductionAndConsumption(response.json()): self.endpoint_type = "PC" return else: From be02f86298e2171321b31eaedf05421a66611a3b Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Sat, 18 Jan 2020 12:40:00 -0600 Subject: [PATCH 061/134] Basic CI/CD (#24) --- .github/workflows/pythonapp.yml | 33 +++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/pythonapp.yml create mode 100644 requirements.txt diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml new file mode 100644 index 0000000..546de3c --- /dev/null +++ b/.github/workflows/pythonapp.yml @@ -0,0 +1,33 @@ +name: Python application + +on: + pull_request: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 . --count --exit-zero --max-complexity=10 --statistics +# - name: Test with pytest +# run: | +# pip install pytest +# pytest diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..059fb92 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests >= 2.22.0 +requests_async >= 0.6.0 From 62fcac73ef61f7e9e4504020550a215c0ff40511 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Sat, 22 Feb 2020 14:32:57 -0800 Subject: [PATCH 062/134] Added update for HA (#22) --- envoy_reader/envoy_reader.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 63cf138..ddad6f3 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -374,6 +374,31 @@ async def inverters_production(self): except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): return self.create_json_errormessage() + async def update(self): + """ + Single entry point for Home Assistant + """ + data = {} + + tasks = [ + self.production(), + self.consumption(), + self.daily_production(), + self.daily_consumption(), + self.seven_days_production(), + self.seven_days_consumption(), + self.lifetime_production(), + self.lifetime_consumption(), + self.inverters_production() + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + + for key, result in zip(tasks, results): + key = key.__name__ + data[key] = result + + return data + def run_in_console(self): """If running this module directly, print all the values in the console.""" From d07146c5a6d4815fcf899b5890960f3e0c74d338 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Sun, 3 May 2020 10:26:10 -0700 Subject: [PATCH 063/134] Replace sync inverter call with async (#29) * Replaced sync with async request * Fixed exception attribute * Removed commented out old code * Removed requests library import intervers_production() was only using requests, and has been replaced with the httpx library, so I removed the import of requests * Removed requests requirement Removed requirement of the requests library as it was replaced with httpx * Added latest httpx version Added latest stable release version of httpx to requirements * Removed unneeded import --- envoy_reader/envoy_reader.py | 17 +++++++++-------- requirements.txt | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ddad6f3..c383de9 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -2,10 +2,10 @@ import json import time -from requests.auth import HTTPDigestAuth -import requests as requests_sync import requests_async as requests import re +import httpx +import h11 """Module to read production and consumption values from an Enphase Envoy on the local network""" @@ -356,11 +356,10 @@ async def inverters_production(self): self.password = self.serial_number_last_six try: - response = requests_sync.get( - "http://{}/api/v1/production/inverters" - .format(self.host), - auth=HTTPDigestAuth(self.username, - self.password)) + async with httpx.AsyncClient() as client: + response = await client.get("http://{}/api/v1/production/inverters" + .format(self.host), + auth=httpx.DigestAuth(self.username, self.password)) if response is not None and response.status_code != 401: response_dict = {} for item in response.json(): @@ -369,10 +368,12 @@ async def inverters_production(self): return response_dict else: response.raise_for_status() - except requests.exceptions.ConnectionError: + except httpx.HTTPError: return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): return self.create_json_errormessage() + except h11.RemoteProtocolError: + await response.close() async def update(self): """ diff --git a/requirements.txt b/requirements.txt index 059fb92..bbc208f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests >= 2.22.0 requests_async >= 0.6.0 +httpx >= 0.12.1 \ No newline at end of file From 9abb7bb84a5bd6627e06c3ee1eb5b140b18d5d4a Mon Sep 17 00:00:00 2001 From: Jesse Rizzo Date: Mon, 4 May 2020 08:05:38 -0500 Subject: [PATCH 064/134] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d925d90..46e4117 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.12.0", + version="0.16.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 326860e29c01ff6d37f6004efb5b7fb05f8ea4bd Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Tue, 5 May 2020 00:11:15 -0700 Subject: [PATCH 065/134] Update setup.py Removed unused library and added new httpx library --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 46e4117..d3a9f57 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ url="https://github.com/jesserizzo/envoy_reader", packages=setuptools.find_packages(), install_requires=[ - "requests >= 2.22.0", + "httpx >= 0.12.1", "requests_async >= 0.6.0" ], classifiers=[ From 78137eb8491358212727e8d4bdd244ff78ee372c Mon Sep 17 00:00:00 2001 From: Jesse Rizzo Date: Sat, 9 May 2020 10:34:25 -0500 Subject: [PATCH 066/134] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d3a9f57..892b902 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.16.0", + version="0.16.1", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From a33ebe53d36547576122efc3e2fdb39ef8221025 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Thu, 16 Jul 2020 14:53:06 -0700 Subject: [PATCH 067/134] Increase timeout Increased timeout from default of 5secs to 30secs --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index e465ffe..ea9247b 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -351,7 +351,7 @@ async def inverters_production(self): try: response = requests_sync.get( "http://{}/api/v1/production/inverters" - .format(self.host), + .format(self.host), timeout=30, auth=HTTPDigestAuth(self.username, self.password)) if response is not None and response.status_code != 401: From 6c7ad30c7d0872074191af3d743b709af31c670c Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Thu, 16 Jul 2020 14:53:06 -0700 Subject: [PATCH 068/134] Increase timeout Increased timeout from default of 5secs to 30secs --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c383de9..9934179 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -358,7 +358,7 @@ async def inverters_production(self): try: async with httpx.AsyncClient() as client: response = await client.get("http://{}/api/v1/production/inverters" - .format(self.host), + .format(self.host), timeout=30, auth=httpx.DigestAuth(self.username, self.password)) if response is not None and response.status_code != 401: response_dict = {} From 7b9d159925c35564802610a0243dfc46220f2745 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 31 Oct 2020 16:24:50 -0700 Subject: [PATCH 069/134] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 892b902..75755db 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.16.1", + version="0.16.2", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 2d186dafa3f855b8ff6e1e561e9172f7296a145f Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 11 Nov 2020 15:52:44 -0800 Subject: [PATCH 070/134] Fixes issue #37 and Reduces HTTP calls --- envoy_reader/envoy_reader.py | 125 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 37 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 9934179..f5e1c19 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -19,6 +19,9 @@ LIFE_PRODUCTION_REGEX = \ r'Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +ENDPOINT_URL_PRODUCTION_JSON = "http://{}/production.json" +ENDPOINT_URL_PRODUCTION_V1 = "http://{}/api/v1/production" +ENDPOINT_URL_PRODUCTION = "http://{}/production" class EnvoyReader(): """Instance of EnvoyReader""" @@ -34,38 +37,52 @@ def __init__(self, host, username="envoy", password=""): self.username = username self.password = password self.endpoint_type = "" - self.endpoint_url = "" self.serial_number_last_six = "" + self.endpoint_production_json_results = "" + self.endpoint_production_v1_results = "" + self.isMeteringEnabled = False + self.isDataRetrieved = False def hasProductionAndConsumption(self, json): """Check if json has keys for both production and consumption""" return "production" in json and "consumption" in json + def hasMeteringSetup(self, json): + """Check if Active Count of Production CTs (eim) installed is greater than one""" + if json["production"][1]["activeCount"] > 0: + return True + else: + return False + + async def getData(self): + self.endpoint_production_json_results = await requests.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await requests.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + self.isDataRetrieved = True + async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" - self.endpoint_url = "http://{}/production.json".format(self.host) - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200 and self.hasProductionAndConsumption(response.json()): + if not self.isDataRetrieved: + await self.getData() + + if self.endpoint_production_json_results.status_code == 200 and self.hasProductionAndConsumption(self.endpoint_production_json_results.json()): + self.isMeteringEnabled = self.hasMeteringSetup(self.endpoint_production_json_results.json()) self.endpoint_type = "PC" return else: - self.endpoint_url = "http://{}/api/v1/production".format(self.host) - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: + if self.endpoint_production_v1_results.status_code == 200: self.endpoint_type = "P" # Envoy-C, production only return else: - self.endpoint_url = "http://{}/production".format(self.host) + endpoint_url = ENDPOINT_URL_PRODUCTION.format(self.host) response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) + endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P0" # older Envoy-C return - self.endpoint_url = "" raise RuntimeError( "Could not connect or determine Envoy model. " + "Check that the device is up at 'http://" + self.host + "'.") @@ -81,10 +98,6 @@ async def get_serial_number(self): self.serial_number_last_six = sn except requests.exceptions.ConnectionError: return self.create_connect_errormessage() - # except - # print( - # "Unable to find device serial number, " + - # "this is needed to read inverter production.") async def call_api(self): """Method to call the Envoy API""" @@ -92,11 +105,14 @@ async def call_api(self): if self.endpoint_type == "": await self.detect_model() - response = await requests.get( - self.endpoint_url, timeout=30, allow_redirects=False) + """Code path won't be used anymore""" if self.endpoint_type == "P" or self.endpoint_type == "PC": - return response.json() # these Envoys have .json + return self.endpoint_production_json_results.json() # these Envoys have .json + + """Leaving here to get data for older Envoys""" if self.endpoint_type == "P0": + response = await requests.get( + ENDPOINT_URL_PRODUCTION, timeout=30, allow_redirects=False) return response.text # these Envoys have .html def create_connect_errormessage(self): @@ -116,17 +132,19 @@ async def production(self): """Call API and parse production values from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() try: if self.endpoint_type == "PC": - raw_json = await self.call_api() - try: - production = raw_json["production"][1]["wNow"] - except IndexError: + raw_json = self.endpoint_production_json_results.json() + if self.isMeteringEnabled: production = raw_json["production"][0]["wNow"] + else: + production = raw_json["production"][1]["wNow"] else: if self.endpoint_type == "P": - raw_json = await self.call_api() + raw_json = self.endpoint_production_v1_results.json() production = raw_json["wattsNow"] else: if self.endpoint_type == "P0": @@ -157,11 +175,13 @@ async def consumption(self): """Call API and parse consumption values from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = self.endpoint_production_json_results.json() consumption = raw_json["consumption"][0]["wNow"] return int(consumption) @@ -174,14 +194,19 @@ async def daily_production(self): """Call API and parse todays production values from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() try: - if self.endpoint_type == "PC": - raw_json = await self.call_api() + if self.endpoint_type == "PC" and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() daily_production = raw_json["production"][1]["whToday"] + elif self.endpoint_type == "PC" and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P": - raw_json = await self.call_api() + raw_json = self.endpoint_production_v1_results.json() daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P0": @@ -215,11 +240,13 @@ async def daily_consumption(self): """Call API and parse todays consumption values from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = self.endpoint_production_json_results.json() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) @@ -233,14 +260,19 @@ async def seven_days_production(self): response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() try: - if self.endpoint_type == "PC": - raw_json = await self.call_api() + if self.endpoint_type == "PC" and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() seven_days_production = raw_json["production"][1]["whLastSevenDays"] + elif self.endpoint_type == "PC" and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P": - raw_json = await self.call_api() + raw_json = self.endpoint_production_v1_results.json() seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P0": @@ -273,11 +305,13 @@ async def seven_days_consumption(self): the response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = self.endpoint_production_json_results.json() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) @@ -290,14 +324,19 @@ async def lifetime_production(self): """Call API and parse the lifetime of production from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() try: - if self.endpoint_type == "PC": - raw_json = await self.call_api() + if self.endpoint_type == "PC" and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() lifetime_production = raw_json["production"][1]["whLifetime"] + elif self.endpoint_type == "PC" and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P": - raw_json = await self.call_api() + raw_json = self.endpoint_production_v1_results.json() lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P0": @@ -330,11 +369,13 @@ async def lifetime_consumption(self): """Call API and parse the lifetime of consumption from response""" if self.endpoint_type == "": await self.detect_model() + if not self.isDataRetrieved: + await self.getData() if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available try: - raw_json = await self.call_api() + raw_json = self.endpoint_production_json_results.json() lifetime_consumption = raw_json["consumption"][0]["whLifetime"] return int(lifetime_consumption) @@ -381,6 +422,11 @@ async def update(self): """ data = {} + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather( + self.getData() + )) + tasks = [ self.production(), self.consumption(), @@ -404,6 +450,11 @@ def run_in_console(self): """If running this module directly, print all the values in the console.""" print("Reading...") + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather( + self.getData() + )) + loop = asyncio.get_event_loop() results = loop.run_until_complete(asyncio.gather( self.production(), From 7c6afee0664cc60e6f6a3d379a4a363a76342fc9 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 11 Nov 2020 16:04:44 -0800 Subject: [PATCH 071/134] Fixed typo and added CT Status to console --- envoy_reader/envoy_reader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f5e1c19..1855fb7 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -139,9 +139,9 @@ async def production(self): if self.endpoint_type == "PC": raw_json = self.endpoint_production_json_results.json() if self.isMeteringEnabled: - production = raw_json["production"][0]["wNow"] - else: production = raw_json["production"][1]["wNow"] + else: + production = raw_json["production"][0]["wNow"] else: if self.endpoint_type == "P": raw_json = self.endpoint_production_v1_results.json() @@ -467,6 +467,7 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production())) + print("Metering (CT) Status: {}".format(self.isMeteringEnabled)) print("production: {}".format(results[0])) print("consumption: {}".format(results[1])) print("daily_production: {}".format(results[2])) From d384ed9343d75004993d8842e11492da4a6064da Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 12 Nov 2020 16:49:29 -0800 Subject: [PATCH 072/134] Fixed async error in update() when ran from HA --- envoy_reader/envoy_reader.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 1855fb7..7efc9a3 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -422,10 +422,7 @@ async def update(self): """ data = {} - loop = asyncio.get_event_loop() - loop.run_until_complete(asyncio.gather( - self.getData() - )) + await self.getData() tasks = [ self.production(), From 3bba15de20a79dd80d5c1a5d087206a227e2a933 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 17 Nov 2020 15:03:49 -0800 Subject: [PATCH 073/134] Removing debug msg output --- envoy_reader/envoy_reader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 7efc9a3..f6e75b5 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -464,7 +464,6 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production())) - print("Metering (CT) Status: {}".format(self.isMeteringEnabled)) print("production: {}".format(results[0])) print("consumption: {}".format(results[1])) print("daily_production: {}".format(results[2])) From bcb3c5c2fbe80da0216a80788148b7b8936cd552 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 21 Nov 2020 09:52:07 -0800 Subject: [PATCH 074/134] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 75755db..f59692f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.16.2", + version="0.17.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From c5c29a74601cc47081bb90338f96f9668c4dec07 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 18:13:48 -0800 Subject: [PATCH 075/134] Raise exceptions to be consumed by Home Assistant --- envoy_reader/envoy_reader.py | 93 ++++++++++-------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f6e75b5..6b1c2b6 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -3,6 +3,7 @@ import time import requests_async as requests +from requests_async.exceptions import HTTPError, RequestException, Timeout import re import httpx import h11 @@ -55,10 +56,13 @@ def hasMeteringSetup(self, json): return False async def getData(self): - self.endpoint_production_json_results = await requests.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await requests.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + try: + self.endpoint_production_json_results = await requests.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await requests.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + except (HTTPError, RequestException, Timeout): + raise self.isDataRetrieved = True async def detect_model(self): @@ -97,7 +101,7 @@ async def get_serial_number(self): sn = response.text.split("")[1].split("")[0][-6:] self.serial_number_last_six = sn except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + raise async def call_api(self): """Method to call the Envoy API""" @@ -123,7 +127,7 @@ def create_connect_errormessage(self): def create_json_errormessage(self): """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.endpoint_url + + return ("Got a response from '" + self.host + "', but metric could not be found. " + "Maybe your model of Envoy doesn't " + "support the requested metric.") @@ -166,10 +170,8 @@ async def production(self): + text) return int(production) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def consumption(self): """Call API and parse consumption values from response""" @@ -185,10 +187,8 @@ async def consumption(self): consumption = raw_json["consumption"][0]["wNow"] return int(consumption) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def daily_production(self): """Call API and parse todays production values from response""" @@ -231,10 +231,8 @@ async def daily_production(self): text) return int(daily_production) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def daily_consumption(self): """Call API and parse todays consumption values from response""" @@ -249,11 +247,8 @@ async def daily_consumption(self): raw_json = self.endpoint_production_json_results.json() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def seven_days_production(self): """Call API and parse the past seven days production values from the @@ -294,11 +289,8 @@ async def seven_days_production(self): raise RuntimeError("No match for 7 Day production, " "check REGEX " + text) return int(seven_days_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def seven_days_consumption(self): """Call API and parse the past seven days consumption values from @@ -314,11 +306,8 @@ async def seven_days_consumption(self): raw_json = self.endpoint_production_json_results.json() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def lifetime_production(self): """Call API and parse the lifetime of production from response""" @@ -359,11 +348,8 @@ async def lifetime_production(self): "No match for Lifetime production, " "check REGEX " + text) return int(lifetime_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def lifetime_consumption(self): """Call API and parse the lifetime of consumption from response""" @@ -378,11 +364,8 @@ async def lifetime_consumption(self): raw_json = self.endpoint_production_json_results.json() lifetime_consumption = raw_json["consumption"][0]["whLifetime"] return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for @@ -393,7 +376,10 @@ async def inverters_production(self): number as the password. Otherwise use the password argument value.""" if self.password == "": if self.serial_number_last_six == "": - await self.get_serial_number() + try: + await self.get_serial_number() + except requests.exceptions.ConnectionError: + raise self.password = self.serial_number_last_six try: @@ -409,39 +395,12 @@ async def inverters_production(self): return response_dict else: response.raise_for_status() - except httpx.HTTPError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): - return self.create_json_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, httpx.RemoteProtocolError): + raise except h11.RemoteProtocolError: await response.close() - - async def update(self): - """ - Single entry point for Home Assistant - """ - data = {} - - await self.getData() - - tasks = [ - self.production(), - self.consumption(), - self.daily_production(), - self.daily_consumption(), - self.seven_days_production(), - self.seven_days_consumption(), - self.lifetime_production(), - self.lifetime_consumption(), - self.inverters_production() - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - - for key, result in zip(tasks, results): - key = key.__name__ - data[key] = result - - return data + except httpx.HTTPError: + response.raise_for_status() def run_in_console(self): """If running this module directly, print all the values in the From db845df10be64a6aca441bcc1548b25c5159b1bc Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 18:14:23 -0800 Subject: [PATCH 076/134] Bumping version to 0.17.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f59692f..4a25254 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.17.0", + version="0.17.1", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 6f6db067a6c381b6f2e6d5ae1cfce4ca66648126 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 18:13:48 -0800 Subject: [PATCH 077/134] Raise exceptions and remove unused update() --- envoy_reader/envoy_reader.py | 93 ++++++++++-------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f6e75b5..6b1c2b6 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -3,6 +3,7 @@ import time import requests_async as requests +from requests_async.exceptions import HTTPError, RequestException, Timeout import re import httpx import h11 @@ -55,10 +56,13 @@ def hasMeteringSetup(self, json): return False async def getData(self): - self.endpoint_production_json_results = await requests.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await requests.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + try: + self.endpoint_production_json_results = await requests.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await requests.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + except (HTTPError, RequestException, Timeout): + raise self.isDataRetrieved = True async def detect_model(self): @@ -97,7 +101,7 @@ async def get_serial_number(self): sn = response.text.split("")[1].split("")[0][-6:] self.serial_number_last_six = sn except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() + raise async def call_api(self): """Method to call the Envoy API""" @@ -123,7 +127,7 @@ def create_connect_errormessage(self): def create_json_errormessage(self): """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.endpoint_url + + return ("Got a response from '" + self.host + "', but metric could not be found. " + "Maybe your model of Envoy doesn't " + "support the requested metric.") @@ -166,10 +170,8 @@ async def production(self): + text) return int(production) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def consumption(self): """Call API and parse consumption values from response""" @@ -185,10 +187,8 @@ async def consumption(self): consumption = raw_json["consumption"][0]["wNow"] return int(consumption) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def daily_production(self): """Call API and parse todays production values from response""" @@ -231,10 +231,8 @@ async def daily_production(self): text) return int(daily_production) - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def daily_consumption(self): """Call API and parse todays consumption values from response""" @@ -249,11 +247,8 @@ async def daily_consumption(self): raw_json = self.endpoint_production_json_results.json() daily_consumption = raw_json["consumption"][0]["whToday"] return int(daily_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def seven_days_production(self): """Call API and parse the past seven days production values from the @@ -294,11 +289,8 @@ async def seven_days_production(self): raise RuntimeError("No match for 7 Day production, " "check REGEX " + text) return int(seven_days_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def seven_days_consumption(self): """Call API and parse the past seven days consumption values from @@ -314,11 +306,8 @@ async def seven_days_consumption(self): raw_json = self.endpoint_production_json_results.json() seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] return int(seven_days_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def lifetime_production(self): """Call API and parse the lifetime of production from response""" @@ -359,11 +348,8 @@ async def lifetime_production(self): "No match for Lifetime production, " "check REGEX " + text) return int(lifetime_production) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def lifetime_consumption(self): """Call API and parse the lifetime of consumption from response""" @@ -378,11 +364,8 @@ async def lifetime_consumption(self): raw_json = self.endpoint_production_json_results.json() lifetime_consumption = raw_json["consumption"][0]["whLifetime"] return int(lifetime_consumption) - - except requests.exceptions.ConnectionError: - return self.create_connect_errormessage() except (json.decoder.JSONDecodeError, KeyError, IndexError): - return self.create_json_errormessage() + raise async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for @@ -393,7 +376,10 @@ async def inverters_production(self): number as the password. Otherwise use the password argument value.""" if self.password == "": if self.serial_number_last_six == "": - await self.get_serial_number() + try: + await self.get_serial_number() + except requests.exceptions.ConnectionError: + raise self.password = self.serial_number_last_six try: @@ -409,39 +395,12 @@ async def inverters_production(self): return response_dict else: response.raise_for_status() - except httpx.HTTPError: - return self.create_connect_errormessage() - except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError): - return self.create_json_errormessage() + except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, httpx.RemoteProtocolError): + raise except h11.RemoteProtocolError: await response.close() - - async def update(self): - """ - Single entry point for Home Assistant - """ - data = {} - - await self.getData() - - tasks = [ - self.production(), - self.consumption(), - self.daily_production(), - self.daily_consumption(), - self.seven_days_production(), - self.seven_days_consumption(), - self.lifetime_production(), - self.lifetime_consumption(), - self.inverters_production() - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - - for key, result in zip(tasks, results): - key = key.__name__ - data[key] = result - - return data + except httpx.HTTPError: + response.raise_for_status() def run_in_console(self): """If running this module directly, print all the values in the From b6ef9664e92d0b39512c0df8ad4a761eda099f4f Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 18:14:23 -0800 Subject: [PATCH 078/134] Bumping version to 0.17.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f59692f..4a25254 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.17.0", + version="0.17.1", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 4610d1af813c1506a3273b4aa53d7dc86152e6aa Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 22:20:07 -0800 Subject: [PATCH 079/134] Returning exception to console results --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6b1c2b6..2feb611 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -421,7 +421,7 @@ def run_in_console(self): self.seven_days_consumption(), self.lifetime_production(), self.lifetime_consumption(), - self.inverters_production())) + self.inverters_production(), return_exceptions=True)) print("production: {}".format(results[0])) print("consumption: {}".format(results[1])) From bf44eabb76b9018ccbc9f106e3826bbea62acdb9 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 3 Dec 2020 23:52:00 -0800 Subject: [PATCH 080/134] Return exception to console results --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 2feb611..c8832cf 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -408,7 +408,7 @@ def run_in_console(self): print("Reading...") loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather( - self.getData() + self.getData(), return_exceptions=True )) loop = asyncio.get_event_loop() From 573a04e314fff00594133d70b6a3aab9fb0daccb Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 5 Dec 2020 23:30:39 -0800 Subject: [PATCH 081/134] Removed deprecated requests_async library --- envoy_reader/envoy_reader.py | 36 +++++++++++++++++++----------------- setup.py | 3 +-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index c8832cf..3e6d797 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -2,8 +2,6 @@ import json import time -import requests_async as requests -from requests_async.exceptions import HTTPError, RequestException, Timeout import re import httpx import h11 @@ -57,11 +55,12 @@ def hasMeteringSetup(self, json): async def getData(self): try: - self.endpoint_production_json_results = await requests.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await requests.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) - except (HTTPError, RequestException, Timeout): + async with httpx.AsyncClient() as client: + self.endpoint_production_json_results = await client.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await client.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + except httpx.HTTPError: raise self.isDataRetrieved = True @@ -81,8 +80,9 @@ async def detect_model(self): return else: endpoint_url = ENDPOINT_URL_PRODUCTION.format(self.host) - response = await requests.get( - endpoint_url, timeout=30, allow_redirects=False) + async with httpx.AsyncClient() as client: + response = await client.get( + endpoint_url, timeout=30, allow_redirects=False) if response.status_code == 200: self.endpoint_type = "P0" # older Envoy-C return @@ -94,13 +94,14 @@ async def detect_model(self): async def get_serial_number(self): """Method to get last six digits of Envoy serial number for auth""" try: - response = await requests.get( - "http://{}/info.xml".format(self.host), - timeout=30, allow_redirects=False) + async with httpx.AsyncClient() as client: + response = await client.get( + "http://{}/info.xml".format(self.host), + timeout=30, allow_redirects=False) if len(response.text) > 0: sn = response.text.split("")[1].split("")[0][-6:] self.serial_number_last_six = sn - except requests.exceptions.ConnectionError: + except httpx.ConnectionError: raise async def call_api(self): @@ -115,8 +116,9 @@ async def call_api(self): """Leaving here to get data for older Envoys""" if self.endpoint_type == "P0": - response = await requests.get( - ENDPOINT_URL_PRODUCTION, timeout=30, allow_redirects=False) + async with httpx.AsyncClient() as client: + response = await client.get( + ENDPOINT_URL_PRODUCTION, timeout=30, allow_redirects=False) return response.text # these Envoys have .html def create_connect_errormessage(self): @@ -378,7 +380,7 @@ async def inverters_production(self): if self.serial_number_last_six == "": try: await self.get_serial_number() - except requests.exceptions.ConnectionError: + except httpx.HTTPError: raise self.password = self.serial_number_last_six @@ -397,7 +399,7 @@ async def inverters_production(self): response.raise_for_status() except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, httpx.RemoteProtocolError): raise - except h11.RemoteProtocolError: + except httpx.RemoteProtocolError: await response.close() except httpx.HTTPError: response.raise_for_status() diff --git a/setup.py b/setup.py index 4a25254..76ec3c8 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,7 @@ url="https://github.com/jesserizzo/envoy_reader", packages=setuptools.find_packages(), install_requires=[ - "httpx >= 0.12.1", - "requests_async >= 0.6.0" + "httpx >= 0.12.1" ], classifiers=[ "Programming Language :: Python :: 3", From 490184c7ee31ddc59791a09f2532317617e092ae Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 5 Dec 2020 23:31:20 -0800 Subject: [PATCH 082/134] Bump version to 0.17.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 76ec3c8..80c6fc1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.17.1", + version="0.17.2", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 6f299bc9563e2c25c40cfda0ef196945c7fb0c1a Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 5 Dec 2020 23:37:32 -0800 Subject: [PATCH 083/134] Removing unused import h11 --- envoy_reader/envoy_reader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 3e6d797..14d8eab 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -4,7 +4,6 @@ import re import httpx -import h11 """Module to read production and consumption values from an Enphase Envoy on the local network""" From 006ec9720949c72c22a7cbade6a835ae9fd16fd3 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 7 Dec 2020 11:21:32 -0800 Subject: [PATCH 084/134] Fixed bug with older Envoy device Production URL --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 14d8eab..3e0e725 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -117,7 +117,7 @@ async def call_api(self): if self.endpoint_type == "P0": async with httpx.AsyncClient() as client: response = await client.get( - ENDPOINT_URL_PRODUCTION, timeout=30, allow_redirects=False) + ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) return response.text # these Envoys have .html def create_connect_errormessage(self): @@ -371,7 +371,7 @@ async def lifetime_consumption(self): async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" - + """If a password was not given as an argument when instantiating the EnvoyReader object than use the last six numbers of the serial number as the password. Otherwise use the password argument value.""" From 6a5c535aeaaec54dda96050d22336d7a06441ef3 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 7 Dec 2020 11:22:33 -0800 Subject: [PATCH 085/134] Check not retrieve inverter data older for Envoys --- envoy_reader/envoy_reader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 3e0e725..d7eeaae 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -372,6 +372,11 @@ async def inverters_production(self): """Hit a different Envoy endpoint and get the production values for individual inverters""" + if self.endpoint_type == "": + await self.detect_model() + if self.endpoint_type == "P0": + return "Inverter data not available for your Envoy device." + """If a password was not given as an argument when instantiating the EnvoyReader object than use the last six numbers of the serial number as the password. Otherwise use the password argument value.""" From 08bb904d8e0aaf9efac498c25aca01d0e19f3eee Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 7 Dec 2020 11:28:41 -0800 Subject: [PATCH 086/134] Aligned console output --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index d7eeaae..6f367ed 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -437,7 +437,7 @@ def run_in_console(self): print("seven_days_consumption: {}".format(results[5])) print("lifetime_production: {}".format(results[6])) print("lifetime_consumption: {}".format(results[7])) - print("inverters_production: {}".format(results[8])) + print("inverters_production: {}".format(results[8])) if __name__ == "__main__": From 3621d71e6af2aff1aca8e17c37b92f33f0f57c72 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 15 Dec 2020 07:47:01 -0800 Subject: [PATCH 087/134] Bump version to 0.18.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 80c6fc1..6c6c89d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.17.2", + version="0.18.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From e095d4a45cc31c80c9beaeeb99ca8bfaa302e687 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 15 Dec 2020 16:42:45 -0800 Subject: [PATCH 088/134] Moving IO operations to data gather methods --- envoy_reader/envoy_reader.py | 187 ++++++++++++++++------------------- 1 file changed, 83 insertions(+), 104 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6f367ed..3eb8b79 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -3,6 +3,7 @@ import time import re +import httpcore import httpx """Module to read production and consumption values from an Enphase Envoy on @@ -19,6 +20,7 @@ ENDPOINT_URL_PRODUCTION_JSON = "http://{}/production.json" ENDPOINT_URL_PRODUCTION_V1 = "http://{}/api/v1/production" +ENDPOINT_URL_PRODUCTION_INVERTERS = "http://{}/api/v1/production/inverters" ENDPOINT_URL_PRODUCTION = "http://{}/production" class EnvoyReader(): @@ -30,14 +32,17 @@ class EnvoyReader(): message_consumption_not_available = ("Consumption data not available for " "your Envoy device.") - def __init__(self, host, username="envoy", password=""): + def __init__(self, host, username="envoy", password="", inverters=False): self.host = host.lower() self.username = username self.password = password + self.get_inverters = inverters self.endpoint_type = "" self.serial_number_last_six = "" self.endpoint_production_json_results = "" self.endpoint_production_v1_results = "" + self.endpoint_production_inverters = "" + self.endpoint_production_results = "" self.isMeteringEnabled = False self.isDataRetrieved = False @@ -59,15 +64,44 @@ async def getData(self): ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) self.endpoint_production_v1_results = await client.get( ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_results = await client.get( + ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) except httpx.HTTPError: - raise + pass + + await self.detect_model() + + if(self.get_inverters): + """If a password was not given as an argument when instantiating + the EnvoyReader object than use the last six numbers of the serial + number as the password. Otherwise use the password argument value.""" + if self.password == "": + if self.serial_number_last_six == "": + try: + await self.get_serial_number() + except httpx.HTTPError: + raise + self.password = self.serial_number_last_six + try: + async with httpx.AsyncClient() as client: + response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), + timeout=30, auth=httpx.DigestAuth(self.username, self.password)) + if response is not None and response.status_code != 401: + self.endpoint_production_inverters = response + else: + response.raise_for_status() + except (httpx.RemoteProtocolError): + raise + except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): + await response.close() + except httpx.HTTPError: + response.raise_for_status() + self.isDataRetrieved = True async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" - if not self.isDataRetrieved: - await self.getData() if self.endpoint_production_json_results.status_code == 200 and self.hasProductionAndConsumption(self.endpoint_production_json_results.json()): self.isMeteringEnabled = self.hasMeteringSetup(self.endpoint_production_json_results.json()) @@ -103,23 +137,6 @@ async def get_serial_number(self): except httpx.ConnectionError: raise - async def call_api(self): - """Method to call the Envoy API""" - # detection of endpoint if not already known - if self.endpoint_type == "": - await self.detect_model() - - """Code path won't be used anymore""" - if self.endpoint_type == "P" or self.endpoint_type == "PC": - return self.endpoint_production_json_results.json() # these Envoys have .json - - """Leaving here to get data for older Envoys""" - if self.endpoint_type == "P0": - async with httpx.AsyncClient() as client: - response = await client.get( - ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) - return response.text # these Envoys have .html - def create_connect_errormessage(self): """Create error message if unable to connect to Envoy""" return ("Unable to connect to Envoy. " + @@ -134,11 +151,8 @@ def create_json_errormessage(self): "support the requested metric.") async def production(self): - """Call API and parse production values from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" try: if self.endpoint_type == "PC": @@ -153,7 +167,7 @@ async def production(self): production = raw_json["wattsNow"] else: if self.endpoint_type == "P0": - text = await self.call_api() + text = self.endpoint_production_results.text() match = re.search( PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -175,11 +189,10 @@ async def production(self): raise async def consumption(self): - """Call API and parse consumption values from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + + """Only return data if Envoy supports Consumption""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -192,11 +205,8 @@ async def consumption(self): raise async def daily_production(self): - """Call API and parse todays production values from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" try: if self.endpoint_type == "PC" and self.isMeteringEnabled: @@ -211,7 +221,7 @@ async def daily_production(self): daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P0": - text = await self.call_api() + text = self.endpoint_production_results.text() match = re.search( DAY_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -236,11 +246,10 @@ async def daily_production(self): raise async def daily_consumption(self): - """Call API and parse todays consumption values from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + + """Only return data if Envoy supports Consumption""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -252,12 +261,8 @@ async def daily_consumption(self): raise async def seven_days_production(self): - """Call API and parse the past seven days production values from the - response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" try: if self.endpoint_type == "PC" and self.isMeteringEnabled: @@ -272,7 +277,7 @@ async def seven_days_production(self): seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P0": - text = await self.call_api() + text = self.endpoint_production_results.text() match = re.search( WEEK_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -294,12 +299,10 @@ async def seven_days_production(self): raise async def seven_days_consumption(self): - """Call API and parse the past seven days consumption values from - the response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + + """Only return data if Envoy supports Consumption""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -311,11 +314,8 @@ async def seven_days_consumption(self): raise async def lifetime_production(self): - """Call API and parse the lifetime of production from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" try: if self.endpoint_type == "PC" and self.isMeteringEnabled: @@ -330,7 +330,7 @@ async def lifetime_production(self): lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P0": - text = await self.call_api() + text = self.endpoint_production_results.text() match = re.search( LIFE_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -353,11 +353,10 @@ async def lifetime_production(self): raise async def lifetime_consumption(self): - """Call API and parse the lifetime of consumption from response""" - if self.endpoint_type == "": - await self.detect_model() - if not self.isDataRetrieved: - await self.getData() + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + + """Only return data if Envoy supports Consumption""" if self.endpoint_type == "P" or self.endpoint_type == "P0": return self.message_consumption_not_available @@ -369,51 +368,28 @@ async def lifetime_consumption(self): raise async def inverters_production(self): - """Hit a different Envoy endpoint and get the production values for - individual inverters""" + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" - if self.endpoint_type == "": - await self.detect_model() + """Only return data if Envoy supports retrieving Inverter data""" if self.endpoint_type == "P0": return "Inverter data not available for your Envoy device." - - """If a password was not given as an argument when instantiating - the EnvoyReader object than use the last six numbers of the serial - number as the password. Otherwise use the password argument value.""" - if self.password == "": - if self.serial_number_last_six == "": - try: - await self.get_serial_number() - except httpx.HTTPError: - raise - self.password = self.serial_number_last_six + response_dict = {} try: - async with httpx.AsyncClient() as client: - response = await client.get("http://{}/api/v1/production/inverters" - .format(self.host), timeout=30, - auth=httpx.DigestAuth(self.username, self.password)) - if response is not None and response.status_code != 401: - response_dict = {} - for item in response.json(): - response_dict[item["serialNumber"]] = [item["lastReportWatts"], - time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] - return response_dict - else: - response.raise_for_status() - except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, httpx.RemoteProtocolError): - raise - except httpx.RemoteProtocolError: - await response.close() - except httpx.HTTPError: - response.raise_for_status() + for item in self.endpoint_production_inverters.json(): + response_dict[item["serialNumber"]] = [item["lastReportWatts"], time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] + except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + return None + + return response_dict def run_in_console(self): """If running this module directly, print all the values in the console.""" print("Reading...") loop = asyncio.get_event_loop() - loop.run_until_complete(asyncio.gather( + dataResults = loop.run_until_complete(asyncio.gather( self.getData(), return_exceptions=True )) @@ -437,7 +413,10 @@ def run_in_console(self): print("seven_days_consumption: {}".format(results[5])) print("lifetime_production: {}".format(results[6])) print("lifetime_consumption: {}".format(results[7])) - print("inverters_production: {}".format(results[8])) + if "401" in str(dataResults): + print("inverters_production: Unable to retrieve inverter data - Authentication failure") + else: + print("inverters_production: {}".format(results[8])) if __name__ == "__main__": @@ -457,8 +436,8 @@ def run_in_console(self): USERNAME = "envoy" if PASSWORD == "": - TESTREADER = EnvoyReader(HOST, USERNAME) + TESTREADER = EnvoyReader(HOST, USERNAME, inverters=True) else: - TESTREADER = EnvoyReader(HOST, USERNAME, PASSWORD) + TESTREADER = EnvoyReader(HOST, USERNAME, PASSWORD, inverters=True) TESTREADER.run_in_console() From e83edc98531295721fddfcb7bb8b93722bc94c89 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 15 Dec 2020 17:02:16 -0800 Subject: [PATCH 089/134] Remove isDataRetrieved flag not being used --- envoy_reader/envoy_reader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 3eb8b79..96116cb 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -44,7 +44,6 @@ def __init__(self, host, username="envoy", password="", inverters=False): self.endpoint_production_inverters = "" self.endpoint_production_results = "" self.isMeteringEnabled = False - self.isDataRetrieved = False def hasProductionAndConsumption(self, json): """Check if json has keys for both production and consumption""" @@ -97,8 +96,6 @@ async def getData(self): except httpx.HTTPError: response.raise_for_status() - self.isDataRetrieved = True - async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production""" From 28e34deb533d962d2b47767fab5b1b28a46fa343 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 15 Dec 2020 20:03:54 -0800 Subject: [PATCH 090/134] Fix typo calling text var not method --- envoy_reader/envoy_reader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 96116cb..43cd4d4 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -164,7 +164,7 @@ async def production(self): production = raw_json["wattsNow"] else: if self.endpoint_type == "P0": - text = self.endpoint_production_results.text() + text = self.endpoint_production_results.text match = re.search( PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -218,7 +218,7 @@ async def daily_production(self): daily_production = raw_json["wattHoursToday"] else: if self.endpoint_type == "P0": - text = self.endpoint_production_results.text() + text = self.endpoint_production_results.text match = re.search( DAY_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -274,7 +274,7 @@ async def seven_days_production(self): seven_days_production = raw_json["wattHoursSevenDays"] else: if self.endpoint_type == "P0": - text = self.endpoint_production_results.text() + text = self.endpoint_production_results.text match = re.search( WEEK_PRODUCTION_REGEX, text, re.MULTILINE) if match: @@ -327,7 +327,7 @@ async def lifetime_production(self): lifetime_production = raw_json["wattHoursLifetime"] else: if self.endpoint_type == "P0": - text = self.endpoint_production_results.text() + text = self.endpoint_production_results.text match = re.search( LIFE_PRODUCTION_REGEX, text, re.MULTILINE) if match: From 48c874ea4229ca97e7313f3d456b0f9a07d467a8 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 16 Dec 2020 11:15:17 -0800 Subject: [PATCH 091/134] Bump version to 0.18.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6c6c89d..213363c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.0", + version="0.18.1", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From a8d6c8207e5e3d79d1f765a241107629f1a5f11c Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 12:24:34 -0800 Subject: [PATCH 092/134] Adding retries when gathering inverter data --- envoy_reader/envoy_reader.py | 82 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 43cd4d4..01c95dc 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -57,44 +57,50 @@ def hasMeteringSetup(self, json): return False async def getData(self): - try: - async with httpx.AsyncClient() as client: - self.endpoint_production_json_results = await client.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await client.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_results = await client.get( - ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) - except httpx.HTTPError: - pass - - await self.detect_model() - if(self.get_inverters): - """If a password was not given as an argument when instantiating - the EnvoyReader object than use the last six numbers of the serial - number as the password. Otherwise use the password argument value.""" - if self.password == "": - if self.serial_number_last_six == "": - try: - await self.get_serial_number() - except httpx.HTTPError: - raise - self.password = self.serial_number_last_six - try: - async with httpx.AsyncClient() as client: - response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), - timeout=30, auth=httpx.DigestAuth(self.username, self.password)) - if response is not None and response.status_code != 401: - self.endpoint_production_inverters = response - else: - response.raise_for_status() - except (httpx.RemoteProtocolError): - raise - except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): - await response.close() - except httpx.HTTPError: - response.raise_for_status() + + try: + async with httpx.AsyncClient() as client: + self.endpoint_production_json_results = await client.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await client.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_results = await client.get( + ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) + except httpx.HTTPError: + pass + + await self.detect_model() + + if(self.get_inverters): + for i in range(0,3): + while True: + """If a password was not given as an argument when instantiating + the EnvoyReader object than use the last six numbers of the serial + number as the password. Otherwise use the password argument value.""" + if self.password == "": + if self.serial_number_last_six == "": + try: + await self.get_serial_number() + except httpx.HTTPError: + raise + self.password = self.serial_number_last_six + try: + async with httpx.AsyncClient() as client: + response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), + timeout=30, auth=httpx.DigestAuth(self.username, self.password)) + if response is not None and response.status_code != 401: + self.endpoint_production_inverters = response + else: + response.raise_for_status() + except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError) as err: + continue + except httpx.HTTPError: + response.raise_for_status() + break + break + if(i == 2): + raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) async def detect_model(self): """Method to determine if the Envoy supports consumption values or @@ -387,7 +393,7 @@ def run_in_console(self): print("Reading...") loop = asyncio.get_event_loop() dataResults = loop.run_until_complete(asyncio.gather( - self.getData(), return_exceptions=True + self.getData(), return_exceptions=False )) loop = asyncio.get_event_loop() From fddf1804bd1abd76a464c8ef83ca39d8bf149a69 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 12:25:02 -0800 Subject: [PATCH 093/134] Fixed typo --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 01c95dc..6958999 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -393,7 +393,7 @@ def run_in_console(self): print("Reading...") loop = asyncio.get_event_loop() dataResults = loop.run_until_complete(asyncio.gather( - self.getData(), return_exceptions=False + self.getData(), return_exceptions=True )) loop = asyncio.get_event_loop() From 8f6e02e47ffa11c9aa9999021db021fa2e3d7719 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 12:26:46 -0800 Subject: [PATCH 094/134] Fixing whitespace and tab issue --- envoy_reader/envoy_reader.py | 80 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6958999..314eaa5 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -57,50 +57,48 @@ def hasMeteringSetup(self, json): return False async def getData(self): + try: + async with httpx.AsyncClient() as client: + self.endpoint_production_json_results = await client.get( + ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_v1_results = await client.get( + ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) + self.endpoint_production_results = await client.get( + ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) + except httpx.HTTPError: + pass + + await self.detect_model() - - try: - async with httpx.AsyncClient() as client: - self.endpoint_production_json_results = await client.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await client.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_results = await client.get( - ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) - except httpx.HTTPError: - pass - - await self.detect_model() - - if(self.get_inverters): - for i in range(0,3): - while True: - """If a password was not given as an argument when instantiating - the EnvoyReader object than use the last six numbers of the serial - number as the password. Otherwise use the password argument value.""" - if self.password == "": - if self.serial_number_last_six == "": - try: - await self.get_serial_number() - except httpx.HTTPError: - raise - self.password = self.serial_number_last_six + if(self.get_inverters): + for i in range(0,3): + while True: + """If a password was not given as an argument when instantiating + the EnvoyReader object than use the last six numbers of the serial + number as the password. Otherwise use the password argument value.""" + if self.password == "": + if self.serial_number_last_six == "": try: - async with httpx.AsyncClient() as client: - response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), - timeout=30, auth=httpx.DigestAuth(self.username, self.password)) - if response is not None and response.status_code != 401: - self.endpoint_production_inverters = response - else: - response.raise_for_status() - except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError) as err: - continue + await self.get_serial_number() except httpx.HTTPError: - response.raise_for_status() - break - break - if(i == 2): - raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) + raise + self.password = self.serial_number_last_six + try: + async with httpx.AsyncClient() as client: + response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), + timeout=30, auth=httpx.DigestAuth(self.username, self.password)) + if response is not None and response.status_code != 401: + self.endpoint_production_inverters = response + else: + response.raise_for_status() + except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError) as err: + continue + except httpx.HTTPError: + response.raise_for_status() + break + break + if(i == 2): + raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) async def detect_model(self): """Method to determine if the Envoy supports consumption values or From 77e5d1a3ec136d9aa15f421d340af87c5fc91fa5 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 13:03:06 -0800 Subject: [PATCH 095/134] Bump version to 0.18.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 213363c..12235ed 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.1", + version="0.18.2", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 0aa456197721d7f5dd8098be08c6877edd24eb34 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 15:16:47 -0800 Subject: [PATCH 096/134] Fix indentation issue --- envoy_reader/envoy_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 314eaa5..9f0bc5f 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -97,8 +97,8 @@ async def getData(self): response.raise_for_status() break break - if(i == 2): - raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) + if(i == 2): + raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) async def detect_model(self): """Method to determine if the Envoy supports consumption values or From c4b1ab90239d131825a0133097fd429cf7ebb661 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 15:33:30 -0800 Subject: [PATCH 097/134] Return None when inverters not supported --- envoy_reader/envoy_reader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 9f0bc5f..6f57921 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -374,7 +374,7 @@ async def inverters_production(self): """Only return data if Envoy supports retrieving Inverter data""" if self.endpoint_type == "P0": - return "Inverter data not available for your Envoy device." + return None response_dict = {} try: @@ -416,6 +416,8 @@ def run_in_console(self): print("lifetime_consumption: {}".format(results[7])) if "401" in str(dataResults): print("inverters_production: Unable to retrieve inverter data - Authentication failure") + elif results[8] is None: + print("inverters_production: Inverter data not available for your Envoy device.") else: print("inverters_production: {}".format(results[8])) From d6add6ddb51b38c80aaa29cd379322c507f78282 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 15:34:08 -0800 Subject: [PATCH 098/134] Bump version to 0.18.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12235ed..c0630f2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.2", + version="0.18.3", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 64ef3cdec14cea5dd517d7c1cfd59fb1375abf4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Apr 2021 14:38:17 -1000 Subject: [PATCH 099/134] Cleanup detection and avoid calling endpoints that are not present - support passing in shared httpx client - get pylint rating to 10/10 --- envoy_reader/envoy_reader.py | 669 +++++++++++++++++++---------------- 1 file changed, 373 insertions(+), 296 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6f57921..f81513f 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,410 +1,467 @@ +"""Module to read production and consumption values from an Enphase Envoy on the local network.""" import asyncio -import json +from json.decoder import JSONDecodeError +import re import time -import re import httpcore import httpx -"""Module to read production and consumption values from an Enphase Envoy on - the local network""" - -PRODUCTION_REGEX = \ - r'Currentl.*\s+\s*(\d+|\d+\.\d+)\s*(W|kW|MW)' -DAY_PRODUCTION_REGEX = \ - r'Today\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' -WEEK_PRODUCTION_REGEX = \ - r'Past Week\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' -LIFE_PRODUCTION_REGEX = \ - r'Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)' +# +# Legacy parser is only used on ancient firmwares +# +PRODUCTION_REGEX = r"Currentl.*\s+\s*(\d+|\d+\.\d+)\s*(W|kW|MW)" +DAY_PRODUCTION_REGEX = r"Today\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)" +WEEK_PRODUCTION_REGEX = ( + r"Past Week\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)" +) +LIFE_PRODUCTION_REGEX = ( + r"Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)" +) +SERIAL_REGEX = re.compile(r"Envoy\s*Serial\s*Number:\s*([0-9]+)") ENDPOINT_URL_PRODUCTION_JSON = "http://{}/production.json" ENDPOINT_URL_PRODUCTION_V1 = "http://{}/api/v1/production" ENDPOINT_URL_PRODUCTION_INVERTERS = "http://{}/api/v1/production/inverters" ENDPOINT_URL_PRODUCTION = "http://{}/production" -class EnvoyReader(): +# pylint: disable=pointless-string-statement + +ENVOY_MODEL_S = "PC" +ENVOY_MODEL_C = "P" +ENVOY_MODEL_LEGACY = "P0" + + +class EnvoyReader: # pylint: disable=too-many-instance-attributes """Instance of EnvoyReader""" + # P0 for older Envoy model C, s/w < R3.9 no json pages # P for production data only (ie. Envoy model C, s/w >= R3.9) # PC for production and consumption data (ie. Envoy model S) - message_consumption_not_available = ("Consumption data not available for " - "your Envoy device.") + message_consumption_not_available = ( + "Consumption data not available for your Envoy device." + ) - def __init__(self, host, username="envoy", password="", inverters=False): + def __init__( # pylint: disable=too-many-arguments + self, host, username="envoy", password="", inverters=False, async_client=None + ): self.host = host.lower() self.username = username self.password = password self.get_inverters = inverters - self.endpoint_type = "" - self.serial_number_last_six = "" - self.endpoint_production_json_results = "" - self.endpoint_production_v1_results = "" - self.endpoint_production_inverters = "" - self.endpoint_production_results = "" - self.isMeteringEnabled = False - - def hasProductionAndConsumption(self, json): - """Check if json has keys for both production and consumption""" - return "production" in json and "consumption" in json - - def hasMeteringSetup(self, json): - """Check if Active Count of Production CTs (eim) installed is greater than one""" - if json["production"][1]["activeCount"] > 0: - return True + self.endpoint_type = None + self.serial_number_last_six = None + self.endpoint_production_json_results = None + self.endpoint_production_v1_results = None + self.endpoint_production_inverters = None + self.endpoint_production_results = None + self.isMeteringEnabled = False # pylint: disable=invalid-name + self._async_client = async_client + + @property + def async_client(self): + """Return the httpx client.""" + return self._async_client or httpx.AsyncClient() + + async def _update(self): + """Update the data.""" + if self.endpoint_type == ENVOY_MODEL_S: + await self._update_from_pc_endpoint() + if self.endpoint_type == ENVOY_MODEL_C: + await self._update_from_p_endpoint() + if self.endpoint_type == ENVOY_MODEL_LEGACY: + await self._update_from_p0_endpoint() + + async def _update_from_pc_endpoint(self): + """Update from PC endpoint.""" + await self._update_endpoint( + "endpoint_production_json_results", ENDPOINT_URL_PRODUCTION_JSON + ) + + async def _update_from_p_endpoint(self): + """Update from P endpoint.""" + await self._update_endpoint( + "endpoint_production_v1_results", ENDPOINT_URL_PRODUCTION_V1 + ) + + async def _update_from_p0_endpoint(self): + """Update from P0 endpoint.""" + await self._update_endpoint( + "endpoint_production_results", ENDPOINT_URL_PRODUCTION + ) + + async def _update_endpoint(self, attr, url): + """Update a property from an endpoint.""" + async with self.async_client as client: + setattr( + self, + attr, + await client.get( + url.format(self.host), timeout=30, allow_redirects=False + ), + ) + + async def getData(self): # pylint: disable=invalid-name + """Fetch data from the endpoint.""" + if not self.endpoint_type: + await self.detect_model() else: - return False + await self._update() - async def getData(self): - try: - async with httpx.AsyncClient() as client: - self.endpoint_production_json_results = await client.get( - ENDPOINT_URL_PRODUCTION_JSON.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_v1_results = await client.get( - ENDPOINT_URL_PRODUCTION_V1.format(self.host), timeout=30, allow_redirects=False) - self.endpoint_production_results = await client.get( - ENDPOINT_URL_PRODUCTION.format(self.host), timeout=30, allow_redirects=False) - except httpx.HTTPError: - pass - - await self.detect_model() - - if(self.get_inverters): - for i in range(0,3): - while True: - """If a password was not given as an argument when instantiating - the EnvoyReader object than use the last six numbers of the serial - number as the password. Otherwise use the password argument value.""" - if self.password == "": - if self.serial_number_last_six == "": - try: - await self.get_serial_number() - except httpx.HTTPError: - raise - self.password = self.serial_number_last_six - try: - async with httpx.AsyncClient() as client: - response = await client.get(ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), - timeout=30, auth=httpx.DigestAuth(self.username, self.password)) - if response is not None and response.status_code != 401: - self.endpoint_production_inverters = response - else: - response.raise_for_status() - except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError) as err: - continue - except httpx.HTTPError: + if not self.get_inverters: + return + + for i in range(0, 3): + while True: + """If a password was not given as an argument when instantiating + the EnvoyReader object than use the last six numbers of the serial + number as the password. Otherwise use the password argument value.""" + if self.password == "" and not self.serial_number_last_six: + await self.get_serial_number() + self.password = self.serial_number_last_six + try: + async with self.async_client as client: + response = await client.get( + ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), + timeout=30, + auth=httpx.DigestAuth(self.username, self.password), + ) + if response is not None and response.status_code != 401: + self.endpoint_production_inverters = response + else: response.raise_for_status() - break + except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): + continue + except httpx.HTTPError: + response.raise_for_status() break - if(i == 2): - raise httpx.RemoteProtocolError(message='Malformed request. Failed after 3 retries.', request=None) + break + if i == 2: + raise httpx.RemoteProtocolError( + message="Malformed request. Failed after 3 retries.", request=None + ) async def detect_model(self): """Method to determine if the Envoy supports consumption values or - only production""" + only production""" - if self.endpoint_production_json_results.status_code == 200 and self.hasProductionAndConsumption(self.endpoint_production_json_results.json()): - self.isMeteringEnabled = self.hasMeteringSetup(self.endpoint_production_json_results.json()) - self.endpoint_type = "PC" + try: + await self._update_from_pc_endpoint() + except httpx.HTTPError: + pass + if ( + self.endpoint_production_json_results + and self.endpoint_production_json_results.status_code == 200 + and has_production_and_consumption( + self.endpoint_production_json_results.json() + ) + ): + self.isMeteringEnabled = has_metering_setup( + self.endpoint_production_json_results.json() + ) + self.endpoint_type = ENVOY_MODEL_S + return + + try: + await self._update_from_p_endpoint() + except httpx.HTTPError: + pass + if ( + self.endpoint_production_v1_results + and self.endpoint_production_v1_results.status_code == 200 + ): + self.endpoint_type = ENVOY_MODEL_C # Envoy-C, production only + return + + try: + await self._update_from_p0_endpoint() + except httpx.HTTPError: + pass + if ( + self.endpoint_production_results + and self.endpoint_production_results.status_code == 200 + ): + self.endpoint_type = ENVOY_MODEL_LEGACY # older Envoy-C return - else: - if self.endpoint_production_v1_results.status_code == 200: - self.endpoint_type = "P" # Envoy-C, production only - return - else: - endpoint_url = ENDPOINT_URL_PRODUCTION.format(self.host) - async with httpx.AsyncClient() as client: - response = await client.get( - endpoint_url, timeout=30, allow_redirects=False) - if response.status_code == 200: - self.endpoint_type = "P0" # older Envoy-C - return raise RuntimeError( - "Could not connect or determine Envoy model. " + - "Check that the device is up at 'http://" + self.host + "'.") + "Could not connect or determine Envoy model. " + + "Check that the device is up at 'http://" + + self.host + + "'." + ) async def get_serial_number(self): """Method to get last six digits of Envoy serial number for auth""" - try: - async with httpx.AsyncClient() as client: - response = await client.get( - "http://{}/info.xml".format(self.host), - timeout=30, allow_redirects=False) - if len(response.text) > 0: - sn = response.text.split("")[1].split("")[0][-6:] - self.serial_number_last_six = sn - except httpx.ConnectionError: - raise + full_serial = await self.get_full_serial_number() + self.serial_number_last_six = full_serial[-6:] + + async def get_full_serial_number(self): + """Method to get the Envoy serial number.""" + async with self.async_client as client: + response = await client.get( + "http://{}/info.xml".format(self.host), + timeout=30, + allow_redirects=True, + ) + if not response.text: + return None + if "" in response.text: + return response.text.split("")[1].split("")[0] + match = SERIAL_REGEX.search(response.text) + if match: + return match.group(1) def create_connect_errormessage(self): """Create error message if unable to connect to Envoy""" - return ("Unable to connect to Envoy. " + - "Check that the device is up at 'http://" - + self.host + "'.") + return ( + "Unable to connect to Envoy. " + + "Check that the device is up at 'http://" + + self.host + + "'." + ) def create_json_errormessage(self): """Create error message if unable to parse JSON response""" - return ("Got a response from '" + self.host + - "', but metric could not be found. " + - "Maybe your model of Envoy doesn't " + - "support the requested metric.") + return ( + "Got a response from '" + + self.host + + "', but metric could not be found. " + + "Maybe your model of Envoy doesn't " + + "support the requested metric." + ) async def production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" - try: - if self.endpoint_type == "PC": - raw_json = self.endpoint_production_json_results.json() - if self.isMeteringEnabled: - production = raw_json["production"][1]["wNow"] - else: - production = raw_json["production"][0]["wNow"] + if self.endpoint_type == ENVOY_MODEL_S: + raw_json = self.endpoint_production_json_results.json() + if self.isMeteringEnabled: + production = raw_json["production"][1]["wNow"] else: - if self.endpoint_type == "P": - raw_json = self.endpoint_production_v1_results.json() - production = raw_json["wattsNow"] + production = raw_json["production"][0]["wNow"] + elif self.endpoint_type == ENVOY_MODEL_C: + raw_json = self.endpoint_production_v1_results.json() + production = raw_json["wattsNow"] + elif self.endpoint_type == ENVOY_MODEL_LEGACY: + text = self.endpoint_production_results.text + match = re.search(PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kW": + production = float(match.group(1)) * 1000 else: - if self.endpoint_type == "P0": - text = self.endpoint_production_results.text - match = re.search( - PRODUCTION_REGEX, text, re.MULTILINE) - if match: - if match.group(2) == "kW": - production = float(match.group(1))*1000 - else: - if match.group(2) == "mW": - production = float( - match.group(1))*1000000 - else: - production = float(match.group(1)) - else: - raise RuntimeError( - "No match for production, check REGEX " - + text) - return int(production) - - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + if match.group(2) == "mW": + production = float(match.group(1)) * 1000000 + else: + production = float(match.group(1)) + else: + raise RuntimeError("No match for production, check REGEX " + text) + return int(production) async def consumption(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" """Only return data if Envoy supports Consumption""" - if self.endpoint_type == "P" or self.endpoint_type == "P0": + if ( + self.endpoint_type == ENVOY_MODEL_C + or self.endpoint_type == ENVOY_MODEL_LEGACY + ): return self.message_consumption_not_available - try: - raw_json = self.endpoint_production_json_results.json() - consumption = raw_json["consumption"][0]["wNow"] - return int(consumption) - - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + raw_json = self.endpoint_production_json_results.json() + consumption = raw_json["consumption"][0]["wNow"] + return int(consumption) async def daily_production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" - try: - if self.endpoint_type == "PC" and self.isMeteringEnabled: - raw_json = self.endpoint_production_json_results.json() - daily_production = raw_json["production"][1]["whToday"] - elif self.endpoint_type == "PC" and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - daily_production = raw_json["wattHoursToday"] - else: - if self.endpoint_type == "P": - raw_json = self.endpoint_production_v1_results.json() - daily_production = raw_json["wattHoursToday"] + if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() + daily_production = raw_json["production"][1]["whToday"] + elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + daily_production = raw_json["wattHoursToday"] + elif self.endpoint_type == ENVOY_MODEL_C: + raw_json = self.endpoint_production_v1_results.json() + daily_production = raw_json["wattHoursToday"] + elif self.endpoint_type == ENVOY_MODEL_LEGACY: + text = self.endpoint_production_results.text + match = re.search(DAY_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + daily_production = float(match.group(1)) * 1000 else: - if self.endpoint_type == "P0": - text = self.endpoint_production_results.text - match = re.search( - DAY_PRODUCTION_REGEX, text, re.MULTILINE) - if match: - if match.group(2) == "kWh": - daily_production = float( - match.group(1))*1000 - else: - if match.group(2) == "MWh": - daily_production = float( - match.group(1))*1000000 - else: - daily_production = float( - match.group(1)) - else: - raise RuntimeError( - "No match for Day production, " - "check REGEX " + - text) - return int(daily_production) - - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + if match.group(2) == "MWh": + daily_production = float(match.group(1)) * 1000000 + else: + daily_production = float(match.group(1)) + else: + raise RuntimeError( + "No match for Day production, " "check REGEX " + text + ) + return int(daily_production) async def daily_consumption(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" """Only return data if Envoy supports Consumption""" - if self.endpoint_type == "P" or self.endpoint_type == "P0": + if ( + self.endpoint_type == ENVOY_MODEL_C + or self.endpoint_type == ENVOY_MODEL_LEGACY + ): return self.message_consumption_not_available - try: - raw_json = self.endpoint_production_json_results.json() - daily_consumption = raw_json["consumption"][0]["whToday"] - return int(daily_consumption) - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + raw_json = self.endpoint_production_json_results.json() + daily_consumption = raw_json["consumption"][0]["whToday"] + return int(daily_consumption) async def seven_days_production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" - try: - if self.endpoint_type == "PC" and self.isMeteringEnabled: - raw_json = self.endpoint_production_json_results.json() - seven_days_production = raw_json["production"][1]["whLastSevenDays"] - elif self.endpoint_type == "PC" and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - seven_days_production = raw_json["wattHoursSevenDays"] - else: - if self.endpoint_type == "P": - raw_json = self.endpoint_production_v1_results.json() - seven_days_production = raw_json["wattHoursSevenDays"] + if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() + seven_days_production = raw_json["production"][1]["whLastSevenDays"] + elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + seven_days_production = raw_json["wattHoursSevenDays"] + elif self.endpoint_type == ENVOY_MODEL_C: + raw_json = self.endpoint_production_v1_results.json() + seven_days_production = raw_json["wattHoursSevenDays"] + elif self.endpoint_type == ENVOY_MODEL_LEGACY: + text = self.endpoint_production_results.text + match = re.search(WEEK_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + seven_days_production = float(match.group(1)) * 1000 else: - if self.endpoint_type == "P0": - text = self.endpoint_production_results.text - match = re.search( - WEEK_PRODUCTION_REGEX, text, re.MULTILINE) - if match: - if match.group(2) == "kWh": - seven_days_production = float( - match.group(1))*1000 - else: - if match.group(2) == "MWh": - seven_days_production = float( - match.group(1))*1000000 - else: - seven_days_production = float( - match.group(1)) - else: - raise RuntimeError("No match for 7 Day production, " - "check REGEX " + text) - return int(seven_days_production) - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + if match.group(2) == "MWh": + seven_days_production = float(match.group(1)) * 1000000 + else: + seven_days_production = float(match.group(1)) + else: + raise RuntimeError( + "No match for 7 Day production, " "check REGEX " + text + ) + return int(seven_days_production) async def seven_days_consumption(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" """Only return data if Envoy supports Consumption""" - if self.endpoint_type == "P" or self.endpoint_type == "P0": + if ( + self.endpoint_type == ENVOY_MODEL_C + or self.endpoint_type == ENVOY_MODEL_LEGACY + ): return self.message_consumption_not_available - try: - raw_json = self.endpoint_production_json_results.json() - seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] - return int(seven_days_consumption) - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + raw_json = self.endpoint_production_json_results.json() + seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"] + return int(seven_days_consumption) async def lifetime_production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" - try: - if self.endpoint_type == "PC" and self.isMeteringEnabled: - raw_json = self.endpoint_production_json_results.json() - lifetime_production = raw_json["production"][1]["whLifetime"] - elif self.endpoint_type == "PC" and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - lifetime_production = raw_json["wattHoursLifetime"] - else: - if self.endpoint_type == "P": - raw_json = self.endpoint_production_v1_results.json() - lifetime_production = raw_json["wattHoursLifetime"] + if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: + raw_json = self.endpoint_production_json_results.json() + lifetime_production = raw_json["production"][1]["whLifetime"] + elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: + raw_json = self.endpoint_production_v1_results.json() + lifetime_production = raw_json["wattHoursLifetime"] + elif self.endpoint_type == ENVOY_MODEL_C: + raw_json = self.endpoint_production_v1_results.json() + lifetime_production = raw_json["wattHoursLifetime"] + elif self.endpoint_type == ENVOY_MODEL_LEGACY: + text = self.endpoint_production_results.text + match = re.search(LIFE_PRODUCTION_REGEX, text, re.MULTILINE) + if match: + if match.group(2) == "kWh": + lifetime_production = float(match.group(1)) * 1000 else: - if self.endpoint_type == "P0": - text = self.endpoint_production_results.text - match = re.search( - LIFE_PRODUCTION_REGEX, text, re.MULTILINE) - if match: - if match.group(2) == "kWh": - lifetime_production = float( - match.group(1))*1000 - else: - if match.group(2) == "MWh": - lifetime_production = float( - match.group(1))*1000000 - else: - lifetime_production = float( - match.group(1)) - else: - raise RuntimeError( - "No match for Lifetime production, " - "check REGEX " + text) - return int(lifetime_production) - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + if match.group(2) == "MWh": + lifetime_production = float(match.group(1)) * 1000000 + else: + lifetime_production = float(match.group(1)) + else: + raise RuntimeError( + "No match for Lifetime production, " "check REGEX " + text + ) + return int(lifetime_production) async def lifetime_consumption(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" """Only return data if Envoy supports Consumption""" - if self.endpoint_type == "P" or self.endpoint_type == "P0": + if ( + self.endpoint_type == ENVOY_MODEL_C + or self.endpoint_type == ENVOY_MODEL_LEGACY + ): return self.message_consumption_not_available - try: - raw_json = self.endpoint_production_json_results.json() - lifetime_consumption = raw_json["consumption"][0]["whLifetime"] - return int(lifetime_consumption) - except (json.decoder.JSONDecodeError, KeyError, IndexError): - raise + raw_json = self.endpoint_production_json_results.json() + lifetime_consumption = raw_json["consumption"][0]["whLifetime"] + return int(lifetime_consumption) async def inverters_production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" """Only return data if Envoy supports retrieving Inverter data""" - if self.endpoint_type == "P0": + if self.endpoint_type == ENVOY_MODEL_LEGACY: return None response_dict = {} try: for item in self.endpoint_production_inverters.json(): - response_dict[item["serialNumber"]] = [item["lastReportWatts"], time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item["lastReportDate"]))] - except (json.decoder.JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + response_dict[item["serialNumber"]] = [ + item["lastReportWatts"], + time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(item["lastReportDate"]) + ), + ] + except ( + JSONDecodeError, + KeyError, + IndexError, + TypeError, + AttributeError, + ): return None return response_dict def run_in_console(self): """If running this module directly, print all the values in the - console.""" + console.""" print("Reading...") loop = asyncio.get_event_loop() - dataResults = loop.run_until_complete(asyncio.gather( - self.getData(), return_exceptions=True - )) + data_results = loop.run_until_complete( + asyncio.gather(self.getData(), return_exceptions=True) + ) loop = asyncio.get_event_loop() - results = loop.run_until_complete(asyncio.gather( - self.production(), - self.consumption(), - self.daily_production(), - self.daily_consumption(), - self.seven_days_production(), - self.seven_days_consumption(), - self.lifetime_production(), - self.lifetime_consumption(), - self.inverters_production(), return_exceptions=True)) + results = loop.run_until_complete( + asyncio.gather( + self.production(), + self.consumption(), + self.daily_production(), + self.daily_consumption(), + self.seven_days_production(), + self.seven_days_consumption(), + self.lifetime_production(), + self.lifetime_consumption(), + self.inverters_production(), + return_exceptions=True, + ) + ) print("production: {}".format(results[0])) print("consumption: {}".format(results[1])) @@ -414,23 +471,33 @@ def run_in_console(self): print("seven_days_consumption: {}".format(results[5])) print("lifetime_production: {}".format(results[6])) print("lifetime_consumption: {}".format(results[7])) - if "401" in str(dataResults): - print("inverters_production: Unable to retrieve inverter data - Authentication failure") + if "401" in str(data_results): + print( + "inverters_production: Unable to retrieve inverter data - Authentication failure" + ) elif results[8] is None: - print("inverters_production: Inverter data not available for your Envoy device.") + print( + "inverters_production: Inverter data not available for your Envoy device." + ) else: print("inverters_production: {}".format(results[8])) if __name__ == "__main__": - HOST = input("Enter the Envoy IP address or host name, " + - "or press enter to use 'envoy' as default: ") + HOST = input( + "Enter the Envoy IP address or host name, " + + "or press enter to use 'envoy' as default: " + ) - USERNAME = input("Enter the Username for Inverter data authentication, " + - "or press enter to use 'envoy' as default: ") + USERNAME = input( + "Enter the Username for Inverter data authentication, " + + "or press enter to use 'envoy' as default: " + ) - PASSWORD = input("Enter the Password for Inverter data authentication, " + - "or press enter to use the default password: ") + PASSWORD = input( + "Enter the Password for Inverter data authentication, " + + "or press enter to use the default password: " + ) if HOST == "": HOST = "envoy" @@ -444,3 +511,13 @@ def run_in_console(self): TESTREADER = EnvoyReader(HOST, USERNAME, PASSWORD, inverters=True) TESTREADER.run_in_console() + + +def has_production_and_consumption(json): + """Check if json has keys for both production and consumption""" + return "production" in json and "consumption" in json + + +def has_metering_setup(json): + """Check if Active Count of Production CTs (eim) installed is greater than one""" + return json["production"][1]["activeCount"] > 0 From 86300bf2e8095b44f2797ac26cf4cd77e7b11759 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Apr 2021 16:09:11 -1000 Subject: [PATCH 100/134] Remove httpx.HTTPError catch since response will be unbound on error --- envoy_reader/envoy_reader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f81513f..fa26a9c 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -133,8 +133,6 @@ async def getData(self): # pylint: disable=invalid-name response.raise_for_status() except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): continue - except httpx.HTTPError: - response.raise_for_status() break break if i == 2: From ef48a4ff02e0c63f8c65d48e410ca68915c2888e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Apr 2021 08:12:42 -1000 Subject: [PATCH 101/134] reorder --- envoy_reader/envoy_reader.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index fa26a9c..ad5ebdf 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -32,6 +32,16 @@ ENVOY_MODEL_LEGACY = "P0" +def has_production_and_consumption(json): + """Check if json has keys for both production and consumption""" + return "production" in json and "consumption" in json + + +def has_metering_setup(json): + """Check if Active Count of Production CTs (eim) installed is greater than one""" + return json["production"][1]["activeCount"] > 0 + + class EnvoyReader: # pylint: disable=too-many-instance-attributes """Instance of EnvoyReader""" @@ -509,13 +519,3 @@ def run_in_console(self): TESTREADER = EnvoyReader(HOST, USERNAME, PASSWORD, inverters=True) TESTREADER.run_in_console() - - -def has_production_and_consumption(json): - """Check if json has keys for both production and consumption""" - return "production" in json and "consumption" in json - - -def has_metering_setup(json): - """Check if Active Count of Production CTs (eim) installed is greater than one""" - return json["production"][1]["activeCount"] > 0 From 7bbf5067d1b1060262fbb33d254d58653816aa74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:04:24 -1000 Subject: [PATCH 102/134] flake8 fixes --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 23 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 8 +++ .github/workflows/build-docs.yml | 34 ++++++++++ .github/workflows/build-main.yml | 79 +++++++++++++++++++++++ .github/workflows/test-and-lint.yml | 47 ++++++++++++++ envoy_reader/envoy_reader.py | 25 +++---- setup.cfg | 34 ++++++++++ tests/__init__.py | 3 + tests/conftest.py | 2 + tests/fixtures/4.2.27/api_v1_production | 6 ++ tests/fixtures/4.2.27/production.json | 1 + 12 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/build-docs.yml create mode 100644 .github/workflows/build-main.yml create mode 100644 .github/workflows/test-and-lint.yml create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/4.2.27/api_v1_production create mode 100644 tests/fixtures/4.2.27/production.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..22f6e17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: '"Something''s wrong..."' +title: '' +labels: '' +assignees: '' + +--- + +## Description +*A clear description of the bug* + + + + +## Expected Behavior +*What did you expect to happen instead?* + + + + +## Reproduction +*A minimal example that exhibits the behavior.* + + + + +## Environment +*Any additional information about your environment* diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3b04215 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: '"It would be really cool if x did y..."' +title: '' +labels: '' +assignees: '' + +--- + +## Use Case +*Please provide a use case to help us understand your request in context* + + + + +## Solution +*Please describe your ideal solution* + + + + +## Alternatives +*Please describe any alternatives you've considered, even if you've dismissed them* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3467dd4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +**Pull request recommendations:** +- [ ] Name your pull request _your-development-type/short-description_. Ex: _feature/read-tiff-files_ +- [ ] Link to any relevant issue in the PR description. Ex: _Resolves [gh-12], adds tiff file format support_ +- [ ] Provide context of changes. +- [ ] Provide relevant tests for your feature or bug fix. +- [ ] Provide or update documentation for any feature added by your pull request. + +Thanks for contributing! diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000..f7932bf --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,34 @@ +name: Documentation + +on: + push: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install .[dev] + - name: Generate Docs + run: | + make gen-docs + touch docs/_build/html/.nojekyll + - name: Publish Docs + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_BRANCH: main # The branch the action should deploy from. + BRANCH: gh-pages # The branch the action should deploy to. + FOLDER: docs/_build/html/ # The folder the action should deploy. + diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml new file mode 100644 index 0000000..8b15f94 --- /dev/null +++ b/.github/workflows/build-main.yml @@ -0,0 +1,79 @@ +name: Build Main + +on: + push: + branches: + - main + schedule: + # + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 + # Run every Monday at 18:00:00 UTC (Monday at 10:00:00 PST) + - cron: '0 18 * * 1' + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.8, 3.9] + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + - name: Test with pytest + run: | + pytest --cov-report xml --cov=envoy_reader tests/ + - name: Upload codecov + uses: codecov/codecov-action@v1 + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + - name: Lint with flake8 + run: | + flake8 envoy_reader --count --verbose --show-source --statistics + - name: Check with black + run: | + black --check envoy_reader + + publish: + if: "contains(github.event.head_commit.message, 'Bump version')" + needs: [test, lint] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build Package + run: | + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: bdraco + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml new file mode 100644 index 0000000..c10b509 --- /dev/null +++ b/.github/workflows/test-and-lint.yml @@ -0,0 +1,47 @@ +name: Test and Lint + +on: pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.8, 3.9] + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + - name: Test with pytest + run: | + pytest envoy_reader/tests/ + - name: Upload codecov + uses: codecov/codecov-action@v1 + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + - name: Lint with flake8 + run: | + flake8 envoy_reader --count --verbose --show-source --statistics + - name: Check with black + run: | + black --check envoy_reader diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ad5ebdf..09cf24e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -1,8 +1,9 @@ """Module to read production and consumption values from an Enphase Envoy on the local network.""" import asyncio -from json.decoder import JSONDecodeError +import logging import re import time +from json.decoder import JSONDecodeError import httpcore import httpx @@ -31,14 +32,16 @@ ENVOY_MODEL_C = "P" ENVOY_MODEL_LEGACY = "P0" +_LOGGER = logging.getLogger(__name__) + def has_production_and_consumption(json): - """Check if json has keys for both production and consumption""" + """Check if json has keys for both production and consumption.""" return "production" in json and "consumption" in json def has_metering_setup(json): - """Check if Active Count of Production CTs (eim) installed is greater than one""" + """Check if Active Count of Production CTs (eim) installed is greater than one.""" return json["production"][1]["activeCount"] > 0 @@ -56,6 +59,7 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, host, username="envoy", password="", inverters=False, async_client=None ): + """Init the EnvoyReader.""" self.host = host.lower() self.username = username self.password = password @@ -104,12 +108,14 @@ async def _update_from_p0_endpoint(self): async def _update_endpoint(self, attr, url): """Update a property from an endpoint.""" async with self.async_client as client: + formatted_url = url.format(self.host) setattr( self, attr, - await client.get( - url.format(self.host), timeout=30, allow_redirects=False - ), + await client.get(formatted_url, timeout=30, allow_redirects=False), + ) + _LOGGER.debug( + "Fetched result from %s: %s", formatted_url, getattr(self, attr) ) async def getData(self): # pylint: disable=invalid-name @@ -151,9 +157,7 @@ async def getData(self): # pylint: disable=invalid-name ) async def detect_model(self): - """Method to determine if the Envoy supports consumption values or - only production""" - + """Method to determine if the Envoy supports consumption values or only production.""" try: await self._update_from_pc_endpoint() except httpx.HTTPError: @@ -447,8 +451,7 @@ async def inverters_production(self): return response_dict def run_in_console(self): - """If running this module directly, print all the values in the - console.""" + """If running this module directly, print all the values in the console.""" print("Reading...") loop = asyncio.get_event_loop() data_results = loop.run_until_complete( diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a5cf1a2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[bumpversion] +current_version = 0.18.3 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version="{current_version}" +replace = version="{new_version}" + +[bumpversion:file:envoy_reader/__init__.py] +search = {current_version} +replace = {new_version} + +[bdist_wheel] +universal = 1 + +[aliases] +test = pytest + +[tool:pytest] +collect_ignore = ['setup.py'] + +[flake8] +exclude = + docs/ +ignore = + E203 + E402 + W291 + W503 + D400 + D401 +max-line-length = 120 + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..6bbbeaa --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +"""Unit test package for envoy_reader.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..faa18be --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- diff --git a/tests/fixtures/4.2.27/api_v1_production b/tests/fixtures/4.2.27/api_v1_production new file mode 100644 index 0000000..f1468f7 --- /dev/null +++ b/tests/fixtures/4.2.27/api_v1_production @@ -0,0 +1,6 @@ +{ + "wattHoursToday": 17920, + "wattHoursSevenDays": 276614, + "wattHoursLifetime": 10279087, + "wattsNow": 5894 +} diff --git a/tests/fixtures/4.2.27/production.json b/tests/fixtures/4.2.27/production.json new file mode 100644 index 0000000..b1f633b --- /dev/null +++ b/tests/fixtures/4.2.27/production.json @@ -0,0 +1 @@ +{"production":[{"type":"inverters","activeCount":34,"readingTime":1618005986,"wNow":5891,"whLifetime":10279087},{"type":"eim","activeCount":0,"measurementType":"production","readingTime":1618006624,"wNow":5814.869,"whLifetime":0.0,"varhLeadLifetime":0.0,"varhLagLifetime":0.0,"vahLifetime":0.0,"rmsCurrent":47.811,"rmsVoltage":243.277,"reactPwr":465.213,"apprntPwr":5816.157,"pwrFactor":1.0,"whToday":0.0,"whLastSevenDays":0.0,"vahToday":0.0,"varhLeadToday":0.0,"varhLagToday":0.0}],"consumption":[{"type":"eim","activeCount":0,"measurementType":"total-consumption","readingTime":1618006624,"wNow":5811.099,"whLifetime":0.0,"varhLeadLifetime":0.0,"varhLagLifetime":0.0,"vahLifetime":0.0,"rmsCurrent":47.534,"rmsVoltage":243.146,"reactPwr":-465.213,"apprntPwr":11557.595,"pwrFactor":0.5,"whToday":0.0,"whLastSevenDays":0.0,"vahToday":0.0,"varhLeadToday":0.0,"varhLagToday":0.0},{"type":"eim","activeCount":0,"measurementType":"net-consumption","readingTime":1618006624,"wNow":-3.769,"whLifetime":0.0,"varhLeadLifetime":0.0,"varhLagLifetime":0.0,"vahLifetime":0.0,"rmsCurrent":0.278,"rmsVoltage":243.015,"reactPwr":-0.0,"apprntPwr":33.697,"pwrFactor":0.0,"whToday":0,"whLastSevenDays":0,"vahToday":0,"varhLeadToday":0,"varhLagToday":0}],"storage":[{"type":"acb","activeCount":0,"readingTime":0,"wNow":0,"whNow":0,"state":"idle"}]} From b0bbdc16c8caa92a71c1c5176ea727bc516138e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:07:33 -1000 Subject: [PATCH 103/134] Add requirements --- setup.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c0630f2..fa899a3 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,50 @@ with open("README.md", "r") as fh: long_description = fh.read() + +setup_requirements = [ + "pytest-runner>=5.2", +] + +test_requirements = [ + "pytest-asyncio", + "black>=19.10b0", + "codecov>=2.1.4", + "flake8>=3.8.3", + "flake8-debugger>=3.2.1", + "pytest>=5.4.3", + "pytest-cov>=2.9.0", + "pytest-raises>=0.11", +] + +dev_requirements = [ + *setup_requirements, + *test_requirements, + "bump2version>=1.0.1", + "coverage>=5.1", + "ipython>=7.15.0", + "m2r2>=0.2.7", + "pytest-runner>=5.2", + "Sphinx>=3.4.3", + "sphinx_rtd_theme>=0.5.1", + "tox>=3.15.2", + "twine>=3.1.1", + "wheel>=0.34.2", +] + +requirements = ["httpx >= 0.12.1"] + +extra_requirements = { + "setup": setup_requirements, + "test": test_requirements, + "dev": dev_requirements, + "all": [ + *requirements, + *dev_requirements, + ], +} + + setuptools.setup( name="envoy_reader", version="0.18.3", @@ -13,9 +57,11 @@ long_description_content_type="text/markdown", url="https://github.com/jesserizzo/envoy_reader", packages=setuptools.find_packages(), - install_requires=[ - "httpx >= 0.12.1" - ], + install_requires=requirements, + setup_requires=setup_requirements, + test_suite="tests", + tests_require=test_requirements, + extras_require=extra_requirements, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From 346e78cf5c7ac013a13f64ae74f464691a0de11e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:08:54 -1000 Subject: [PATCH 104/134] fix test location in ci --- .github/workflows/test-and-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index c10b509..d772dc7 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -22,7 +22,7 @@ jobs: pip install .[test] - name: Test with pytest run: | - pytest envoy_reader/tests/ + pytest tests/ - name: Upload codecov uses: codecov/codecov-action@v1 From 52c36d24accda9e848d172f5784ee68a6119147f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:26:53 -1000 Subject: [PATCH 105/134] prep work to install respx for testing --- envoy_reader/envoy_reader.py | 23 ++++++++--------------- setup.py | 3 ++- tests/fixtures/3.17.3/api_v1_production | 6 ++++++ tests/fixtures/3.9.36/api_v1_production | 6 ++++++ tests/fixtures/5.0.49/api_v1_production | 6 ++++++ 5 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/3.17.3/api_v1_production create mode 100644 tests/fixtures/3.9.36/api_v1_production create mode 100644 tests/fixtures/5.0.49/api_v1_production diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 09cf24e..1eeb73d 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -107,16 +107,15 @@ async def _update_from_p0_endpoint(self): async def _update_endpoint(self, attr, url): """Update a property from an endpoint.""" + formatted_url = url.format(self.host) async with self.async_client as client: - formatted_url = url.format(self.host) - setattr( - self, - attr, - await client.get(formatted_url, timeout=30, allow_redirects=False), - ) - _LOGGER.debug( - "Fetched result from %s: %s", formatted_url, getattr(self, attr) + response = await client.get( + formatted_url, timeout=30, allow_redirects=False ) + setattr(self, attr, response) + _LOGGER.debug( + "Fetched result from %s: %s: %s", formatted_url, response, response.text + ) async def getData(self): # pylint: disable=invalid-name """Fetch data from the endpoint.""" @@ -439,13 +438,7 @@ async def inverters_production(self): "%Y-%m-%d %H:%M:%S", time.localtime(item["lastReportDate"]) ), ] - except ( - JSONDecodeError, - KeyError, - IndexError, - TypeError, - AttributeError, - ): + except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): return None return response_dict diff --git a/setup.py b/setup.py index fa899a3..09552ae 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ "pytest>=5.4.3", "pytest-cov>=2.9.0", "pytest-raises>=0.11", + "respx>=0.16.3", ] dev_requirements = [ @@ -34,7 +35,7 @@ "wheel>=0.34.2", ] -requirements = ["httpx >= 0.12.1"] +requirements = ["httpx>=0.12.1"] extra_requirements = { "setup": setup_requirements, diff --git a/tests/fixtures/3.17.3/api_v1_production b/tests/fixtures/3.17.3/api_v1_production new file mode 100644 index 0000000..71a98df --- /dev/null +++ b/tests/fixtures/3.17.3/api_v1_production @@ -0,0 +1,6 @@ +{ + "wattHoursToday": 5481, + "wattHoursSevenDays": 389581, + "wattHoursLifetime": 93706280, + "wattsNow": 5463 +} diff --git a/tests/fixtures/3.9.36/api_v1_production b/tests/fixtures/3.9.36/api_v1_production new file mode 100644 index 0000000..7e6d856 --- /dev/null +++ b/tests/fixtures/3.9.36/api_v1_production @@ -0,0 +1,6 @@ +{ + "wattHoursToday": 1460, + "wattHoursSevenDays": 130349, + "wattHoursLifetime": 6012540, + "wattsNow": 1271 +} diff --git a/tests/fixtures/5.0.49/api_v1_production b/tests/fixtures/5.0.49/api_v1_production new file mode 100644 index 0000000..4aa61ea --- /dev/null +++ b/tests/fixtures/5.0.49/api_v1_production @@ -0,0 +1,6 @@ +{ + "wattHoursToday": 5046, + "wattHoursSevenDays": 445686, + "wattHoursLifetime": 88742152, + "wattsNow": 4859 +} From 37b1e38513eeb36a7c827c5b4d9867239a18c750 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:35:33 -1000 Subject: [PATCH 106/134] cleanup retry logic --- envoy_reader/envoy_reader.py | 59 ++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 1eeb73d..a41417f 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -127,36 +127,43 @@ async def getData(self): # pylint: disable=invalid-name if not self.get_inverters: return + inverters_url = ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host) + inverters_auth = httpx.DigestAuth(self.username, self.password) + for i in range(0, 3): - while True: - """If a password was not given as an argument when instantiating - the EnvoyReader object than use the last six numbers of the serial - number as the password. Otherwise use the password argument value.""" - if self.password == "" and not self.serial_number_last_six: - await self.get_serial_number() - self.password = self.serial_number_last_six - try: - async with self.async_client as client: - response = await client.get( - ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host), - timeout=30, - auth=httpx.DigestAuth(self.username, self.password), - ) - if response is not None and response.status_code != 401: - self.endpoint_production_inverters = response - else: - response.raise_for_status() - except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): - continue - break - break - if i == 2: - raise httpx.RemoteProtocolError( - message="Malformed request. Failed after 3 retries.", request=None - ) + try: + async with self.async_client as client: + response = await client.get( + inverters_url, + timeout=30, + auth=inverters_auth, + ) + + _LOGGER.debug( + "Fetched result from %s: %s: %s", + inverters_url, + response, + response.text, + ) + if response is not None and response.status_code != 401: + self.endpoint_production_inverters = response + return + else: + response.raise_for_status() + except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): + if i == 2: + raise + continue async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production.""" + # If a password was not given as an argument when instantiating + # the EnvoyReader object than use the last six numbers of the serial + # number as the password. Otherwise use the password argument value. + if self.password == "" and not self.serial_number_last_six: + await self.get_serial_number() + self.password = self.serial_number_last_six + try: await self._update_from_pc_endpoint() except httpx.HTTPError: From fef68f564a5053ec381e88258703ca1a60308817 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 09:55:46 -1000 Subject: [PATCH 107/134] Add more fixtures --- .../3.17.3/api_v1_production_inverters | 248 +++++++++++++++ .../3.9.36/api_v1_production_inverters | 74 +++++ .../5.0.49/api_v1_production_inverters | 282 ++++++++++++++++++ tests/fixtures/5.0.49/production.json | 1 + 4 files changed, 605 insertions(+) create mode 100644 tests/fixtures/3.17.3/api_v1_production_inverters create mode 100644 tests/fixtures/3.9.36/api_v1_production_inverters create mode 100644 tests/fixtures/5.0.49/api_v1_production_inverters create mode 100644 tests/fixtures/5.0.49/production.json diff --git a/tests/fixtures/3.17.3/api_v1_production_inverters b/tests/fixtures/3.17.3/api_v1_production_inverters new file mode 100644 index 0000000..9817faf --- /dev/null +++ b/tests/fixtures/3.17.3/api_v1_production_inverters @@ -0,0 +1,248 @@ +[ + { + "serialNumber": "121512041640", + "lastReportDate": 1618082927, + "lastReportWatts": 200, + "maxReportWatts": 249 + }, + { + "serialNumber": "121512036336", + "lastReportDate": 1618082932, + "lastReportWatts": 199, + "maxReportWatts": 247 + }, + { + "serialNumber": "121512043093", + "lastReportDate": 1618082928, + "lastReportWatts": 208, + "maxReportWatts": 255 + }, + { + "serialNumber": "121512039005", + "lastReportDate": 1618082933, + "lastReportWatts": 55, + "maxReportWatts": 254 + }, + { + "serialNumber": "121512041456", + "lastReportDate": 1618082937, + "lastReportWatts": 13, + "maxReportWatts": 79 + }, + { + "serialNumber": "121512043153", + "lastReportDate": 1618082935, + "lastReportWatts": 18, + "maxReportWatts": 146 + }, + { + "serialNumber": "121512038691", + "lastReportDate": 1618082942, + "lastReportWatts": 26, + "maxReportWatts": 247 + }, + { + "serialNumber": "121512039090", + "lastReportDate": 1618082946, + "lastReportWatts": 32, + "maxReportWatts": 194 + }, + { + "serialNumber": "121512038982", + "lastReportDate": 1618082950, + "lastReportWatts": 203, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512042344", + "lastReportDate": 1618082952, + "lastReportWatts": 205, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512039143", + "lastReportDate": 1618082956, + "lastReportWatts": 104, + "maxReportWatts": 245 + }, + { + "serialNumber": "121512009183", + "lastReportDate": 1618082961, + "lastReportWatts": 204, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512039093", + "lastReportDate": 1618082966, + "lastReportWatts": 209, + "maxReportWatts": 256 + }, + { + "serialNumber": "121512042132", + "lastReportDate": 1618082924, + "lastReportWatts": 200, + "maxReportWatts": 250 + }, + { + "serialNumber": "121512043135", + "lastReportDate": 1618082923, + "lastReportWatts": 205, + "maxReportWatts": 254 + }, + { + "serialNumber": "121512043173", + "lastReportDate": 1618082966, + "lastReportWatts": 200, + "maxReportWatts": 247 + }, + { + "serialNumber": "121512039018", + "lastReportDate": 1618082964, + "lastReportWatts": 27, + "maxReportWatts": 252 + }, + { + "serialNumber": "121512036221", + "lastReportDate": 1618082963, + "lastReportWatts": 8, + "maxReportWatts": 116 + }, + { + "serialNumber": "121512038619", + "lastReportDate": 1618082962, + "lastReportWatts": 203, + "maxReportWatts": 252 + }, + { + "serialNumber": "121512038919", + "lastReportDate": 1618082959, + "lastReportWatts": 102, + "maxReportWatts": 238 + }, + { + "serialNumber": "121512006273", + "lastReportDate": 1618082959, + "lastReportWatts": 206, + "maxReportWatts": 254 + }, + { + "serialNumber": "121512043222", + "lastReportDate": 1618082957, + "lastReportWatts": 207, + "maxReportWatts": 254 + }, + { + "serialNumber": "121512038416", + "lastReportDate": 1618082953, + "lastReportWatts": 151, + "maxReportWatts": 251 + }, + { + "serialNumber": "121512043200", + "lastReportDate": 1618082955, + "lastReportWatts": 203, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512044424", + "lastReportDate": 1618082954, + "lastReportWatts": 106, + "maxReportWatts": 239 + }, + { + "serialNumber": "121512041747", + "lastReportDate": 1618082925, + "lastReportWatts": 64, + "maxReportWatts": 248 + }, + { + "serialNumber": "121512039075", + "lastReportDate": 1618082930, + "lastReportWatts": 102, + "maxReportWatts": 237 + }, + { + "serialNumber": "121512043587", + "lastReportDate": 1618082934, + "lastReportWatts": 202, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512043086", + "lastReportDate": 1618082942, + "lastReportWatts": 202, + "maxReportWatts": 250 + }, + { + "serialNumber": "121512039091", + "lastReportDate": 1618082939, + "lastReportWatts": 27, + "maxReportWatts": 252 + }, + { + "serialNumber": "121512039181", + "lastReportDate": 1618082943, + "lastReportWatts": 101, + "maxReportWatts": 238 + }, + { + "serialNumber": "121512033008", + "lastReportDate": 1618082947, + "lastReportWatts": 101, + "maxReportWatts": 243 + }, + { + "serialNumber": "121512037453", + "lastReportDate": 1618082949, + "lastReportWatts": 205, + "maxReportWatts": 255 + }, + { + "serialNumber": "121512038421", + "lastReportDate": 1618082949, + "lastReportWatts": 14, + "maxReportWatts": 233 + }, + { + "serialNumber": "121512038845", + "lastReportDate": 1618082945, + "lastReportWatts": 203, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512038901", + "lastReportDate": 1618082944, + "lastReportWatts": 102, + "maxReportWatts": 245 + }, + { + "serialNumber": "121512039124", + "lastReportDate": 1618082938, + "lastReportWatts": 205, + "maxReportWatts": 254 + }, + { + "serialNumber": "121512036220", + "lastReportDate": 1618082927, + "lastReportWatts": 198, + "maxReportWatts": 245 + }, + { + "serialNumber": "121512038762", + "lastReportDate": 1618082930, + "lastReportWatts": 203, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512043574", + "lastReportDate": 1618082936, + "lastReportWatts": 203, + "maxReportWatts": 253 + }, + { + "serialNumber": "121512036250", + "lastReportDate": 1618082940, + "lastReportWatts": 20, + "maxReportWatts": 190 + } +] diff --git a/tests/fixtures/3.9.36/api_v1_production_inverters b/tests/fixtures/3.9.36/api_v1_production_inverters new file mode 100644 index 0000000..33e0159 --- /dev/null +++ b/tests/fixtures/3.9.36/api_v1_production_inverters @@ -0,0 +1,74 @@ +[ + { + "serialNumber": "121547058993", + "lastReportDate": 1618083961, + "lastReportWatts": 138, + "maxReportWatts": 231 + }, + { + "serialNumber": "121547060394", + "lastReportDate": 1618083966, + "lastReportWatts": 138, + "maxReportWatts": 238 + }, + { + "serialNumber": "121603034267", + "lastReportDate": 1618083956, + "lastReportWatts": 138, + "maxReportWatts": 244 + }, + { + "serialNumber": "121547060402", + "lastReportDate": 1618083962, + "lastReportWatts": 138, + "maxReportWatts": 240 + }, + { + "serialNumber": "121547060638", + "lastReportDate": 1618083966, + "lastReportWatts": 139, + "maxReportWatts": 241 + }, + { + "serialNumber": "121547060646", + "lastReportDate": 1618083957, + "lastReportWatts": 139, + "maxReportWatts": 240 + }, + { + "serialNumber": "121603025842", + "lastReportDate": 1618083963, + "lastReportWatts": 139, + "maxReportWatts": 260 + }, + { + "serialNumber": "121603039216", + "lastReportDate": 1618083968, + "lastReportWatts": 139, + "maxReportWatts": 273 + }, + { + "serialNumber": "121547060652", + "lastReportDate": 1618083959, + "lastReportWatts": 140, + "maxReportWatts": 245 + }, + { + "serialNumber": "121547060495", + "lastReportDate": 1618083959, + "lastReportWatts": 135, + "maxReportWatts": 228 + }, + { + "serialNumber": "121603038867", + "lastReportDate": 1618083964, + "lastReportWatts": 138, + "maxReportWatts": 242 + }, + { + "serialNumber": "121547058983", + "lastReportDate": 1618083969, + "lastReportWatts": 137, + "maxReportWatts": 238 + } +] diff --git a/tests/fixtures/5.0.49/api_v1_production_inverters b/tests/fixtures/5.0.49/api_v1_production_inverters new file mode 100644 index 0000000..79a84e9 --- /dev/null +++ b/tests/fixtures/5.0.49/api_v1_production_inverters @@ -0,0 +1,282 @@ +[ + { + "serialNumber": "121547059079", + "lastReportDate": 1618083244, + "devType": 1, + "lastReportWatts": 130, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059193", + "lastReportDate": 1618083250, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059360", + "lastReportDate": 1618083245, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060384", + "lastReportDate": 1618083250, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059363", + "lastReportDate": 1618083255, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060650", + "lastReportDate": 1618083253, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059128", + "lastReportDate": 1618083262, + "devType": 1, + "lastReportWatts": 135, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059108", + "lastReportDate": 1618083266, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060593", + "lastReportDate": 1618083271, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059102", + "lastReportDate": 1618083273, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060590", + "lastReportDate": 1618083277, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060671", + "lastReportDate": 1618083283, + "devType": 1, + "lastReportWatts": 135, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059354", + "lastReportDate": 1618083287, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059253", + "lastReportDate": 1618083289, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060392", + "lastReportDate": 1618083288, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059112", + "lastReportDate": 1618083286, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060647", + "lastReportDate": 1618083285, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 258 + }, + { + "serialNumber": "121547060643", + "lastReportDate": 1618083284, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059217", + "lastReportDate": 1618083281, + "devType": 1, + "lastReportWatts": 137, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547055830", + "lastReportDate": 1618083280, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060592", + "lastReportDate": 1618083279, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060758", + "lastReportDate": 1618083274, + "devType": 1, + "lastReportWatts": 130, + "maxReportWatts": 255 + }, + { + "serialNumber": "121547059333", + "lastReportDate": 1618083277, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060727", + "lastReportDate": 1618083275, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059008", + "lastReportDate": 1618083240, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060773", + "lastReportDate": 1618083247, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059202", + "lastReportDate": 1618083251, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060761", + "lastReportDate": 1618083260, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060412", + "lastReportDate": 1618083258, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059355", + "lastReportDate": 1618083263, + "devType": 1, + "lastReportWatts": 131, + "maxReportWatts": 258 + }, + { + "serialNumber": "121547060415", + "lastReportDate": 1618083267, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060670", + "lastReportDate": 1618083270, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060396", + "lastReportDate": 1618083269, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059107", + "lastReportDate": 1618083265, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059889", + "lastReportDate": 1618083264, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547060383", + "lastReportDate": 1618083257, + "devType": 1, + "lastReportWatts": 135, + "maxReportWatts": 258 + }, + { + "serialNumber": "121547060766", + "lastReportDate": 1618083242, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059359", + "lastReportDate": 1618083247, + "devType": 1, + "lastReportWatts": 134, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059357", + "lastReportDate": 1618083254, + "devType": 1, + "lastReportWatts": 133, + "maxReportWatts": 257 + }, + { + "serialNumber": "121547059381", + "lastReportDate": 1618083259, + "devType": 1, + "lastReportWatts": 132, + "maxReportWatts": 257 + } +] diff --git a/tests/fixtures/5.0.49/production.json b/tests/fixtures/5.0.49/production.json new file mode 100644 index 0000000..704875f --- /dev/null +++ b/tests/fixtures/5.0.49/production.json @@ -0,0 +1 @@ +{"production":[{"type":"inverters","activeCount":40,"readingTime":1618084193,"wNow":6335,"whLifetime":88745376}],"storage":[{"type":"acb","activeCount":0,"readingTime":0,"wNow":0,"whNow":0,"state":"idle"}]} From 3e262adb15905440ee88730ee9c70f0e29b3c968 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:02:18 -1000 Subject: [PATCH 108/134] fix v4 --- envoy_reader/envoy_reader.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index a41417f..6e08418 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -82,7 +82,9 @@ async def _update(self): """Update the data.""" if self.endpoint_type == ENVOY_MODEL_S: await self._update_from_pc_endpoint() - if self.endpoint_type == ENVOY_MODEL_C: + if self.endpoint_type == ENVOY_MODEL_C or ( + self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled + ): await self._update_from_p_endpoint() if self.endpoint_type == ENVOY_MODEL_LEGACY: await self._update_from_p0_endpoint() @@ -256,10 +258,8 @@ async def production(self): if self.endpoint_type == ENVOY_MODEL_S: raw_json = self.endpoint_production_json_results.json() - if self.isMeteringEnabled: - production = raw_json["production"][1]["wNow"] - else: - production = raw_json["production"][0]["wNow"] + idx = 1 if self.isMeteringEnabled else 0 + production = raw_json["production"][idx]["wNow"] elif self.endpoint_type == ENVOY_MODEL_C: raw_json = self.endpoint_production_v1_results.json() production = raw_json["wattsNow"] @@ -300,10 +300,9 @@ async def daily_production(self): if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: raw_json = self.endpoint_production_json_results.json() daily_production = raw_json["production"][1]["whToday"] - elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - daily_production = raw_json["wattHoursToday"] - elif self.endpoint_type == ENVOY_MODEL_C: + elif self.endpoint_type == ENVOY_MODEL_C or ( + self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled + ): raw_json = self.endpoint_production_v1_results.json() daily_production = raw_json["wattHoursToday"] elif self.endpoint_type == ENVOY_MODEL_LEGACY: @@ -345,10 +344,9 @@ async def seven_days_production(self): if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: raw_json = self.endpoint_production_json_results.json() seven_days_production = raw_json["production"][1]["whLastSevenDays"] - elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - seven_days_production = raw_json["wattHoursSevenDays"] - elif self.endpoint_type == ENVOY_MODEL_C: + elif self.endpoint_type == ENVOY_MODEL_C or ( + self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled + ): raw_json = self.endpoint_production_v1_results.json() seven_days_production = raw_json["wattHoursSevenDays"] elif self.endpoint_type == ENVOY_MODEL_LEGACY: @@ -390,10 +388,9 @@ async def lifetime_production(self): if self.endpoint_type == ENVOY_MODEL_S and self.isMeteringEnabled: raw_json = self.endpoint_production_json_results.json() lifetime_production = raw_json["production"][1]["whLifetime"] - elif self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled: - raw_json = self.endpoint_production_v1_results.json() - lifetime_production = raw_json["wattHoursLifetime"] - elif self.endpoint_type == ENVOY_MODEL_C: + elif self.endpoint_type == ENVOY_MODEL_C or ( + self.endpoint_type == ENVOY_MODEL_S and not self.isMeteringEnabled + ): raw_json = self.endpoint_production_v1_results.json() lifetime_production = raw_json["wattHoursLifetime"] elif self.endpoint_type == ENVOY_MODEL_LEGACY: From ba1614bed60649292c3b05ba951655b9a137fe00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:18:33 -1000 Subject: [PATCH 109/134] merge retry logic --- envoy_reader/envoy_reader.py | 58 +++++++++++++++++------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 6e08418..3f0acf5 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -110,14 +110,21 @@ async def _update_from_p0_endpoint(self): async def _update_endpoint(self, attr, url): """Update a property from an endpoint.""" formatted_url = url.format(self.host) - async with self.async_client as client: - response = await client.get( - formatted_url, timeout=30, allow_redirects=False - ) - setattr(self, attr, response) - _LOGGER.debug( - "Fetched result from %s: %s: %s", formatted_url, response, response.text + response = await self._async_fetch_with_retry( + formatted_url, timeout=30, allow_redirects=False ) + setattr(self, attr, response) + _LOGGER.debug("Fetched from %s: %s: %s", formatted_url, response, response.text) + + async def _async_fetch_with_retry(self, url, **kwargs): + """Retry 3 times to fetch the url if there is a transport error.""" + for attempt in range(3): + try: + async with self.async_client as client: + return await client.get(url, **kwargs) + except httpx.TransportError: + if attempt == 2: + raise async def getData(self): # pylint: disable=invalid-name """Fetch data from the endpoint.""" @@ -132,30 +139,19 @@ async def getData(self): # pylint: disable=invalid-name inverters_url = ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host) inverters_auth = httpx.DigestAuth(self.username, self.password) - for i in range(0, 3): - try: - async with self.async_client as client: - response = await client.get( - inverters_url, - timeout=30, - auth=inverters_auth, - ) - - _LOGGER.debug( - "Fetched result from %s: %s: %s", - inverters_url, - response, - response.text, - ) - if response is not None and response.status_code != 401: - self.endpoint_production_inverters = response - return - else: - response.raise_for_status() - except (httpcore.RemoteProtocolError, httpx.RemoteProtocolError): - if i == 2: - raise - continue + response = await self._async_fetch_with_retry( + inverters_url, timeout=30, auth=inverters_auth + ) + _LOGGER.debug( + "Fetched from %s: %s: %s", + inverters_url, + response, + response.text, + ) + if response.status_code == 401: + response.raise_for_status() + self.endpoint_production_inverters = response + return async def detect_model(self): """Method to determine if the Envoy supports consumption values or only production.""" From c3c420743c4c8b9ad5f79b9f3db4d5d6011b0976 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:19:21 -1000 Subject: [PATCH 110/134] Remove unused import --- envoy_reader/envoy_reader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 3f0acf5..ad79ca4 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -5,7 +5,6 @@ import time from json.decoder import JSONDecodeError -import httpcore import httpx # From 71e9747a28b5e23f747ff4d32843f21ddce99e24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:21:03 -1000 Subject: [PATCH 111/134] tweaks --- envoy_reader/envoy_reader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ad79ca4..e4e4e5e 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -110,7 +110,7 @@ async def _update_endpoint(self, attr, url): """Update a property from an endpoint.""" formatted_url = url.format(self.host) response = await self._async_fetch_with_retry( - formatted_url, timeout=30, allow_redirects=False + formatted_url, allow_redirects=False ) setattr(self, attr, response) _LOGGER.debug("Fetched from %s: %s: %s", formatted_url, response, response.text) @@ -120,7 +120,7 @@ async def _async_fetch_with_retry(self, url, **kwargs): for attempt in range(3): try: async with self.async_client as client: - return await client.get(url, **kwargs) + return await client.get(url, timeout=30, **kwargs) except httpx.TransportError: if attempt == 2: raise @@ -139,7 +139,7 @@ async def getData(self): # pylint: disable=invalid-name inverters_auth = httpx.DigestAuth(self.username, self.password) response = await self._async_fetch_with_retry( - inverters_url, timeout=30, auth=inverters_auth + inverters_url, auth=inverters_auth ) _LOGGER.debug( "Fetched from %s: %s: %s", From 00207f65338a49a3f8839cc43c12fd86e8e4a813 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:55:08 -1000 Subject: [PATCH 112/134] tests --- envoy_reader/envoy_reader.py | 8 +- tests/test_envoy_reader.py | 175 +++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 tests/test_envoy_reader.py diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index e4e4e5e..1915578 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -175,6 +175,8 @@ async def detect_model(self): self.isMeteringEnabled = has_metering_setup( self.endpoint_production_json_results.json() ) + if not self.isMeteringEnabled: + await self._update_from_p_endpoint() self.endpoint_type = ENVOY_MODEL_S return @@ -210,14 +212,14 @@ async def detect_model(self): async def get_serial_number(self): """Method to get last six digits of Envoy serial number for auth""" full_serial = await self.get_full_serial_number() - self.serial_number_last_six = full_serial[-6:] + if full_serial: + self.serial_number_last_six = full_serial[-6:] async def get_full_serial_number(self): """Method to get the Envoy serial number.""" async with self.async_client as client: - response = await client.get( + response = await self._async_fetch_with_retry( "http://{}/info.xml".format(self.host), - timeout=30, allow_redirects=True, ) if not response.text: diff --git a/tests/test_envoy_reader.py b/tests/test_envoy_reader.py new file mode 100644 index 0000000..8d71257 --- /dev/null +++ b/tests/test_envoy_reader.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +"""Tests for envoy_reader.py.""" +# -*- coding: utf-8 -*- +import json +from pathlib import Path + +import pytest +import respx +from httpx import Response + +from envoy_reader.envoy_reader import EnvoyReader + + +def _fixtures_dir() -> Path: + return Path(__file__).parent / "fixtures" + + +def _load_json_fixture(version, name) -> dict: + with open(_fixtures_dir() / version / name, "r") as read_in: + return json.load(read_in) + + +@pytest.mark.asyncio +@respx.mock +async def test_with_4_2_27_firmware(): + """Verify with 4.2.27 firmware.""" + version = "4.2.27" + respx.get("/info.xml").mock(return_value=Response(200, text="")) + respx.get("/production.json").mock( + return_value=Response(200, json=_load_json_fixture(version, "production.json")) + ) + respx.get("/api/v1/production").mock( + return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + ) + + reader = EnvoyReader("127.0.0.1", inverters=False) + await reader.getData() + + assert await reader.consumption() == 5811 + assert await reader.production() == 5891 + assert await reader.daily_consumption() == 0 + assert await reader.daily_production() == 17920 + assert await reader.seven_days_consumption() == 0 + assert await reader.seven_days_production() == 276614 + assert await reader.lifetime_consumption() == 0 + assert await reader.lifetime_production() == 10279087 + assert await reader.inverters_production() is None + + +@pytest.mark.asyncio +@respx.mock +async def test_with_5_0_49_firmware(): + """Verify with 5.0.49 firmware.""" + version = "5.0.49" + + respx.get("/info.xml").mock(return_value=Response(200, text="")) + respx.get("/production.json").mock( + return_value=Response(200, json=_load_json_fixture(version, "production.json")) + ) + respx.get("/api/v1/production").mock( + return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + ) + respx.get("/api/v1/production/inverters").mock( + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production_inverters") + ) + ) + reader = EnvoyReader("127.0.0.1", inverters=True) + await reader.getData() + + assert ( + await reader.consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.production() == 4859 + assert ( + await reader.daily_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.daily_production() == 5046 + assert ( + await reader.seven_days_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.seven_days_production() == 445686 + assert ( + await reader.lifetime_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.lifetime_production() == 88742152 + assert isinstance(await reader.inverters_production(), dict) + + +@pytest.mark.asyncio +@respx.mock +async def test_with_3_9_36_firmware(): + """Verify with 3.9.36 firmware.""" + version = "3.9.36" + + respx.get("/info.xml").mock(return_value=Response(200, text="")) + respx.get("/production.json").mock(return_value=Response(404)) + respx.get("/api/v1/production").mock( + return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + ) + respx.get("/api/v1/production/inverters").mock( + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production_inverters") + ) + ) + reader = EnvoyReader("127.0.0.1", inverters=True) + await reader.getData() + + assert ( + await reader.consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.production() == 1271 + assert ( + await reader.daily_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.daily_production() == 1460 + assert ( + await reader.seven_days_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.seven_days_production() == 130349 + assert ( + await reader.lifetime_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.lifetime_production() == 6012540 + assert isinstance(await reader.inverters_production(), dict) + + +@pytest.mark.asyncio +@respx.mock +async def test_with_3_17_3_firmware(): + """Verify with 3.17.3 firmware.""" + version = "3.17.3" + + respx.get("/info.xml").mock(return_value=Response(200, text="")) + respx.get("/production.json").mock(return_value=Response(404)) + respx.get("/api/v1/production").mock( + return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + ) + respx.get("/api/v1/production/inverters").mock( + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production_inverters") + ) + ) + reader = EnvoyReader("127.0.0.1", inverters=True) + await reader.getData() + + assert ( + await reader.consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.production() == 5463 + assert ( + await reader.daily_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.daily_production() == 5481 + assert ( + await reader.seven_days_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.seven_days_production() == 389581 + assert ( + await reader.lifetime_consumption() + == "Consumption data not available for your Envoy device." + ) + assert await reader.lifetime_production() == 93706280 + assert isinstance(await reader.inverters_production(), dict) From 36150fae22430e42f08529805a252b093fa9c5c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 10:56:51 -1000 Subject: [PATCH 113/134] tests --- envoy_reader/envoy_reader.py | 9 ++++----- tests/test_envoy_reader.py | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 1915578..93184a5 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -217,11 +217,10 @@ async def get_serial_number(self): async def get_full_serial_number(self): """Method to get the Envoy serial number.""" - async with self.async_client as client: - response = await self._async_fetch_with_retry( - "http://{}/info.xml".format(self.host), - allow_redirects=True, - ) + response = await self._async_fetch_with_retry( + "http://{}/info.xml".format(self.host), + allow_redirects=True, + ) if not response.text: return None if "" in response.text: diff --git a/tests/test_envoy_reader.py b/tests/test_envoy_reader.py index 8d71257..07a6a3f 100644 --- a/tests/test_envoy_reader.py +++ b/tests/test_envoy_reader.py @@ -30,7 +30,9 @@ async def test_with_4_2_27_firmware(): return_value=Response(200, json=_load_json_fixture(version, "production.json")) ) respx.get("/api/v1/production").mock( - return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production") + ) ) reader = EnvoyReader("127.0.0.1", inverters=False) @@ -58,7 +60,9 @@ async def test_with_5_0_49_firmware(): return_value=Response(200, json=_load_json_fixture(version, "production.json")) ) respx.get("/api/v1/production").mock( - return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production") + ) ) respx.get("/api/v1/production/inverters").mock( return_value=Response( @@ -100,7 +104,9 @@ async def test_with_3_9_36_firmware(): respx.get("/info.xml").mock(return_value=Response(200, text="")) respx.get("/production.json").mock(return_value=Response(404)) respx.get("/api/v1/production").mock( - return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production") + ) ) respx.get("/api/v1/production/inverters").mock( return_value=Response( @@ -142,7 +148,9 @@ async def test_with_3_17_3_firmware(): respx.get("/info.xml").mock(return_value=Response(200, text="")) respx.get("/production.json").mock(return_value=Response(404)) respx.get("/api/v1/production").mock( - return_value=Response(200, json=_load_json_fixture(version, "api_v1_production")) + return_value=Response( + 200, json=_load_json_fixture(version, "api_v1_production") + ) ) respx.get("/api/v1/production/inverters").mock( return_value=Response( From b528a922628057ee69ffc1583f78231e943020bc Mon Sep 17 00:00:00 2001 From: Greg Date: Sun, 11 Apr 2021 12:01:27 -0700 Subject: [PATCH 114/134] Bumping envoy_reader version Bumping version for clean up in PR #64 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09552ae..311c6ac 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.3", + version="0.18.4", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 8feb02b83b1c53601c9046e4de818a2f53d1b864 Mon Sep 17 00:00:00 2001 From: StuartW Date: Wed, 14 Apr 2021 21:11:13 +1000 Subject: [PATCH 115/134] Update envoy_reader.py getData() Update getData() to add optional input parameter to not read inverters. Inverters only update every 5 or 15 mins, depending on Envoy-S configuration, whereas production data updates every minute. Also, requesting inverter data at too short an interval can cause the Envoy to start lagging and eventually timeout. See https://thecomputerperson.wordpress.com/2016/08/03/enphase-envoy-s-data-scraping/#comment-1565 for more details. --- envoy_reader/envoy_reader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 93184a5..ec72433 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -125,14 +125,14 @@ async def _async_fetch_with_retry(self, url, **kwargs): if attempt == 2: raise - async def getData(self): # pylint: disable=invalid-name - """Fetch data from the endpoint.""" + async def getData(self, getInverters=True): # pylint: disable=invalid-name + """Fetch data from the endpoint and if inverters selected default to fetching inverter data each read request.""" if not self.endpoint_type: await self.detect_model() else: await self._update() - if not self.get_inverters: + if not self.get_inverters or not getInverters: return inverters_url = ENDPOINT_URL_PRODUCTION_INVERTERS.format(self.host) From 76f98a8c169fde9c39565555e872c147f2ea588f Mon Sep 17 00:00:00 2001 From: StuartW Date: Wed, 14 Apr 2021 21:28:20 +1000 Subject: [PATCH 116/134] Update envoy_reader.py Reduced comment length --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ec72433..f4a09fe 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -126,7 +126,7 @@ async def _async_fetch_with_retry(self, url, **kwargs): raise async def getData(self, getInverters=True): # pylint: disable=invalid-name - """Fetch data from the endpoint and if inverters selected default to fetching inverter data each read request.""" + """Fetch data from the endpoint and if inverters selected default to fetching inverter data.""" if not self.endpoint_type: await self.detect_model() else: From ca84e9c326c3900d7abf49e31b6862031104c580 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 14 Apr 2021 19:01:09 -0700 Subject: [PATCH 117/134] Remove schedule build Given the low number of changes to the repository I feel we can get away with running Workflows with PR merges and commits to main --- .github/workflows/build-main.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml index 8b15f94..fb42f29 100644 --- a/.github/workflows/build-main.yml +++ b/.github/workflows/build-main.yml @@ -4,11 +4,6 @@ on: push: branches: - main - schedule: - # - # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 - # Run every Monday at 18:00:00 UTC (Monday at 10:00:00 PST) - - cron: '0 18 * * 1' jobs: test: From b2b14664b6a275fdbaaccc5858c2050ba4f17414 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 14 Apr 2021 19:07:03 -0700 Subject: [PATCH 118/134] Added build badge to Readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b34867d..1729bfa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Main](https://github.com/jesserizzo/envoy_reader/actions/workflows/build-main.yml/badge.svg)](https://github.com/jesserizzo/envoy_reader/actions/workflows/build-main.yml) + A program to read from an Enphase Envoy on the local network. Reads electricity production and consumption (if available) for the current moment, current day, the last seven days, and the lifetime of the Envoy. Also reads production from individual inverters if supported. From f1e747c6c9fbd135ff220bb881803b5e60681905 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 14 Apr 2021 19:14:46 -0700 Subject: [PATCH 119/134] Added Latest release badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1729bfa..e92e80b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Main](https://github.com/jesserizzo/envoy_reader/actions/workflows/build-main.yml/badge.svg)](https://github.com/jesserizzo/envoy_reader/actions/workflows/build-main.yml) +[![Latest Release](https://img.shields.io/github/v/release/jesserizzo/envoy_reader)](https://img.shields.io/github/v/release/jesserizzo/envoy_reader) A program to read from an Enphase Envoy on the local network. Reads electricity production and consumption (if available) for the current moment, current day, the last seven days, and the lifetime of the Envoy. Also reads production from individual inverters if supported. From f611d5d30821ef8db81d962f4f4574bd0c9a0cda Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Thu, 15 Apr 2021 07:56:24 -0500 Subject: [PATCH 120/134] Rename master to main (#66) * Rename master to main --- .github/workflows/pythonapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 546de3c..c15c643 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -3,7 +3,7 @@ name: Python application on: pull_request: branches: - - master + - main jobs: build: From a4816c1f7bf91266c051463b03d3c25bd2f6b531 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 13:11:38 -0700 Subject: [PATCH 121/134] Bumping version to 0.18.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 311c6ac..c3af023 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.4", + version="0.18.5", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From e938f70862d32c29426117aeae2b6f6674495146 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 17:02:58 -0700 Subject: [PATCH 122/134] Added battery status function --- envoy_reader/envoy_reader.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index f4a09fe..32a4c7d 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -443,6 +443,25 @@ async def inverters_production(self): return response_dict + async def battery_status(self): + if self.endpoint_type == ENVOY_MODEL_LEGACY: + return None + + response_dict = {} + try: + raw_json = self.endpoint_production_json_results.json() + response_dict["type"] = raw_json["storage"][0]["type"] + response_dict["activeCount"] = raw_json["storage"][0]["activeCount"] + response_dict["readingTime"] = raw_json["storage"][0]["readingTime"] + response_dict["wNow"] = raw_json["storage"][0]["wNow"] + response_dict["whNow"] = raw_json["storage"][0]["whNow"] + response_dict["state"] = raw_json["storage"][0]["state"] + except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + return None + + print(response_dict) + return response_dict + def run_in_console(self): """If running this module directly, print all the values in the console.""" print("Reading...") @@ -463,7 +482,8 @@ def run_in_console(self): self.lifetime_production(), self.lifetime_consumption(), self.inverters_production(), - return_exceptions=True, + self.battery_status(), + return_exceptions=False, ) ) @@ -485,6 +505,7 @@ def run_in_console(self): ) else: print("inverters_production: {}".format(results[8])) + print("battery: {}".format(results[9])) if __name__ == "__main__": From e183ba2916988d25ef4954677655c2cbd26dc637 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 17:27:07 -0700 Subject: [PATCH 123/134] Removing debug --- envoy_reader/envoy_reader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 32a4c7d..603c48b 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -459,7 +459,6 @@ async def battery_status(self): except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): return None - print(response_dict) return response_dict def run_in_console(self): @@ -483,7 +482,7 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production(), self.battery_status(), - return_exceptions=False, + return_exceptions=True, ) ) From b23dc9683cd6030f0e4d6e781651f20115c42312 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 19:29:13 -0700 Subject: [PATCH 124/134] Fix errors with other Envoy types --- envoy_reader/envoy_reader.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 603c48b..8ddf919 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -51,6 +51,10 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes # P for production data only (ie. Envoy model C, s/w >= R3.9) # PC for production and consumption data (ie. Envoy model S) + message_battery_not_available = ( + "Battery storage data not available for your Envoy device." + ) + message_consumption_not_available = ( "Consumption data not available for your Envoy device." ) @@ -444,19 +448,26 @@ async def inverters_production(self): return response_dict async def battery_status(self): - if self.endpoint_type == ENVOY_MODEL_LEGACY: - return None + if self.endpoint_type == ENVOY_MODEL_LEGACY or self.endpoint_type == ENVOY_MODEL_C: + return self.message_battery_not_available response_dict = {} try: raw_json = self.endpoint_production_json_results.json() + except (JSONDecodeError): + return None + + if "storage" not in raw_json.keys(): + return self.message_battery_not_available + + try: response_dict["type"] = raw_json["storage"][0]["type"] response_dict["activeCount"] = raw_json["storage"][0]["activeCount"] response_dict["readingTime"] = raw_json["storage"][0]["readingTime"] response_dict["wNow"] = raw_json["storage"][0]["wNow"] response_dict["whNow"] = raw_json["storage"][0]["whNow"] response_dict["state"] = raw_json["storage"][0]["state"] - except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + except (KeyError, IndexError, TypeError, AttributeError): return None return response_dict @@ -482,7 +493,7 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production(), self.battery_status(), - return_exceptions=True, + return_exceptions=False, ) ) From 63342e3182f19f13d0a8b3ebca2e58b0e0422c78 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 19:32:53 -0700 Subject: [PATCH 125/134] Removing extra whitespace --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 8ddf919..17af3e5 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -54,7 +54,7 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes message_battery_not_available = ( "Battery storage data not available for your Envoy device." ) - + message_consumption_not_available = ( "Consumption data not available for your Envoy device." ) From 92a99df64db71c5064fe7b0f50f715ba24f6fb0d Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Apr 2021 19:36:07 -0700 Subject: [PATCH 126/134] Fixed black issue --- envoy_reader/envoy_reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 17af3e5..32a0efd 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -448,7 +448,10 @@ async def inverters_production(self): return response_dict async def battery_status(self): - if self.endpoint_type == ENVOY_MODEL_LEGACY or self.endpoint_type == ENVOY_MODEL_C: + if ( + self.endpoint_type == ENVOY_MODEL_LEGACY + or self.endpoint_type == ENVOY_MODEL_C + ): return self.message_battery_not_available response_dict = {} From d3782e2c3fc59c6ea89ecd2f4ebdf58bef391fdb Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Fri, 23 Apr 2021 00:28:09 -0700 Subject: [PATCH 127/134] Optimzed code and return when battery is found --- envoy_reader/envoy_reader.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 32a0efd..9b5719b 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -454,26 +454,15 @@ async def battery_status(self): ): return self.message_battery_not_available - response_dict = {} try: raw_json = self.endpoint_production_json_results.json() except (JSONDecodeError): return None - if "storage" not in raw_json.keys(): + if "percentFull" not in raw_json["storage"][0].keys(): return self.message_battery_not_available - try: - response_dict["type"] = raw_json["storage"][0]["type"] - response_dict["activeCount"] = raw_json["storage"][0]["activeCount"] - response_dict["readingTime"] = raw_json["storage"][0]["readingTime"] - response_dict["wNow"] = raw_json["storage"][0]["wNow"] - response_dict["whNow"] = raw_json["storage"][0]["whNow"] - response_dict["state"] = raw_json["storage"][0]["state"] - except (KeyError, IndexError, TypeError, AttributeError): - return None - - return response_dict + return raw_json["storage"][0] def run_in_console(self): """If running this module directly, print all the values in the console.""" From 4cd7c53fb42f72d94683c2f6d393cae80ec30d35 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 23 Apr 2021 15:48:08 -0700 Subject: [PATCH 128/134] Bumping version to 0.19.0 due to new feature --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c3af023..fc4add1 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ setuptools.setup( name="envoy_reader", - version="0.18.5", + version="0.19.0", author="Jesse Rizzo", author_email="jesse.rizzo@gmail.com", description="A program to read from an Enphase Envoy on the local network", From 73a8c34f8d76f9ba3d9085ffdd74c9dd825a22ff Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 23 Apr 2021 15:54:00 -0700 Subject: [PATCH 129/134] Added comments for battery_status function --- envoy_reader/envoy_reader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 9b5719b..9487f36 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -448,6 +448,7 @@ async def inverters_production(self): return response_dict async def battery_status(self): + """Return battery data from Envoys that support and have batteries installed""" if ( self.endpoint_type == ENVOY_MODEL_LEGACY or self.endpoint_type == ENVOY_MODEL_C @@ -459,6 +460,9 @@ async def battery_status(self): except (JSONDecodeError): return None + """For Envoys that support batteries but do not have them installed the""" + """percentFull will not be available in the JSON results. The API will""" + """only return battery data if batteries are installed.""" if "percentFull" not in raw_json["storage"][0].keys(): return self.message_battery_not_available From 3668de9aded3ab6c2d439d47afdac4e7c4734f2e Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 23 Apr 2021 15:57:47 -0700 Subject: [PATCH 130/134] Renamed function --- envoy_reader/envoy_reader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 9487f36..bf65552 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -447,7 +447,7 @@ async def inverters_production(self): return response_dict - async def battery_status(self): + async def battery_storage(self): """Return battery data from Envoys that support and have batteries installed""" if ( self.endpoint_type == ENVOY_MODEL_LEGACY @@ -488,7 +488,7 @@ def run_in_console(self): self.lifetime_production(), self.lifetime_consumption(), self.inverters_production(), - self.battery_status(), + self.battery_storage(), return_exceptions=False, ) ) @@ -511,7 +511,7 @@ def run_in_console(self): ) else: print("inverters_production: {}".format(results[8])) - print("battery: {}".format(results[9])) + print("battery_storage: {}".format(results[9])) if __name__ == "__main__": From b814e1de1fb596654694a7686bb321713043d118 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 23 Apr 2021 16:08:54 -0700 Subject: [PATCH 131/134] Reverting return_exceptions boolean --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index bf65552..5870a78 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -489,7 +489,7 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production(), self.battery_storage(), - return_exceptions=False, + return_exceptions=True, ) ) From 54b0e330fb748b3846d3ca77c24bd5c866b9f619 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 29 Apr 2021 14:11:32 -0700 Subject: [PATCH 132/134] Added enpower status --- envoy_reader/envoy_reader.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index 5870a78..a662ea7 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -20,6 +20,7 @@ ) SERIAL_REGEX = re.compile(r"Envoy\s*Serial\s*Number:\s*([0-9]+)") +ENDPOINT_URL_HOME_JSON = "http://{}/home.json" ENDPOINT_URL_PRODUCTION_JSON = "http://{}/production.json" ENDPOINT_URL_PRODUCTION_V1 = "http://{}/api/v1/production" ENDPOINT_URL_PRODUCTION_INVERTERS = "http://{}/api/v1/production/inverters" @@ -59,6 +60,10 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes "Consumption data not available for your Envoy device." ) + message_enpower_not_available = ( + "Enpower Smart Switch status is not available for your Envoy device." + ) + def __init__( # pylint: disable=too-many-arguments self, host, username="envoy", password="", inverters=False, async_client=None ): @@ -69,6 +74,7 @@ def __init__( # pylint: disable=too-many-arguments self.get_inverters = inverters self.endpoint_type = None self.serial_number_last_six = None + self.endpoint_home_json_results = None self.endpoint_production_json_results = None self.endpoint_production_v1_results = None self.endpoint_production_inverters = None @@ -97,6 +103,9 @@ async def _update_from_pc_endpoint(self): await self._update_endpoint( "endpoint_production_json_results", ENDPOINT_URL_PRODUCTION_JSON ) + await self._update_endpoint( + "endpoint_home_json_results", ENDPOINT_URL_HOME_JSON + ) async def _update_from_p_endpoint(self): """Update from P endpoint.""" @@ -468,6 +477,24 @@ async def battery_storage(self): return raw_json["storage"][0] + async def enpower_status(self): + """Return Enpower Smart Swirch data from Envoys that support it.""" + if ( + self.endpoint_type == ENVOY_MODEL_LEGACY + or self.endpoint_type == ENVOY_MODEL_C + ): + return self.message_enpower_not_available + + try: + raw_json = self.endpoint_production_json_results.json() + except (JSONDecodeError): + return None + + if "enpower" not in raw_json.keys(): + return self.message_battery_not_available + + return raw_json["enpower"] + def run_in_console(self): """If running this module directly, print all the values in the console.""" print("Reading...") @@ -489,6 +516,7 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production(), self.battery_storage(), + self.enpower_status(), return_exceptions=True, ) ) @@ -512,6 +540,7 @@ def run_in_console(self): else: print("inverters_production: {}".format(results[8])) print("battery_storage: {}".format(results[9])) + print("enpower_status: {}".format(results[10])) if __name__ == "__main__": From c9bee6f597da21ab0359596eed5a33bfb3dcf19d Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 29 Apr 2021 14:12:34 -0700 Subject: [PATCH 133/134] Fixed message output --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index a662ea7..ee24c3f 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -491,7 +491,7 @@ async def enpower_status(self): return None if "enpower" not in raw_json.keys(): - return self.message_battery_not_available + return self.message_enpower_not_available return raw_json["enpower"] From 891cc6a9054e8f360facad60467323c88ac74281 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 29 Apr 2021 14:24:04 -0700 Subject: [PATCH 134/134] Fixed endpoint variable --- envoy_reader/envoy_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envoy_reader/envoy_reader.py b/envoy_reader/envoy_reader.py index ee24c3f..2509127 100644 --- a/envoy_reader/envoy_reader.py +++ b/envoy_reader/envoy_reader.py @@ -486,7 +486,7 @@ async def enpower_status(self): return self.message_enpower_not_available try: - raw_json = self.endpoint_production_json_results.json() + raw_json = self.endpoint_home_json_results.json() except (JSONDecodeError): return None