Skip to content

Commit 425b5f6

Browse files
xchillxxclaude
andcommitted
Dependent device follows the real state of its dependency (v3.1.4)
A device that depends on another (pool heat pump on the pool pump) was switched off at once as soon as the pump was *planned* off, while the pump itself was still running out its 10-minute countdown. It now goes off at once only when the pump is really off - in the same cycle the pump is switched off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 66126d8 commit 425b5f6

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

‎custom_components/surplus_pilot/coordinator.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,12 @@ async def _apply_devices(self, now: datetime, dev_on: dict[str, bool]) -> None:
654654
if want == actual:
655655
self._pending.pop(did, None)
656656
continue
657-
immediate = not want and dec.reason in ("ausserhalb_zeitfenster", "wartet_auf_abhaengigkeit")
657+
# off at once only when it can't run any more: outside its window,
658+
# or the device it depends on is really off (not merely planned
659+
# off and still counting down - then it follows with its own delay)
660+
dep_id = d.get(CONF_DEV_DEPENDS_ON)
661+
immediate = not want and (dec.reason == "ausserhalb_zeitfenster" or (
662+
dec.reason == "wartet_auf_abhaengigkeit" and not (dep_id and dev_on.get(dep_id))))
658663
if want:
659664
delay = DEVICE_FORCED_ON_DELAY_S if dec.path == "pflicht" else DEVICE_ON_DELAY_S
660665
else:
@@ -668,6 +673,7 @@ async def _apply_devices(self, now: datetime, dev_on: dict[str, bool]) -> None:
668673
if immediate or (now - pend[1]).total_seconds() >= delay:
669674
if self.active:
670675
await device_switch(self.hass, d, want)
676+
dev_on[did] = want # a dependent device further down sees it this cycle
671677
self._log(f"{d.get(CONF_DEV_NAME)} {'an' if want else 'aus'} "
672678
f"({REASON_TEXT.get(dec.reason, dec.reason)})")
673679
self._pending.pop(did, None)

‎custom_components/surplus_pilot/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"iot_class": "local_polling",
1010
"issue_tracker": "https://github.com/xchillxx/surplus-pilot/issues",
1111
"requirements": [],
12-
"version": "3.1.3"
12+
"version": "3.1.4"
1313
}

‎tests/test_integration.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,50 @@ async def fake(call):
252252
assert co.car.decision is not None # decided on the first cycle, no 10 min wait
253253
for did, (want, since) in pending_before.items():
254254
assert co._pending.get(did, (None, None))[1] in (since, None)
255+
256+
257+
async def test_dependent_device_follows_pump_not_its_plan(hass: HomeAssistant, freezer):
258+
"""07.10. 13:51 live: pump planned off (countdown 8 min, still running),
259+
the pool heat pump was switched off at once 'waiting for dependency'."""
260+
freezer.move_to("2026-10-07 09:01:00+00:00")
261+
set_world(hass, pv=9.0, load=0.6, soc=96, car_soc=80, export=2.0)
262+
hass.states.async_set("switch.car_charge", "off")
263+
hass.states.async_set("switch.miner", "on")
264+
hass.states.async_set("switch.pump", "on")
265+
hass.states.async_set("climate.pool", "heat")
266+
entry = MockConfigEntry(domain=DOMAIN, data=DATA)
267+
entry.add_to_hass(hass)
268+
assert await hass.config_entries.async_setup(entry.entry_id)
269+
await hass.async_block_till_done()
270+
calls = []
271+
272+
async def fake(call):
273+
calls.append((call.domain, call.service, dict(call.data)))
274+
ent = call.data.get("entity_id")
275+
if call.domain == "switch":
276+
hass.states.async_set(ent, "on" if call.service == "turn_on" else "off")
277+
elif call.domain == "climate":
278+
hass.states.async_set(ent, call.data["hvac_mode"])
279+
280+
for dom, srv in (("switch", "turn_on"), ("switch", "turn_off"), ("number", "set_value"),
281+
("climate", "set_hvac_mode")):
282+
hass.services.async_register(dom, srv, fake)
283+
284+
def tick(pv):
285+
freezer.tick(timedelta(minutes=1))
286+
set_world(hass, pv=pv, load=0.6 + 0.15 + 1.3 + 0.86, soc=80, car_soc=80, export=0.0)
287+
async_fire_time_changed(hass, dt_util.utcnow())
288+
289+
for _ in range(8): # surplus gone: pump counts down, stays on for now
290+
tick(1.0)
291+
await hass.async_block_till_done()
292+
assert hass.states.get("switch.pump").state == "on"
293+
assert hass.states.get("climate.pool").state == "heat"
294+
for _ in range(6):
295+
tick(1.0)
296+
await hass.async_block_till_done()
297+
assert hass.states.get("switch.pump").state == "off"
298+
assert hass.states.get("climate.pool").state == "off" # same cycle as the pump, not 20 min later
299+
i_pump = calls.index(("switch", "turn_off", {"entity_id": "switch.pump"}))
300+
i_wp = next(i for i, c in enumerate(calls) if c[0] == "climate")
301+
assert i_wp > i_pump

0 commit comments

Comments
 (0)