Skip to content

Commit f5c331e

Browse files
committed
Use Neurio for TEDAPI data
`METER_X` and `METER_Y` are specific to a Tesla electrical meter. If your system doesn't have these, Tesla's documentation says to get data from the Neurio. References: - https://energylibrary.tesla.com/docs/Public/EnergyStorage/Powerwall/General/MeteringGuide/en-us/GUID-932E78BF-6DFF-4CCA-9741-E4793E9F4314.html - https://energylibrary.tesla.com/docs/Public/EnergyStorage/Powerwall/General/MeteringGuide/en-us/GUID-A46FB1D7-AEE5-4B66-99FF-39FE576F2B2D.html
1 parent b17afac commit f5c331e

File tree

2 files changed

+127
-78
lines changed

2 files changed

+127
-78
lines changed

pypowerwall/tedapi/__init__.py

Lines changed: 90 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import time
5353
from functools import wraps
5454
from http import HTTPStatus
55-
from typing import Dict, List
55+
from typing import Dict, List, Tuple
5656

5757
import requests
5858
import urllib3
@@ -960,8 +960,90 @@ def get_fan_speeds(self, force=False):
960960
Get the fan speeds for the Powerwall / Inverter
961961
"""
962962
return self.extract_fan_speeds(self.get_device_controller(force=force))
963-
964-
963+
964+
965+
def derive_meter_config(self, config) -> dict:
966+
# Build meter Lookup if available
967+
meter_config = {}
968+
if not "meters" in config:
969+
return meter_config
970+
# Loop through each meter and use device_serial as the key
971+
for meter in config['meters']:
972+
if meter.get('type') != "neurio_w2_tcp":
973+
continue
974+
device_serial = lookup(meter, ['connection', 'device_serial'])
975+
if not device_serial:
976+
continue
977+
# Check to see if we already have this meter in meter_config
978+
if device_serial in meter_config:
979+
cts = meter.get('cts', [False] * 4)
980+
if not isinstance(cts, list):
981+
cts = [False] * 4
982+
for i, ct in enumerate(cts):
983+
if not ct:
984+
continue
985+
meter_config[device_serial]['cts'][i] = True
986+
meter_config[device_serial]['location'][i] = meter.get('location', "")
987+
else:
988+
# New meter, add to meter_config
989+
cts = meter.get('cts', [False] * 4)
990+
if not isinstance(cts, list):
991+
cts = [False] * 4
992+
location = meter.get('location', "")
993+
meter_config[device_serial] = {
994+
"type": meter.get('type'),
995+
"location": [location] * 4,
996+
"cts": cts,
997+
"inverted": meter.get('inverted'),
998+
"connection": meter.get('connection'),
999+
"real_power_scale_factor": meter.get('real_power_scale_factor', 1)
1000+
}
1001+
return meter_config
1002+
1003+
1004+
def aggregate_neurio_data(self, config_data, status_data, meter_config_data) -> Tuple[dict, dict]:
1005+
# Create NEURIO block
1006+
neurio_flat = {}
1007+
neurio_hierarchy = {}
1008+
# Loop through each Neurio device serial number
1009+
for c, n in enumerate(lookup(status_data, ['neurio', 'readings']) or {}, start=1000):
1010+
# Loop through each CT on the Neurio device
1011+
sn = n.get('serial', str(c))
1012+
cts_flat = {}
1013+
for i, ct in enumerate(n['dataRead'] or {}):
1014+
# Only show if we have a meter configuration and cts[i] is true
1015+
cts_bool = lookup(meter_config_data, [sn, 'cts'])
1016+
if isinstance(cts_bool, list) and i < len(cts_bool):
1017+
if not cts_bool[i]:
1018+
# Skip this CT
1019+
continue
1020+
factor = lookup(meter_config_data, [sn, 'real_power_scale_factor']) or 1
1021+
location = lookup(meter_config_data, [sn, 'location'])
1022+
ct_hierarchy = {
1023+
"Index": i,
1024+
"InstRealPower": ct.get('realPowerW', 0) * factor,
1025+
"InstReactivePower": ct.get('reactivePowerVAR'),
1026+
"InstVoltage": ct.get('voltageV'),
1027+
"InstCurrent": ct.get('currentA'),
1028+
"Location": location[i] if location and len(location) > i else None
1029+
}
1030+
neurio_hierarchy[f"CT{i}"] = ct_hierarchy
1031+
cts_flat.update({f"NEURIO_CT{i}_" + key: value for key, value in ct_hierarchy.items() if key != "Index"})
1032+
meter_manufacturer = "NEURIO" if lookup(meter_config_data, [sn, "type"]) == "neurio_w2_tcp" else None
1033+
rest = {
1034+
"componentParentDin": lookup(config_data, ['vin']),
1035+
"firmwareVersion": None,
1036+
"lastCommunicationTime": lookup(n, ['timestamp']),
1037+
"manufacturer": meter_manufacturer,
1038+
"meterAttributes": {
1039+
"meterLocation": []
1040+
},
1041+
"serialNumber": sn
1042+
}
1043+
neurio_flat[f"NEURIO--{sn}"] = {**cts_flat, **rest}
1044+
return (neurio_flat, neurio_hierarchy)
1045+
1046+
9651047
# Vitals API Mapping Function
9661048
def vitals(self, force=False):
9671049
"""
@@ -984,38 +1066,6 @@ def calculate_dc_power(V, I):
9841066
if not isinstance(status, dict) or not isinstance(config, dict):
9851067
return None
9861068

987-
# Build meter Lookup if available
988-
meter_config = {}
989-
if "meters" in config:
990-
# Loop through each meter and use device_serial as the key
991-
for meter in config['meters']:
992-
if meter.get('type') == "neurio_w2_tcp":
993-
device_serial = lookup(meter, ['connection', 'device_serial'])
994-
if device_serial:
995-
# Check to see if we already have this meter in meter_config
996-
if device_serial in meter_config:
997-
cts = meter.get('cts', [False] * 4)
998-
if not isinstance(cts, list):
999-
cts = [False] * 4
1000-
for i, ct in enumerate(cts):
1001-
if ct:
1002-
meter_config[device_serial]['cts'][i] = True
1003-
meter_config[device_serial]['location'][i] = meter.get('location', "")
1004-
else:
1005-
# New meter, add to meter_config
1006-
cts = meter.get('cts', [False] * 4)
1007-
if not isinstance(cts, list):
1008-
cts = [False] * 4
1009-
location = meter.get('location', "")
1010-
meter_config[device_serial] = {
1011-
"type": meter.get('type'),
1012-
"location": [location] * 4,
1013-
"cts": cts,
1014-
"inverted": meter.get('inverted'),
1015-
"connection": meter.get('connection'),
1016-
"real_power_scale_factor": meter.get('real_power_scale_factor', 1)
1017-
}
1018-
10191069
# Create Header
10201070
tesla = {}
10211071
header = {}
@@ -1025,45 +1075,11 @@ def calculate_dc_power(V, I):
10251075
"gateway": self.gw_ip,
10261076
"pyPowerwall": __version__,
10271077
}
1028-
1029-
# Create NEURIO block
1030-
neurio = {}
1031-
c = 1000
1032-
# Loop through each Neurio device serial number
1033-
for n in lookup(status, ['neurio', 'readings']) or {}:
1034-
# Loop through each CT on the Neurio device
1035-
sn = n.get('serial', str(c))
1036-
cts = {}
1037-
c = c + 1
1038-
for i, ct in enumerate(n['dataRead'] or {}):
1039-
# Only show if we have a meter configuration and cts[i] is true
1040-
cts_bool = lookup(meter_config, [sn, 'cts'])
1041-
if isinstance(cts_bool, list) and i < len(cts_bool):
1042-
if not cts_bool[i]:
1043-
# Skip this CT
1044-
continue
1045-
factor = lookup(meter_config, [sn, 'real_power_scale_factor']) or 1
1046-
device = f"NEURIO_CT{i}_"
1047-
cts[device + "InstRealPower"] = lookup(ct, ['realPowerW']) * factor
1048-
cts[device + "InstReactivePower"] = lookup(ct, ['reactivePowerVAR'])
1049-
cts[device + "InstVoltage"] = lookup(ct, ['voltageV'])
1050-
cts[device + "InstCurrent"] = lookup(ct, ['currentA'])
1051-
location = lookup(meter_config, [sn, 'location'])
1052-
cts[device + "Location"] = location[i] if len(location) > i else None
1053-
meter_manufacturer = None
1054-
if lookup(meter_config, [sn, 'type']) == "neurio_w2_tcp":
1055-
meter_manufacturer = "NEURIO"
1056-
rest = {
1057-
"componentParentDin": lookup(config, ['vin']),
1058-
"firmwareVersion": None,
1059-
"lastCommunicationTime": lookup(n, ['timestamp']),
1060-
"manufacturer": meter_manufacturer,
1061-
"meterAttributes": {
1062-
"meterLocation": []
1063-
},
1064-
"serialNumber": sn
1065-
}
1066-
neurio[f"NEURIO--{sn}"] = {**cts, **rest}
1078+
neurio = self.aggregate_neurio_data(
1079+
config_data=config,
1080+
status_data=status,
1081+
meter_config_data=self.derive_meter_config(config)
1082+
)[0]
10671083

10681084
# Create PVAC, PVS, and TESLA blocks - Assume the are aligned
10691085
pvac = {}

pypowerwall/tedapi/pypowerwall_tedapi.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,44 @@ def get_api_meters_aggregates(self, **kwargs) -> Optional[Union[dict, list, str,
335335
v2n = v_site.get("ISLAND_VL2N_Main", 0)
336336
v3n = v_site.get("ISLAND_VL3N_Main", 0)
337337
vll_site = compute_LL_voltage(v1n, v2n, v3n)
338+
338339
meter_x = lookup(status, ("esCan","bus","SYNC","METER_X_AcMeasurements")) or {}
339-
i1 = meter_x.get("METER_X_CTA_I", 0)
340-
i2 = meter_x.get("METER_X_CTB_I", 0)
341-
i3 = meter_x.get("METER_X_CTC_I", 0)
342-
i_site = i1 + i2 + i3
340+
i_site = i1 = i2 = i3 = 0
341+
neurio_readings = lookup(status, ("neurio", "readings"))
342+
if meter_x and not meter_x["isMIA"]:
343+
i1 = meter_x.get("METER_X_CTA_I", 0)
344+
i2 = meter_x.get("METER_X_CTB_I", 0)
345+
i3 = meter_x.get("METER_X_CTC_I", 0)
346+
i_site = i1 + i2 + i3
347+
elif neurio_readings and len(neurio_readings) > 0:
348+
vll_site = v1n = v2n = v3n = 0
349+
neurio_data = self.tedapi.aggregate_neurio_data(
350+
config_data=config,
351+
status_data=status,
352+
meter_config_data=self.tedapi.derive_meter_config(config)
353+
)
354+
for i, data in enumerate(neurio_data[1].values()):
355+
if data["Location"] != "site":
356+
continue
357+
current = math.copysign(data.get("InstCurrent", 0), data.get("InstRealPower", 0))
358+
voltage = data.get("InstVoltage", 0)
359+
if i == 1:
360+
i1 = current
361+
v1n = voltage
362+
elif i == 2:
363+
i2 = current
364+
v2n = voltage
365+
elif i == 3:
366+
i3 = current
367+
v3n = voltage
368+
nonzero = [x for x in (i1, i2, i3) if x != 0]
369+
i_site = sum(nonzero) / len(nonzero) if nonzero else 0
370+
vll_site = compute_LL_voltage(
371+
v1n = v1n,
372+
v2n = v2n,
373+
v3n = v3n if i == 3 else None
374+
)
375+
343376
# Compute solar (pv) voltages and current
344377
v_solar = lookup(status, ("esCan","bus","PVS")) or []
345378
sum_vll_solar = 0

0 commit comments

Comments
 (0)