Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions apps/predbat/octopus.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
DATE_STR_FORMAT = "%Y-%m-%d"
DATE_TIME_STR_FORMAT = "%Y-%m-%dT%H:%M:%S%z"

# Sentinel distinguishing "attribute not present on the entity" from a genuinely empty list,
# since both would otherwise look the same (falsy) to callers.
_ATTRIBUTE_UNSET = object()

# Night-rate window definitions: start time, end time, whether the window crosses midnight.
# Keys: "eco7" (Economy 7), "go" (Octopus GO / generic day-night), "iog" (Intelligent GO TOU).
OCTOPUS_NIGHT_RATE_WINDOWS = {
Expand Down Expand Up @@ -2939,12 +2943,22 @@ def fetch_octopus_sessions(self, axle_sessions=None):
entity_id = self.get_arg("octopus_saving_session", indirect=False)
if entity_id:
state = self.get_arg("octopus_saving_session", False)
joined_events = self.get_state_wrapper(entity_id=entity_id, attribute="joined_events")
if not joined_events:
entity_id = entity_id.replace("binary_sensor.", "event.").replace("_sessions", "_session_events")
joined_events = self.get_state_wrapper(entity_id=entity_id, attribute="joined_events")

available_events = self.get_state_wrapper(entity_id=entity_id, attribute="available_events")
joined_events = self.get_state_wrapper(entity_id=entity_id, attribute="joined_events", default=_ATTRIBUTE_UNSET)
available_events = self.get_state_wrapper(entity_id=entity_id, attribute="available_events", default=_ATTRIBUTE_UNSET)
if joined_events is _ATTRIBUTE_UNSET and available_events is _ATTRIBUTE_UNSET:
# Legacy binary_sensor entities carry neither attribute at all - fall back to the
# newer event entity naming convention, but only adopt it if it actually has data.
# A configured entity that has the attributes but with no events right now (empty
# lists) is a valid state and must not trigger this fallback.
fallback_entity_id = entity_id.replace("binary_sensor.", "event.").replace("_sessions", "_session_events")
fallback_joined_events = self.get_state_wrapper(entity_id=fallback_entity_id, attribute="joined_events", default=_ATTRIBUTE_UNSET)
fallback_available_events = self.get_state_wrapper(entity_id=fallback_entity_id, attribute="available_events", default=_ATTRIBUTE_UNSET)
if fallback_joined_events is not _ATTRIBUTE_UNSET or fallback_available_events is not _ATTRIBUTE_UNSET:
entity_id = fallback_entity_id
joined_events = fallback_joined_events
available_events = fallback_available_events
joined_events = [] if joined_events is _ATTRIBUTE_UNSET else joined_events
available_events = [] if available_events is _ATTRIBUTE_UNSET else available_events

if available_events and not self.get_arg("octopus_saving_auto_join", True):
self.log("Octopus: Saving session auto-join is disabled, not joining available events")
Expand Down
73 changes: 73 additions & 0 deletions apps/predbat/tests/test_saving_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,79 @@ def setup_items():
return failed


def test_saving_session_custom_entity_no_rewrite_match(my_predbat):
"""
Test that available_events is read from the configured entity even when its name
does not match the binary_sensor -> event rewrite pattern (no '_sessions' substring),
and no rewritten entity exists at all.
Covers GitHub issue #4573
"""
print("Test saving session with custom entity name that does not match the rewrite pattern (issue #4573)")
ha = my_predbat.ha_interface
failed = False
date_today = datetime.now().strftime("%Y-%m-%d")
tz_offset = int(my_predbat.midnight_utc.tzinfo.utcoffset(my_predbat.midnight_utc).total_seconds() / 3600)
tz_offset = f"{tz_offset:02d}"

# Custom entity name bridging a saving session event. It has no '_sessions' substring
# so the legacy binary_sensor -> event rewrite would point at a non-existent entity if
# it were ever applied. joined_events is empty (nothing joined yet) but available_events
# is populated - this is exactly the state auto-join needs to act on.
session_binary = f"""
state: off
available_events:
- id: 9999
start: '{date_today}T18:00:00+{tz_offset}:00'
end: '{date_today}T19:00:00+{tz_offset}:00'
duration_in_minutes: 60
rewarded_octopoints: null
octopoints_per_kwh: 505
code: EVENT_TEST
joined_events: []
friendly_name: Predbat Octopus Power Down For Predbat
"""

saved_args = my_predbat.args.copy()
try:
ha.dummy_items.clear()
ha.dummy_items["binary_sensor.predbat_octopus_power_down_for_predbat"] = yaml.safe_load(session_binary)
ha.dummy_items["sensor.octopus_free_session"] = {}
my_predbat.args["octopus_saving_session"] = "binary_sensor.predbat_octopus_power_down_for_predbat"
my_predbat.args["octopus_free_session"] = "sensor.octopus_free_session"
if "octopus_free_url" in my_predbat.args:
del my_predbat.args["octopus_free_url"]
if "octopus_saving_session_join" in my_predbat.args:
del my_predbat.args["octopus_saving_session_join"]
my_predbat.args["octopus_saving_session_octopoints_per_penny"] = 10
# Reset throttle so a join is attempted
my_predbat.octopus_last_joined_try = None

ha.service_store_enable = True
ha.service_store = []
my_predbat.fetch_octopus_sessions()
service_result = ha.get_service_store()
ha.service_store_enable = False

join_calls = [svc for svc in service_result if "join" in svc[0]]
if len(join_calls) != 1:
print(f"ERROR: Expected 1 join call reading available_events from the configured entity, got {len(join_calls)}: {service_result}")
failed = True
elif join_calls[0][1].get("entity_id") != "binary_sensor.predbat_octopus_power_down_for_predbat":
print(f"ERROR: Expected join call to use the configured entity, got {join_calls[0][1]}")
failed = True
else:
print(" PASS: available_events read from the configured entity despite no rewrite match")

if not failed:
print("PASS: Custom entity name (no rewrite match) auto-join test passed")
finally:
my_predbat.args = saved_args
# Restore default throttle state so we do not leak it to other tests
my_predbat.octopus_last_joined_try = None

return failed


def test_saving_session_default_rate(my_predbat):
"""
Test that saving sessions with no octopoints_per_kwh use the default rate
Expand Down
11 changes: 10 additions & 1 deletion apps/predbat/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@
from tests.test_solax import run_solax_tests
from tests.test_sigenergy import run_sigenergy_tests
from tests.test_single_debug import run_single_debug
from tests.test_saving_session import test_saving_session, test_saving_session_null_octopoints, test_saving_session_notify_config, test_saving_session_default_rate, test_saving_session_axle_conflict, test_saving_session_auto_join_toggle
from tests.test_saving_session import (
test_saving_session,
test_saving_session_null_octopoints,
test_saving_session_notify_config,
test_saving_session_default_rate,
test_saving_session_axle_conflict,
test_saving_session_auto_join_toggle,
test_saving_session_custom_entity_no_rewrite_match,
)
from tests.test_secrets import run_secrets_tests
from tests.test_ge_cloud import test_ge_cloud
from tests.test_teslemetry import test_teslemetry
Expand Down Expand Up @@ -436,6 +444,7 @@ def main():
("saving_session_default_rate", test_saving_session_default_rate, "Saving session default rate injection test", False),
("saving_session_axle_conflict", test_saving_session_axle_conflict, "Saving session Axle conflict avoidance test (issue #4120)", False),
("saving_session_auto_join_toggle", test_saving_session_auto_join_toggle, "Saving session auto-join toggle test (issue #4120)", False),
("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),
("alert_feed", test_alert_feed, "Alert feed tests", False),
("fox_api", run_fox_api_tests, "Fox API tests", False),
("deye_const", run_deye_const_tests, "DEYE constants tests", False),
Expand Down
Loading