Skip to content

Commit

Permalink
Merge branch 'teslacloud' of https://github.com/jasonacox/pypowerwall
Browse files Browse the repository at this point in the history
…into teslacloud
  • Loading branch information
jasonacox committed Dec 29, 2023
2 parents e9af8cf + 41cff9a commit 238708a
Show file tree
Hide file tree
Showing 3 changed files with 440 additions and 346 deletions.
56 changes: 32 additions & 24 deletions proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import ssl
from transform import get_static, inject_js

BUILD = "t33"
BUILD = "t34"
ALLOWLIST = [
'/api/status', '/api/site_info/site_name', '/api/meters/site',
'/api/meters/solar', '/api/sitemaster', '/api/powerwalls',
Expand All @@ -65,7 +65,7 @@
debugmode = os.getenv("PW_DEBUG", "no")
cache_expire = int(os.getenv("PW_CACHE_EXPIRE", "5"))
browser_cache = int(os.getenv("PW_BROWSER_CACHE", "0"))
timeout = int(os.getenv("PW_TIMEOUT", "10"))
timeout = int(os.getenv("PW_TIMEOUT", "5"))
pool_maxsize = int(os.getenv("PW_POOL_MAXSIZE", "15"))
https_mode = os.getenv("PW_HTTPS", "no")
port = int(os.getenv("PW_PORT", "8675"))
Expand Down Expand Up @@ -140,8 +140,9 @@ def get_value(a, key):
if siteid is not None and siteid != str(pw.Tesla.siteid):
log.info("Switch to Site ID %s" % siteid)
if not pw.Tesla.change_site(siteid):
log.error("Fatal Error: Unable to initialize pyPowerwall")
os._exit(1)
log.error("Fatal Error: Unable to connect. Please fix config and restart.")
while True:
time.sleep(5) # Infinite loop to keep container running
else:
log.info("pyPowerwall Proxy Server - Local Mode")
log.info("Connected to Energy Gateway %s (%s)" % (host, pw.site_name()))
Expand Down Expand Up @@ -234,18 +235,21 @@ def do_GET(self):
# Alerts
message = pw.alerts(jsonformat=True)
elif self.path == '/alerts/pw':
# Alerts in dictionary/object format
pwalerts = {}
idx = 1
alerts = pw.alerts()
for alert in alerts:
pwalerts[alert] = 1
message = json.dumps(pwalerts)
# Alerts in dictionary/object format
pwalerts = {}
idx = 1
alerts = pw.alerts()
if alerts is None:
message = None
else:
for alert in alerts:
pwalerts[alert] = 1
message = json.dumps(pwalerts)
elif self.path == '/freq':
# Frequency, Current, Voltage and Grid Status
fcv = {}
idx = 1
vitals = pw.vitals()
vitals = pw.vitals() or {}
for device in vitals:
d = vitals[device]
if device.startswith('TEPINV'):
Expand All @@ -266,7 +270,7 @@ def do_GET(self):
# Battery Data
pod = {}
idx = 1
vitals = pw.vitals()
vitals = pw.vitals() or {}
for device in vitals:
d = vitals[device]
if device.startswith('TEPOD'):
Expand All @@ -285,23 +289,27 @@ def do_GET(self):
pod["PW%d_POD_nom_full_pack_energy" % idx] = get_value(d, 'POD_nom_full_pack_energy')
idx = idx + 1
pod["backup_reserve_percent"] = pw.get_reserve()
d = pw.system_status()
d = pw.system_status() or {}
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()
message = json.dumps(pod)
elif self.path == '/version':
# Firmware Version
v = {}
v["version"] = pw.version()
val = pw.version().split(" ")[0]
val = ''.join(i for i in val if i.isdigit() or i in './\\')
while len(val.split('.')) < 3:
val = val + ".0"
l = [int(x, 10) for x in val.split('.')]
l.reverse()
v["vint"] = sum(x * (100 ** i) for i, x in enumerate(l))
message = json.dumps(v)
version = pw.version()
if version is None:
message = None
else:
v = {}
v["version"] = version
val = v["version"].split(" ")[0]
val = ''.join(i for i in val if i.isdigit() or i in './\\')
while len(val.split('.')) < 3:
val = val + ".0"
l = [int(x, 10) for x in val.split('.')]
l.reverse()
v["vint"] = sum(x * (100 ** i) for i, x in enumerate(l))
message = json.dumps(v)
elif self.path == '/help':
# Display friendly help screen link and stats
proxystats['ts'] = int(time.time())
Expand Down
50 changes: 26 additions & 24 deletions pypowerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
email # (required) email used for logging into the gateway
timezone # (required) desired timezone
pwcacheexpire = 5 # Set API cache timeout in seconds
timeout = 10 # Timeout for HTTPS calls in seconds
timeout = 5 # Timeout for HTTPS calls in seconds
poolmaxsize = 10 # Pool max size for http connection re-use (persistent connections disabled if zero)
cloudmode = False # If True, use Tesla cloud for data (default is False)
Expand Down Expand Up @@ -62,7 +62,7 @@
from . import tesla_pb2 # Protobuf definition for vitals
from . import cloud # Tesla Cloud API

version_tuple = (0, 7, 0)
version_tuple = (0, 7, 1)
version = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'jasonacox'

Expand All @@ -89,7 +89,7 @@ class ConnectionError(Exception):
pass

class Powerwall(object):
def __init__(self, host="", password="", email="nobody@nowhere.com", timezone="America/Los_Angeles", pwcacheexpire=5, timeout=10, poolmaxsize=10, cloudmode=False):
def __init__(self, host="", password="", email="nobody@nowhere.com", timezone="America/Los_Angeles", pwcacheexpire=5, timeout=5, poolmaxsize=10, cloudmode=False):
"""
Represents a Tesla Energy Gateway Powerwall device.
Expand All @@ -112,7 +112,7 @@ def __init__(self, host="", password="", email="nobody@nowhere.com", timezone="A
self.password = password
self.email = email
self.timezone = timezone
self.timeout = timeout # 10s timeout for http calls
self.timeout = timeout # 5s timeout for http calls
self.poolmaxsize = poolmaxsize # pool max size for http connection re-use
self.auth = {} # caches authentication cookies
self.pwcachetime = {} # holds the cached data timestamps for api
Expand Down Expand Up @@ -225,7 +225,7 @@ def poll(self, api='/api/site_info/site_name', jsonformat=False, raw=False, recu
# Check cache
if(api in self.pwcache and api in self.pwcachetime):
# is it expired?
if(time.time() - self.pwcachetime[api] < self.pwcacheexpire):
if(time.perf_counter() - self.pwcachetime[api] < self.pwcacheexpire):
payload = self.pwcache[api]
# We do the override here to ensure that we cache the force entry
if force:
Expand Down Expand Up @@ -266,7 +266,7 @@ def poll(self, api='/api/site_info/site_name', jsonformat=False, raw=False, recu
else:
payload = r.text
self.pwcache[api] = payload
self.pwcachetime[api] = time.time()
self.pwcachetime[api] = time.perf_counter()
if(jsonformat):
try:
data = json.loads(payload)
Expand All @@ -286,14 +286,13 @@ def level(self, scale=False):
Note: Tesla App reserves 5% of battery = ( (batterylevel / 0.95) - (5 / 0.95) )
"""
# Return power level percentage for battery
level = 0
payload = self.poll('/api/system_status/soe', jsonformat=True)
if(payload is not None and 'percentage' in payload):
if payload is not None and 'percentage' in payload:
level = payload['percentage']
if scale:
return ((level / 0.95) - (5 / 0.95))
else:
if scale:
level = (level / 0.95) - (5 / 0.95)
return level
return None

def power(self):
"""
Expand Down Expand Up @@ -543,6 +542,8 @@ def status(self, param=None, jsonformat=False):
cellular_disabled = payload['cellular_disabled']
"""
payload = self.poll('/api/status', jsonformat=True)
if payload is None:
return None
if param is None:
if jsonformat:
return json.dumps(payload, indent=4, sort_keys=True)
Expand Down Expand Up @@ -616,15 +617,14 @@ def get_reserve(self, scale=True):
scale = If True (default) use Tesla's 5% reserve calculation
Tesla App reserves 5% of battery = ( (batterylevel / 0.95) - (5 / 0.95) )
"""
data = self.poll('/api/operation')
if data is None:
return None
data = json.loads(data)
percent = float(data['backup_reserve_percent'])
if scale:
# Get percentage based on Tesla App scale
percent = float((percent / 0.95) - (5 / 0.95))
return percent
data = self.poll('/api/operation', jsonformat=True)
if data is not None and 'backup_reserve_percent' in data:
percent = float(data['backup_reserve_percent'])
if scale:
# Get percentage based on Tesla App scale
percent = float((percent / 0.95) - (5 / 0.95))
return percent
return None

def grid_status(self, type="string"):
"""
Expand Down Expand Up @@ -655,7 +655,7 @@ def grid_status(self, type="string"):
return gridmap[grid_status][type]
except:
# The payload from powerwall was not valid
log.debug("ERROR Invalid return value received from gateway: " + str(payload.grid_status))
log.debug('ERROR unable to parse payload for grid_status: %r' % payload)
return None

def system_status(self, jsonformat=False):
Expand Down Expand Up @@ -767,9 +767,11 @@ def get_time_remaining(self):
return d['response']['time_remaining_hours']

# Compute based on battery level and load
d = self.system_status()
if 'nominal_energy_remaining' in d:
return d['nominal_energy_remaining']/self.load()
d = self.system_status() or {}
if 'nominal_energy_remaining' in d and d['nominal_energy_remaining'] is not None:
load = self.load() or 0
if load > 0:
return d['nominal_energy_remaining']/load
# Default
return None

Loading

0 comments on commit 238708a

Please sign in to comment.