Skip to content

Commit

Permalink
feat: Added select sensor for intelligent target time to make it easi…
Browse files Browse the repository at this point in the history
…er to pick a valid time. The existing time sensor is deprecated and will be removed in a future release (45 minutes dev time)
  • Loading branch information
BottlecapDave committed Dec 29, 2024
1 parent 52bb498 commit 7554228
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 2 deletions.
6 changes: 5 additions & 1 deletion _docs/entities/intelligent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion custom_components/octopus_energy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
117 changes: 117 additions & 0 deletions custom_components/octopus_energy/intelligent/target_time_select.py
Original file line number Diff line number Diff line change
@@ -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}')
51 changes: 51 additions & 0 deletions custom_components/octopus_energy/select.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions custom_components/octopus_energy/time.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions custom_components/octopus_energy/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}

0 comments on commit 7554228

Please sign in to comment.