diff --git a/_docs/entities/intelligent.md b/_docs/entities/intelligent.md index 68dc2a88..e67e9952 100644 --- a/_docs/entities/intelligent.md +++ b/_docs/entities/intelligent.md @@ -52,6 +52,27 @@ Each item in `planned_dispatch` or `completed_dispatches` have the following att You can use the [data_last_retrieved sensor](./diagnostics.md#intelligent-dispatches-data-last-retrieved) to determine when the underlying data was last retrieved from the OE servers. +### Current State + +`sensor.octopus_energy_{{ACCOUNT_ID}}_intelligent_state` + +This sensor displays the current state of your intelligent provider as told by the OE API. The value of this sensor can be one of the following + +* `AUTHENTICATION_PENDING` - ready to start authentication and authorization, or auth is in progress. +* `AUTHENTICATION_FAILED` - failed to connect and ready to restart authentication and authorization. +* `AUTHENTICATION_COMPLETE`- ready to start test (if needed) or pending live where auth or telemetry is delayed. +* `TEST_CHARGE_IN_PROGRESS` - connection and smart control test has successfully started and is occurring. +* `TEST_CHARGE_FAILED` - connection or smart control test has failed or could not start, ready to retry test. +* `TEST_CHARGE_NOT_AVAILABLE` - not currently capable of smart control test (e.g. away from home or unplugged). +* `SETUP_COMPLETE` - test is complete (if needed) and device is live, but not ready for smart control. +* `SMART_CONTROL_CAPABLE` - live and ready for smart control (e.g. at home and plugged in) but none is scheduled. +* `SMART_CONTROL_IN_PROGRESS` - smart control (e.g. smart charging) is scheduled or is currently occurring. +* `BOOSTING` - user has overridden the schedule to immediately boost (e.g. bump charge now). +* `SMART_CONTROL_OFF` - smart control has been (temporarily) disabled (e.g. by the user with holiday mode). +* `SMART_CONTROL_NOT_AVAILABLE` - not currently capable of smart control (e.g. away from home or unplugged). +* `LOST_CONNECTION` - lost connection to the device, ready to re-auth (if not temporary / automatic fix). +* `RETIRED` - / de-authed (re-auth not possible, re-register device to onboard again). + ### Bump Charge `switch.octopus_energy_{{ACCOUNT_ID}}_intelligent_bump_charge` diff --git a/custom_components/octopus_energy/api_client/__init__.py b/custom_components/octopus_energy/api_client/__init__.py index e7009d44..3c01af56 100644 --- a/custom_components/octopus_energy/api_client/__init__.py +++ b/custom_components/octopus_energy/api_client/__init__.py @@ -115,6 +115,12 @@ }}''' intelligent_dispatches_query = '''query {{ + devices(accountNumber: "{account_id}", deviceId: "{device_id}") {{ + id + status {{ + currentState + }} + }} plannedDispatches(accountNumber: "{account_id}") {{ start end @@ -1258,7 +1264,7 @@ async def async_get_gas_standing_charge(self, product_code: str, tariff_code: st _LOGGER.warning(f'Failed to connect. Timeout of {self._timeout} exceeded.') raise TimeoutException() - async def async_get_intelligent_dispatches(self, account_id: str): + async def async_get_intelligent_dispatches(self, account_id: str, device_id: str): """Get the user's intelligent dispatches""" await self.async_refresh_token() @@ -1266,14 +1272,21 @@ async def async_get_intelligent_dispatches(self, account_id: str): client = self._create_client_session() url = f'{self._base_url}/v1/graphql/' # Get account response - payload = { "query": intelligent_dispatches_query.format(account_id=account_id) } + payload = { "query": intelligent_dispatches_query.format(account_id=account_id, device_id=device_id) } headers = { "Authorization": f"JWT {self._graphql_token}" } async with client.post(url, json=payload, headers=headers) as response: response_body = await self.__async_read_response__(response, url) _LOGGER.debug(f'async_get_intelligent_dispatches: {response_body}') + current_state = None + if (response_body is not None and "data" in response_body and "devices" in response_body["data"]): + for device in response_body["data"]["devices"]: + if device["id"] == device_id: + current_state = device["status"]["currentState"] + if (response_body is not None and "data" in response_body): return IntelligentDispatches( + current_state, list(map(lambda ev: IntelligentDispatchItem( as_utc(parse_datetime(ev["start"])), as_utc(parse_datetime(ev["end"])), diff --git a/custom_components/octopus_energy/api_client/intelligent_dispatches.py b/custom_components/octopus_energy/api_client/intelligent_dispatches.py index 36fd20df..8effa98f 100644 --- a/custom_components/octopus_energy/api_client/intelligent_dispatches.py +++ b/custom_components/octopus_energy/api_client/intelligent_dispatches.py @@ -22,13 +22,16 @@ def __init__( self.location = location class IntelligentDispatches: + current_state: str | None planned: list[IntelligentDispatchItem] completed: list[IntelligentDispatchItem] def __init__( self, + current_state: str | None, planned: list[IntelligentDispatchItem], completed: list[IntelligentDispatchItem] ): + self.current_state = current_state self.planned = planned self.completed = completed \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/intelligent_dispatches.py b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py index 4017f535..e4084a19 100644 --- a/custom_components/octopus_energy/coordinators/intelligent_dispatches.py +++ b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py @@ -72,7 +72,7 @@ async def async_refresh_intelligent_dispatches( raised_exception = None if has_intelligent_tariff(current, account_info) and intelligent_device is not None: try: - dispatches = await client.async_get_intelligent_dispatches(account_id) + dispatches = await client.async_get_intelligent_dispatches(account_id, intelligent_device.id) _LOGGER.debug(f'Intelligent dispatches retrieved for account {account_id}') except Exception as e: if isinstance(e, ApiException) == False: diff --git a/custom_components/octopus_energy/intelligent/__init__.py b/custom_components/octopus_energy/intelligent/__init__.py index 95f55a90..f84174e3 100644 --- a/custom_components/octopus_energy/intelligent/__init__.py +++ b/custom_components/octopus_energy/intelligent/__init__.py @@ -21,6 +21,7 @@ def mock_intelligent_dispatches() -> IntelligentDispatches: planned: list[IntelligentDispatchItem] = [] completed: list[IntelligentDispatchItem] = [] + current_state = "SMART_CONTROL_CAPABLE" dispatches = [ IntelligentDispatchItem( @@ -71,12 +72,18 @@ def mock_intelligent_dispatches() -> IntelligentDispatches: ) for dispatch in dispatches: + if utcnow() >= dispatch.start and utcnow() <= dispatch.end: + if dispatch.source == INTELLIGENT_SOURCE_SMART_CHARGE: + current_state = "SMART_CONTROL_IN_PROGRESS" + elif dispatch.source == INTELLIGENT_SOURCE_BUMP_CHARGE: + current_state = "BOOSTING" + if (dispatch.end > utcnow()): planned.append(dispatch) else: completed.append(dispatch) - return IntelligentDispatches(planned, completed) + return IntelligentDispatches(current_state, planned, completed) def mock_intelligent_settings(): return IntelligentSettings( @@ -206,13 +213,15 @@ def __init__(self, charge_limit_supported: bool, planned_dispatches_supported: bool, ready_time_supported: bool, - smart_charge_supported: bool): + smart_charge_supported: bool, + current_state_supported: bool): self.is_default_features = is_default_features self.bump_charge_supported = bump_charge_supported self.charge_limit_supported = charge_limit_supported self.planned_dispatches_supported = planned_dispatches_supported self.ready_time_supported = ready_time_supported self.smart_charge_supported = smart_charge_supported + self.current_state_supported = current_state_supported FULLY_SUPPORTED_INTELLIGENT_PROVIDERS = [ "DAIKIN", @@ -237,8 +246,8 @@ def __init__(self, def get_intelligent_features(provider: str) -> IntelligentFeatures: normalised_provider = provider.upper() if provider is not None else None if normalised_provider is not None and normalised_provider in FULLY_SUPPORTED_INTELLIGENT_PROVIDERS: - return IntelligentFeatures(False, True, True, True, True, True) + return IntelligentFeatures(False, True, True, True, True, True, True) elif normalised_provider == "OHME": - return IntelligentFeatures(False, False, False, False, False, False) + return IntelligentFeatures(False, False, False, False, False, False, False) - return IntelligentFeatures(True, False, False, False, False, False) \ No newline at end of file + return IntelligentFeatures(True, False, False, False, False, False, False) \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/current_state.py b/custom_components/octopus_energy/intelligent/current_state.py new file mode 100644 index 00000000..8913b77e --- /dev/null +++ b/custom_components/octopus_energy/intelligent/current_state.py @@ -0,0 +1,88 @@ +import logging + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + RestoreSensor, +) + +from .base import OctopusEnergyIntelligentSensor +from ..coordinators.intelligent_dispatches import IntelligentDispatchesCoordinatorResult +from ..utils.attributes import dict_to_typed_dict +from ..api_client.intelligent_device import IntelligentDevice + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentCurrentState(CoordinatorEntity, OctopusEnergyIntelligentSensor, RestoreSensor): + """Sensor for determining the current intelligent state.""" + + def __init__(self, hass: HomeAssistant, coordinator, device: IntelligentDevice, account_id: str): + """Init sensor.""" + + CoordinatorEntity.__init__(self, coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._account_id = account_id + self._state = None + + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_{self._account_id}_intelligent_state" + + @property + def name(self): + """Name of the sensor.""" + return f"Intelligent State ({self._account_id})" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:power-plug-battery" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def native_value(self): + return self._state + + @callback + def _handle_coordinator_update(self) -> None: + """Retrieve the current rate for the sensor.""" + # Find the current rate. We only need to do this every half an hour + current = now() + result: IntelligentDispatchesCoordinatorResult = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + if (result is not None and result.dispatches is not None): + _LOGGER.debug(f"Updating OctopusEnergyIntelligentCurrentState for '{self._device.id}'") + + self._state = result.dispatches.current_state + else: + self._state = None + + self._attributes = dict_to_typed_dict(self._attributes) + super()._handle_coordinator_update() + + 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 = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state + self._attributes = dict_to_typed_dict(state.attributes, []) + _LOGGER.debug(f'Restored OctopusEnergyIntelligentCurrentState state: {self._state}') diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index 0f516371..5c6e476d 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,5 +1,4 @@ -from datetime import datetime, timedelta -from custom_components.octopus_energy.api_client.intelligent_device import IntelligentDevice +from datetime import timedelta import voluptuous as vol import logging @@ -57,6 +56,9 @@ from .diagnostics_entities.intelligent_dispatches_data_last_retrieved import OctopusEnergyIntelligentDispatchesDataLastRetrieved from .diagnostics_entities.intelligent_settings_data_last_retrieved import OctopusEnergyIntelligentSettingsDataLastRetrieved from .diagnostics_entities.free_electricity_sessions_data_last_retrieved import OctopusEnergyFreeElectricitySessionsDataLastRetrieved +from .api_client.intelligent_device import IntelligentDevice +from .intelligent.current_state import OctopusEnergyIntelligentCurrentState +from .intelligent import get_intelligent_features from .utils.debug_overrides import async_get_debug_override @@ -302,6 +304,10 @@ async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_ent intelligent_dispatches_coordinator = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] if DATA_INTELLIGENT_DISPATCHES_COORDINATOR in hass.data[DOMAIN][account_id] else None if intelligent_dispatches_coordinator is not None: entities.append(OctopusEnergyIntelligentDispatchesDataLastRetrieved(hass, intelligent_dispatches_coordinator, account_id)) + + intelligent_features = get_intelligent_features(intelligent_device.provider) + if intelligent_features.current_state_supported: + entities.append(OctopusEnergyIntelligentCurrentState(hass, intelligent_dispatches_coordinator, intelligent_device, account_id)) intelligent_settings_coordinator = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_SETTINGS_COORDINATOR] if DATA_INTELLIGENT_SETTINGS_COORDINATOR in hass.data[DOMAIN][account_id] else None if intelligent_settings_coordinator is not None: diff --git a/tests/integration/api_client/test_get_intelligent_dispatches.py b/tests/integration/api_client/test_get_intelligent_dispatches.py index da730fef..8cb566c2 100644 --- a/tests/integration/api_client/test_get_intelligent_dispatches.py +++ b/tests/integration/api_client/test_get_intelligent_dispatches.py @@ -4,7 +4,7 @@ from custom_components.octopus_energy.api_client import OctopusEnergyApiClient @pytest.mark.asyncio -async def test_when_get_intelligent_dispatches_is_called_for_account_on_different_tariff_then_results_are_returned(): +async def test_when_get_intelligent_dispatches_is_called_for_account_on_different_tariff_then_exception_is_raised(): # Arrange context = get_test_context() @@ -14,7 +14,7 @@ async def test_when_get_intelligent_dispatches_is_called_for_account_on_differen # Act exception_raised = False try: - await client.async_get_intelligent_dispatches(account_id) + await client.async_get_intelligent_dispatches(account_id, "123") except: exception_raised = True diff --git a/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py b/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py index e477f18b..110132d1 100644 --- a/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py +++ b/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py @@ -103,7 +103,7 @@ def fire_event(name, metadata): account_info = None existing_rates = ElectricityRatesCoordinatorResult(period_from, 1, create_rate_data(period_from, period_to, [2, 4])) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -143,7 +143,7 @@ def fire_event(name, metadata): account_info = get_account_info(False) existing_rates = ElectricityRatesCoordinatorResult(period_from, 1, create_rate_data(period_from, period_to, [2, 4])) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -184,7 +184,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(current - timedelta(minutes=4, seconds=59), 1, create_rate_data(period_from, period_to, [2, 4])) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -243,7 +243,7 @@ def fire_event(name, metadata): account_info = get_account_info() expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, expected_rates_unsorted) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -280,8 +280,8 @@ def fire_event(name, metadata): @pytest.mark.parametrize("dispatches_result",[ (None), (IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, None)), - (IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches(None, []))), - (IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], None))), + (IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", None, []))), + (IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], None))), ]) async def test_when_dispatches_is_not_defined_and_existing_rates_is_none_then_rates_retrieved(dispatches_result): expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) @@ -366,7 +366,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=60), 1, create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4])) expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, expected_rates) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -417,7 +417,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=1), 1, create_rate_data(expected_period_from, expected_period_to, [2, 4], default_tariff_code)) expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, existing_rates.rates) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -474,7 +474,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=1), 1, create_rate_data(expected_period_from, expected_period_to, [2, 4], f"{default_tariff_code}-diff")) expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, existing_rates.rates) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -534,7 +534,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=60), 1, create_rate_data(expected_period_from, expected_rates[0]["start"], [1], default_tariff_code)) expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, expected_rates) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -614,7 +614,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=60), 1, create_rate_data(expected_period_from, expected_rates[0]["start"], [1], f"{default_tariff_code}-new")) expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, expected_rates) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -677,6 +677,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -764,6 +765,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -839,7 +841,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_from, 1, create_rate_data(period_from, period_to, [1, 2, 3, 4])) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -886,7 +888,7 @@ def fire_event(name, metadata): account_info = get_account_info() existing_rates = ElectricityRatesCoordinatorResult(period_from, 1, create_rate_data(period_from, period_to, [1, 2, 3, 4])) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -939,6 +941,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(existing_rates.last_evaluated + timedelta(seconds=1), 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -1024,6 +1027,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(existing_rates.last_evaluated + timedelta(seconds=1), 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -1078,6 +1082,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(existing_rates.last_evaluated - timedelta(seconds=1), 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -1184,6 +1189,7 @@ def fire_event(name, metadata): expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) dispatches_result = IntelligentDispatchesCoordinatorResult(existing_rates.last_evaluated + timedelta(seconds=1), 1, IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [ IntelligentDispatchItem( expected_dispatch_start, @@ -1361,7 +1367,7 @@ def fire_event(name, metadata): account_info = get_account_info() expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, 1, expected_rates_unsorted) - dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches([], [])) + dispatches_result = IntelligentDispatchesCoordinatorResult(dispatches_last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") diff --git a/tests/unit/coordinators/test_async_refresh_intelligent_dispatches.py b/tests/unit/coordinators/test_async_refresh_intelligent_dispatches.py index 0c030102..3e7dec56 100644 --- a/tests/unit/coordinators/test_async_refresh_intelligent_dispatches.py +++ b/tests/unit/coordinators/test_async_refresh_intelligent_dispatches.py @@ -50,7 +50,7 @@ def get_account_info(is_active_agreement = True, active_product_code = product_c @pytest.mark.asyncio async def test_when_account_info_is_none_then_existing_settings_returned(): - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called @@ -81,7 +81,7 @@ async def async_merge_dispatch_data(*args, **kwargs): @pytest.mark.asyncio async def test_when_intelligent_device_is_none_then_none_returned(): - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called @@ -113,7 +113,7 @@ async def async_merge_dispatch_data(*args, **kwargs): @pytest.mark.asyncio async def test_when_not_on_intelligent_tariff_then_none_returned(): - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called @@ -194,7 +194,7 @@ async def async_merge_dispatch_data(*args, **kwargs): @pytest.mark.asyncio async def test_when_next_refresh_is_in_the_future_then_existing_dispatches_returned(): current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z") - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called @@ -230,7 +230,7 @@ async def async_merge_dispatch_data(*args, **kwargs): (IntelligentDispatchesCoordinatorResult(last_retrieved, 1, None)), ]) async def test_when_existing_settings_is_none_then_settings_retrieved(existing_settings): - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called, expected_dispatches @@ -263,7 +263,7 @@ async def async_merge_dispatch_data(*args, **kwargs): @pytest.mark.asyncio async def test_when_existing_settings_is_old_then_settings_retrieved(): - expected_dispatches = IntelligentDispatches([], []) + expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], []) mock_api_called = False async def async_mock_get_intelligent_dispatches(*args, **kwargs): nonlocal mock_api_called @@ -309,7 +309,7 @@ async def async_merge_dispatch_data(*args, **kwargs): return completed_dispatches account_info = get_account_info() - existing_settings = IntelligentDispatchesCoordinatorResult(last_retrieved, 1, IntelligentDispatches([], [])) + existing_settings = IntelligentDispatchesCoordinatorResult(last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_intelligent_dispatches=async_mock_get_intelligent_dispatches): client = OctopusEnergyApiClient("NOT_REAL") @@ -345,7 +345,7 @@ async def async_merge_dispatch_data(*args, **kwargs): return completed_dispatches account_info = get_account_info() - existing_settings = IntelligentDispatchesCoordinatorResult(last_retrieved, 1, IntelligentDispatches([], [])) + existing_settings = IntelligentDispatchesCoordinatorResult(last_retrieved, 1, IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_intelligent_dispatches=async_mock_get_intelligent_dispatches): client = OctopusEnergyApiClient("NOT_REAL") diff --git a/tests/unit/coordinators/test_previous_consumption_and_rates.py b/tests/unit/coordinators/test_previous_consumption_and_rates.py index 1e2259c2..79c2df56 100644 --- a/tests/unit/coordinators/test_previous_consumption_and_rates.py +++ b/tests/unit/coordinators/test_previous_consumption_and_rates.py @@ -889,6 +889,7 @@ def fire_event(name, metadata): ) intelligent_dispatches = IntelligentDispatches( + "SMART_CONTROL_IN_PROGRESS", [], [ IntelligentDispatchItem( diff --git a/tests/unit/intelligent/test_get_intelligent_features.py b/tests/unit/intelligent/test_get_intelligent_features.py index 6b2d5f4e..b6cd5587 100644 --- a/tests/unit/intelligent/test_get_intelligent_features.py +++ b/tests/unit/intelligent/test_get_intelligent_features.py @@ -3,47 +3,47 @@ from custom_components.octopus_energy.intelligent import get_intelligent_features @pytest.mark.asyncio -@pytest.mark.parametrize("provider,expected_bump_charge_supported,expected_charge_limit_supported,expected_planned_dispatches_supported,expected_ready_time_supported,expected_smart_charge_supported,expected_default_features",[ - ("DAIKIN", True, True, True, True, True, False), - ("ECOBEE", True, True, True, True, True, False), - ("ENERGIZER", True, True, True, True, True, False), - ("ENPHASE", True, True, True, True, True, False), - ("ENODE", True, True, True, True, True, False), - ("FORD", True, True, True, True, True, False), - ("GIVENERGY", True, True, True, True, True, False), - ("HUAWEI", True, True, True, True, True, False), - ("JEDLIX", True, True, True, True, True, False), - ("MYENERGI", True, True, True, True, True, False), - ("OCPP_WALLBOX", True, True, True, True, True, False), - ("SENSI", True, True, True, True, True, False), - ("SMARTCAR", True, True, True, True, True, False), - ("TESLA", True, True, True, True, True, False), - ("SMART_PEAR", True, True, True, True, True, False), - ("HYPERVOLT", True, True, True, True, True, False), - ("INDRA", True, True, True, True, True, False), - ("OHME", False, False, False, False, False, False), - ("DAIKIN".lower(), True, True, True, True, True, False), - ("ECOBEE".lower(), True, True, True, True, True, False), - ("ENERGIZER".lower(), True, True, True, True, True, False), - ("ENPHASE".lower(), True, True, True, True, True, False), - ("ENODE".lower(), True, True, True, True, True, False), - ("FORD".lower(), True, True, True, True, True, False), - ("GIVENERGY".lower(), True, True, True, True, True, False), - ("HUAWEI".lower(), True, True, True, True, True, False), - ("JEDLIX".lower(), True, True, True, True, True, False), - ("MYENERGI".lower(), True, True, True, True, True, False), - ("OCPP_WALLBOX".lower(), True, True, True, True, True, False), - ("SENSI".lower(), True, True, True, True, True, False), - ("SMARTCAR".lower(), True, True, True, True, True, False), - ("TESLA".lower(), True, True, True, True, True, False), - ("SMART_PEAR".lower(), True, True, True, True, True, False), - ("HYPERVOLT".lower(), True, True, True, True, True, False), - ("INDRA".lower(), True, True, True, True, True, False), - ("OHME".lower(), False, False, False, False, False, False), +@pytest.mark.parametrize("provider,expected_bump_charge_supported,expected_charge_limit_supported,expected_planned_dispatches_supported,expected_ready_time_supported,expected_smart_charge_supported,expected_default_features,expected_current_state_supported",[ + ("DAIKIN", True, True, True, True, True, False, True), + ("ECOBEE", True, True, True, True, True, False, True), + ("ENERGIZER", True, True, True, True, True, False, True), + ("ENPHASE", True, True, True, True, True, False, True), + ("ENODE", True, True, True, True, True, False, True), + ("FORD", True, True, True, True, True, False, True), + ("GIVENERGY", True, True, True, True, True, False, True), + ("HUAWEI", True, True, True, True, True, False, True), + ("JEDLIX", True, True, True, True, True, False, True), + ("MYENERGI", True, True, True, True, True, False, True), + ("OCPP_WALLBOX", True, True, True, True, True, False, True), + ("SENSI", True, True, True, True, True, False, True), + ("SMARTCAR", True, True, True, True, True, False, True), + ("TESLA", True, True, True, True, True, False, True), + ("SMART_PEAR", True, True, True, True, True, False, True), + ("HYPERVOLT", True, True, True, True, True, False, True), + ("INDRA", True, True, True, True, True, False, True), + ("OHME", False, False, False, False, False, False, False), + ("DAIKIN".lower(), True, True, True, True, True, False, True), + ("ECOBEE".lower(), True, True, True, True, True, False, True), + ("ENERGIZER".lower(), True, True, True, True, True, False, True), + ("ENPHASE".lower(), True, True, True, True, True, False, True), + ("ENODE".lower(), True, True, True, True, True, False, True), + ("FORD".lower(), True, True, True, True, True, False, True), + ("GIVENERGY".lower(), True, True, True, True, True, False, True), + ("HUAWEI".lower(), True, True, True, True, True, False, True), + ("JEDLIX".lower(), True, True, True, True, True, False, True), + ("MYENERGI".lower(), True, True, True, True, True, False, True), + ("OCPP_WALLBOX".lower(), True, True, True, True, True, False, True), + ("SENSI".lower(), True, True, True, True, True, False, True), + ("SMARTCAR".lower(), True, True, True, True, True, False, True), + ("TESLA".lower(), True, True, True, True, True, False, True), + ("SMART_PEAR".lower(), True, True, True, True, True, False, True), + ("HYPERVOLT".lower(), True, True, True, True, True, False, True), + ("INDRA".lower(), True, True, True, True, True, False, True), + ("OHME".lower(), False, False, False, False, False, False, False), # Unexpected providers - ("unexpected".lower(), False, False, False, False, False, True), - ("".lower(), False, False, False, False, False, True), - (None, False, False, False, False, False, True), + ("unexpected".lower(), False, False, False, False, False, True, False), + ("".lower(), False, False, False, False, False, True, False), + (None, False, False, False, False, False, True, False), ]) async def test_when_provider_provided_then_expected_features_returned( provider: str, @@ -52,7 +52,8 @@ async def test_when_provider_provided_then_expected_features_returned( expected_planned_dispatches_supported: bool, expected_ready_time_supported: bool, expected_smart_charge_supported: bool, - expected_default_features: bool + expected_default_features: bool, + expected_current_state_supported: bool ): # Act result = get_intelligent_features(provider) @@ -64,4 +65,5 @@ async def test_when_provider_provided_then_expected_features_returned( assert result.planned_dispatches_supported == expected_planned_dispatches_supported assert result.ready_time_supported == expected_ready_time_supported assert result.smart_charge_supported == expected_smart_charge_supported - assert result.is_default_features == expected_default_features \ No newline at end of file + assert result.is_default_features == expected_default_features + assert result.current_state_supported == expected_current_state_supported \ No newline at end of file