diff --git a/custom_components/electric_kiwi/__init__.py b/custom_components/electric_kiwi/__init__.py index 96827b3..a31bbea 100644 --- a/custom_components/electric_kiwi/__init__.py +++ b/custom_components/electric_kiwi/__init__.py @@ -1,30 +1,30 @@ """The Electric Kiwi integration.""" + from __future__ import annotations import aiohttp from electrickiwi_api import ElectricKiwiApi -from electrickiwi_api.exceptions import AuthException, ApiException +from electrickiwi_api.exceptions import ApiException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from . import api -from .const import DOMAIN from .coordinator import ( ElectricKiwiAccountDataCoordinator, + ElectricKiwiConfigEntry, ElectricKiwiHOPDataCoordinator, + ElectricKiwiRuntimeData, ) -PLATFORMS: list[Platform] = [ - Platform.SENSOR, - Platform.SELECT, -] +PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: ElectricKiwiConfigEntry +) -> bool: """Set up Electric Kiwi from a config entry.""" implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -44,18 +44,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err ek_api = ElectricKiwiApi( - api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session) + api.ConfigEntryElectricKiwiAuth( + aiohttp_client.async_get_clientsession(hass), session + ) ) - account_coordinator = ElectricKiwiAccountDataCoordinator(hass, ek_api) - hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, ek_api) - - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "account_coordinator": account_coordinator, - "hop_coordinator": hop_coordinator, - } + hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, entry, ek_api) + account_coordinator = ElectricKiwiAccountDataCoordinator(hass, entry, ek_api) - # we need to set the client number and connection id try: await ek_api.set_active_session() await hop_coordinator.async_config_entry_first_refresh() @@ -63,14 +58,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except ApiException as err: raise ConfigEntryNotReady from err + entry.runtime_data = ElectricKiwiRuntimeData( + hop=hop_coordinator, account=account_coordinator + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: ElectricKiwiConfigEntry +) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry( + hass: HomeAssistant, config_entry: ElectricKiwiConfigEntry +) -> bool: + """Migrate old entry.""" + if config_entry.version == 1 and config_entry.minor_version == 1: + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, config_entry + ) + ) - return unload_ok + session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + + ek_api = ElectricKiwiApi( + api.ConfigEntryElectricKiwiAuth( + aiohttp_client.async_get_clientsession(hass), session + ) + ) + try: + ek_session = await ek_api.get_active_session() + except ApiException: + return False + unique_id = str(ek_session.data.customer_number) + hass.config_entries.async_update_entry( + config_entry, unique_id=unique_id, minor_version=2 + ) + + return True diff --git a/custom_components/electric_kiwi/api.py b/custom_components/electric_kiwi/api.py index d445cc6..9f7ff33 100644 --- a/custom_components/electric_kiwi/api.py +++ b/custom_components/electric_kiwi/api.py @@ -2,17 +2,16 @@ from __future__ import annotations -from typing import cast - from aiohttp import ClientSession from electrickiwi_api import AbstractAuth -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import API_BASE_URL -class AsyncConfigEntryAuth(AbstractAuth): +class ConfigEntryElectricKiwiAuth(AbstractAuth): """Provide Electric Kiwi authentication tied to an OAuth2 based config entry.""" def __init__( @@ -21,12 +20,29 @@ def __init__( oauth_session: config_entry_oauth2_flow.OAuth2Session, ) -> None: """Initialize Electric Kiwi auth.""" + # add host when ready for production "https://api.electrickiwi.co.nz" defaults to dev super().__init__(websession, API_BASE_URL) self._oauth_session = oauth_session async def async_get_access_token(self) -> str: """Return a valid access token.""" - if not self._oauth_session.valid_token: - await self._oauth_session.async_ensure_token_valid() + await self._oauth_session.async_ensure_token_valid() + + return str(self._oauth_session.token["access_token"]) + - return cast(str, self._oauth_session.token["access_token"]) +class ConfigFlowElectricKiwiAuth(AbstractAuth): + """Provide Electric Kiwi authentication tied to an OAuth2 based config flow.""" + + def __init__( + self, + hass: HomeAssistant, + token: str, + ) -> None: + """Initialize ConfigFlowFitbitApi.""" + super().__init__(aiohttp_client.async_get_clientsession(hass), API_BASE_URL) + self._token = token + + async def async_get_access_token(self) -> str: + """Return the token for the Electric Kiwi API.""" + return self._token diff --git a/custom_components/electric_kiwi/config_flow.py b/custom_components/electric_kiwi/config_flow.py index c2c80aa..b83fd89 100644 --- a/custom_components/electric_kiwi/config_flow.py +++ b/custom_components/electric_kiwi/config_flow.py @@ -1,14 +1,19 @@ """Config flow for Electric Kiwi.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from electrickiwi_api import ElectricKiwiApi +from electrickiwi_api.exceptions import ApiException + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult +from homeassistant.const import CONF_NAME from homeassistant.helpers import config_entry_oauth2_flow +from . import api from .const import DOMAIN, SCOPE_VALUES @@ -17,13 +22,10 @@ class ElectricKiwiOauth2FlowHandler( ): """Config flow to handle Electric Kiwi OAuth2 authentication.""" + VERSION = 1 + MINOR_VERSION = 2 DOMAIN = DOMAIN - def __init__(self) -> None: - """Set up instance.""" - super().__init__() - self._reauth_entry: ConfigEntry | None = None - @property def logger(self) -> logging.Logger: """Return logger.""" @@ -34,26 +36,41 @@ def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return {"scope": SCOPE_VALUES} - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" - self._reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form(step_id="reauth_confirm") + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_NAME: self._get_reauth_entry().title}, + ) return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an entry for Electric Kiwi.""" - existing_entry = await self.async_set_unique_id(DOMAIN) - if existing_entry: - self.hass.config_entries.async_update_entry(existing_entry, data=data) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") - return await super().async_oauth_create_entry(data) + ek_api = ElectricKiwiApi( + api.ConfigFlowElectricKiwiAuth(self.hass, data["token"]["access_token"]) + ) + + try: + session = await ek_api.get_active_session() + except ApiException: + return self.async_abort(reason="connection_error") + + unique_id = str(session.data.customer_number) + await self.async_set_unique_id(unique_id) + if self.source == SOURCE_REAUTH: + self._abort_if_unique_id_mismatch(reason="wrong_account") + return self.async_update_reload_and_abort( + self._get_reauth_entry(), data=data + ) + + self._abort_if_unique_id_configured() + return self.async_create_entry(title=unique_id, data=data) diff --git a/custom_components/electric_kiwi/const.py b/custom_components/electric_kiwi/const.py index e7404a8..c51422a 100644 --- a/custom_components/electric_kiwi/const.py +++ b/custom_components/electric_kiwi/const.py @@ -8,14 +8,4 @@ OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token" API_BASE_URL = "https://api.electrickiwi.co.nz" -SCOPE_VALUES = "read_connection_detail read_billing_frequency read_account_running_balance read_consumption_summary read_consumption_averages read_hop_intervals_config read_hop_connection save_hop_connection read_session" - - -ATTR_TOTAL_RUNNING_BALANCE = "total_running_balance" -ATTR_TOTAL_CURRENT_BALANCE = "total_account_balance" -ATTR_NEXT_BILLING_DATE = "next_billing_date" -ATTR_HOP_PERCENTAGE = "hop_percentage" - -ATTR_EK_HOP_SELECT = "hop_select" -ATTR_EK_HOP_START = "hop_sensor_start" -ATTR_EK_HOP_END = "hop_sensor_end" +SCOPE_VALUES = "read_customer_details read_connection_detail read_connection read_billing_address get_bill_address read_billing_frequency read_billing_details read_billing_bills read_billing_bill read_billing_bill_id read_billing_bill_file read_account_running_balance read_customer_account_summary read_consumption_summary download_consumption_file read_consumption_averages get_consumption_averages read_hop_intervals_config read_hop_intervals read_hop_connection read_hop_specific_connection save_hop_connection save_hop_specific_connection read_outage_contact get_outage_contact_info_for_icp read_session read_session_data_login" diff --git a/custom_components/electric_kiwi/coordinator.py b/custom_components/electric_kiwi/coordinator.py index 82beabf..635b55b 100644 --- a/custom_components/electric_kiwi/coordinator.py +++ b/custom_components/electric_kiwi/coordinator.py @@ -1,13 +1,18 @@ """Electric Kiwi coordinators.""" + +from __future__ import annotations + +import asyncio from collections import OrderedDict +from dataclasses import dataclass from datetime import timedelta import logging -import async_timeout from electrickiwi_api import ElectricKiwiApi from electrickiwi_api.exceptions import ApiException, AuthException -from electrickiwi_api.model import AccountBalance, Hop, HopIntervals +from electrickiwi_api.model import AccountSummary, Hop, HopIntervals +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,38 +20,45 @@ _LOGGER = logging.getLogger(__name__) ACCOUNT_SCAN_INTERVAL = timedelta(hours=6) -HOP_SCAN_INTERVAL = timedelta(minutes=15) +HOP_SCAN_INTERVAL = timedelta(minutes=20) + + +@dataclass +class ElectricKiwiRuntimeData: + """ElectricKiwi runtime data.""" + + hop: ElectricKiwiHOPDataCoordinator + account: ElectricKiwiAccountDataCoordinator + + +type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData] -class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator): - """ElectricKiwi Data object.""" +class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountSummary]): + """ElectricKiwi Account Data object.""" - def __init__(self, hass: HomeAssistant, ek_api: ElectricKiwiApi) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: ElectricKiwiConfigEntry, + ek_api: ElectricKiwiApi, + ) -> None: """Initialize ElectricKiwiAccountDataCoordinator.""" super().__init__( hass, _LOGGER, - # Name of the data. For logging purposes. + config_entry=entry, name="Electric Kiwi Account Data", - # Polling interval. Will only be polled if there are subscribers. update_interval=ACCOUNT_SCAN_INTERVAL, ) - self._ek_api = ek_api - - async def _async_update_data(self) -> AccountBalance: - """Fetch data from API endpoint. + self.ek_api = ek_api - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ + async def _async_update_data(self) -> AccountSummary: + """Fetch data from Account balance API endpoint.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with async_timeout.timeout(60): - return await self._ek_api.get_account_balance() + async with asyncio.timeout(60): + return await self.ek_api.get_account_summary() except AuthException as auth_err: - # Raising ConfigEntryAuthFailed will cancel future updates - # and start a config flow with SOURCE_REAUTH (async_step_reauth) raise ConfigEntryAuthFailed from auth_err except ApiException as api_err: raise UpdateFailed( @@ -55,19 +67,25 @@ async def _async_update_data(self) -> AccountBalance: class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): - """ElectricKiwi Data object.""" - - def __init__(self, hass: HomeAssistant, ek_api: ElectricKiwiApi) -> None: + """ElectricKiwi HOP Data object.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ElectricKiwiConfigEntry, + ek_api: ElectricKiwiApi, + ) -> None: """Initialize ElectricKiwiAccountDataCoordinator.""" super().__init__( hass, _LOGGER, + config_entry=entry, # Name of the data. For logging purposes. name="Electric Kiwi HOP Data", # Polling interval. Will only be polled if there are subscribers. update_interval=HOP_SCAN_INTERVAL, ) - self._ek_api = ek_api + self.ek_api = ek_api self.hop_intervals: HopIntervals | None = None def get_hop_options(self) -> dict[str, int]: @@ -82,7 +100,7 @@ def get_hop_options(self) -> dict[str, int]: async def async_update_hop(self, hop_interval: int) -> Hop: """Update selected hop and data.""" try: - self.async_set_updated_data(await self._ek_api.post_hop(hop_interval)) + self.async_set_updated_data(await self.ek_api.post_hop(hop_interval)) except AuthException as auth_err: raise ConfigEntryAuthFailed from auth_err except ApiException as api_err: @@ -98,9 +116,9 @@ async def _async_update_data(self) -> Hop: filters the intervals to remove ones that are not active """ try: - async with async_timeout.timeout(60): + async with asyncio.timeout(60): if self.hop_intervals is None: - hop_intervals: HopIntervals = await self._ek_api.get_hop_intervals() + hop_intervals: HopIntervals = await self.ek_api.get_hop_intervals() hop_intervals.intervals = OrderedDict( filter( lambda pair: pair[1].active == 1, @@ -109,7 +127,7 @@ async def _async_update_data(self) -> Hop: ) self.hop_intervals = hop_intervals - return await self._ek_api.get_hop() + return await self.ek_api.get_hop() except AuthException as auth_err: raise ConfigEntryAuthFailed from auth_err except ApiException as api_err: diff --git a/custom_components/electric_kiwi/manifest.json b/custom_components/electric_kiwi/manifest.json index a777a1a..364ec36 100644 --- a/custom_components/electric_kiwi/manifest.json +++ b/custom_components/electric_kiwi/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/electric_kiwi", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["electrickiwi-api==0.8.4"], - "version": "0.8.6" + "requirements": ["electrickiwi-api==0.9.11"], + "version": "0.9.0" } diff --git a/custom_components/electric_kiwi/oauth2.py b/custom_components/electric_kiwi/oauth2.py index ce3e473..9a6c4cd 100644 --- a/custom_components/electric_kiwi/oauth2.py +++ b/custom_components/electric_kiwi/oauth2.py @@ -1,4 +1,5 @@ """OAuth2 implementations for Toon.""" + from __future__ import annotations import base64 @@ -72,5 +73,4 @@ async def _token_request(self, data: dict) -> dict: resp = await session.post(self.token_url, data=data, headers=headers) resp.raise_for_status() - resp_json = cast(dict, await resp.json()) - return resp_json + return cast(dict, await resp.json()) diff --git a/custom_components/electric_kiwi/select.py b/custom_components/electric_kiwi/select.py index ce84933..30e02b5 100644 --- a/custom_components/electric_kiwi/select.py +++ b/custom_components/electric_kiwi/select.py @@ -1,65 +1,38 @@ """Support for Electric Kiwi hour of free power.""" + from __future__ import annotations -from dataclasses import dataclass import logging -from typing import Final - -from electrickiwi_api import ElectricKiwiApi from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_EK_HOP_SELECT, DOMAIN, ATTRIBUTION -from .coordinator import ElectricKiwiHOPDataCoordinator +from .const import ATTRIBUTION +from .coordinator import ElectricKiwiConfigEntry, ElectricKiwiHOPDataCoordinator _LOGGER = logging.getLogger(__name__) +ATTR_EK_HOP_SELECT = "hop_select" - -@dataclass -class ElectricKiwiHOPSelectDescriptionMixin: - """Define an entity description mixin for select entities.""" - - options_dict: dict[str, int] | None - - -@dataclass -class ElectricKiwiHOPDescription( - SelectEntityDescription, ElectricKiwiHOPSelectDescriptionMixin -): - """Class to describe an Electric Kiwi select entity.""" - - -HOP_SELECT_TYPES: Final[ElectricKiwiHOPDescription, ...] = ( - ElectricKiwiHOPDescription( - entity_category=EntityCategory.CONFIG, - key=ATTR_EK_HOP_SELECT, - name="Hour of free power", - options_dict=None, - ), +HOP_SELECT = SelectEntityDescription( + entity_category=EntityCategory.CONFIG, + key=ATTR_EK_HOP_SELECT, + translation_key="hop_selector", ) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ElectricKiwiConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Electric Kiwi Sensor Setup.""" - hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id][ - "hop_coordinator" - ] + """Electric Kiwi select setup.""" + hop_coordinator = entry.runtime_data.hop - _LOGGER.debug("Setting up HOP entity") - entities = [ - ElectricKiwiSelectHOPEntity( - hop_coordinator, description - ) - for description in HOP_SELECT_TYPES - ] - async_add_entities(entities) + _LOGGER.debug("Setting up select entity") + async_add_entities([ElectricKiwiSelectHOPEntity(hop_coordinator, HOP_SELECT)]) class ElectricKiwiSelectHOPEntity( @@ -79,8 +52,10 @@ def __init__( ) -> None: """Initialise the HOP selection entity.""" super().__init__(coordinator) - self._attr_unique_id = (f"{coordinator._ek_api.customer_number}" - f"_{coordinator._ek_api.connection_id}_{description.key}") + self._attr_unique_id = ( + f"{coordinator.ek_api.customer_number}" + f"_{coordinator.ek_api.electricity.identifier}_{description.key}" + ) self.entity_description = description self.values_dict = coordinator.get_hop_options() self._attr_options = list(self.values_dict) @@ -88,10 +63,10 @@ def __init__( @property def current_option(self) -> str | None: """Return the currently selected option.""" - return ( - f'{self.coordinator.data.start.start_time}' - f' - {self.coordinator.data.end.end_time}' - ) + return ( + f"{self.coordinator.data.start.start_time}" + f" - {self.coordinator.data.end.end_time}" + ) async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/custom_components/electric_kiwi/sensor.py b/custom_components/electric_kiwi/sensor.py index e0d07ef..410d708 100644 --- a/custom_components/electric_kiwi/sensor.py +++ b/custom_components/electric_kiwi/sensor.py @@ -1,13 +1,12 @@ -"""Support for Electric Kiwi account balance.""" +"""Support for Electric Kiwi sensors.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -import logging -from electrickiwi_api import ElectricKiwiApi -from electrickiwi_api.model import AccountBalance, Hop +from electrickiwi_api.model import AccountSummary, Hop from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,111 +14,96 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_DOLLAR, PERCENTAGE -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from .const import ( - ATTR_EK_HOP_END, - ATTR_EK_HOP_START, - ATTR_HOP_PERCENTAGE, - ATTR_NEXT_BILLING_DATE, - ATTR_TOTAL_CURRENT_BALANCE, - ATTR_TOTAL_RUNNING_BALANCE, - ATTRIBUTION, - DOMAIN, -) +from .const import ATTRIBUTION from .coordinator import ( ElectricKiwiAccountDataCoordinator, + ElectricKiwiConfigEntry, ElectricKiwiHOPDataCoordinator, ) +ATTR_EK_HOP_START = "hop_power_start" +ATTR_EK_HOP_END = "hop_power_end" +ATTR_TOTAL_RUNNING_BALANCE = "total_running_balance" +ATTR_TOTAL_CURRENT_BALANCE = "total_account_balance" +ATTR_NEXT_BILLING_DATE = "next_billing_date" +ATTR_HOP_PERCENTAGE = "hop_percentage" + -@dataclass -class ElectricKiwiRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription): + """Describes Electric Kiwi sensor entity.""" - value_func: Callable[[AccountBalance], str | datetime | None] + value_func: Callable[[AccountSummary], float | datetime] -@dataclass -class ElectricKiwiSensorEntityDescription( - SensorEntityDescription, ElectricKiwiRequiredKeysMixin -): - """Describes Electric Kiwi sensor entity.""" +def _get_hop_percentage(account_balance: AccountSummary) -> float: + """Return the hop percentage from account summary.""" + if power := account_balance.services.get("power"): + if connection := power.connections[0]: + return float(connection.hop_percentage) + return 0.0 -ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiSensorEntityDescription, ...] = ( - ElectricKiwiSensorEntityDescription( +ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = ( + ElectricKiwiAccountSensorEntityDescription( key=ATTR_TOTAL_RUNNING_BALANCE, - name="Total running balance", - icon="mdi:currency-usd", + translation_key="total_running_balance", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, native_unit_of_measurement=CURRENCY_DOLLAR, - value_func=lambda account_balance: str(account_balance.total_running_balance), + value_func=lambda account_balance: float(account_balance.total_running_balance), ), - ElectricKiwiSensorEntityDescription( + ElectricKiwiAccountSensorEntityDescription( key=ATTR_TOTAL_CURRENT_BALANCE, - name="Total current balance", - icon="mdi:currency-usd", + translation_key="total_current_balance", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, native_unit_of_measurement=CURRENCY_DOLLAR, - value_func=lambda account_balance: str(account_balance.total_account_balance), + value_func=lambda account_balance: float(account_balance.total_account_balance), ), - ElectricKiwiSensorEntityDescription( + ElectricKiwiAccountSensorEntityDescription( key=ATTR_NEXT_BILLING_DATE, - name="Next billing date", - icon="mdi:calendar", + translation_key="next_billing_date", device_class=SensorDeviceClass.DATE, value_func=lambda account_balance: datetime.strptime( account_balance.next_billing_date, "%Y-%m-%d" ), ), - ElectricKiwiSensorEntityDescription( + ElectricKiwiAccountSensorEntityDescription( key=ATTR_HOP_PERCENTAGE, - name="Hour of power savings", - icon="", + translation_key="hop_power_savings", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - value_func=lambda account_balance: str( - account_balance.connections[0].hop_percentage - ), + value_func=_get_hop_percentage, ), ) -@dataclass -class ElectricKiwiHOPRequiredKeysMixin: - """Mixin for required HOP keys.""" +@dataclass(frozen=True, kw_only=True) +class ElectricKiwiHOPSensorEntityDescription(SensorEntityDescription): + """Describes Electric Kiwi HOP sensor entity.""" value_func: Callable[[Hop], datetime] -@dataclass -class ElectricKiwiHOPSensorEntityDescription( - SensorEntityDescription, - ElectricKiwiHOPRequiredKeysMixin, -): - """Describes Electric Kiwi HOP sensor entity.""" - - def _check_and_move_time(hop: Hop, time: str) -> datetime: """Return the time a day forward if HOP end_time is in the past.""" date_time = datetime.combine( dt_util.start_of_local_day(), datetime.strptime(time, "%I:%M %p").time(), - dt_util.DEFAULT_TIME_ZONE, + dt_util.get_default_time_zone(), ) end_time = datetime.combine( dt_util.start_of_local_day(), datetime.strptime(hop.end.end_time, "%I:%M %p").time(), - dt_util.DEFAULT_TIME_ZONE, + dt_util.get_default_time_zone(), ) if end_time < dt_util.now(): @@ -127,17 +111,16 @@ def _check_and_move_time(hop: Hop, time: str) -> datetime: return date_time - -HOP_SENSOR_TYPE: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( +HOP_SENSOR_TYPES: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( ElectricKiwiHOPSensorEntityDescription( key=ATTR_EK_HOP_START, - name="Hour of free power start", + translation_key="hop_free_power_start", device_class=SensorDeviceClass.TIMESTAMP, value_func=lambda hop: _check_and_move_time(hop, hop.start.start_time), ), ElectricKiwiHOPSensorEntityDescription( key=ATTR_EK_HOP_END, - name="Hour of free power end", + translation_key="hop_free_power_end", device_class=SensorDeviceClass.TIMESTAMP, value_func=lambda hop: _check_and_move_time(hop, hop.end.end_time), ), @@ -145,34 +128,29 @@ def _check_and_move_time(hop: Hop, time: str) -> datetime: async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ElectricKiwiConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Electric Kiwi Sensor Setup.""" - account_coordinator: ElectricKiwiAccountDataCoordinator = hass.data[DOMAIN][ - entry.entry_id - ]["account_coordinator"] + """Electric Kiwi Sensors Setup.""" + account_coordinator = entry.runtime_data.account - entities = [ + entities: list[SensorEntity] = [ ElectricKiwiAccountEntity( account_coordinator, description, ) for description in ACCOUNT_SENSOR_TYPES ] - async_add_entities(entities) - - hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id][ - "hop_coordinator" - ] - hop_entities = [ - ElectricKiwiHOPEntity( - hop_coordinator, - description, - ) - for description in HOP_SENSOR_TYPE - ] - async_add_entities(hop_entities) + hop_coordinator = entry.runtime_data.hop + entities.extend( + [ + ElectricKiwiHOPEntity(hop_coordinator, description) + for description in HOP_SENSOR_TYPES + ] + ) + async_add_entities(entities) class ElectricKiwiAccountEntity( @@ -180,26 +158,26 @@ class ElectricKiwiAccountEntity( ): """Entity object for Electric Kiwi sensor.""" - entity_description: ElectricKiwiSensorEntityDescription + entity_description: ElectricKiwiAccountSensorEntityDescription + _attr_has_entity_name = True _attr_attribution = ATTRIBUTION def __init__( self, coordinator: ElectricKiwiAccountDataCoordinator, - description: ElectricKiwiSensorEntityDescription, + description: ElectricKiwiAccountSensorEntityDescription, ) -> None: """Entity object for Electric Kiwi sensor.""" super().__init__(coordinator) - self.customer_number = self.coordinator._ek_api.customer_number - self.connection_id = self.coordinator._ek_api.connection_id - self._attr_unique_id = (f"{coordinator._ek_api.customer_number}" - f"_{coordinator._ek_api.connection_id}_{description.key}") - self._balance: AccountBalance + self._attr_unique_id = ( + f"{coordinator.ek_api.customer_number}" + f"_{coordinator.ek_api.electricity.identifier}_{description.key}" + ) self.entity_description = description @property - def native_value(self) -> datetime | str | None: + def native_value(self) -> float | datetime: """Return the state of the sensor.""" return self.entity_description.value_func(self.coordinator.data) @@ -210,6 +188,7 @@ class ElectricKiwiHOPEntity( """Entity object for Electric Kiwi sensor.""" entity_description: ElectricKiwiHOPSensorEntityDescription + _attr_has_entity_name = True _attr_attribution = ATTRIBUTION def __init__( @@ -220,10 +199,10 @@ def __init__( """Entity object for Electric Kiwi sensor.""" super().__init__(coordinator) - self.customer_number = self.coordinator._ek_api.customer_number - self.connection_id = self.coordinator._ek_api.connection_id - self._attr_unique_id = (f"{coordinator._ek_api.customer_number}" - f"_{coordinator._ek_api.connection_id}_{description.key}") + self._attr_unique_id = ( + f"{coordinator.ek_api.customer_number}" + f"_{coordinator.ek_api.electricity.identifier}_{description.key}" + ) self.entity_description = description @property diff --git a/custom_components/electric_kiwi/strings.json b/custom_components/electric_kiwi/strings.json index 9c5e8b5..5e0a2ef 100644 --- a/custom_components/electric_kiwi/strings.json +++ b/custom_components/electric_kiwi/strings.json @@ -14,13 +14,41 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "entity": { + "sensor": { + "hop_free_power_start": { + "name": "Hour of free power start" + }, + "hop_free_power_end": { + "name": "Hour of free power end" + }, + "total_running_balance": { + "name": "Total running balance" + }, + "total_current_balance": { + "name": "Total current balance" + }, + "next_billing_date": { + "name": "Next billing date" + }, + "hop_power_savings": { + "name": "Hour of power savings" + } + }, + "select": { "hop_selector": { "name": "Hour of free power" } } } } diff --git a/hacs.json b/hacs.json index 3811e51..ed61f5c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Electric Kiwi", "country": "NO", - "homeassistant": "2022.12.0" + "homeassistant": "2024.12.0" }