From 7554228bdc507e1f1f918f63d50874c995db6d5f Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sun, 29 Dec 2024 12:15:02 +0000 Subject: [PATCH] feat: Added select sensor for intelligent target time to make it easier to pick a valid time. The existing time sensor is deprecated and will be removed in a future release (45 minutes dev time) --- _docs/entities/intelligent.md | 6 +- custom_components/octopus_energy/__init__.py | 2 +- .../intelligent/target_time_select.py | 117 ++++++++++++++++++ custom_components/octopus_energy/select.py | 51 ++++++++ custom_components/octopus_energy/time.py | 13 ++ .../octopus_energy/translations/en.json | 4 + 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 custom_components/octopus_energy/intelligent/target_time_select.py create mode 100644 custom_components/octopus_energy/select.py diff --git a/_docs/entities/intelligent.md b/_docs/entities/intelligent.md index 6ded8a75..2c158700 100644 --- a/_docs/entities/intelligent.md +++ b/_docs/entities/intelligent.md @@ -102,7 +102,7 @@ This sensor is used to see and set the charge target for your future intelligent ### Target Time -`time.octopus_energy_{{ACCOUNT_ID}}_intelligent_target_time` +`select.octopus_energy_{{ACCOUNT_ID}}_intelligent_target_time` This sensor is used to see and set the target time for your future intelligent charges. @@ -116,6 +116,10 @@ This sensor is used to see and set the target time for your future intelligent c You can use the [data_last_retrieved sensor](./diagnostics.md#intelligent-settings-data-last-retrieved) to determine when the underlying data was last retrieved from the OE servers. +!!! warning + + There is a time based sensor called `select.octopus_energy_{{ACCOUNT_ID}}_intelligent_target_time` which represents this functionality. This is a legacy sensor which will be removed in the future. + ## Migrating from megakid/ha_octopus_intelligent? If you're moving to this integration from [megakid/ha_octopus_intelligent](https://github.com/megakid/ha_octopus_intelligent), below is a quick guide on what entities you should use diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 393fbde6..6c14f344 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -67,7 +67,7 @@ REPAIR_UNKNOWN_INTELLIGENT_PROVIDER ) -ACCOUNT_PLATFORMS = ["sensor", "binary_sensor", "number", "switch", "text", "time", "event"] +ACCOUNT_PLATFORMS = ["sensor", "binary_sensor", "number", "switch", "text", "time", "event", "select"] TARGET_RATE_PLATFORMS = ["binary_sensor"] COST_TRACKER_PLATFORMS = ["sensor"] TARIFF_COMPARISON_PLATFORMS = ["sensor"] diff --git a/custom_components/octopus_energy/intelligent/target_time_select.py b/custom_components/octopus_energy/intelligent/target_time_select.py new file mode 100644 index 00000000..2ec2c30a --- /dev/null +++ b/custom_components/octopus_energy/intelligent/target_time_select.py @@ -0,0 +1,117 @@ +import logging +from datetime import datetime, time, timedelta +import time as time_time + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.select import SelectEntity +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import OctopusEnergyIntelligentSensor +from ..api_client import OctopusEnergyApiClient +from ..coordinators.intelligent_settings import IntelligentCoordinatorResult +from ..utils.attributes import dict_to_typed_dict + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentTargetTimeSelect(CoordinatorEntity, SelectEntity, OctopusEnergyIntelligentSensor, RestoreEntity): + """Sensor for setting the target time to charge the car to the desired percentage.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): + """Init sensor.""" + # Pass coordinator to base class + CoordinatorEntity.__init__(self, coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = None + self._last_updated = None + self._client = client + self._account_id = account_id + self._attributes = {} + self.entity_id = generate_entity_id("select.{}", self.unique_id, hass=hass) + + self._options = [] + current_time = datetime(2025, 1, 1, 4, 0) + final_time = datetime(2025, 1, 1, 11, 30) + while current_time < final_time: + self._options.append(f"{current_time.hour:02}:{current_time.minute:02}") + current_time = current_time + timedelta(minutes=30) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_{self._account_id}_intelligent_target_time" + + @property + def name(self): + """Name of the sensor.""" + return f"Intelligent Target Time ({self._account_id})" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:battery-clock" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def options(self) -> list[str]: + """Return the available tariffs.""" + return self._options + + @property + def current_option(self) -> str: + return self._state + + @callback + def _handle_coordinator_update(self) -> None: + """The time that the car should be ready by.""" + settings_result: IntelligentCoordinatorResult = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + if settings_result is None or (self._last_updated is not None and self._last_updated > settings_result.last_retrieved): + return + + if settings_result.settings is not None: + self._state = f"{settings_result.settings.ready_time_weekday.hour:02}:{settings_result.settings.ready_time_weekday.minute:02}" + + self._attributes = dict_to_typed_dict(self._attributes) + super()._handle_coordinator_update() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + parts = option.split(":") + value = time(int(parts[0]), int(parts[1])) + await self._client.async_update_intelligent_car_target_time( + self._account_id, + self._device.id, + value, + ) + self._state = value + self._last_updated = utcnow() + self.async_write_ha_state() + + 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: + self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state + self._attributes = dict_to_typed_dict(state.attributes) + + if (self._state is None): + self._state = None + + _LOGGER.debug(f'Restored OctopusEnergyIntelligentTargetTime state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/select.py b/custom_components/octopus_energy/select.py new file mode 100644 index 00000000..0e53773f --- /dev/null +++ b/custom_components/octopus_energy/select.py @@ -0,0 +1,51 @@ +import logging + +from .intelligent.target_time_select import OctopusEnergyIntelligentTargetTimeSelect +from .api_client import OctopusEnergyApiClient +from .intelligent import get_intelligent_features +from .api_client.intelligent_device import IntelligentDevice + +from .const import ( + CONFIG_ACCOUNT_ID, + DATA_CLIENT, + DATA_INTELLIGENT_DEVICE, + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_INTELLIGENT_SETTINGS_COORDINATOR +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + if CONFIG_MAIN_API_KEY in config: + await async_setup_intelligent_sensors(hass, config, async_add_entities) + + return True + +async def async_setup_intelligent_sensors(hass, config, async_add_entities): + _LOGGER.debug('Setting up intelligent sensors') + + entities = [] + + account_id = config[CONFIG_ACCOUNT_ID] + + client = hass.data[DOMAIN][account_id][DATA_CLIENT] + intelligent_device: IntelligentDevice = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DEVICE] if DATA_INTELLIGENT_DEVICE in hass.data[DOMAIN][account_id] else None + if intelligent_device is not None: + intelligent_features = get_intelligent_features(intelligent_device.provider) + settings_coordinator = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][account_id][DATA_CLIENT] + + if intelligent_features.ready_time_supported: + entities.append(OctopusEnergyIntelligentTargetTimeSelect(hass, settings_coordinator, client, intelligent_device, account_id)) + + async_add_entities(entities) \ No newline at end of file diff --git a/custom_components/octopus_energy/time.py b/custom_components/octopus_energy/time.py index f29a9c14..ed90b138 100644 --- a/custom_components/octopus_energy/time.py +++ b/custom_components/octopus_energy/time.py @@ -1,5 +1,7 @@ import logging +from homeassistant.helpers import issue_registry as ir + from .intelligent.target_time import OctopusEnergyIntelligentTargetTime from .api_client import OctopusEnergyApiClient from .intelligent import get_intelligent_features @@ -46,6 +48,17 @@ async def async_setup_intelligent_sensors(hass, config, async_add_entities): client: OctopusEnergyApiClient = hass.data[DOMAIN][account_id][DATA_CLIENT] if intelligent_features.ready_time_supported: + ir.async_create_issue( + hass, + DOMAIN, + "intelligent_target_time_deprecated", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="intelligent_target_time_deprecated", + translation_placeholders={ "account_id": account_id }, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues/1079", + ) + entities.append(OctopusEnergyIntelligentTargetTime(hass, settings_coordinator, client, intelligent_device, account_id)) async_add_entities(entities) \ No newline at end of file diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json index 79c66c64..cfe4dde8 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -318,6 +318,10 @@ "unknown_intelligent_provider": { "title": "Unknown intelligent provider \"{provider}\"", "description": "You have an intelligent provider of \"{provider}\" which is not recognised and therefore a reduced feature set has been enabled. Click on \"Learn More\" with instructions on what to do next." + }, + "intelligent_target_time_deprecated": { + "title": "Intelligent target time sensor has been deprecated", + "description": "The target time sensor (defaults to time.octopus_energy_{account_id}_intelligent_target_time) has been deprecated in favour of a select based sensor (select.octopus_energy_{account_id}_intelligent_target_time) to make it easier to select a valid time. This old sensor will be removed in a future release." } } } \ No newline at end of file