From b2b44b5697611e48b446a1066c4c7f8a41ef8a32 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Thu, 9 Nov 2023 20:14:19 +0000 Subject: [PATCH] feat: Added new events for new and all saving sessions --- .../octopus_energy/api_client/__init__.py | 31 +- .../api_client/saving_sessions.py | 34 ++ .../octopus_energy/binary_sensor.py | 5 +- custom_components/octopus_energy/const.py | 3 + .../coordinators/saving_sessions.py | 105 +++++- .../octopus_energy/octoplus/__init__.py | 26 +- .../octoplus/saving_sessions.py | 31 +- .../api_client/test_get_saving_sessions.py | 21 +- .../test_async_refresh_saving_sessions.py | 310 ++++++++++++++++++ .../test_current_saving_sessions_event.py | 26 +- .../test_get_next_saving_sessions_event.py | 36 +- 11 files changed, 525 insertions(+), 103 deletions(-) create mode 100644 tests/unit/coordinators/test_async_refresh_saving_sessions.py diff --git a/custom_components/octopus_energy/api_client/__init__.py b/custom_components/octopus_energy/api_client/__init__.py index d3e1dd89..c95dce6b 100644 --- a/custom_components/octopus_energy/api_client/__init__.py +++ b/custom_components/octopus_energy/api_client/__init__.py @@ -11,7 +11,7 @@ from .intelligent_settings import IntelligentSettings from .intelligent_dispatches import IntelligentDispatchItem, IntelligentDispatches -from .saving_sessions import JoinSavingSessionResponse +from .saving_sessions import JoinSavingSessionResponse, SavingSession, SavingSessionsResponse _LOGGER = logging.getLogger(__name__) @@ -242,12 +242,19 @@ octoplus_saving_session_query = '''query {{ savingSessions {{ + events {{ + id + rewardPerKwhInOctoPoints + startAt + endAt + }} account(accountNumber: "{account_id}") {{ hasJoinedCampaign joinedEvents {{ eventId startAt endAt + rewardGivenInOctoPoints }} }} }} @@ -462,7 +469,7 @@ async def async_get_account(self, account_id): return None - async def async_get_saving_sessions(self, account_id: str): + async def async_get_saving_sessions(self, account_id: str) -> SavingSessionsResponse: """Get the user's seasons savings""" await self.async_refresh_token() @@ -475,14 +482,18 @@ async def async_get_saving_sessions(self, account_id: str): response_body = await self.__async_read_response__(account_response, url) if (response_body is not None and "data" in response_body): - return { - "events": list(map(lambda ev: { - "start": as_utc(parse_datetime(ev["startAt"])), - "end": as_utc(parse_datetime(ev["endAt"])) - }, response_body["data"]["savingSessions"]["account"]["joinedEvents"])) - } + return SavingSessionsResponse(list(map(lambda ev: SavingSession(ev["id"], + as_utc(parse_datetime(ev["startAt"])), + as_utc(parse_datetime(ev["endAt"])), + ev["rewardPerKwhInOctoPoints"]), + response_body["data"]["savingSessions"]["events"])), + list(map(lambda ev: SavingSession(ev["eventId"], + as_utc(parse_datetime(ev["startAt"])), + as_utc(parse_datetime(ev["endAt"])), + ev["rewardGivenInOctoPoints"]), + response_body["data"]["savingSessions"]["account"]["joinedEvents"]))) else: - _LOGGER.error("Failed to retrieve account") + _LOGGER.error("Failed to retrieve saving sessions") return None @@ -1020,7 +1031,7 @@ async def __async_read_response__(self, response, url): elif response.status not in [401, 403, 404]: msg = f'Failed to send request ({url}): {response.status}; {text}' _LOGGER.debug(msg) - raise RequestError(msg) + raise RequestError(msg, []) return None data_as_json = None diff --git a/custom_components/octopus_energy/api_client/saving_sessions.py b/custom_components/octopus_energy/api_client/saving_sessions.py index 0ebdd987..a60046e5 100644 --- a/custom_components/octopus_energy/api_client/saving_sessions.py +++ b/custom_components/octopus_energy/api_client/saving_sessions.py @@ -1,3 +1,5 @@ +import datetime + class JoinSavingSessionResponse: is_successful: bool errors: list[str] @@ -9,3 +11,35 @@ def __init__( ): self.is_successful = is_successful self.errors = errors + +class SavingSession: + id: str + start: datetime + end: datetime + octopoints: int + duration_in_minutes: int + + def __init__( + self, + id: str, + start: datetime, + end: datetime, + octopoints: int + ): + self.id = id + self.start = start + self.end = end + self.octopoints = octopoints + self.duration_in_minutes = (end - start).total_seconds() / 60 + +class SavingSessionsResponse: + upcoming_events: list[SavingSession] + joined_events: list[SavingSession] + + def __init__( + self, + upcoming_events: list[SavingSession], + joined_events: list[SavingSession] + ): + self.upcoming_events = upcoming_events + self.joined_events = joined_events diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py index e3cbb37f..8e05a01a 100644 --- a/custom_components/octopus_energy/binary_sensor.py +++ b/custom_components/octopus_energy/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.util.dt import (utcnow) from .electricity.off_peak import OctopusEnergyElectricityOffPeak -from .octoplus import OctopusEnergySavingSessions +from .octoplus.saving_sessions import OctopusEnergySavingSessions from .target_rates.target_rate import OctopusEnergyTargetRate from .intelligent.dispatching import OctopusEnergyIntelligentDispatching from .api_client import OctopusEnergyApiClient @@ -86,12 +86,13 @@ async def async_setup_main_sensors(hass, entry, async_add_entities): account_info = hass.data[DOMAIN][DATA_ACCOUNT] account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + client = hass.data[DOMAIN][DATA_CLIENT] now = utcnow() has_intelligent_tariff = False intelligent_mpan = None intelligent_serial_number = None - entities = [OctopusEnergySavingSessions(hass, saving_session_coordinator, account_id)] + entities = [OctopusEnergySavingSessions(hass, saving_session_coordinator, client, account_id)] if len(account_info["electricity_meter_points"]) > 0: for point in account_info["electricity_meter_points"]: diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index 82bce9d6..92be61a3 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -90,6 +90,9 @@ EVENT_GAS_PREVIOUS_CONSUMPTION_RATES = "octopus_energy_gas_previous_consumption_rates" EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES = "octopus_energy_gas_previous_consumption_override_rates" +EVENT_NEW_SAVING_SESSION = "octopus_energy_new_saving_session" +EVENT_ALL_SAVING_SESSIONS = "octopus_energy_all_saving_sessions" + # During BST, two records are returned before the rest of the data is available MINIMUM_CONSUMPTION_DATA_LENGTH = 3 diff --git a/custom_components/octopus_energy/coordinators/saving_sessions.py b/custom_components/octopus_energy/coordinators/saving_sessions.py index 44ca314b..dc129f95 100644 --- a/custom_components/octopus_energy/coordinators/saving_sessions.py +++ b/custom_components/octopus_energy/coordinators/saving_sessions.py @@ -1,5 +1,6 @@ import logging -from datetime import timedelta +from datetime import datetime, timedelta +from typing import Callable, Any from homeassistant.util.dt import (now) from homeassistant.helpers.update_coordinator import ( @@ -13,32 +14,116 @@ DATA_ACCOUNT_ID, DATA_SAVING_SESSIONS, DATA_SAVING_SESSIONS_COORDINATOR, + EVENT_ALL_SAVING_SESSIONS, + EVENT_NEW_SAVING_SESSION, ) from ..api_client import OctopusEnergyApiClient +from ..api_client.saving_sessions import SavingSession _LOGGER = logging.getLogger(__name__) +class SavingSessionsCoordinatorResult: + last_retrieved: datetime + nonjoined_events: list[SavingSession] + joined_events: list[SavingSession] + + def __init__(self, last_retrieved: datetime, nonjoined_events: list[SavingSession], joined_events: list[SavingSession]): + self.last_retrieved = last_retrieved + self.nonjoined_events = nonjoined_events + self.joined_events = joined_events + +def filter_nonjoined_events(current: datetime, upcoming_events: list[SavingSession], joined_events: list[SavingSession]) -> list[SavingSession]: + filtered_events = [] + for upcoming_event in upcoming_events: + + is_joined = False + for joined_event in joined_events: + if joined_event.id == upcoming_event.id: + is_joined = True + break + + if (upcoming_event.start >= current and is_joined == False): + filtered_events.append(upcoming_event) + + return filtered_events + +async def async_refresh_saving_sessions( + current: datetime, + client: OctopusEnergyApiClient, + account_id: str, + existing_saving_sessions_result: SavingSessionsCoordinatorResult, + fire_event: Callable[[str, "dict[str, Any]"], None], +) -> SavingSessionsCoordinatorResult: + if existing_saving_sessions_result is None or current.minute % 30 == 0: + try: + result = await client.async_get_saving_sessions(account_id) + nonjoined_events = filter_nonjoined_events(current, result.upcoming_events, result.joined_events) + + for nonjoined_event in nonjoined_events: + is_new = True + + if existing_saving_sessions_result is not None: + for existing_nonjoined_event in existing_saving_sessions_result.nonjoined_events: + if existing_nonjoined_event.id == nonjoined_event.id: + is_new = False + break + + if is_new: + fire_event(EVENT_NEW_SAVING_SESSION, { + "account_id": account_id, + "event_id": nonjoined_event.id, + "event_start": nonjoined_event.start, + "event_end": nonjoined_event.end, + "event_octopoints": nonjoined_event.octopoints + }) + + fire_event(EVENT_ALL_SAVING_SESSIONS, { + "account_id": account_id, + "nonjoined_events": list(map(lambda ev: { + "id": ev.id, + "start": ev.start, + "end": ev.end, + "octopoints": ev.octopoints + }, nonjoined_events)), + "joined_events": list(map(lambda ev: { + "id": ev.id, + "start": ev.start, + "end": ev.end, + "octopoints": ev.octopoints + }, result.joined_events)), + }) + + return SavingSessionsCoordinatorResult(current, nonjoined_events, result.joined_events) + except: + _LOGGER.debug('Failed to retrieve saving session information') + + return existing_saving_sessions_result + async def async_setup_saving_sessions_coordinators(hass): + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + async def async_update_saving_sessions(): """Fetch data from API endpoint.""" # Only get data every half hour or if we don't have any data current = now() client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] - if DATA_SAVING_SESSIONS not in hass.data[DOMAIN] or current.minute % 30 == 0: - - try: - savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) - hass.data[DOMAIN][DATA_SAVING_SESSIONS] = savings - except: - _LOGGER.debug('Failed to retrieve saving session information') - + + result = await async_refresh_saving_sessions( + current, + client, + account_id, + hass.data[DOMAIN][DATA_SAVING_SESSIONS] if DATA_SAVING_SESSIONS in hass.data[DOMAIN] else None, + hass.bus.async_fire + ) + + hass.data[DOMAIN][DATA_SAVING_SESSIONS] = result return hass.data[DOMAIN][DATA_SAVING_SESSIONS] hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] = DataUpdateCoordinator( hass, _LOGGER, - name="saving_sessions", + name=f"{account_id}_saving_sessions", update_method=async_update_saving_sessions, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes diff --git a/custom_components/octopus_energy/octoplus/__init__.py b/custom_components/octopus_energy/octoplus/__init__.py index 7ecf5681..7bc19727 100644 --- a/custom_components/octopus_energy/octoplus/__init__.py +++ b/custom_components/octopus_energy/octoplus/__init__.py @@ -1,28 +1,20 @@ -def current_saving_sessions_event(current_date, events): - current_event = None +import datetime +from ..api_client.saving_sessions import SavingSession +def current_saving_sessions_event(current_date: datetime, events: list[SavingSession]) -> SavingSession or None: if events is not None: for event in events: - if (event["start"] <= current_date and event["end"] >= current_date): - current_event = { - "start": event["start"], - "end": event["end"], - "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 - } - break + if (event.start <= current_date and event.end >= current_date): + return event - return current_event + return None -def get_next_saving_sessions_event(current_date, events): +def get_next_saving_sessions_event(current_date: datetime, events: list[SavingSession]) -> SavingSession or None: next_event = None if events is not None: for event in events: - if event["start"] > current_date and (next_event == None or event["start"] < next_event["start"]): - next_event = { - "start": event["start"], - "end": event["end"], - "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 - } + if event.start > current_date and (next_event == None or event.start < next_event.start): + next_event = event return next_event \ No newline at end of file diff --git a/custom_components/octopus_energy/octoplus/saving_sessions.py b/custom_components/octopus_energy/octoplus/saving_sessions.py index 26b901f2..609d8c45 100644 --- a/custom_components/octopus_energy/octoplus/saving_sessions.py +++ b/custom_components/octopus_energy/octoplus/saving_sessions.py @@ -18,6 +18,7 @@ ) from ..utils import account_id_to_unique_key from ..api_client import OctopusEnergyApiClient +from ..coordinators.saving_sessions import SavingSessionsCoordinatorResult _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,12 @@ def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiCli self._state = None self._events = [] self._attributes = { - "joined_events": [], - "next_joined_event_start": None + "current_joined_event_start": None, + "current_joined_event_end": None, + "current_joined_event_duration_in_minutes": None, + "next_joined_event_start": None, + "next_joined_event_end": None, + "next_joined_event_duration_in_minutes": None } self.entity_id = generate_entity_id("binary_sensor.{}", self.unique_id, hass=hass) @@ -63,14 +68,16 @@ def extra_state_attributes(self): @property def is_on(self): """Determine if the user is in a saving session.""" - saving_session = self.coordinator.data if self.coordinator is not None else None - if (saving_session is not None and "events" in saving_session): - self._events = saving_session["events"] + saving_session: SavingSessionsCoordinatorResult = self.coordinator.data if self.coordinator is not None else None + if (saving_session is not None): + self._events = saving_session.joined_events else: self._events = [] self._attributes = { - "joined_events": self._events, + "current_joined_event_start": None, + "current_joined_event_end": None, + "current_joined_event_duration_in_minutes": None, "next_joined_event_start": None, "next_joined_event_end": None, "next_joined_event_duration_in_minutes": None @@ -80,17 +87,17 @@ def is_on(self): current_event = current_saving_sessions_event(current_date, self._events) if (current_event is not None): self._state = True - self._attributes["current_joined_event_start"] = current_event["start"] - self._attributes["current_joined_event_end"] = current_event["end"] - self._attributes["current_joined_event_duration_in_minutes"] = current_event["duration_in_minutes"] + self._attributes["current_joined_event_start"] = current_event.start + self._attributes["current_joined_event_end"] = current_event.end + self._attributes["current_joined_event_duration_in_minutes"] = current_event.duration_in_minutes else: self._state = False next_event = get_next_saving_sessions_event(current_date, self._events) if (next_event is not None): - self._attributes["next_joined_event_start"] = next_event["start"] - self._attributes["next_joined_event_end"] = next_event["end"] - self._attributes["next_joined_event_duration_in_minutes"] = next_event["duration_in_minutes"] + self._attributes["next_joined_event_start"] = next_event.start + self._attributes["next_joined_event_end"] = next_event.end + self._attributes["next_joined_event_duration_in_minutes"] = next_event.duration_in_minutes return self._state diff --git a/tests/integration/api_client/test_get_saving_sessions.py b/tests/integration/api_client/test_get_saving_sessions.py index 39bdaba1..a4ea1721 100644 --- a/tests/integration/api_client/test_get_saving_sessions.py +++ b/tests/integration/api_client/test_get_saving_sessions.py @@ -12,12 +12,21 @@ async def test_when_get_saving_sessions_is_called_then_events_are_returned(): account_id = context["account_id"] # Act - savings = await client.async_get_saving_sessions(account_id) + result = await client.async_get_saving_sessions(account_id) # Assert - assert savings is not None + assert result is not None + + assert result.upcoming_events is not None + for event in result.upcoming_events: + assert event.id is not None + assert event.start is not None + assert event.end is not None + assert event.octopoints >= 0 - assert "events" in savings - for event in savings["events"]: - assert "start" in event - assert "end" in event \ No newline at end of file + assert result.joined_events is not None + for event in result.joined_events: + assert event.id is not None + assert event.start is not None + assert event.end is not None + assert event.octopoints >= 0 \ No newline at end of file diff --git a/tests/unit/coordinators/test_async_refresh_saving_sessions.py b/tests/unit/coordinators/test_async_refresh_saving_sessions.py new file mode 100644 index 00000000..ffd27248 --- /dev/null +++ b/tests/unit/coordinators/test_async_refresh_saving_sessions.py @@ -0,0 +1,310 @@ +from custom_components.octopus_energy.const import EVENT_ALL_SAVING_SESSIONS, EVENT_NEW_SAVING_SESSION +import pytest +import mock +from datetime import datetime, timedelta + +from custom_components.octopus_energy.coordinators.saving_sessions import SavingSessionsCoordinatorResult, async_refresh_saving_sessions +from custom_components.octopus_energy.api_client import OctopusEnergyApiClient +from custom_components.octopus_energy.api_client.saving_sessions import SavingSession, SavingSessionsResponse + +def assert_raised_new_saving_session_event( + raised_event: dict, + account_id: str, + expected_event: SavingSession +): + assert "account_id" in raised_event + assert raised_event["account_id"] == account_id + + assert "event_id" in raised_event + assert raised_event["event_id"] == expected_event.id + + assert "event_start" in raised_event + assert raised_event["event_start"] == expected_event.start + + assert "event_end" in raised_event + assert raised_event["event_end"] == expected_event.end + + assert "event_octopoints" in raised_event + assert raised_event["event_octopoints"] == expected_event.octopoints + +def assert_raised_all_saving_session_event( + raised_event: dict, + account_id: str, + nonjoined_events: list[SavingSession], + joined_events: list[SavingSession] +): + assert "account_id" in raised_event + assert raised_event["account_id"] == account_id + + assert "nonjoined_events" in raised_event + for idx, actual_event in enumerate(raised_event["nonjoined_events"]): + expected_event = nonjoined_events[idx] + + assert "id" in actual_event + assert actual_event["id"] == expected_event.id + + assert "start" in actual_event + assert actual_event["start"] == expected_event.start + + assert "end" in actual_event + assert actual_event["end"] == expected_event.end + + assert "octopoints" in actual_event + assert actual_event["octopoints"] == expected_event.octopoints + + for idx, actual_event in enumerate(raised_event["joined_events"]): + expected_event = joined_events[idx] + + assert "id" in actual_event + assert actual_event["id"] == expected_event.id + + assert "start" in actual_event + assert actual_event["start"] == expected_event.start + + assert "end" in actual_event + assert actual_event["end"] == expected_event.end + + assert "octopoints" in actual_event + assert actual_event["octopoints"] == expected_event.octopoints + +@pytest.mark.asyncio +async def test_when_now_is_not_at_30_minute_mark_and_previous_data_is_available_then_previous_data_returned(): + # Arrange + client = OctopusEnergyApiClient("NOT_REAL") + account_id = "ABC123" + previous_data=SavingSessionsCoordinatorResult(datetime.now(), [], []) + + for minute in range(0, 59): + if (minute == 0 or minute == 30): + continue + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + minuteStr = f'{minute}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minuteStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result == previous_data + + assert len(actual_fired_events) == 0 + +@pytest.mark.asyncio +@pytest.mark.parametrize("minutes",[ + (0), + (30), +]) +async def test_when_upcoming_events_contains_events_in_past_then_events_filtered_out(minutes): + # Arrange + account_id = "ABC123" + previous_data=None + + minutesStr = f'{minutes}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + expected_saving_session = SavingSession("1", current_utc_timestamp - timedelta(minutes=1), current_utc_timestamp + timedelta(minutes=31), 1) + async def async_mocked_get_saving_sessions(*args, **kwargs): + return SavingSessionsResponse([expected_saving_session], []) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_saving_sessions=async_mocked_get_saving_sessions): + client = OctopusEnergyApiClient("NOT_REAL") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result is not None + + assert len(actual_fired_events) == 1 + assert_raised_all_saving_session_event(actual_fired_events[EVENT_ALL_SAVING_SESSIONS], account_id, [], []) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minutes",[ + (0), + (30), +]) +async def test_when_upcoming_events_contains_joined_events_then_events_filtered_out(minutes): + # Arrange + account_id = "ABC123" + previous_data=None + + minutesStr = f'{minutes}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + expected_saving_session = SavingSession("1", current_utc_timestamp + timedelta(minutes=1), current_utc_timestamp + timedelta(minutes=31), 1) + async def async_mocked_get_saving_sessions(*args, **kwargs): + return SavingSessionsResponse([expected_saving_session], [expected_saving_session]) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_saving_sessions=async_mocked_get_saving_sessions): + client = OctopusEnergyApiClient("NOT_REAL") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result is not None + + assert len(actual_fired_events) == 1 + assert_raised_all_saving_session_event(actual_fired_events[EVENT_ALL_SAVING_SESSIONS], account_id, [], [expected_saving_session]) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minutes",[ + (0), + (30), +]) +async def test_when_upcoming_events_present_and_no_previous_data_then_new_event_fired(minutes): + # Arrange + account_id = "ABC123" + previous_data=None + + minutesStr = f'{minutes}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + expected_saving_session = SavingSession("1", current_utc_timestamp + timedelta(minutes=1), current_utc_timestamp + timedelta(minutes=31), 1) + async def async_mocked_get_saving_sessions(*args, **kwargs): + return SavingSessionsResponse([expected_saving_session], []) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_saving_sessions=async_mocked_get_saving_sessions): + client = OctopusEnergyApiClient("NOT_REAL") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result is not None + + assert len(actual_fired_events) == 2 + assert_raised_new_saving_session_event(actual_fired_events[EVENT_NEW_SAVING_SESSION], account_id, expected_saving_session) + assert_raised_all_saving_session_event(actual_fired_events[EVENT_ALL_SAVING_SESSIONS], account_id, [expected_saving_session], []) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minutes",[ + (0), + (30), +]) +async def test_when_upcoming_events_present_and_not_in_previous_data_then_new_event_fired(minutes): + # Arrange + account_id = "ABC123" + previous_data = SavingSessionsCoordinatorResult(datetime.now(), [], []) + + minutesStr = f'{minutes}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + expected_saving_session = SavingSession("1", current_utc_timestamp + timedelta(minutes=1), current_utc_timestamp + timedelta(minutes=31), 1) + async def async_mocked_get_saving_sessions(*args, **kwargs): + return SavingSessionsResponse([expected_saving_session], []) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_saving_sessions=async_mocked_get_saving_sessions): + client = OctopusEnergyApiClient("NOT_REAL") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result is not None + + assert len(actual_fired_events) == 2 + assert_raised_new_saving_session_event(actual_fired_events[EVENT_NEW_SAVING_SESSION], account_id, expected_saving_session) + assert_raised_all_saving_session_event(actual_fired_events[EVENT_ALL_SAVING_SESSIONS], account_id, [expected_saving_session], []) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minutes",[ + (0), + (30), +]) +async def test_when_upcoming_events_present_and_in_previous_data_then_new_event_not_fired(minutes): + # Arrange + account_id = "ABC123" + + minutesStr = f'{minutes}'.zfill(2) + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + expected_saving_session = SavingSession("1", current_utc_timestamp + timedelta(minutes=1), current_utc_timestamp + timedelta(minutes=31), 1) + async def async_mocked_get_saving_sessions(*args, **kwargs): + return SavingSessionsResponse([], [expected_saving_session]) + + previous_data = SavingSessionsCoordinatorResult(datetime.now(), [], [expected_saving_session]) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_saving_sessions=async_mocked_get_saving_sessions): + client = OctopusEnergyApiClient("NOT_REAL") + + # Act + result = await async_refresh_saving_sessions( + current_utc_timestamp, + client, + account_id, + previous_data, + fire_event + ) + + # Assert + assert result is not None + + assert len(actual_fired_events) == 1 + assert_raised_all_saving_session_event(actual_fired_events[EVENT_ALL_SAVING_SESSIONS], account_id, [], [expected_saving_session]) \ No newline at end of file diff --git a/tests/unit/octoplus/test_current_saving_sessions_event.py b/tests/unit/octoplus/test_current_saving_sessions_event.py index 2be8885f..e9cd128b 100644 --- a/tests/unit/octoplus/test_current_saving_sessions_event.py +++ b/tests/unit/octoplus/test_current_saving_sessions_event.py @@ -2,6 +2,7 @@ import pytest from custom_components.octopus_energy.octoplus import current_saving_sessions_event +from custom_components.octopus_energy.api_client.saving_sessions import SavingSession @pytest.mark.asyncio @pytest.mark.parametrize("current_date",[ @@ -10,18 +11,9 @@ ]) async def test_when_active_event_present_then_true_is_returned(current_date): events = [ - { - "start": datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - } + SavingSession("1", datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("2", datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("3", datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0) ] result = current_saving_sessions_event( @@ -30,9 +22,8 @@ async def test_when_active_event_present_then_true_is_returned(current_date): ) assert result is not None - assert result["start"] == events[1]["start"] - assert result["end"] == events[1]["end"] - assert result["duration_in_minutes"] == 60 + assert result == events[1] + assert result.duration_in_minutes == 60 @pytest.mark.asyncio @pytest.mark.parametrize("current_date",[ @@ -41,10 +32,7 @@ async def test_when_active_event_present_then_true_is_returned(current_date): ]) async def test_when_no_active_event_present_then_false_is_returned(current_date): events = [ - { - "start": datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - } + SavingSession("1", datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), ] result = current_saving_sessions_event( diff --git a/tests/unit/octoplus/test_get_next_saving_sessions_event.py b/tests/unit/octoplus/test_get_next_saving_sessions_event.py index cf6c5540..32df0773 100644 --- a/tests/unit/octoplus/test_get_next_saving_sessions_event.py +++ b/tests/unit/octoplus/test_get_next_saving_sessions_event.py @@ -2,22 +2,14 @@ import pytest from custom_components.octopus_energy.octoplus import get_next_saving_sessions_event +from custom_components.octopus_energy.api_client.saving_sessions import SavingSession @pytest.mark.asyncio async def test_when_future_events_present_then_next_event_returned(): events = [ - { - "start": datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - } + SavingSession("1", datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("2", datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("3", datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0) ] current_date = datetime.strptime("2022-12-04T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -27,25 +19,15 @@ async def test_when_future_events_present_then_next_event_returned(): events, ) - assert result["start"] == events[1]["start"] - assert result["end"] == events[1]["end"] - assert result["duration_in_minutes"] == 60 + assert result == events[1] + assert result.duration_in_minutes == 60 @pytest.mark.asyncio async def test_when_no_future_events_present_then_none_returned(): events = [ - { - "start": datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - }, - { - "start": datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - } + SavingSession("1", datetime.strptime("2022-12-06T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-06T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("2", datetime.strptime("2022-12-05T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-05T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0), + SavingSession("3", datetime.strptime("2022-12-07T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), datetime.strptime("2022-12-07T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 0) ] current_date = datetime.strptime("2022-12-08T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z")