Skip to content

Commit

Permalink
feat: created new event entity to capture available and joined octopl…
Browse files Browse the repository at this point in the history
…us saving session events

BREAKING CHANGE:
The joined events attribute has been removed from the saving session binary sensor in favour of this
  • Loading branch information
BottlecapDave committed Nov 10, 2023
1 parent 17c1dff commit b402c06
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 52 deletions.
3 changes: 3 additions & 0 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
savingSessions {{
events {{
id
code
rewardPerKwhInOctoPoints
startAt
endAt
Expand Down Expand Up @@ -483,11 +484,13 @@ async def async_get_saving_sessions(self, account_id: str) -> SavingSessionsResp

if (response_body is not None and "data" in response_body):
return SavingSessionsResponse(list(map(lambda ev: SavingSession(ev["id"],
ev["code"],
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"],
None,
as_utc(parse_datetime(ev["startAt"])),
as_utc(parse_datetime(ev["endAt"])),
ev["rewardGivenInOctoPoints"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __init__(

class SavingSession:
id: str
code: str
start: datetime
end: datetime
octopoints: int
Expand All @@ -22,24 +23,26 @@ class SavingSession:
def __init__(
self,
id: str,
code: str,
start: datetime,
end: datetime,
octopoints: int
):
self.id = id
self.code = code
self.start = start
self.end = end
self.octopoints = octopoints
self.duration_in_minutes = (end - start).total_seconds() / 60

class SavingSessionsResponse:
upcoming_events: list[SavingSession]
available_events: list[SavingSession]
joined_events: list[SavingSession]

def __init__(
self,
upcoming_events: list[SavingSession],
available_events: list[SavingSession],
joined_events: list[SavingSession]
):
self.upcoming_events = upcoming_events
self.available_events = available_events
self.joined_events = joined_events
4 changes: 2 additions & 2 deletions custom_components/octopus_energy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
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"
EVENT_NEW_SAVING_SESSION = "octopus_energy_new_octoplus_saving_session"
EVENT_ALL_SAVING_SESSIONS = "octopus_energy_all_octoplus_saving_sessions"

# During BST, two records are returned before the rest of the data is available
MINIMUM_CONSUMPTION_DATA_LENGTH = 3
Expand Down
39 changes: 20 additions & 19 deletions custom_components/octopus_energy/coordinators/saving_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,17 @@

class SavingSessionsCoordinatorResult:
last_retrieved: datetime
nonjoined_events: list[SavingSession]
available_events: list[SavingSession]
joined_events: list[SavingSession]

def __init__(self, last_retrieved: datetime, nonjoined_events: list[SavingSession], joined_events: list[SavingSession]):
def __init__(self, last_retrieved: datetime, available_events: list[SavingSession], joined_events: list[SavingSession]):
self.last_retrieved = last_retrieved
self.nonjoined_events = nonjoined_events
self.available_events = available_events
self.joined_events = joined_events

def filter_nonjoined_events(current: datetime, upcoming_events: list[SavingSession], joined_events: list[SavingSession]) -> list[SavingSession]:
def filter_available_events(current: datetime, available_events: list[SavingSession], joined_events: list[SavingSession]) -> list[SavingSession]:
filtered_events = []
for upcoming_event in upcoming_events:

for upcoming_event in available_events:
is_joined = False
for joined_event in joined_events:
if joined_event.id == upcoming_event.id:
Expand All @@ -58,43 +57,45 @@ async def async_refresh_saving_sessions(
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)
available_events = filter_available_events(current, result.available_events, result.joined_events)

for nonjoined_event in nonjoined_events:
for available_event in available_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:
for existing_available_event in existing_saving_sessions_result.available_events:
if existing_available_event.id == available_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
"event_code": available_event.code,
"event_id": available_event.id,
"event_start": available_event.start,
"event_end": available_event.end,
"event_octopoints_per_kwh": available_event.octopoints
})

fire_event(EVENT_ALL_SAVING_SESSIONS, {
"account_id": account_id,
"nonjoined_events": list(map(lambda ev: {
"available_events": list(map(lambda ev: {
"id": ev.id,
"code": ev.code,
"start": ev.start,
"end": ev.end,
"octopoints": ev.octopoints
}, nonjoined_events)),
"octopoints_per_kwh": ev.octopoints
}, available_events)),
"joined_events": list(map(lambda ev: {
"id": ev.id,
"start": ev.start,
"end": ev.end,
"octopoints": ev.octopoints
"rewarded_octopoints": ev.octopoints
}, result.joined_events)),
})

return SavingSessionsCoordinatorResult(current, nonjoined_events, result.joined_events)
return SavingSessionsCoordinatorResult(current, available_events, result.joined_events)
except:
_LOGGER.debug('Failed to retrieve saving session information')

Expand Down
3 changes: 2 additions & 1 deletion custom_components/octopus_energy/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .gas.rates_previous_day import OctopusEnergyGasPreviousDayRates
from .gas.rates_previous_consumption import OctopusEnergyGasPreviousConsumptionRates
from .gas.rates_previous_consumption_override import OctopusEnergyGasPreviousConsumptionOverrideRates
from .octoplus.saving_sessions_events import OctopusEnergyOctoplusSavingSessionEvents

from .const import (
DOMAIN,
Expand Down Expand Up @@ -41,7 +42,7 @@ async def async_setup_main_sensors(hass, entry, async_add_entities):
account_info = hass.data[DOMAIN][DATA_ACCOUNT]

now = utcnow()
entities = []
entities = [OctopusEnergyOctoplusSavingSessionEvents(hass, account_info["id"])]
if len(account_info["electricity_meter_points"]) > 0:
for point in account_info["electricity_meter_points"]:
# We only care about points that have active agreements
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging

from homeassistant.core import HomeAssistant, callback

from homeassistant.components.event import (
EventEntity,
)
from homeassistant.helpers.restore_state import RestoreEntity

from ..const import EVENT_ALL_SAVING_SESSIONS
from ..utils import account_id_to_unique_key

_LOGGER = logging.getLogger(__name__)

class OctopusEnergyOctoplusSavingSessionEvents(EventEntity, RestoreEntity):
"""Sensor for displaying the upcoming saving sessions."""

def __init__(self, hass: HomeAssistant, account_id: str):
"""Init sensor."""

self._account_id = account_id
self._hass = hass
self._state = None
self._last_updated = None

self._attr_event_types = [EVENT_ALL_SAVING_SESSIONS]

@property
def unique_id(self):
"""The id of the sensor."""
return f"octopus_energy_{account_id_to_unique_key(self._account_id)}_octoplus_saving_session_events"

@property
def name(self):
"""Name of the sensor."""
return f"Octopus Energy {self._account_id} Octoplus Saving Session Events"

async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
# If not None, we got an initial value.
await super().async_added_to_hass()
state = await self.async_get_last_state()

if state is not None and self._state is None:
self._state = state.state
self._attributes = {}
for x in state.attributes.keys():
self._attributes[x] = state.attributes[x]

_LOGGER.debug(f'Restored OctopusEnergyOctoplusSavingSessionEvents state: {self._state}')

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event)

@callback
def _async_handle_event(self, event) -> None:
if (event.data is not None and "account_id" in event.data and event.data["account_id"] == self._account_id):
self._trigger_event(event.event_type, event.data)
self.async_write_ha_state()
6 changes: 4 additions & 2 deletions tests/integration/api_client/test_get_saving_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ async def test_when_get_saving_sessions_is_called_then_events_are_returned():
# Assert
assert result is not None

assert result.upcoming_events is not None
for event in result.upcoming_events:
assert result.available_events is not None
for event in result.available_events:
assert event.id is not None
assert event.code is not None
assert event.start is not None
assert event.end is not None
assert event.octopoints >= 0

assert result.joined_events is not None
for event in result.joined_events:
assert event.id is not None
assert event.code is None
assert event.start is not None
assert event.end is not None
assert event.octopoints >= 0
36 changes: 21 additions & 15 deletions tests/unit/coordinators/test_async_refresh_saving_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,45 @@ def assert_raised_new_saving_session_event(
assert "event_id" in raised_event
assert raised_event["event_id"] == expected_event.id

assert "event_code" in raised_event
assert raised_event["event_code"] == expected_event.code

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
assert "event_octopoints_per_kwh" in raised_event
assert raised_event["event_octopoints_per_kwh"] == expected_event.octopoints

def assert_raised_all_saving_session_event(
raised_event: dict,
account_id: str,
nonjoined_events: list[SavingSession],
available_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 "available_events" in raised_event
for idx, actual_event in enumerate(raised_event["available_events"]):
expected_event = available_events[idx]

assert "id" in actual_event
assert actual_event["id"] == expected_event.id

assert "code" in actual_event
assert actual_event["code"] == expected_event.code

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
assert "octopoints_per_kwh" in actual_event
assert actual_event["octopoints_per_kwh"] == expected_event.octopoints

for idx, actual_event in enumerate(raised_event["joined_events"]):
expected_event = joined_events[idx]
Expand All @@ -64,8 +70,8 @@ def assert_raised_all_saving_session_event(
assert "end" in actual_event
assert actual_event["end"] == expected_event.end

assert "octopoints" in actual_event
assert actual_event["octopoints"] == expected_event.octopoints
assert "rewarded_octopoints" in actual_event
assert actual_event["rewarded_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():
Expand Down Expand Up @@ -120,7 +126,7 @@ def fire_event(name, metadata):
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)
expected_saving_session = SavingSession("1", "ABC", 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], [])

Expand Down Expand Up @@ -161,7 +167,7 @@ def fire_event(name, metadata):
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)
expected_saving_session = SavingSession("1", "ABC", 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])

Expand Down Expand Up @@ -202,7 +208,7 @@ def fire_event(name, metadata):
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)
expected_saving_session = SavingSession("1", "ABC", 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], [])

Expand Down Expand Up @@ -244,7 +250,7 @@ def fire_event(name, metadata):
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)
expected_saving_session = SavingSession("1", "ABC", 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], [])

Expand Down Expand Up @@ -285,7 +291,7 @@ def fire_event(name, metadata):
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)
expected_saving_session = SavingSession("1", "ABC", 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])

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/octoplus/test_current_saving_sessions_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
])
async def test_when_active_event_present_then_true_is_returned(current_date):
events = [
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)
SavingSession("1", "ABC", 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", "ABC", 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", "ABC", 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(
Expand All @@ -32,7 +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 = [
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("1", "ABC", 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(
Expand Down

0 comments on commit b402c06

Please sign in to comment.