From b1f58a1ec5ccfadcc448a5e566420dd156b525a1 Mon Sep 17 00:00:00 2001 From: John James Date: Wed, 16 Apr 2025 16:35:43 -0700 Subject: [PATCH 1/3] Issue #162: add /pw/XXX endpoints to expose Powerwall() API methods as JSON-returning endpoints --- proxy/server.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/proxy/server.py b/proxy/server.py index eea0489..86be56a 100755 --- a/proxy/server.py +++ b/proxy/server.py @@ -68,6 +68,33 @@ DISABLED = [ '/api/customer/registration', ] +PW_ALLOWLIST = [ + '/pw/level', + '/pw/power', + '/pw/site', + '/pw/solar', + '/pw/battery', + '/pw/battery_blocks', + '/pw/load', + '/pw/grid', + '/pw/home', + '/pw/vitals', + '/pw/aggregates', + '/pw/temps', + '/pw/strings', + '/pw/din', + '/pw/uptime', + '/pw/version', + '/pw/status', + '/pw/system_status', + '/pw/grid_status', + '/pw/site_name', + '/pw/alerts', + '/pw/is_connected', + '/pw/get_reserve', + '/pw/get_mode', + '/pw/get_time_remaining', +] web_root = os.path.join(os.path.dirname(__file__), "web") # Configuration for Proxy - Check for environmental variables @@ -730,6 +757,81 @@ def do_GET(self): message = json.dumps(fans) else: message = '{}' + elif self.path in PW_ALLOWLIST: + # Allowed API Calls - Proxy to Powerwall + ftype = 'application/json' + if self.path == '/pw/level': + powerwall_level_doc = {} + powerwall_level_doc["level"] = pw.level() + message = json.dumps(powerwall_level_doc) + elif self.path == '/pw/power': + message = json.dumps(pw.power()) + elif self.path == '/pw/site': + message = json.dumps(pw.site(True)) + elif self.path == '/pw/solar': + message = json.dumps(pw.solar(True)) + elif self.path == '/pw/battery': + message = json.dumps(pw.battery(True)) + elif self.path == '/pw/battery_blocks': + message = json.dumps(pw.battery_blocks()) + elif self.path == '/pw/load': + message = json.dumps(pw.load(True)) + elif self.path == '/pw/grid': + message = json.dumps(pw.grid(True)) + elif self.path == '/pw/home': + message = json.dumps(pw.home(True)) + elif self.path == '/pw/vitals': + message = json.dumps(pw.vitals()) + elif self.path == '/pw/temps': + message = json.dumps(pw.temps()) + elif self.path == '/pw/strings': + message = json.dumps(pw.strings(False, True)) + elif self.path == '/pw/din': + powerwall_din_doc = {} + powerwall_din_doc["din"] = pw.din() + message = json.dumps(powerwall_din_doc) + elif self.path == '/pw/uptime': + powerwall_uptime_doc = {} + powerwall_uptime_doc["uptime"] = pw.uptime() + message = json.dumps(powerwall_uptime_doc) + elif self.path == '/pw/version': + powerwall_version_doc = {} + powerwall_version_doc["version"] = pw.version() + message = json.dumps(powerwall_version_doc) + elif self.path == '/pw/status': + message = json.dumps(pw.status()) + elif self.path == '/pw/system_status': + message = json.dumps(pw.system_status(False)) + elif self.path == '/pw/grid_status': + grid_status_str = pw.grid_status(type="json") + message = json.loads(grid_status_str) + message = grid_status_str + elif self.path == '/pw/aggregates': + message = json.dumps(pw.poll('/api/meters/aggregates', False)) + elif self.path == '/pw/site_name': + powerwall_site_name_doc = {} + powerwall_site_name_doc["site_name"] = pw.site_name() + message = json.dumps(powerwall_site_name_doc) + elif self.path == '/pw/alerts': + powerwall_alerts_doc = {} + powerwall_alerts_doc["alerts"] = pw.alerts() + message = json.dumps(powerwall_alerts_doc) + elif self.path == '/pw/is_connected': + powerwall_connected_doc = {} + powerwall_connected_doc["is_connected"] = pw.is_connected() + message = json.dumps(powerwall_connected_doc) + elif self.path == '/pw/get_reserve': + powerwall_reserve_doc = {} + powerwall_reserve_doc["reserve"] = pw.get_reserve() + message = json.dumps(powerwall_reserve_doc) + elif self.path == '/pw/get_mode': + powerwall_mode_doc = {} + powerwall_mode_doc["mode"] = pw.get_mode() + message = json.dumps(powerwall_mode_doc) + elif self.path == '/pw/get_time_remaining': + powerwall_time_remaining_doc = {} + powerwall_time_remaining_doc["time_remaining"] = pw.get_time_remaining() + message = json.dumps(powerwall_time_remaining_doc) else: # Everything else - Set auth headers required for web application proxystats['gets'] = proxystats['gets'] + 1 From 46fc58d5ddfeb932f1c38eb99e3f3c6e7df8be12 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Wed, 16 Apr 2025 22:29:49 -0700 Subject: [PATCH 2/3] Refactor Powerwall API handling to use a mapping approach --- proxy/server.py | 138 +++++++++++++----------------------------------- 1 file changed, 36 insertions(+), 102 deletions(-) diff --git a/proxy/server.py b/proxy/server.py index 86be56a..7833d7a 100755 --- a/proxy/server.py +++ b/proxy/server.py @@ -68,33 +68,6 @@ DISABLED = [ '/api/customer/registration', ] -PW_ALLOWLIST = [ - '/pw/level', - '/pw/power', - '/pw/site', - '/pw/solar', - '/pw/battery', - '/pw/battery_blocks', - '/pw/load', - '/pw/grid', - '/pw/home', - '/pw/vitals', - '/pw/aggregates', - '/pw/temps', - '/pw/strings', - '/pw/din', - '/pw/uptime', - '/pw/version', - '/pw/status', - '/pw/system_status', - '/pw/grid_status', - '/pw/site_name', - '/pw/alerts', - '/pw/is_connected', - '/pw/get_reserve', - '/pw/get_mode', - '/pw/get_time_remaining', -] web_root = os.path.join(os.path.dirname(__file__), "web") # Configuration for Proxy - Check for environmental variables @@ -757,81 +730,42 @@ def do_GET(self): message = json.dumps(fans) else: message = '{}' - elif self.path in PW_ALLOWLIST: - # Allowed API Calls - Proxy to Powerwall - ftype = 'application/json' - if self.path == '/pw/level': - powerwall_level_doc = {} - powerwall_level_doc["level"] = pw.level() - message = json.dumps(powerwall_level_doc) - elif self.path == '/pw/power': - message = json.dumps(pw.power()) - elif self.path == '/pw/site': - message = json.dumps(pw.site(True)) - elif self.path == '/pw/solar': - message = json.dumps(pw.solar(True)) - elif self.path == '/pw/battery': - message = json.dumps(pw.battery(True)) - elif self.path == '/pw/battery_blocks': - message = json.dumps(pw.battery_blocks()) - elif self.path == '/pw/load': - message = json.dumps(pw.load(True)) - elif self.path == '/pw/grid': - message = json.dumps(pw.grid(True)) - elif self.path == '/pw/home': - message = json.dumps(pw.home(True)) - elif self.path == '/pw/vitals': - message = json.dumps(pw.vitals()) - elif self.path == '/pw/temps': - message = json.dumps(pw.temps()) - elif self.path == '/pw/strings': - message = json.dumps(pw.strings(False, True)) - elif self.path == '/pw/din': - powerwall_din_doc = {} - powerwall_din_doc["din"] = pw.din() - message = json.dumps(powerwall_din_doc) - elif self.path == '/pw/uptime': - powerwall_uptime_doc = {} - powerwall_uptime_doc["uptime"] = pw.uptime() - message = json.dumps(powerwall_uptime_doc) - elif self.path == '/pw/version': - powerwall_version_doc = {} - powerwall_version_doc["version"] = pw.version() - message = json.dumps(powerwall_version_doc) - elif self.path == '/pw/status': - message = json.dumps(pw.status()) - elif self.path == '/pw/system_status': - message = json.dumps(pw.system_status(False)) - elif self.path == '/pw/grid_status': - grid_status_str = pw.grid_status(type="json") - message = json.loads(grid_status_str) - message = grid_status_str - elif self.path == '/pw/aggregates': - message = json.dumps(pw.poll('/api/meters/aggregates', False)) - elif self.path == '/pw/site_name': - powerwall_site_name_doc = {} - powerwall_site_name_doc["site_name"] = pw.site_name() - message = json.dumps(powerwall_site_name_doc) - elif self.path == '/pw/alerts': - powerwall_alerts_doc = {} - powerwall_alerts_doc["alerts"] = pw.alerts() - message = json.dumps(powerwall_alerts_doc) - elif self.path == '/pw/is_connected': - powerwall_connected_doc = {} - powerwall_connected_doc["is_connected"] = pw.is_connected() - message = json.dumps(powerwall_connected_doc) - elif self.path == '/pw/get_reserve': - powerwall_reserve_doc = {} - powerwall_reserve_doc["reserve"] = pw.get_reserve() - message = json.dumps(powerwall_reserve_doc) - elif self.path == '/pw/get_mode': - powerwall_mode_doc = {} - powerwall_mode_doc["mode"] = pw.get_mode() - message = json.dumps(powerwall_mode_doc) - elif self.path == '/pw/get_time_remaining': - powerwall_time_remaining_doc = {} - powerwall_time_remaining_doc["time_remaining"] = pw.get_time_remaining() - message = json.dumps(powerwall_time_remaining_doc) + elif self.path.startswith('/pw/'): + # Map library functions into /pw/ API calls + path = self.path[4:] # Remove '/pw/' prefix + simple_mappings = { + 'level': lambda: {'level': pw.level()}, + 'power': pw.power, + 'site': lambda: pw.site(True), + 'solar': lambda: pw.solar(True), + 'battery': lambda: pw.battery(True), + 'battery_blocks': pw.battery_blocks, + 'load': lambda: pw.load(True), + 'grid': lambda: pw.grid(True), + 'home': lambda: pw.home(True), + 'vitals': pw.vitals, + 'temps': pw.temps, + 'strings': lambda: pw.strings(False, True), + 'din': lambda: {'din': pw.din()}, + 'uptime': lambda: {'uptime': pw.uptime()}, + 'version': lambda: {'version': pw.version()}, + 'status': pw.status, + 'system_status': lambda: pw.system_status(False), + 'grid_status': lambda: json.loads(pw.grid_status(type="json")), + 'aggregates': lambda: pw.poll('/api/meters/aggregates', False), + 'site_name': lambda: {'site_name': pw.site_name()}, + 'alerts': lambda: {'alerts': pw.alerts()}, + 'is_connected': lambda: {'is_connected': pw.is_connected()}, + 'get_reserve': lambda: {'reserve': pw.get_reserve()}, + 'get_mode': lambda: {'mode': pw.get_mode()}, + 'get_time_remaining': lambda: {'time_remaining': pw.get_time_remaining()} + } + # Check if the path is in the simple mappings + if path in simple_mappings: + result = simple_mappings[path]() + else: + result = {"error": "Invalid Request"} + message = json.dumps(result) else: # Everything else - Set auth headers required for web application proxystats['gets'] = proxystats['gets'] + 1 From bee32aae710d0ef1c9a5e4acdcd0dbee6ddac82e Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Wed, 16 Apr 2025 22:32:09 -0700 Subject: [PATCH 3/3] Update release notes and increment build version to t72 --- proxy/RELEASE.md | 4 ++++ proxy/server.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/proxy/RELEASE.md b/proxy/RELEASE.md index 75158bf..5dd8e35 100644 --- a/proxy/RELEASE.md +++ b/proxy/RELEASE.md @@ -1,5 +1,9 @@ ## pyPowerwall Proxy Release Notes +### Proxy t72 (16 Apr 2025) + +* Add routes to map library functions into `/pw/` APIs (e.g. /pw/power) + ### Proxy t71 (6 Apr 2025) * Add routes for fan speeds: `/fans` and `/fans/pw` (simple enumerated values for dashboard) diff --git a/proxy/server.py b/proxy/server.py index 7833d7a..7c22c3b 100755 --- a/proxy/server.py +++ b/proxy/server.py @@ -54,7 +54,7 @@ import pypowerwall from pypowerwall import parse_version -BUILD = "t71" +BUILD = "t72" ALLOWLIST = [ '/api/status', '/api/site_info/site_name', '/api/meters/site', '/api/meters/solar', '/api/sitemaster', '/api/powerwalls',