Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonacox committed Jun 15, 2024
2 parents a95a33f + db471ce commit 13c6e36
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 59 deletions.
6 changes: 6 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# RELEASE NOTES

## v0.10.5 - Minor Fixes

* Fix for TEDAPI "full" (e.g. Powerwall 3) mode, including `grid_status` bug resulting in false reports of grid status, `level()` bug where data gap resulted in 0% state of charge and `alerts()` where data gap from tedapi resulted in a `null` alert.
* Add TEDAPI API call locking to limit load caused by concurrent polling.
* Proxy - Add battery full_pack and remaining energy data to `/pod` API call for all cases.

## v0.10.4 - Powerwall 3 Local API Support

* Add local support for Powerwall 3 using TEDAPI.
Expand Down
4 changes: 4 additions & 0 deletions proxy/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## pyPowerwall Proxy Release Notes

### Proxy t62 (13 Jun 2024)

* Add battery full_pack and remaining energy data to `/pod` API call for all cases.

### Proxy t61 (9 Jun 2024)

* Fix 404 bug that would throw error when user requested non-supported URI.
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.10.4
pypowerwall==0.10.5
bs4==0.0.2
8 changes: 3 additions & 5 deletions proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
from transform import get_static, inject_js
from urllib.parse import urlparse, parse_qs

BUILD = "t61"
BUILD = "t62"
ALLOWLIST = [
'/api/status', '/api/site_info/site_name', '/api/meters/site',
'/api/meters/solar', '/api/sitemaster', '/api/powerwalls',
Expand Down Expand Up @@ -497,10 +497,8 @@ def do_GET(self):
pod["PW%d_POD_nom_full_pack_energy" % idx] = get_value(v, 'POD_nom_full_pack_energy')
idx = idx + 1
# Aggregate data
if pod:
# Only poll if we have battery data
pod["nominal_full_pack_energy"] = get_value(d, 'nominal_full_pack_energy')
pod["nominal_energy_remaining"] = get_value(d, 'nominal_energy_remaining')
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)
Expand Down
4 changes: 2 additions & 2 deletions pypowerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
from typing import Union, Optional
import time

version_tuple = (0, 10, 4)
version_tuple = (0, 10, 5)
version = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'jasonacox'

Expand Down Expand Up @@ -606,7 +606,7 @@ def alerts(self, jsonformat=False, alertsonly=True) -> Union[list, str]:
alert = grid_status.get('grid_status')
if alert == 'SystemGridConnected' and 'SystemConnectedToGrid' not in alerts:
alerts.append('SystemConnectedToGrid')
else:
elif alert:
alerts.append(alert)
if grid_status.get('grid_services_active'):
alerts.append('GridServicesActive')
Expand Down
131 changes: 85 additions & 46 deletions pypowerwall/tedapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def __init__(self, gw_pwd: str, debug: bool = False, pwcacheexpire: int = 5, tim
self.gw_ip = host
self.din = None
self.pw3 = False # Powerwall 3 Gateway only supports TEDAPI
self.apilock = {} # holds the api lock status
if not gw_pwd:
raise ValueError("Missing gw_pwd")
if self.debug:
Expand Down Expand Up @@ -177,6 +178,14 @@ def get_config(self,force=False):
"vin": "1232100-00-E--TG11234567890"
}
"""
# Check for lock and wait if api request already sent
if 'config' in self.apilock:
locktime = time.perf_counter()
while self.apilock['config']:
time.sleep(0.2)
if time.perf_counter() >= locktime + self.timeout:
log.debug(f" -- tedapi: Timeout waiting for config (unable to acquire lock)")
return None
# Check Cache
if not force and "config" in self.pwcachetime:
if time.time() - self.pwcachetime["config"] < self.pwconfigexpire:
Expand All @@ -202,30 +211,41 @@ def get_config(self,force=False):
pb.message.config.send.file = "config.json"
pb.tail.value = 1
url = f'https://{self.gw_ip}/tedapi/v1'
r = requests.post(url, auth=('Tesla_Energy_Device', self.gw_pwd), verify=False,
headers={'Content-type': 'application/octet-string'},
data=pb.SerializeToString())
log.debug(f"Response Code: {r.status_code}")
if r.status_code in BUSY_CODES:
# Rate limited - Switch to cooldown mode for 5 minutes
self.pwcooldown = time.perf_counter() + 300
log.error('Possible Rate limited by Powerwall at - Activating 5 minute cooldown')
return None
if r.status_code != 200:
log.error(f"Error fetching config: {r.status_code}")
return None
# Decode response
tedapi = tedapi_pb2.Message()
tedapi.ParseFromString(r.content)
payload = tedapi.message.config.recv.file.text
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
log.error(f"Error Decoding JSON: {e}")
data = {}
log.debug(f"Configuration: {data}")
self.pwcachetime["config"] = time.time()
self.pwcache["config"] = data
# Set lock
self.apilock['config'] = True
r = requests.post(url, auth=('Tesla_Energy_Device', self.gw_pwd), verify=False,
headers={'Content-type': 'application/octet-string'},
data=pb.SerializeToString())
log.debug(f"Response Code: {r.status_code}")
if r.status_code in BUSY_CODES:
# Rate limited - Switch to cooldown mode for 5 minutes
self.pwcooldown = time.perf_counter() + 300
log.error('Possible Rate limited by Powerwall at - Activating 5 minute cooldown')
self.apilock['config'] = False
return None
if r.status_code != 200:
log.error(f"Error fetching config: {r.status_code}")
self.apilock['config'] = False
return None
# Decode response
tedapi = tedapi_pb2.Message()
tedapi.ParseFromString(r.content)
payload = tedapi.message.config.recv.file.text
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
log.error(f"Error Decoding JSON: {e}")
data = {}
log.debug(f"Configuration: {data}")
self.pwcachetime["config"] = time.time()
self.pwcache["config"] = data
except Exception as e:
log.error(f"Error fetching config: {e}")
data = None
finally:
# Release lock
self.apilock['config'] = False
return data

def get_status(self, force=False):
Expand Down Expand Up @@ -269,6 +289,14 @@ def get_status(self, force=False):
}
"""
# Check for lock and wait if api request already sent
if 'status' in self.apilock:
locktime = time.perf_counter()
while self.apilock['status']:
time.sleep(0.2)
if time.perf_counter() >= locktime + self.timeout:
log.debug(f" -- tedapi: Timeout waiting for status (unable to acquire lock)")
return None
# Check Cache
if not force and "status" in self.pwcachetime:
if time.time() - self.pwcachetime["status"] < self.pwcacheexpire:
Expand Down Expand Up @@ -297,30 +325,41 @@ def get_status(self, force=False):
pb.message.payload.send.b.value = "{}"
pb.tail.value = 1
url = f'https://{self.gw_ip}/tedapi/v1'
r = requests.post(url, auth=('Tesla_Energy_Device', self.gw_pwd), verify=False,
headers={'Content-type': 'application/octet-string'},
data=pb.SerializeToString())
log.debug(f"Response Code: {r.status_code}")
if r.status_code in BUSY_CODES:
# Rate limited - Switch to cooldown mode for 5 minutes
self.pwcooldown = time.perf_counter() + 300
log.error('Possible Rate limited by Powerwall at - Activating 5 minute cooldown')
return None
if r.status_code != 200:
log.error(f"Error fetching status: {r.status_code}")
return None
# Decode response
tedapi = tedapi_pb2.Message()
tedapi.ParseFromString(r.content)
payload = tedapi.message.payload.recv.text
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
log.error(f"Error Decoding JSON: {e}")
data = {}
log.debug(f"Status: {data}")
self.pwcachetime["status"] = time.time()
self.pwcache["status"] = data
# Set lock
self.apilock['status'] = True
r = requests.post(url, auth=('Tesla_Energy_Device', self.gw_pwd), verify=False,
headers={'Content-type': 'application/octet-string'},
data=pb.SerializeToString())
log.debug(f"Response Code: {r.status_code}")
if r.status_code in BUSY_CODES:
# Rate limited - Switch to cooldown mode for 5 minutes
self.pwcooldown = time.perf_counter() + 300
log.error('Possible Rate limited by Powerwall at - Activating 5 minute cooldown')
self.apilock['status'] = False
return None
if r.status_code != 200:
log.error(f"Error fetching status: {r.status_code}")
self.apilock['status'] = False
return None
# Decode response
tedapi = tedapi_pb2.Message()
tedapi.ParseFromString(r.content)
payload = tedapi.message.payload.recv.text
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
log.error(f"Error Decoding JSON: {e}")
data = {}
log.debug(f"Status: {data}")
self.pwcachetime["status"] = time.time()
self.pwcache["status"] = data
except Exception as e:
log.error(f"Error fetching status: {e}")
data = None
finally:
# Release lock
self.apilock['status'] = False
return data

def connect(self):
Expand Down
16 changes: 11 additions & 5 deletions pypowerwall/tedapi/pypowerwall_tedapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ def get_time_remaining(self, force: bool = False) -> Optional[float]:

def get_api_system_status_soe(self, **kwargs) -> Optional[Union[dict, list, str, bytes]]:
force = kwargs.get('force', False)
percentage_charged = self.tedapi.battery_level(force=force) or 0
percentage_charged = self.tedapi.battery_level(force=force)
if not percentage_charged:
return None
data = {
"percentage": percentage_charged
}
Expand Down Expand Up @@ -203,8 +205,10 @@ def get_api_status(self, **kwargs) -> Optional[Union[dict, list, str, bytes]]:
def get_api_system_status_grid_status(self, **kwargs) -> Optional[Union[dict, list, str, bytes]]:
force = kwargs.get('force', False)
status = self.tedapi.get_status(force=force)
alerts = lookup(status, ["control", "alerts", "active"]) or []
if "SystemConnectedToGrid" in alerts:
grid_state = lookup(status, ["esCan", "bus", "ISLANDER", "ISLAND_GridConnection", "ISLAND_GridConnected"])
if not grid_state:
return None
if grid_state == "ISLAND_GridConnected_Connected":
grid_status = "SystemGridConnected"
else:
grid_status = "SystemIslandedActive"
Expand Down Expand Up @@ -347,8 +351,10 @@ def get_api_system_status(self, **kwargs) -> Optional[Union[dict, list, str, byt
if not isinstance(status, dict) or not isinstance(config, dict):
return None
status = self.tedapi.get_status(force=force)
alerts = lookup(status, ["control", "alerts", "active"])
if "SystemConnectedToGrid" in alerts:
grid_state = lookup(status, ["esCan", "bus", "ISLANDER", "ISLAND_GridConnection", "ISLAND_GridConnected"])
if not grid_state:
grid_status = None
elif grid_state == "ISLAND_GridConnected_Connected":
grid_status = "SystemGridConnected"
else:
grid_status = "SystemIslandedActive"
Expand Down

0 comments on commit 13c6e36

Please sign in to comment.