Skip to content

Commit

Permalink
Add Powerwall temps
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonacox committed Nov 25, 2024
1 parent 5d455f6 commit 3649088
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 5 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.11.2 - Add Controller Data

* TEDAPI: Add `get_device_controller()` to get device data which includes Powerwall THC_AmbientTemp data. Credit to @ygelfand for discovery and reported in https://github.com/jasonacox/Powerwall-Dashboard/discussions/392#discussioncomment-11360474
* Updated `vitals()` to include Powerwall temperature data.
* Proxy Updated to t66 to include API response for /tedapi/controller.

## v0.11.1 - PW3 and FleetAPI Bugfix

* TEDAPI: Fix bug with activeAlerts logic causing errors on systems with multiple Powerwall 3's. Identified by @rmotapar in https://github.com/jasonacox/Powerwall-Dashboard/issues/387#issuecomment-2336431741
Expand Down
6 changes: 4 additions & 2 deletions proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
import pypowerwall
from pypowerwall import parse_version

BUILD = "t65"
BUILD = "t66"
ALLOWLIST = [
'/api/status', '/api/site_info/site_name', '/api/meters/site',
'/api/meters/solar', '/api/sitemaster', '/api/powerwalls',
Expand Down Expand Up @@ -619,7 +619,7 @@ def do_GET(self):
elif self.path.startswith('/tedapi'):
# TEDAPI Specific Calls
if pw.tedapi:
message = '{"error": "Use /tedapi/config, /tedapi/status, /tedapi/components, /tedapi/battery"}'
message = '{"error": "Use /tedapi/config, /tedapi/status, /tedapi/components, /tedapi/battery, /tedapi/controller"}'
if self.path == '/tedapi/config':
message = json.dumps(pw.tedapi.get_config())
if self.path == '/tedapi/status':
Expand All @@ -628,6 +628,8 @@ def do_GET(self):
message = json.dumps(pw.tedapi.get_components())
if self.path == '/tedapi/battery':
message = json.dumps(pw.tedapi.get_battery_blocks())
if self.path == '/tedapi/controller':
message = json.dumps(pw.tedapi.get_device_controller())
else:
message = '{"error": "TEDAPI not enabled"}'
elif self.path.startswith('/cloud'):
Expand Down
2 changes: 1 addition & 1 deletion pypowerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
from typing import Union, Optional
import time

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

Expand Down
107 changes: 105 additions & 2 deletions pypowerwall/tedapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
get_components() - Get the Powerwall 3 Device Information
get_battery_block(din) - Get the Powerwall 3 Battery Block Information
get_pw3_vitals() - Get the Powerwall 3 Vitals Information
get_device_controller() - Get the Powerwall Device Controller Status
Note:
This module requires access to the Powerwall Gateway. You can add a route to
Expand Down Expand Up @@ -367,6 +368,98 @@ def get_status(self, force=False):
self.apilock['status'] = False
return data

def get_device_controller(self, force=False):
"""
Get the Powerwall Device Controller Status
Similar to get_status but with additional data:
{
"components": {}, // Additional data
"control": {},
"esCan": {},
"ieee20305": {}, // Additional data
"neurio": {},
"pw3Can": {},
"system": {},
"teslaRemoteMeter": {} // Additional data
}
TODO: Refactor to combine tedapi queries
"""
# Check for lock and wait if api request already sent
if 'controller' in self.apilock:
locktime = time.perf_counter()
while self.apilock['controller']:
time.sleep(0.2)
if time.perf_counter() >= locktime + self.timeout:
log.debug(" -- tedapi: Timeout waiting for controller data (unable to acquire lock)")
return None
# Check Cache
if not force and "controller" in self.pwcachetime:
if time.time() - self.pwcachetime["controller"] < self.pwcacheexpire:
log.debug("Using Cached Payload")
return self.pwcache["controller"]
if not force and self.pwcooldown > time.perf_counter():
# Rate limited - return None
log.debug('Rate limit cooldown period - Pausing API calls')
return None
# Check Connection
if not self.din:
if not self.connect():
log.error("Not Connected - Unable to get controller data")
return None
# Fetch Current Status from Powerwall
log.debug("Get controller data from Powerwall")
# Build Protobuf to fetch controller data
pb = tedapi_pb2.Message()
pb.message.deliveryChannel = 1
pb.message.sender.local = 1
pb.message.recipient.din = self.din # DIN of Powerwall
pb.message.payload.send.num = 2
pb.message.payload.send.payload.value = 1
pb.message.payload.send.payload.text = 'query DeviceControllerQuery($msaComp:ComponentFilter$msaSignals:[String!]){control{systemStatus{nominalFullPackEnergyWh nominalEnergyRemainingWh}islanding{customerIslandMode contactorClosed microGridOK gridOK disableReasons}meterAggregates{location realPowerW}alerts{active}siteShutdown{isShutDown reasons}batteryBlocks{din disableReasons}pvInverters{din disableReasons}}system{time supportMode{remoteService{isEnabled expiryTime sessionId}}sitemanagerStatus{isRunning}updateUrgencyCheck{urgency version{version gitHash}timestamp}}neurio{isDetectingWiredMeters readings{firmwareVersion serial dataRead{voltageV realPowerW reactivePowerVAR currentA}timestamp}pairings{serial shortId status errors macAddress hostname isWired modbusPort modbusId lastUpdateTimestamp}}teslaRemoteMeter{meters{din reading{timestamp firmwareVersion ctReadings{voltageV realPowerW reactivePowerVAR energyExportedWs energyImportedWs currentA}}firmwareUpdate{updating numSteps currentStep currentStepProgress progress}}detectedWired{din serialPort}}pw3Can{firmwareUpdate{isUpdating progress{updating numSteps currentStep currentStepProgress progress}}enumeration{inProgress}}esCan{bus{PVAC{packagePartNumber packageSerialNumber subPackagePartNumber subPackageSerialNumber PVAC_Status{isMIA PVAC_Pout PVAC_State PVAC_Vout PVAC_Fout}PVAC_InfoMsg{PVAC_appGitHash}PVAC_Logging{isMIA PVAC_PVCurrent_A PVAC_PVCurrent_B PVAC_PVCurrent_C PVAC_PVCurrent_D PVAC_PVMeasuredVoltage_A PVAC_PVMeasuredVoltage_B PVAC_PVMeasuredVoltage_C PVAC_PVMeasuredVoltage_D PVAC_VL1Ground PVAC_VL2Ground}alerts{isComplete isMIA active}}PINV{PINV_Status{isMIA PINV_Fout PINV_Pout PINV_Vout PINV_State PINV_GridState}PINV_AcMeasurements{isMIA PINV_VSplit1 PINV_VSplit2}PINV_PowerCapability{isComplete isMIA PINV_Pnom}alerts{isComplete isMIA active}}PVS{PVS_Status{isMIA PVS_State PVS_vLL PVS_StringA_Connected PVS_StringB_Connected PVS_StringC_Connected PVS_StringD_Connected PVS_SelfTestState}PVS_Logging{PVS_numStringsLockoutBits PVS_sbsComplete}alerts{isComplete isMIA active}}THC{packagePartNumber packageSerialNumber THC_InfoMsg{isComplete isMIA THC_appGitHash}THC_Logging{THC_LOG_PW_2_0_EnableLineState}}POD{POD_EnergyStatus{isMIA POD_nom_energy_remaining POD_nom_full_pack_energy}POD_InfoMsg{POD_appGitHash}}SYNC{packagePartNumber packageSerialNumber SYNC_InfoMsg{isMIA SYNC_appGitHash SYNC_assemblyId}METER_X_AcMeasurements{isMIA isComplete METER_X_CTA_InstRealPower METER_X_CTA_InstReactivePower METER_X_CTA_I METER_X_VL1N METER_X_CTB_InstRealPower METER_X_CTB_InstReactivePower METER_X_CTB_I METER_X_VL2N METER_X_CTC_InstRealPower METER_X_CTC_InstReactivePower METER_X_CTC_I METER_X_VL3N}METER_Y_AcMeasurements{isMIA isComplete METER_Y_CTA_InstRealPower METER_Y_CTA_InstReactivePower METER_Y_CTA_I METER_Y_VL1N METER_Y_CTB_InstRealPower METER_Y_CTB_InstReactivePower METER_Y_CTB_I METER_Y_VL2N METER_Y_CTC_InstRealPower METER_Y_CTC_InstReactivePower METER_Y_CTC_I METER_Y_VL3N}}ISLANDER{ISLAND_GridConnection{ISLAND_GridConnected isComplete}ISLAND_AcMeasurements{ISLAND_VL1N_Main ISLAND_FreqL1_Main ISLAND_VL2N_Main ISLAND_FreqL2_Main ISLAND_VL3N_Main ISLAND_FreqL3_Main ISLAND_VL1N_Load ISLAND_FreqL1_Load ISLAND_VL2N_Load ISLAND_FreqL2_Load ISLAND_VL3N_Load ISLAND_FreqL3_Load ISLAND_GridState isComplete isMIA}}}enumeration{inProgress numACPW numPVI}firmwareUpdate{isUpdating powerwalls{updating numSteps currentStep currentStepProgress progress}msa{updating numSteps currentStep currentStepProgress progress}msa1{updating numSteps currentStep currentStepProgress progress}sync{updating numSteps currentStep currentStepProgress progress}pvInverters{updating numSteps currentStep currentStepProgress progress}}phaseDetection{inProgress lastUpdateTimestamp powerwalls{din progress phase}}inverterSelfTests{isRunning isCanceled pinvSelfTestsResults{din overall{status test summary setMagnitude setTime tripMagnitude tripTime accuracyMagnitude accuracyTime currentMagnitude timestamp lastError}testResults{status test summary setMagnitude setTime tripMagnitude tripTime accuracyMagnitude accuracyTime currentMagnitude timestamp lastError}}}}components{msa:components(filter:$msaComp){partNumber serialNumber signals(names:$msaSignals){name value textValue boolValue timestamp}activeAlerts{name}}}ieee20305{longFormDeviceID polledResources{url name pollRateSeconds lastPolledTimestamp}controls{defaultControl{mRID setGradW opModEnergize opModMaxLimW opModImpLimW opModExpLimW opModGenLimW opModLoadLimW}activeControls{opModEnergize opModMaxLimW opModImpLimW opModExpLimW opModGenLimW opModLoadLimW}}registration{dateTimeRegistered pin}}}'
pb.message.payload.send.code = b'0\x81\x87\x02B\x01A\x95\x12\xe3B\xd1\xca\x1a\xd3\x00\xf6}\x0bE@/\x9a\x9f\xc0\r\x06%\xac,\x0ej!)\nd\xef\xe67\x8b\xafb\xd7\xf8&\x0b.\xc1\xac\xd9!\x1f\xd6\x83\xffkIm\xf3\\J\xd8\xeeiTY\xde\x7f\xc5xR\x02A\x1dC\x03H\xfb8"\xb0\xe4\xd6\x18\xde\x11\xc45\xb2\xa9VB\xa6J\x8f\x08\x9d\xba\x86\xf1 W\xcdJ\x8c\x02*\x05\x12\xcb{<\x9b\xc8g\xc9\x9d9\x8bR\xb3\x89\xb8\xf1\xf1\x0f\x0e\x16E\xed\xd7\xbf\xd5&)\x92.\x12'
pb.message.payload.send.b.value = '{"msaComp":{"types" :["PVS","PVAC", "TESYNC", "TEPINV", "TETHC", "STSTSM", "TEMSA", "TEPINV" ]},\n\t"msaSignals":[\n\t"MSA_pcbaId",\n\t"MSA_usageId",\n\t"MSA_appGitHash",\n\t"MSA_HeatingRateOccurred",\n\t"THC_AmbientTemp",\n\t"METER_Z_CTA_InstRealPower",\n\t"METER_Z_CTA_InstReactivePower",\n\t"METER_Z_CTA_I",\n\t"METER_Z_VL1G",\n\t"METER_Z_CTB_InstRealPower",\n\t"METER_Z_CTB_InstReactivePower",\n\t"METER_Z_CTB_I",\n\t"METER_Z_VL2G"]}'
pb.tail.value = 1
url = f'https://{self.gw_ip}/tedapi/v1'
try:
# Set lock
self.apilock['controller'] = True
r = requests.post(url, auth=('Tesla_Energy_Device', self.gw_pwd), verify=False,
headers={'Content-type': 'application/octet-string'},
data=pb.SerializeToString(), timeout=self.timeout)
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['controller'] = False
return None
if r.status_code != 200:
log.error(f"Error fetching controller data: {r.status_code}")
self.apilock['controller'] = False
return None
# Decode response
tedapi = tedapi_pb2.Message()
tedapi.ParseFromString(r.content)
payload = tedapi.message.payload.recv.text
log.debug(f"Payload: {payload}")
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["controller"] = time.time()
self.pwcache["controller"] = data
except Exception as e:
log.error(f"Error fetching controller data: {e}")
data = None
finally:
# Release lock
self.apilock['controller'] = False
return data

def get_firmware_version(self, force=False, details=False):
"""
Get the Powerwall Firmware Version
Expand Down Expand Up @@ -881,8 +974,9 @@ def calculate_dc_power(V, I):
power = V * I
return power

status = self.get_status(force)
# status = self.get_status(force)
config = self.get_config(force)
status = self.get_device_controller(force)

if not isinstance(status, dict) or not isinstance(config, dict):
return None
Expand Down Expand Up @@ -1092,6 +1186,15 @@ def calculate_dc_power(V, I):
}
}

# Get Dictionary of Powerwall Temperatures
temp_sensors = {}
for i in lookup(status, ['components', 'msa']) or []:
i_keys = i.keys()
if "signals" in i_keys and "serialNumber" in i_keys and i["serialNumber"]:
for s in i["signals"]:
if "name" in s and s["name"] == "THC_AmbientTemp" and "serialNumber" in i and "value" in s:
temp_sensors[i["serialNumber"]] = s["value"]

# Create TETHC, TEPINV and TEPOD blocks
tethc = {} # parent
tepinv = {}
Expand All @@ -1106,7 +1209,7 @@ def calculate_dc_power(V, I):
# TETHC block
parent_name = f"TETHC--{packagePartNumber}--{packageSerialNumber}"
tethc[parent_name] = {
"THC_AmbientTemp": None,
"THC_AmbientTemp": temp_sensors.get(packageSerialNumber, None),
"THC_State": None,
"alerts": lookup(p, ['alerts', 'active']) or [],
"componentParentDin": f"STSTSM--{lookup(config, ['vin'])}",
Expand Down

0 comments on commit 3649088

Please sign in to comment.