Skip to content

Commit 7297699

Browse files
fix(charger-control): judge each car on its own dispatch sensor under an Ohme claim
Since #5443 the Octopus component keeps wiring its whole dispatch-sensor list into octopus_intelligent_slot while Ohme holds car_slot_owner (the claim now covers only the ready time and charge limit). The car_slot_owner shortcut in charger_control_octopus_drives_charger()/_drives_car() predates that and applied to every car, so with an Ohme on car 0 and a second car whose Intelligent device is the car itself, a Zappi or GE EVC on car 1 was left to Octopus: follows_car ignored, and a held charger handed over stopped. The sensor's own is_charger now decides; the claim only stands in when the sensor does not say (the Ohme's own slot sensor). drives_car no longer reads an "unknown" is_charger as "the car". Regression test fails on the merge without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 61b4947 commit 7297699

3 files changed

Lines changed: 54 additions & 11 deletions

File tree

‎apps/predbat/car_charger_control.py‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,28 @@ def charger_control_octopus_drives_charger(self, car_n):
190190
if not slot:
191191
# Not wired for this car - but the Octopus component may simply not have got there yet
192192
return None if self.charger_control_octopus_discovering() else False
193-
owner = getattr(self.base, "car_slot_owner", None)
194-
if owner and owner != "octopus":
195-
# Another charger component (Ohme) supplies the Intelligent slots from the charger itself
196-
return True
193+
is_charger = self.charger_control_dispatch_is_charger(slot)
194+
if is_charger is None:
195+
owner = getattr(self.base, "car_slot_owner", None)
196+
if owner and owner != "octopus":
197+
# Another charger component (Ohme) wired its own sensor as the Intelligent slots, which
198+
# does not say - but it only does so for the charger Octopus drives. Asked only once the
199+
# sensor is silent: since #5443 Octopus's own dispatch sensors stay wired under the
200+
# claim, and those say per car, so a second car's device may well not be the charger
201+
return True
202+
return is_charger
203+
204+
def charger_control_dispatch_is_charger(self, slot):
205+
"""What a dispatch sensor says about whether its Intelligent device is the charger: True, False or None.
206+
207+
None for a sensor that does not say - not published yet, one that never does (the Octopus
208+
Energy HA integration, an Ohme slot sensor), or one that has lost its value, which is not
209+
the same as "not the charger".
210+
"""
197211
is_charger = self.get_state_wrapper(slot, attribute="is_charger")
198212
if is_charger is None:
199-
# Not published yet, or a sensor that does not say (the Octopus Energy HA integration)
200213
return None
201214
if isinstance(is_charger, str) and is_charger.strip().lower() in ("unknown", "unavailable", "none", ""):
202-
# A sensor that has lost its value does not say, which is not the same as "not the charger"
203215
return None
204216
return parse_control_setting(is_charger)
205217

@@ -214,10 +226,9 @@ def charger_control_octopus_drives_car(self, car_n):
214226
slot = self.charger_control_dispatch_sensor(car_n)
215227
if not slot:
216228
return False
217-
owner = getattr(self.base, "car_slot_owner", None)
218-
if owner and owner != "octopus":
219-
return False
220-
return parse_control_setting(self.get_state_wrapper(slot, attribute="is_charger")) is False
229+
# Not short-cut on car_slot_owner: since #5443 Octopus's dispatch sensors stay wired while Ohme
230+
# holds the claim, so a second car's sensor can say its device is the car itself
231+
return self.charger_control_dispatch_is_charger(slot) is False
221232

222233
def charger_control_octopus_discovering(self):
223234
"""Is the Octopus or Kraken component still to wire its Intelligent devices into the car slots?

‎apps/predbat/tests/test_car_charger_control.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,37 @@ def test_octopus_rule_per_car():
771771
assert component.commands == [("b", "off", 1)], component.commands
772772

773773

774+
def test_octopus_rule_other_slot_owner_second_car():
775+
"""Since #5443 Octopus's own dispatch sensors stay wired while Ohme holds the car slot claim, so
776+
a second car is judged on what its own sensor says rather than handed to Octopus with the Ohme."""
777+
component = FakeComponent([FakeCharger("a"), FakeCharger("b")])
778+
component.base.num_cars = 2
779+
component.base.components = FakeComponents(None)
780+
component.base.car_slot_owner = "ohme"
781+
dispatch_b = "binary_sensor.predbat_octopus_intelligent_dispatch_b"
782+
component.args["octopus_intelligent_slot"] = [DISPATCH, dispatch_b]
783+
component.sensors = {DISPATCH: {"is_charger": True}, dispatch_b: {"is_charger": False}}
784+
assert component.charger_control_octopus_drives_charger(0) is True
785+
assert component.charger_control_octopus_drives_charger(1) is False
786+
assert component.charger_control_octopus_drives_car(0) is False
787+
assert component.charger_control_octopus_drives_car(1) is True
788+
789+
# The Ohme's own slot sensor does not say, and the claim still means Octopus drives that charger
790+
component.sensors = {DISPATCH: {}, dispatch_b: {"is_charger": "unknown"}}
791+
assert component.charger_control_octopus_drives_charger(0) is True
792+
assert component.charger_control_octopus_drives_car(0) is False
793+
# A sensor that has lost its value is not the car either
794+
assert component.charger_control_octopus_drives_car(1) is False
795+
796+
# Following the car, Predbat drives the second car's charger to its dispatches and leaves the Ohme's alone
797+
component.sensors = {DISPATCH: {"is_charger": True}, dispatch_b: {"is_charger": False}}
798+
component.args["octopus_intelligent_charger_follows_car"] = True
799+
_plan(component, 0, [])
800+
_plan(component, 1, [])
801+
run_async(component.charger_control_tick(_now()))
802+
assert component.commands == [("b", "off", 1)], component.commands
803+
804+
774805
def test_unpublished_plan_dispatch_charge_stops_when_the_dispatch_ends():
775806
"""A dispatch starts a car whose plan is not published yet; when the dispatch ends the charger
776807
Predbat now holds is stopped, not left running with nobody in control."""
@@ -931,6 +962,7 @@ def run_car_charger_control_tests(my_predbat=None):
931962
test_octopus_rule_unmatched_regex_is_not_a_sensor()
932963
test_octopus_rule_other_slot_owner_hands_off()
933964
test_octopus_rule_per_car()
965+
test_octopus_rule_other_slot_owner_second_car()
934966
test_unpublished_plan_dispatch_charge_stops_when_the_dispatch_ends()
935967
test_plan_that_goes_missing_keeps_its_last_windows()
936968
test_guest_charging_releases_and_resumes()

‎docs/car-charging.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ The GivEnergy Gateway's `gateway_evc_control` (above) is unchanged and stays opt
591591

592592
On Octopus Intelligent (or E.ON/EDF SmartFlex through the Kraken component), what Predbat does depends on which device Octopus drives for that car. Predbat reads this from the car's own dispatch sensor in `octopus_intelligent_slot`:
593593

594-
- **Octopus drives the charger** - your charger is the Intelligent device, or the Intelligent slots come from the Ohme. Predbat leaves the charger alone, and hands it back if it was holding it, so the two do not fight. Handing back never starts a charge: a charger Predbat had stopped is left for Octopus to start, an Ohme has max charge turned off, and a Zappi goes back to its own mode (Eco+ in place of Fast).
594+
- **Octopus drives the charger** - your charger is the Intelligent device, or that car's Intelligent slots come from the Ohme. Predbat leaves the charger alone, and hands it back if it was holding it, so the two do not fight. Handing back never starts a charge: a charger Predbat had stopped is left for Octopus to start, an Ohme has max charge turned off, and a Zappi goes back to its own mode (Eco+ in place of Fast).
595595
- **Octopus drives the car** - the car itself is the Intelligent device, so the car starts and stops its own charge. By default Predbat leaves the charger alone, and hands back a charger it was holding as usual, so one it had stopped is started again rather than left stopped. Turn on **switch.predbat_octopus_intelligent_charger_follows_car** (*expert mode*) to have Predbat drive the charger instead: on while a dispatch is running, off otherwise, so the car cannot charge outside the dispatches on its own timers. It follows both its plan and the dispatch times on the car's dispatch sensor, so a new dispatch starts the charger within a few minutes of Octopus announcing it rather than waiting for the next plan, and the charger stops when the dispatch ends.
596596
- **Predbat cannot tell** - for example the dispatch sensor comes from the Octopus Energy integration, which does not say, or Predbat's Octopus component has not discovered your devices yet. Predbat leaves the charger alone unless you have set its control setting to `true`, which tells Predbat your charger is not the Octopus device. A charger Predbat was holding is handed back as usual, so a charger it had stopped is started again rather than left stopped with nobody in charge. When the dispatch sensor does not say, Predbat logs a warning naming the setting to change; while it is only waiting for its Octopus or Kraken component to find your devices, it just notes that it is waiting and leaves the charger as it is. If that component has still not found your devices after 15 minutes, Predbat warns and treats it as not being able to tell.
597597

0 commit comments

Comments
 (0)