Skip to content

Commit

Permalink
Merge pull request #83 from jasonacox/v0.8.3
Browse files Browse the repository at this point in the history
V0.8.3 - Control APIs
  • Loading branch information
jasonacox authored Apr 14, 2024
2 parents 9ca8260 + 61b5c69 commit f5afa54
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 32 deletions.
5 changes: 5 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# RELEASE NOTES

## v0.8.3 - Error Handling

* Added additional error handling logic to clean up exceptions.
* Proxy: Added command APIs for setting backup reserve and operating mode.

## v0.8.2 - 503 Error Handling

* Added 5 minute cooldown for HTTP 503 Service Unavailable errors from API calls.
Expand Down
109 changes: 83 additions & 26 deletions proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,37 @@

This pyPowerwall Caching Proxy handles authentication to the Powerwall Gateway and will proxy API calls to /api/meters/aggregates (power metrics), /api/system_status/soe (battery level), and many others (see [HELP](https://github.com/jasonacox/pypowerwall/blob/main/proxy/HELP.md) for full list). With the instructions below, you can containerize this proxy and run it as an endpoint for tools like telegraf to pull metrics without needing to authenticate.

Because pyPowerwall is designed to cache the auth and high frequency API calls, while also utilising HTTP persistent connections, this will reduce the load on the Gateway and prevent crash/restart issues that can happen if too many session are created on the Gateway.
**Cache**: Because pyPowerwall is designed to cache the auth and high frequency API calls and use HTTP persistent connections. This will help reduce the load on the Gateway and prevent crash/restart issues that can happen if too many session are created on the Gateway. Logic in pypowerwall will also activate cooldown modes if the Gateway responds with errors indicating overload.

Docker: docker pull [jasonacox/pypowerwall](https://hub.docker.com/r/jasonacox/pypowerwall)
**Local or Cloud**: The proxy uses the built in abstraction of pypowerwall to operate in two modes: `local mode` and `cloud mode`. Local mode will connect directly with your Powerwall's Tesla Energy Gateway (TEG) to pull realtime data. Cloud mode will connect to the Tesla cloud APIs to pull realtime data. Cloud mode has lower fidelity than local mode and does not include some data points available on the the local API.

**Control Mode**: An optional mode allows the proxy to send control commands to set backup reserve percentage and mode of the Powerwall. This requires that you set and use the `PW_CONTROL_SECRET` environmental variable. For safety reasons, this mode is disabled by default and should be used with caution.

## Quick Start

1. Run the Docker Container to listen on port 8675. Update the `-e` values for your Powerwall (see [Environmental Settings](https://github.com/jasonacox/pypowerwall/tree/main/proxy#environmental-settings) for options):

```bash
docker run \
-d \
-p 8675:8675 \
-e PW_PORT='8675' \
-e PW_PASSWORD='password' \
-e PW_EMAIL='email@example.com' \
-e PW_HOST='IP_of_Powerwall_Gateway' \
-e PW_TIMEZONE='America/Los_Angeles' \
-e TZ='America/Los_Angeles' \
-e PW_CACHE_EXPIRE='5' \
-e PW_DEBUG='no' \
-e PW_HTTPS='no' \
-e PW_STYLE='clear' \
--name pypowerwall \
--restart unless-stopped \
jasonacox/pypowerwall
-d \
-p 8675:8675 \
-e PW_PORT='8675' \
-e PW_PASSWORD='password' \
-e PW_EMAIL='email@example.com' \
-e PW_HOST='IP_of_Powerwall_Gateway' \
-e PW_TIMEZONE='America/Los_Angeles' \
-e TZ='America/Los_Angeles' \
-e PW_CACHE_EXPIRE='5' \
-e PW_DEBUG='no' \
-e PW_HTTPS='no' \
-e PW_STYLE='clear' \
--name pypowerwall \
--restart unless-stopped \
jasonacox/pypowerwall

# Cloud Mode Setup - Optional
docker exec -it pypowerwall python3 -m pypowerwall setup -email=email@example.com
docker restart pypowerwall
```

2. Test the Proxy
Expand Down Expand Up @@ -68,11 +74,11 @@ The `Dockerfile` here will allow you to containerize the proxy server for clean

```bash
docker run \
-d \
-p 8675:8675 \
--name pypowerwall \
--restart unless-stopped \
pypowerwall
-d \
-p 8675:8675 \
--name pypowerwall \
--restart unless-stopped \
pypowerwall
```

3. Test the Proxy
Expand Down Expand Up @@ -142,10 +148,10 @@ The pyPowerwall Proxy will react to the following environmental variables with (
Powerwall Settings
* PW_PASSWORD - Powerwall customer password ("password")
* PW_EMAIL - Powerwall customer email ("email@example.com")
* PW_HOST - Powerwall hostname or IP address ("hostname")
* PW_TIMEZONE - Local timezone ("America/Los_Angeles")
* PW_PASSWORD - Powerwall customer password ("password") [required]
* PW_EMAIL - Powerwall customer email ("email@example.com") [required for cloudmode]
* PW_HOST - Powerwall hostname or IP address ("hostname") [required for local mode]
* PW_TIMEZONE - Local timezone ("America/Los_Angeles") [optional]
Proxy Settings
Expand All @@ -163,6 +169,57 @@ Proxy Settings
* white (uses `#ffffff` ![#ffffff](https://via.placeholder.com/12/ffffff/000000.png?text=+))
* grafana (uses `#161719` ![#161719](https://via.placeholder.com/12/161719/000000.png?text=+))
* grafana-dark (uses `#111217` ![#111217](https://via.placeholder.com/12/111217/000000.png?text=+))
* PW_AUTH_PATH - Location (path) for authentication and cache files ("")
* PW_AUTH_MODE - Use `cookie` (default) or `token` for authentication
* PW_CACHE_FILE - Proxy cache file path, with override PW_AUTH_PATH if provided (".powerwall")
* PW_SITEID - For `cloud mode`, if you have multiple sites configured, use this site ID ("")
* PW_CONTROL_SECRET - If provided, will activate the Powerwall control commands to adjust Powerwall backup reserve level and mode (disabled by default)
## Control Mode
If the `PW_CONTROL_SECRET` environmental variable is set, the proxy will attempt to connect ot the cloud in addition to local mode setup (if you are using local mode). The `PW_EMAIL` must match your Tesla account and you need to run **cloud setup** before using this mode.
_WARNING_: Activating control mode means that the proxy can make changes to your system. This will be available to anyone who can access the proxy. For safety reasons, this mode is disabled by default and should be used with caution.
```bash
# Run Proxy
docker run \
-d \
-p 8675:8675 \
-e PW_PORT='8675' \
-e PW_PASSWORD='password' \
-e PW_EMAIL='email@example.com' \
-e PW_HOST='IP_of_Powerwall_Gateway' \
-e PW_TIMEZONE='America/Los_Angeles' \
-e TZ='America/Los_Angeles' \
-e PW_CONTROL_SECRET='YourSecretToken' \
--name pypowerwall \
--restart unless-stopped \
jasonacox/pypowerwall
# Setup Cloud
docker exec -it pypowerwall python3 -m pypowerwall setup -email=email@example.com
docker restart pypowerwall
```
APIs
* Set Mode: `/control/mode?token=$PW_CONTROL_SECRET&value=self_consumption`
* Set Reserve: `/control/reserve?token=$PW_CONTROL_SECRET&value=20`
Examples
```bash
# Set Mode
curl "http://localhost:8675/control/mode?token=$PW_CONTROL_SECRET&value=self_consumption"
# Set Reserve
curl "http://localhost:8675/control/reserve?token=$PW_CONTROL_SECRET&value=20"
# Omit Value to Read Settings
curl "http://localhost:8675/control/mode?token=$PW_CONTROL_SECRET"
curl "http://localhost:8675/control/reserve?token=$PW_CONTROL_SECRET"
```
## Release Notes
Expand Down
17 changes: 17 additions & 0 deletions proxy/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
## pyPowerwall Proxy Release Notes

### Proxy t54 (13 Apr 2024)

* Fix `/pod` API to add `time_remaining_hours` and `backup_reserve_percent` for cloud mode.
* Added GET command APIs to set backup reserve and operating mode settings. Requires setting `PW_CONTROL_SECRET`. Use with caution.

```bash
# Set Mode
curl http://localhost:8675/control/mode?token=$PW_CONTROL_SECRET&value=self_consumption

# Set Reserve
curl http://localhost:8675/control/reserve?token=$PW_CONTROL_SECRET&value=20

# Omit Value to Read Settings
curl http://localhost:8675/control/mode?token=$PW_CONTROL_SECRET
curl http://localhost:8675/control/reserve?token=$PW_CONTROL_SECRET
```

### Proxy t53 (11 Apr 2024)

* Add DISABLED API handling logic.
Expand Down
2 changes: 1 addition & 1 deletion proxy/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pypowerwall==0.8.2
pypowerwall==0.8.3
bs4==0.0.2
75 changes: 72 additions & 3 deletions proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
/strings data.
Set: PW_EMAIL and leave PW_HOST blank to use this mode.
Control Mode
An optional mode is to enable control commands to set backup reserve
percentage and mode of the Powerwall. This requires that you set
and use the PW_CONTROL_SECRET environmental variable. This mode
is disabled by default and should be used with caution.
Set: PW_CONTROL_SECRET to enable this mode.
"""
import datetime
import json
Expand All @@ -45,8 +52,9 @@
import pypowerwall
from pypowerwall import parse_version
from transform import get_static, inject_js
from urllib.parse import urlparse, parse_qs

BUILD = "t53"
BUILD = "t54"
ALLOWLIST = [
'/api/status', '/api/site_info/site_name', '/api/meters/site',
'/api/meters/solar', '/api/sitemaster', '/api/powerwalls',
Expand Down Expand Up @@ -84,6 +92,7 @@
if authpath:
cf = os.path.join(authpath, ".powerwall")
cachefile = os.getenv("PW_CACHE_FILE", cf)
control_secret = os.getenv("PW_CONTROL_SECRET", "")

# Global Stats
proxystats = {
Expand Down Expand Up @@ -181,6 +190,20 @@ def get_value(a, key):
log.info("pyPowerwall Proxy Server - Local Mode")
log.info("Connected to Energy Gateway %s (%s)" % (host, pw.site_name().strip()))

if control_secret:
log.info("Control Commands Activating - WARNING: Use with caution!")
try:
if pw.cloudmode:
pw_control = pw
else:
pw_control = pypowerwall.Powerwall("", password, email, siteid=siteid,
authpath=authpath, authmode=authmode,
cachefile=cachefile)
except Exception as e:
log.error("Control Mode Failed: Unable to connect to cloud - Run Setup")
control_secret = ""
if pw_control:
log.info("Control Mode Enabled: Cloud Mode Connected")

class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
Expand Down Expand Up @@ -387,10 +410,10 @@ def do_GET(self):
# Aggregate data
if pod:
# Only poll if we have battery data
pod["time_remaining_hours"] = pw.get_time_remaining()
pod["backup_reserve_percent"] = pw.get_reserve()
pod["nominal_full_pack_energy"] = get_value(d, 'nominal_full_pack_energy')
pod["nominal_energy_remaining"] = get_value(d, 'nominal_energy_remaining')
pod["time_remaining_hours"] = pw.get_time_remaining()
pod["backup_reserve_percent"] = pw.get_reserve()
message: str = json.dumps(pod)
elif self.path == '/version':
# Firmware Version
Expand Down Expand Up @@ -444,6 +467,52 @@ def do_GET(self):
elif self.path in ALLOWLIST:
# Allowed API Calls - Proxy to Powerwall
message: str = pw.poll(self.path, jsonformat=True)
elif self.path.startswith('/control/'):
# URL = /control/{action}?value={value}&token={token}
message = None
if not control_secret:
message = '{"error": "Control Commands Disabled - Set PW_CONTROL_SECRET to enable"}'
else:
try:
action = urlparse(self.path).path.split('/')[2]
query_params = parse_qs(urlparse(self.path).query)
value = query_params.get('value', [''])[0]
token = query_params.get('token', [''])[0]
except Exception as e:
message = '{"error": "Control Command Error: Invalid URL"}'
log.error(f"Control Command Error: {e}")
if not message:
# Check if unable to connect to cloud
if pw_control.client is None:
message = '{"error": "Control Command Error: Unable to connect to cloud mode - Run Setup"}'
log.error("Control Command Error: Unable to connect to cloud mode - Run Setup")
else:
if token == control_secret:
if action == 'reserve':
# ensure value is an integer
if not value:
# return current reserve level in json string
message = '{"reserve": %s}' % pw_control.get_reserve()
else:
if value.isdigit():
message = json.dumps(pw_control.set_reserve(int(value)))
log.info(f"Control Command: Set Reserve to {value}")
else:
message = '{"error": "Control Command Value Invalid"}'
elif action == 'mode':
if not value:
# return current mode in json string
message = '{"mode": "%s"}' % pw_control.get_mode()
else:
if value in ['self_consumption', 'backup', 'autonomous']:
message = json.dumps(pw_control.set_mode(value))
log.info(f"Control Command: Set Mode to {value}")
else:
message = '{"error": "Control Command Value Invalid"}'
else:
message = '{"error": "Control Command Not Found"}'
else:
message = '{"error": "Control Command Token Invalid"}'
else:
# Everything else - Set auth headers required for web application
proxystats['gets'] = proxystats['gets'] + 1
Expand Down
8 changes: 6 additions & 2 deletions pypowerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@

urllib3.disable_warnings() # Disable SSL warnings

version_tuple = (0, 8, 2)
version_tuple = (0, 8, 3)
version = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'jasonacox'

Expand Down Expand Up @@ -602,6 +602,10 @@ def grid_status(self, type="string") -> Optional[Union[str, int]]:

payload: dict = self.poll('/api/system_status/grid_status')

if payload is None:
log.error(f"Failed to get /api/system_status/grid_status")
return None

if type == "json":
return json.dumps(payload, indent=4, sort_keys=True)

Expand All @@ -613,7 +617,7 @@ def grid_status(self, type="string") -> Optional[Union[str, int]]:
'SystemMicroGridFaulted': {'string': 'DOWN', 'numeric': 0},
'SystemWaitForUser': {'string': 'DOWN', 'numeric': 0}}

grid_status = payload['grid_status']
grid_status = payload.get('grid_status')
status = gridmap.get(grid_status, {}).get(type)
if status is None:
log.debug(f"ERROR unable to parse payload '{payload}' for grid_status of type: {type}")
Expand Down

0 comments on commit f5afa54

Please sign in to comment.