Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Powerwall Temps #114

Merged
merged 3 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# RELEASE NOTES

## v0.12.0 - 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.
* Remove Negative Solar Values [Option] by @jasonacox in https://github.com/jasonacox/pypowerwall/pull/113
* Solar-Only Cloud Access - Fix errors with site references by @Nexarian in https://github.com/jasonacox/pypowerwall/pull/115

## 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, 12, 0)
version = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'jasonacox'

Expand Down
106 changes: 104 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,14 @@ def calculate_dc_power(V, I):
}
}

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

# Create TETHC, TEPINV and TEPOD blocks
tethc = {} # parent
tepinv = {}
Expand All @@ -1106,7 +1208,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
Loading