From 3649088cfda611aa8817cbc106bc5a52e07ec6cc Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Sun, 24 Nov 2024 22:02:32 -0800 Subject: [PATCH 1/3] Add Powerwall temps --- RELEASE.md | 6 ++ proxy/server.py | 6 +- pypowerwall/__init__.py | 2 +- pypowerwall/tedapi/__init__.py | 107 ++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index a68376f..716603e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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 diff --git a/proxy/server.py b/proxy/server.py index be4a2a2..cfbdfec 100755 --- a/proxy/server.py +++ b/proxy/server.py @@ -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', @@ -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': @@ -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'): diff --git a/pypowerwall/__init__.py b/pypowerwall/__init__.py index 7c0ff5e..4b8237e 100644 --- a/pypowerwall/__init__.py +++ b/pypowerwall/__init__.py @@ -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' diff --git a/pypowerwall/tedapi/__init__.py b/pypowerwall/tedapi/__init__.py index 579b5aa..3eb2b98 100644 --- a/pypowerwall/tedapi/__init__.py +++ b/pypowerwall/tedapi/__init__.py @@ -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 @@ -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 @@ -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 @@ -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 = {} @@ -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'])}", From f7e8f7abcea11a9998224641baf53b173fa2ab28 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Tue, 26 Nov 2024 19:47:27 -0800 Subject: [PATCH 2/3] Bump to v0.12.0 --- RELEASE.md | 4 +++- pypowerwall/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 716603e..2335982 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,10 +1,12 @@ # RELEASE NOTES -## v0.11.2 - Add Controller Data +## 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 diff --git a/pypowerwall/__init__.py b/pypowerwall/__init__.py index 4b8237e..b577e5c 100644 --- a/pypowerwall/__init__.py +++ b/pypowerwall/__init__.py @@ -88,7 +88,7 @@ from typing import Union, Optional import time -version_tuple = (0, 11, 2) +version_tuple = (0, 12, 0) version = __version__ = '%d.%d.%d' % version_tuple __author__ = 'jasonacox' From afd9871fe5c5f88e2da5647209ec67428029bb29 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Tue, 26 Nov 2024 20:25:16 -0800 Subject: [PATCH 3/3] Add code to handle empty msa payload --- pypowerwall/tedapi/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pypowerwall/tedapi/__init__.py b/pypowerwall/tedapi/__init__.py index 3eb2b98..6dc93ad 100644 --- a/pypowerwall/tedapi/__init__.py +++ b/pypowerwall/tedapi/__init__.py @@ -1189,10 +1189,9 @@ 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"]: + 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 "serialNumber" in i and "value" in s: + 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