Skip to content

Commit 3cbc4d0

Browse files
committed
feat(e_v3): rename ac_* to inverter_*, add alt/derived sensors, unify cycle sensor
1 parent d1c5631 commit 3cbc4d0

6 files changed

Lines changed: 279 additions & 54 deletions

File tree

custom_components/marstek_modbus/coordinator.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
6464
self.BUTTON_DEFINITIONS = []
6565
self.EFFICIENCY_SENSOR_DEFINITIONS = []
6666
self.STORED_ENERGY_SENSOR_DEFINITIONS = []
67-
self.CYCLE_SENSOR_DEFINITIONS = []
67+
self.DERIVED_SENSOR_DEFINITIONS = []
6868

6969
# Combine all sensor definitions for polling
7070
self._all_definitions = []
@@ -233,7 +233,7 @@ async def async_load_registers(self, version: str | None = None):
233233
self.BUTTON_DEFINITIONS = data.get("BUTTON_DEFINITIONS", [])
234234
self.EFFICIENCY_SENSOR_DEFINITIONS = data.get("EFFICIENCY_SENSOR_DEFINITIONS", [])
235235
self.STORED_ENERGY_SENSOR_DEFINITIONS = data.get("STORED_ENERGY_SENSOR_DEFINITIONS", [])
236-
self.CYCLE_SENSOR_DEFINITIONS = data.get("CYCLE_SENSOR_DEFINITIONS", [])
236+
self.DERIVED_SENSOR_DEFINITIONS = data.get("DERIVED_SENSOR_DEFINITIONS", [])
237237

238238
# Combine into a single list for polling
239239
self._all_definitions = (
@@ -461,7 +461,7 @@ async def _async_update_data(self):
461461
all_definitions_for_deps = (
462462
self.EFFICIENCY_SENSOR_DEFINITIONS
463463
+ self.STORED_ENERGY_SENSOR_DEFINITIONS
464-
+ self.CYCLE_SENSOR_DEFINITIONS
464+
+ self.DERIVED_SENSOR_DEFINITIONS
465465
)
466466
dependency_keys_set = {
467467
dep_key
@@ -738,7 +738,7 @@ def get_registers(version: str):
738738
- BUTTON_DEFINITIONS
739739
- EFFICIENCY_SENSOR_DEFINITIONS
740740
- STORED_ENERGY_SENSOR_DEFINITIONS
741-
- CYCLE_SENSOR_DEFINITIONS
741+
- DERIVED_SENSOR_DEFINITIONS
742742
743743
If an unknown version is requested, the function falls back to the v1/v2
744744
register set (because v1 and v2 share the same registers in this integration).
@@ -819,8 +819,8 @@ def _normalize_section(section):
819819
"STORED_ENERGY_SENSOR_DEFINITIONS": _normalize_section(
820820
data.get("STORED_ENERGY_SENSOR_DEFINITIONS")
821821
),
822-
"CYCLE_SENSOR_DEFINITIONS": _normalize_section(
823-
data.get("CYCLE_SENSOR_DEFINITIONS")
822+
"DERIVED_SENSOR_DEFINITIONS": _normalize_section(
823+
data.get("DERIVED_SENSOR_DEFINITIONS")
824824
),
825825
}
826826
except Exception as e:

custom_components/marstek_modbus/registers/e_v3.yaml

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ MISSING:
88
sensor:
99
- sn_code
1010
- software_version
11-
- fault_status
12-
- alarm_status
1311
binary:
1412
- discharge_limit_mode
1513
select:
@@ -115,6 +113,16 @@ SENSOR_DEFINITIONS:
115113
data_type: int16
116114
precision: 1
117115
scan_interval: medium
116+
battery_voltage_alt:
117+
register: 30000
118+
scale: 0.1
119+
unit: V
120+
device_class: voltage
121+
state_class: measurement
122+
enabled_by_default: false
123+
data_type: uint16
124+
precision: 1
125+
scan_interval: medium
118126
battery_power:
119127
register: 30001
120128
count: 1
@@ -126,6 +134,46 @@ SENSOR_DEFINITIONS:
126134
data_type: int16
127135
precision: 1
128136
scan_interval: high
137+
internal_temperature_alt:
138+
register: 30002
139+
scale: 0.1
140+
unit: °C
141+
device_class: temperature
142+
state_class: measurement
143+
enabled_by_default: false
144+
data_type: int16
145+
precision: 1
146+
scan_interval: medium
147+
internal_mos1_temperature_alt:
148+
register: 30003
149+
scale: 0.1
150+
unit: °C
151+
device_class: temperature
152+
state_class: measurement
153+
enabled_by_default: false
154+
data_type: int16
155+
precision: 1
156+
scan_interval: medium
157+
inverter_voltage_alt:
158+
register: 30004
159+
scale: 0.1
160+
unit: V
161+
device_class: voltage
162+
state_class: measurement
163+
enabled_by_default: false
164+
data_type: uint16
165+
precision: 1
166+
scan_interval: medium
167+
ac_offgrid_voltage_alt:
168+
register: 30005
169+
scale: 0.1
170+
unit: V
171+
device_class: voltage
172+
state_class: measurement
173+
enabled_by_default: false
174+
data_type: uint16
175+
precision: 1
176+
scan_interval: medium
129177
internal_temperature:
130178
register: 35000
131179
scale: 0.1
@@ -156,7 +204,7 @@ SENSOR_DEFINITIONS:
156204
data_type: int16
157205
precision: 2
158206
scan_interval: medium
159-
ac_voltage:
207+
inverter_voltage:
160208
register: 32200
161209
scale: 0.1
162210
unit: V
@@ -166,7 +214,7 @@ SENSOR_DEFINITIONS:
166214
data_type: uint16
167215
precision: 1
168216
scan_interval: medium
169-
ac_current:
217+
inverter_current:
170218
register: 37004
171219
scale: 0.004
172220
unit: A
@@ -176,7 +224,7 @@ SENSOR_DEFINITIONS:
176224
data_type: int16
177225
precision: 1
178226
scan_interval: medium
179-
ac_power:
227+
inverter_power:
180228
register: 30006
181229
count: 1
182230
scale: 1
@@ -187,7 +235,17 @@ SENSOR_DEFINITIONS:
187235
data_type: int16
188236
precision: 0
189237
scan_interval: high
190-
ac_frequency:
238+
ac_offgrid_power_alt:
239+
register: 30007
240+
scale: 1
241+
unit: W
242+
device_class: power
243+
state_class: measurement
244+
enabled_by_default: false
245+
data_type: int16
246+
precision: 0
247+
scan_interval: high
248+
inverter_frequency:
191249
register: 32204
192250
scale: 0.1
193251
unit: Hz
@@ -508,11 +566,11 @@ SENSOR_DEFINITIONS:
508566
data_type: uint16
509567
precision: 1
510568
scan_interval: medium
511-
ac_offgrid_current:
569+
ac_offgrid_voltage_raw:
512570
register: 32301
513-
scale: 0.01
514-
unit: A
515-
device_class: current
571+
scale: 0.1
572+
unit: V
573+
device_class: voltage
516574
state_class: measurement
517575
enabled_by_default: false
518576
data_type: uint16
@@ -548,6 +606,28 @@ SENSOR_DEFINITIONS:
548606
category: diagnostic
549607
enabled_by_default: false
550608
scan_interval: medium
609+
alarm_status:
610+
register: 36000
611+
count: 2
612+
scale: 1
613+
unit: null
614+
icon: "mdi:alert"
615+
category: diagnostic
616+
enabled_by_default: false
617+
data_type: uint32
618+
precision: 0
619+
scan_interval: medium
620+
fault_status:
621+
register: 36100
622+
count: 2
623+
scale: 1
624+
unit: null
625+
icon: "mdi:alert-circle"
626+
category: diagnostic
627+
enabled_by_default: false
628+
data_type: uint32
629+
precision: 0
630+
scan_interval: medium
551631

552632
BINARY_SENSOR_DEFINITIONS:
553633
wifi_status:
@@ -1031,7 +1111,7 @@ EFFICIENCY_SENSOR_DEFINITIONS:
10311111
mode: conversion
10321112
dependency_keys:
10331113
battery_power: battery_power
1034-
ac_power: ac_power
1114+
ac_power: inverter_power
10351115

10361116
STORED_ENERGY_SENSOR_DEFINITIONS:
10371117
stored_energy:
@@ -1043,12 +1123,49 @@ STORED_ENERGY_SENSOR_DEFINITIONS:
10431123
soc: battery_soc
10441124
capacity: battery_total_energy
10451125

1046-
CYCLE_SENSOR_DEFINITIONS:
1126+
DERIVED_SENSOR_DEFINITIONS:
1127+
ac_power:
1128+
key: ac_power
1129+
unit: W
1130+
device_class: power
1131+
state_class: measurement
1132+
enabled_by_default: true
1133+
icon: "mdi:transmission-tower"
1134+
mode: difference
1135+
dependency_keys:
1136+
a: ac_offgrid_power
1137+
b: inverter_power
1138+
ac_current:
1139+
key: ac_current
1140+
unit: A
1141+
device_class: current
1142+
state_class: measurement
1143+
enabled_by_default: true
1144+
icon: "mdi:transmission-tower"
1145+
mode: difference_quotient
1146+
dependency_keys:
1147+
a: ac_offgrid_power
1148+
b: inverter_power
1149+
c: inverter_voltage
1150+
# ac_offgrid_current: register 32301 returns duplicate voltage (same as 32300).
1151+
# Calculated as ac_offgrid_power / ac_offgrid_voltage instead.
1152+
ac_offgrid_current:
1153+
key: ac_offgrid_current
1154+
unit: A
1155+
device_class: current
1156+
state_class: measurement
1157+
enabled_by_default: true
1158+
icon: "mdi:power-socket"
1159+
mode: quotient
1160+
dependency_keys:
1161+
a: ac_offgrid_power
1162+
b: ac_offgrid_voltage
10471163
battery_cycle_count_calc:
10481164
key: battery_cycle_count_calc
10491165
icon: "mdi:counter"
10501166
category: diagnostic
10511167
state_class: measurement
1168+
mode: quotient
10521169
dependency_keys:
1053-
discharge: total_discharging_energy
1054-
capacity: battery_total_energy
1170+
a: total_discharging_energy
1171+
b: battery_total_energy

custom_components/marstek_modbus/sensor.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def async_setup_entry(
3434
(MarstekSensor, coordinator.SENSOR_DEFINITIONS),
3535
(MarstekEfficiencySensor, coordinator.EFFICIENCY_SENSOR_DEFINITIONS),
3636
(MarstekStoredEnergySensor, coordinator.STORED_ENERGY_SENSOR_DEFINITIONS),
37-
(MarstekBatteryCycleSensor, coordinator.CYCLE_SENSOR_DEFINITIONS),
37+
(MarstekDerivedSensor, coordinator.DERIVED_SENSOR_DEFINITIONS),
3838
)
3939
for entity_cls, definitions in sensor_groups:
4040
entities.extend(entity_cls(coordinator, definition) for definition in definitions)
@@ -347,6 +347,18 @@ def get_dependency_keys(self):
347347
"""Return the keys this sensor depends on."""
348348
return self.definition.get("dependency_keys", {})
349349

350+
@property
351+
def available(self) -> bool:
352+
"""Return True only when all dependency values are present in coordinator data."""
353+
data = getattr(self.coordinator, "data", None)
354+
if not isinstance(data, dict):
355+
return False
356+
return all(
357+
dep_key in data and data.get(dep_key) is not None
358+
for dep_key in self.get_dependency_keys().values()
359+
if dep_key
360+
)
361+
350362
@property
351363
def entity_type(self) -> str:
352364
"""
@@ -487,15 +499,30 @@ def calculate_value(self, dep_values: dict):
487499
return efficiency_rounded
488500

489501

490-
class MarstekBatteryCycleSensor(MarstekCalculatedSensor):
491-
"""Calculate estimated battery cycles from discharge energy and capacity."""
502+
class MarstekDerivedSensor(MarstekCalculatedSensor):
503+
"""Sensor that derives a value from two or three dependency keys using a simple arithmetic mode.
504+
505+
Supported modes:
506+
- "difference": a - b
507+
- "difference_quotient": (a - b) / c
508+
- "quotient": a / b
509+
"""
492510

493511
def calculate_value(self, dep_values: dict):
494-
discharge = dep_values.get("discharge")
495-
capacity = dep_values.get("capacity")
496-
if discharge is None or capacity in (None, 0):
497-
return None
512+
mode = self.definition.get("mode")
513+
if mode == "difference":
514+
return round(dep_values["a"] - dep_values["b"], 1)
515+
if mode == "difference_quotient":
516+
c = dep_values["c"]
517+
if c == 0:
518+
return None
519+
return round((dep_values["a"] - dep_values["b"]) / c, 2)
520+
if mode == "quotient":
521+
b = dep_values["b"]
522+
if b == 0:
523+
return None
524+
return round(dep_values["a"] / b, 2)
525+
_LOGGER.warning("%s unknown derived mode '%s'", self._key, mode)
526+
return None
527+
498528

499-
cycles = round(discharge / capacity, 2)
500-
self._attr_native_value = cycles
501-
return cycles

0 commit comments

Comments
 (0)