Skip to content

Commit 9508292

Browse files
committed
fix: implement wallbox power sensor with zero-override for non-charging state
1 parent a156ddd commit 9508292

3 files changed

Lines changed: 143 additions & 2 deletions

File tree

custom_components/enpal_webparser/entity_factory.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,59 @@ async def async_added_to_hass(self):
124124
self._handle_coordinator_update()
125125

126126

127-
def build_sensor_entity(sensor: dict, coordinator: DataUpdateCoordinator) -> SensorEntity:
127+
# Wallbox sensors whose value must be forced to 0 when not actively charging.
128+
# Works around an Enpal firmware bug where these values freeze after charging ends.
129+
_WALLBOX_ZERO_OVERRIDE_IDS = {
130+
"power_wallbox_connector_1_charging",
131+
"current_wallbox_connector_1_phase_a",
132+
"current_wallbox_connector_1_phase_b",
133+
"current_wallbox_connector_1_phase_c",
134+
}
135+
136+
137+
def build_sensor_entity(
138+
sensor: dict,
139+
coordinator: DataUpdateCoordinator,
140+
use_wallbox: bool = False,
141+
) -> SensorEntity:
128142
"""
129143
Factory function: Builds the appropriate sensor entity.
130144
Extendable for special cases or subclasses.
131145
"""
132146
if sensor.get("device_class") == "energy":
133147
return EnpalEnergySensor(sensor, coordinator)
148+
if use_wallbox and make_id(sensor.get("name", "")) in _WALLBOX_ZERO_OVERRIDE_IDS:
149+
return EnpalWallboxPowerSensor(sensor, coordinator)
134150
return EnpalBaseSensor(sensor, coordinator)
135151

136152

153+
class EnpalWallboxPowerSensor(EnpalBaseSensor):
154+
"""Wallbox power/current sensor that reports 0 when not charging.
155+
156+
Works around an Enpal firmware bug where power and current values
157+
freeze at their last reading after a charging session ends.
158+
"""
159+
160+
_WALLBOX_STATUS_ENTITY = "sensor.wallbox_status"
161+
162+
@property
163+
def native_value(self):
164+
raw = self._sensor.get("value")
165+
status_state = self.hass.states.get(self._WALLBOX_STATUS_ENTITY)
166+
if status_state is not None and status_state.state != "charging":
167+
return 0
168+
return raw
169+
170+
@property
171+
def extra_state_attributes(self):
172+
attrs = super().extra_state_attributes
173+
status_state = self.hass.states.get(self._WALLBOX_STATUS_ENTITY)
174+
if status_state is not None and status_state.state != "charging":
175+
attrs["enpal_raw_value"] = self._sensor.get("value")
176+
attrs["enpal_zero_reason"] = "wallbox not charging"
177+
return attrs
178+
179+
137180
class EnpalEnergySensor(EnpalBaseSensor):
138181
def __init__(self, sensor: dict, coordinator: DataUpdateCoordinator):
139182
super().__init__(sensor, coordinator)

custom_components/enpal_webparser/sensor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,12 @@ async def _on_push_data(result: dict) -> None:
150150
for sensor in coordinator.data:
151151
_LOGGER.info("[Enpal] Name: %s -> UID: %s", sensor["name"], make_id(sensor["name"]))
152152

153+
use_wallbox = entry.options.get("use_wallbox", False)
154+
153155
entities = []
154156
for sensor_dict in coordinator.data:
155157
_LOGGER.debug("[Enpal] Adding sensor entity: %s", sensor_dict["name"])
156-
entities.append(build_sensor_entity(sensor_dict, coordinator))
158+
entities.append(build_sensor_entity(sensor_dict, coordinator, use_wallbox=use_wallbox))
157159

158160

159161
# Create cumulative energy sensor with smart fallback for different inverter types

custom_components/enpal_webparser/tests/test_entity_factory.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
build_sensor_entity,
2222
EnpalBaseSensor,
2323
EnpalEnergySensor,
24+
EnpalWallboxPowerSensor,
2425
)
2526

2627
class DummyCoordinator(DataUpdateCoordinator):
@@ -117,3 +118,98 @@ async def test_build_energy_sensor_full(hass: HomeAssistant, mock_sensor_dict, h
117118
assert "enpal_last_update" in sensor.extra_state_attributes
118119

119120

121+
# ---------- Wallbox power zero-override tests ----------
122+
123+
class _FakeState:
124+
"""Minimal stand-in for homeassistant.core.State."""
125+
def __init__(self, state: str):
126+
self.state = state
127+
128+
129+
class _FakeHass:
130+
"""Minimal hass stub with a states registry."""
131+
def __init__(self, states_map: dict):
132+
self._map = states_map
133+
134+
class _States:
135+
def __init__(self, m):
136+
self._m = m
137+
def get(self, entity_id):
138+
return self._m.get(entity_id)
139+
140+
@property
141+
def states(self):
142+
return self._States(self._map)
143+
144+
145+
def _wallbox_power_sensor(status_state: str | None):
146+
"""Create an EnpalWallboxPowerSensor with a faked wallbox status."""
147+
sensor_dict = {
148+
"name": "Power Wallbox Connector 1 Charging",
149+
"value": "4500",
150+
"unit": "W",
151+
"device_class": "power",
152+
"enabled": True,
153+
"group": "Wallbox",
154+
}
155+
entity = build_sensor_entity(sensor_dict, DummyCoordinator(), use_wallbox=True)
156+
assert isinstance(entity, EnpalWallboxPowerSensor)
157+
158+
if status_state is not None:
159+
entity.hass = _FakeHass({"sensor.wallbox_status": _FakeState(status_state)})
160+
else:
161+
entity.hass = _FakeHass({})
162+
return entity
163+
164+
165+
def test_wallbox_power_zero_when_not_charging():
166+
"""Power sensor returns 0 when wallbox status is not 'charging'."""
167+
entity = _wallbox_power_sensor("connected")
168+
assert entity.native_value == 0
169+
assert entity.extra_state_attributes.get("enpal_raw_value") == "4500"
170+
assert entity.extra_state_attributes.get("enpal_zero_reason") == "wallbox not charging"
171+
172+
173+
def test_wallbox_power_passthrough_when_charging():
174+
"""Power sensor returns raw value when wallbox status is 'charging'."""
175+
entity = _wallbox_power_sensor("charging")
176+
assert entity.native_value == "4500"
177+
assert "enpal_raw_value" not in entity.extra_state_attributes
178+
179+
180+
def test_wallbox_power_passthrough_when_status_missing():
181+
"""Power sensor returns raw value when wallbox_status entity doesn't exist."""
182+
entity = _wallbox_power_sensor(None)
183+
assert entity.native_value == "4500"
184+
assert "enpal_raw_value" not in entity.extra_state_attributes
185+
186+
187+
def test_wallbox_current_sensor_zero_override():
188+
"""Current sensor also gets zero-override treatment."""
189+
sensor_dict = {
190+
"name": "Current Wallbox Connector 1 Phase (A)",
191+
"value": "12.31",
192+
"unit": "A",
193+
"device_class": "current",
194+
"enabled": True,
195+
"group": "Wallbox",
196+
}
197+
entity = build_sensor_entity(sensor_dict, DummyCoordinator(), use_wallbox=True)
198+
assert isinstance(entity, EnpalWallboxPowerSensor)
199+
entity.hass = _FakeHass({"sensor.wallbox_status": _FakeState("connected")})
200+
assert entity.native_value == 0
201+
202+
203+
def test_wallbox_power_not_used_without_flag():
204+
"""Without use_wallbox=True, the regular base sensor is returned."""
205+
sensor_dict = {
206+
"name": "Power Wallbox Connector 1 Charging",
207+
"value": "4500",
208+
"unit": "W",
209+
"device_class": "power",
210+
}
211+
entity = build_sensor_entity(sensor_dict, DummyCoordinator(), use_wallbox=False)
212+
assert isinstance(entity, EnpalBaseSensor)
213+
assert not isinstance(entity, EnpalWallboxPowerSensor)
214+
215+

0 commit comments

Comments
 (0)