Skip to content

Commit dabf8a7

Browse files
feat(octopus): try new Power Down join service, fall back to deprecated one
Bottle Cap Dave's Octopus Energy integration renamed join_octoplus_saving_session_event to join_octoplus_power_down_session_event (ADR-0004). Both remain registered until the old one is retired in January 2027, so try the new name first and fall back to the old one on failure. Part of #4548 (point 3). TODO(#4599) tracks removing the fallback once the old service name is gone upstream.
1 parent 004f298 commit dabf8a7

4 files changed

Lines changed: 126 additions & 5 deletions

File tree

apps/predbat/octopus.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,11 +2987,21 @@ def fetch_octopus_sessions(self, axle_sessions=None):
29872987
self.log("Octopus: Joining Octopus saving event code {} {}-{} at rate {} p/kWh".format(code, start_time.strftime("%a %d/%m %H:%M"), end_time.strftime("%H:%M"), saving_rate))
29882988
entity_id_join = self.get_arg("octopus_saving_session_join", indirect=False)
29892989
if entity_id_join:
2990-
# Join via selector
2990+
# Join via selector (Octopus Energy Direct, or any other integration wired
2991+
# up this way) - unaffected by which Bottle Cap Dave service name is current
29912992
self.call_service_wrapper("select/select_option", entity_id=entity_id_join, option=code)
29922993
else:
2993-
# Join via octopus event (Bottle Cap Dave)
2994-
self.call_service_wrapper("octopus_energy/join_octoplus_saving_session_event", event_code=code, entity_id=entity_id)
2994+
# Join via Bottle Cap Dave's Octopus Energy HA integration. Try the current
2995+
# service name first (join_octoplus_power_down_session_event, which
2996+
# superseded join_octoplus_saving_session_event - see the integration's
2997+
# ADR-0004), falling back to the old name for anyone on an integration
2998+
# version that predates the rename. Both remain registered until the old
2999+
# one is removed in January 2027, so this fallback is a temporary bridge,
3000+
# not a permanent branch.
3001+
# TODO(#4599): remove this fallback once the old service name is retired upstream.
3002+
if not self.call_service_wrapper("octopus_energy/join_octoplus_power_down_session_event", event_code=code, entity_id=entity_id):
3003+
self.log("Note: octopus_energy/join_octoplus_power_down_session_event not available, falling back to the deprecated join_octoplus_saving_session_event service")
3004+
self.call_service_wrapper("octopus_energy/join_octoplus_saving_session_event", event_code=code, entity_id=entity_id)
29953005
if self.get_arg("set_event_notify"):
29963006
self.call_notify("Predbat: Joined Octopus saving event {}-{}, {} p/kWh".format(start_time.strftime("%a %d/%m %H:%M"), end_time.strftime("%H:%M"), saving_rate))
29973007
self.octopus_last_joined_try = self.now_utc

apps/predbat/tests/test_infra.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def __init__(self):
146146
self.dummy_items = {}
147147
self.service_store_enable = False
148148
self.service_store = []
149+
self.service_store_fail = set()
149150
self.db_primary = False
150151

151152
def get_service_store(self):
@@ -189,7 +190,10 @@ def call_service(self, service, **kwargs):
189190
print("Calling service: {} {}".format(service, kwargs))
190191
if self.service_store_enable:
191192
self.service_store.append([service, kwargs])
192-
return None
193+
# Services in service_store_fail simulate a service that doesn't exist (e.g. testing a
194+
# try-new-service-then-fall-back-to-old caller) - everything else succeeds, matching real
195+
# HA behaviour for a registered service call.
196+
return None if service in self.service_store_fail else True
193197

194198
if service == "number/set_value":
195199
entity_id = kwargs.get("entity_id", None)

apps/predbat/tests/test_saving_session.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def test_saving_session(my_predbat):
105105
# Example format Sat 25/01
106106
date_today_service = datetime.now().strftime("%a %d/%m")
107107
expected_service = [
108-
["octopus_energy/join_octoplus_saving_session_event", {"event_code": 987654, "entity_id": "event.octopus_energy_a_12345678_octoplus_saving_session_event"}],
108+
["octopus_energy/join_octoplus_power_down_session_event", {"event_code": 987654, "entity_id": "event.octopus_energy_a_12345678_octoplus_saving_session_event"}],
109109
["notify/notify", {"message": "Predbat: Joined Octopus saving event {} 18:30-19:30, 50.0 p/kWh".format(date_today_service)}],
110110
]
111111

@@ -494,6 +494,111 @@ def setup_items():
494494
return failed
495495

496496

497+
def test_saving_session_join_service_fallback(my_predbat):
498+
"""
499+
Test that auto-join tries the current Bottle Cap Dave join service
500+
(join_octoplus_power_down_session_event) first, falling back to the deprecated
501+
join_octoplus_saving_session_event only when the current one is unavailable (e.g. an
502+
integration version that predates the Power Down rename). Covers GitHub issue #4548 point 3.
503+
"""
504+
print("Test saving session join service fallback (issue #4548 point 3)")
505+
ha = my_predbat.ha_interface
506+
failed = False
507+
date_today = datetime.now().strftime("%Y-%m-%d")
508+
tz_offset = int(my_predbat.midnight_utc.tzinfo.utcoffset(my_predbat.midnight_utc).total_seconds() / 3600)
509+
tz_offset = f"{tz_offset:02d}"
510+
511+
session_binary = """
512+
state: off
513+
current_joined_event_start: null
514+
current_joined_event_end: null
515+
current_joined_event_duration_in_minutes: null
516+
next_joined_event_start: null
517+
next_joined_event_end: null
518+
next_joined_event_duration_in_minutes: null
519+
icon: mdi:leaf
520+
friendly_name: Octoplus Saving Session
521+
"""
522+
523+
session_sensor = f"""
524+
state: '2025-01-23T12:10:11.108+{tz_offset}:00'
525+
event_types: octopus_energy_all_octoplus_saving_sessions
526+
event_type: octopus_energy_all_octoplus_saving_sessions
527+
account_id: A-4DD6C5EE
528+
available_events:
529+
- id: 9999
530+
start: '{date_today}T18:30:00+{tz_offset}:00'
531+
end: '{date_today}T19:30:00+{tz_offset}:00'
532+
duration_in_minutes: 60
533+
rewarded_octopoints: null
534+
octopoints_per_kwh: 500
535+
code: TEST123
536+
joined_events: []
537+
friendly_name: Octoplus Saving Session Events
538+
"""
539+
540+
def setup_items():
541+
ha.dummy_items.clear()
542+
ha.dummy_items["binary_sensor.octopus_energy_test_octoplus_saving_sessions"] = yaml.safe_load(session_binary)
543+
ha.dummy_items["event.octopus_energy_test_octoplus_saving_session_event"] = yaml.safe_load(session_sensor)
544+
ha.dummy_items["sensor.octopus_free_session"] = {}
545+
my_predbat.args["octopus_saving_session"] = "event.octopus_energy_test_octoplus_saving_session_event"
546+
my_predbat.args["octopus_free_session"] = "sensor.octopus_free_session"
547+
if "octopus_free_url" in my_predbat.args:
548+
del my_predbat.args["octopus_free_url"]
549+
my_predbat.args["octopus_saving_session_octopoints_per_penny"] = 10
550+
# No octopus_saving_session_join configured, so this exercises the Bottle Cap Dave service
551+
# branch, not the select-entity branch (Octopus Energy Direct or similar)
552+
if "octopus_saving_session_join" in my_predbat.args:
553+
del my_predbat.args["octopus_saving_session_join"]
554+
my_predbat.octopus_last_joined_try = None
555+
556+
# Test 1: current service available -> used directly, no fallback
557+
print(" Test 1: Current service used when available")
558+
setup_items()
559+
ha.service_store_fail = set()
560+
ha.service_store_enable = True
561+
ha.service_store = []
562+
my_predbat.fetch_octopus_sessions()
563+
service_result = ha.get_service_store()
564+
ha.service_store_enable = False
565+
566+
services_called = [svc[0] for svc in service_result]
567+
if "octopus_energy/join_octoplus_power_down_session_event" not in services_called:
568+
print(f"ERROR: Expected the current service to be called, got {services_called}")
569+
failed = True
570+
elif "octopus_energy/join_octoplus_saving_session_event" in services_called:
571+
print(f"ERROR: Deprecated service should not be called when the current one succeeds, got {services_called}")
572+
failed = True
573+
else:
574+
print(" PASS: Current service called, no fallback")
575+
576+
# Test 2: current service unavailable (older integration) -> falls back to the deprecated one
577+
print(" Test 2: Falls back to deprecated service when the current one is unavailable")
578+
setup_items()
579+
ha.service_store_fail = {"octopus_energy/join_octoplus_power_down_session_event"}
580+
ha.service_store_enable = True
581+
ha.service_store = []
582+
my_predbat.fetch_octopus_sessions()
583+
service_result = ha.get_service_store()
584+
ha.service_store_enable = False
585+
ha.service_store_fail = set()
586+
587+
services_called = [svc[0] for svc in service_result]
588+
if "octopus_energy/join_octoplus_saving_session_event" not in services_called:
589+
print(f"ERROR: Expected fallback to the deprecated service, got {services_called}")
590+
failed = True
591+
else:
592+
print(" PASS: Fell back to the deprecated service")
593+
594+
if not failed:
595+
print("PASS: All join service fallback tests passed")
596+
597+
my_predbat.octopus_last_joined_try = None
598+
599+
return failed
600+
601+
497602
def test_saving_session_auto_join_toggle(my_predbat):
498603
"""
499604
Test that the octopus_saving_auto_join switch controls whether available saving sessions are auto-joined

apps/predbat/unit_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
test_saving_session_notify_config,
7171
test_saving_session_default_rate,
7272
test_saving_session_axle_conflict,
73+
test_saving_session_join_service_fallback,
7374
test_saving_session_auto_join_toggle,
7475
test_saving_session_custom_entity_no_rewrite_match,
7576
)
@@ -443,6 +444,7 @@ def main():
443444
("saving_session_notify", test_saving_session_notify_config, "Saving session notification config tests", False),
444445
("saving_session_default_rate", test_saving_session_default_rate, "Saving session default rate injection test", False),
445446
("saving_session_axle_conflict", test_saving_session_axle_conflict, "Saving session Axle conflict avoidance test (issue #4120)", False),
447+
("saving_session_join_service_fallback", test_saving_session_join_service_fallback, "Saving session join service fallback test (issue #4548 point 3)", False),
446448
("saving_session_auto_join_toggle", test_saving_session_auto_join_toggle, "Saving session auto-join toggle test (issue #4120)", False),
447449
("saving_session_custom_entity_no_rewrite_match", test_saving_session_custom_entity_no_rewrite_match, "Saving session custom entity no rewrite match test (issue #4573)", False),
448450
("alert_feed", test_alert_feed, "Alert feed tests", False),

0 commit comments

Comments
 (0)