diff --git a/custom_components/iotawatt/__init__.py b/custom_components/iotawatt/__init__.py index 8b4d746..7987004 100644 --- a/custom_components/iotawatt/__init__.py +++ b/custom_components/iotawatt/__init__.py @@ -1,147 +1,24 @@ """The iotawatt integration.""" -from datetime import timedelta -import logging -from typing import Dict, List - -from httpx import AsyncClient -from iotawattpy.iotawatt import Iotawatt -import voluptuous as vol - -from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) -from .const import ( - COORDINATOR, - DEFAULT_ICON, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - IOTAWATT_API, - SIGNAL_ADD_DEVICE, -) +from .const import DOMAIN +from .coordinator import IotawattUpdater -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) +PLATFORMS = ("sensor",) -PLATFORMS = ["sensor"] - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the iotawatt component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up iotawatt from a config entry.""" - polling_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - - session = AsyncClient() - if "username" in entry.data.keys(): - api = Iotawatt( - entry.data["name"], - entry.data["host"], - session, - entry.data["username"], - entry.data["password"], - ) - else: - api = Iotawatt( - entry.data["name"], - entry.data["host"], - session, - ) - - coordinator = IotawattUpdater( - hass, - api=api, - name="IoTaWatt", - update_interval=polling_interval, - ) - - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady - - hass.data[DOMAIN][entry.entry_id] = { - COORDINATOR: coordinator, - IOTAWATT_API: api, - } - - for component in PLATFORMS: - _LOGGER.info(f"Setting up platform: {component}") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - + coordinator = IotawattUpdater(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -class IotawattUpdater(DataUpdateCoordinator): - """Class to manage fetching update data from the IoTaWatt Energy Device.""" - - def __init__(self, hass: HomeAssistant, api: str, name: str, update_interval: int): - """Initialize IotaWattUpdater object.""" - self.api = api - self.sensorlist: Dict[str, List[str]] = {} - - super().__init__( - hass=hass, - logger=_LOGGER, - name=name, - update_interval=timedelta(seconds=update_interval), - ) - - async def _async_update_data(self): - """Fetch sensors from IoTaWatt device.""" - - await self.api.update() - sensors = self.api.getSensors() - - for sensor in sensors["sensors"]: - if sensor not in self.sensorlist: - to_add = { - "entity": sensor, - "mac_address": sensors["sensors"][sensor].hub_mac_address, - "name": sensors["sensors"][sensor].getName(), - } - async_dispatcher_send(self.hass, SIGNAL_ADD_DEVICE, to_add) - self.sensorlist[sensor] = sensors["sensors"][sensor] - - return sensors - - -class IotaWattEntity(CoordinatorEntity, SensorEntity): - """Defines the base IoTaWatt Energy Device entity.""" - - def __init__(self, coordinator: IotawattUpdater, entity, mac_address, name): - """Initialize the IoTaWatt Entity.""" - super().__init__(coordinator) - - self._entity = entity - self._name = name - self._icon = DEFAULT_ICON - self._mac_address = mac_address - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._mac_address - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self): - """Return the icon for the entity.""" - return self._icon \ No newline at end of file +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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 unload_ok diff --git a/custom_components/iotawatt/config_flow.py b/custom_components/iotawatt/config_flow.py index a81fb05..9ec860e 100644 --- a/custom_components/iotawatt/config_flow.py +++ b/custom_components/iotawatt/config_flow.py @@ -1,97 +1,102 @@ """Config flow for iotawatt integration.""" -import json +from __future__ import annotations + import logging -import httpx -from httpx import AsyncClient from iotawattpy.iotawatt import Iotawatt import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import httpx_client -from .const import DOMAIN +from .const import CONNECTION_ERRORS, DOMAIN _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): str, - vol.Required(CONF_HOST): str, - } -) - - -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - session = AsyncClient() - iotawatt = Iotawatt(data["name"], data["host"], session) +async def validate_input( + hass: core.HomeAssistant, data: dict[str, str] +) -> dict[str, str]: + """Validate the user input allows us to connect.""" + iotawatt = Iotawatt( + "", + data[CONF_HOST], + httpx_client.get_async_client(hass), + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), + ) try: is_connected = await iotawatt.connect() - _LOGGER.debug("isConnected: %s", is_connected) - except (KeyError, json.JSONDecodeError, httpx.HTTPError): - raise CannotConnect + except CONNECTION_ERRORS: + return {"base": "cannot_connect"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return {"base": "unknown"} if not is_connected: - raise InvalidAuth + return {"base": "invalid_auth"} - # Return info that you want to store in the config entry. - return {"title": data["name"]} + return {} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for iotawatt.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize.""" self._data = {} - self._errors = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) + user_input = {} - errors = {} - self._data.update(user_input) + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + } + ) + if not user_input: + return self.async_show_form(step_id="user", data_schema=schema) - try: - await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: + if not (errors := await validate_input(self.hass, user_input)): + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + if errors == {"base": "invalid_auth"}: + self._data.update(user_input) return await self.async_step_auth() - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return self.async_create_entry(title=self._data["name"], data=user_input) - - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors - ) + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) async def async_step_auth(self, user_input=None): """Authenticate user if authentication is enabled on the IoTaWatt device.""" + if user_input is None: + user_input = {} + data_schema = vol.Schema( { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, } ) - _LOGGER.debug("Data: %s", self._data) - if user_input is None: + if not user_input: return self.async_show_form(step_id="auth", data_schema=data_schema) - self._data.update(user_input) - return self.async_create_entry(title=self._data["name"], data=self._data) + + data = {**self._data, **user_input} + + if errors := await validate_input(self.hass, data): + return self.async_show_form( + step_id="auth", data_schema=data_schema, errors=errors + ) + + return self.async_create_entry(title=data[CONF_HOST], data=data) class CannotConnect(exceptions.HomeAssistantError): @@ -99,4 +104,4 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" \ No newline at end of file + """Error to indicate there is invalid auth.""" diff --git a/custom_components/iotawatt/const.py b/custom_components/iotawatt/const.py index 43a1928..0b80e10 100644 --- a/custom_components/iotawatt/const.py +++ b/custom_components/iotawatt/const.py @@ -1,9 +1,14 @@ -"""Constants for the iotawatt integration.""" +"""Constants for the IoTaWatt integration.""" +from __future__ import annotations + +import json + +import httpx -DEFAULT_ICON = "mdi:flash" -DEFAULT_SCAN_INTERVAL = 30 DOMAIN = "iotawatt" -COORDINATOR = "coordinator" -IOTAWATT_API = "iotawatt_api" -SIGNAL_ADD_DEVICE = "iotawatt_add_device" -SIGNAL_DELETE_DEVICE = "iotawatt_delete_device" +VOLT_AMPERE_REACTIVE = "VAR" +VOLT_AMPERE_REACTIVE_HOURS = "VARh" + +ATTR_LAST_UPDATE = "last_update" + +CONNECTION_ERRORS = (KeyError, json.JSONDecodeError, httpx.HTTPError) diff --git a/custom_components/iotawatt/coordinator.py b/custom_components/iotawatt/coordinator.py new file mode 100644 index 0000000..ef89d37 --- /dev/null +++ b/custom_components/iotawatt/coordinator.py @@ -0,0 +1,76 @@ +"""IoTaWatt DataUpdateCoordinator.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import logging + +from iotawattpy.iotawatt import Iotawatt + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import httpx_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONNECTION_ERRORS + +_LOGGER = logging.getLogger(__name__) + + +class IotawattUpdater(DataUpdateCoordinator): + """Class to manage fetching update data from the IoTaWatt Energy Device.""" + + api: Iotawatt | None = None + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize IotaWattUpdater object.""" + self.entry = entry + super().__init__( + hass=hass, + logger=_LOGGER, + name=entry.title, + update_interval=timedelta(seconds=30), + ) + + self._last_run = None + self._refresh_requested = False + + def update_last_run(self, last_run: datetime): + """Notify coordinator of a sensor last update time.""" + # We want to fetch the data from the iotawatt since HA was last shutdown. + # We retrieve from the sensor last updated. + # This method is called from each sensor upon their state being restored. + if self._last_run is None or last_run > self._last_run: + self._last_run = last_run # type: ignore + + async def request_refresh(self): + """Request a refresh of the iotawatt sensors.""" + if self._refresh_requested: + return + self._refresh_requested = True + await self.async_request_refresh() + + async def _async_update_data(self): + """Fetch sensors from IoTaWatt device.""" + if self.api is None: + api = Iotawatt( + self.entry.title, + self.entry.data[CONF_HOST], + httpx_client.get_async_client(self.hass), + self.entry.data.get(CONF_USERNAME), + self.entry.data.get(CONF_PASSWORD), + ) + try: + is_authenticated = await api.connect() + except CONNECTION_ERRORS as err: + raise UpdateFailed("Connection failed") from err + + if not is_authenticated: + raise UpdateFailed("Authentication error") + + self.api = api + + await self.api.update(lastUpdate=self._last_run) + self._last_run = None + self._refresh_requested = False + return self.api.getSensors() diff --git a/custom_components/iotawatt/manifest.json b/custom_components/iotawatt/manifest.json index bca2076..8fb5b99 100644 --- a/custom_components/iotawatt/manifest.json +++ b/custom_components/iotawatt/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/iotawatt", "issue_tracker": "https://github.com/gtdiehl/iotawatt_ha", "requirements": [ - "iotawattpy==0.0.9" + "iotawattpy==0.1.0" ], "ssdp": [], "zeroconf": [], @@ -14,5 +14,6 @@ "codeowners": [ "@gtdiehl" ], - "version": "0.0.8" + "version": "0.1.0", + "iot_class": "local_polling" } diff --git a/custom_components/iotawatt/sensor.py b/custom_components/iotawatt/sensor.py index dba99d2..fd37e55 100644 --- a/custom_components/iotawatt/sensor.py +++ b/custom_components/iotawatt/sensor.py @@ -1,137 +1,266 @@ """Support for IoTaWatt Energy monitor.""" +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Callable + +from iotawattpy.sensor import Sensor -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( + DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, ) from homeassistant.core import callback -from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity, entity_registry, update_coordinator +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt -from . import IotaWattEntity -from .const import COORDINATOR, DOMAIN, SIGNAL_ADD_DEVICE +from .const import ( + ATTR_LAST_UPDATE, + DOMAIN, + VOLT_AMPERE_REACTIVE, + VOLT_AMPERE_REACTIVE_HOURS, +) +from .coordinator import IotawattUpdater _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Add sensors for passed config_entry in HA.""" +@dataclass +class IotaWattSensorEntityDescription(SensorEntityDescription): + """Class describing IotaWatt sensor entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] - entities = [] + value: Callable | None = None - for idx, ent in enumerate(coordinator.data["sensors"]): - entity = IotaWattSensor( - coordinator=coordinator, - entity=ent, - mac_address=coordinator.data["sensors"][ent].hub_mac_address, - name=coordinator.data["sensors"][ent].getName(), - ) - entities.append(entity) - async_add_entities(entities) +ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = { + "Amps": IotaWattSensorEntityDescription( + "Amps", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_CURRENT, + entity_registry_enabled_default=False, + ), + "Hz": IotaWattSensorEntityDescription( + "Hz", + native_unit_of_measurement=FREQUENCY_HERTZ, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash", + entity_registry_enabled_default=False, + ), + "PF": IotaWattSensorEntityDescription( + "PF", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_POWER_FACTOR, + value=lambda value: value * 100, + entity_registry_enabled_default=False, + ), + "Watts": IotaWattSensorEntityDescription( + "Watts", + native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_POWER, + ), + "WattHours": IotaWattSensorEntityDescription( + "WattHours", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + "VA": IotaWattSensorEntityDescription( + "VA", + native_unit_of_measurement=POWER_VOLT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash", + entity_registry_enabled_default=False, + ), + "VAR": IotaWattSensorEntityDescription( + "VAR", + native_unit_of_measurement=VOLT_AMPERE_REACTIVE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash", + entity_registry_enabled_default=False, + ), + "VARh": IotaWattSensorEntityDescription( + "VARh", + native_unit_of_measurement=VOLT_AMPERE_REACTIVE_HOURS, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:flash", + entity_registry_enabled_default=False, + ), + "Volts": IotaWattSensorEntityDescription( + "Volts", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + entity_registry_enabled_default=False, + ), +} - async def async_new_entities(sensor_info): - """Remove an entity.""" - ent = sensor_info["entity"] - hub_mac_address = sensor_info["mac_address"] - name = sensor_info["name"] - entity = IotaWattSensor( +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add sensors for passed config_entry in HA.""" + coordinator: IotawattUpdater = hass.data[DOMAIN][config_entry.entry_id] + created = set() + + @callback + def _create_entity(key: str) -> IotaWattSensor: + """Create a sensor entity.""" + created.add(key) + return IotaWattSensor( coordinator=coordinator, - entity=ent, - mac_address=hub_mac_address, - name=name, + key=key, + entity_description=ENTITY_DESCRIPTION_KEY_MAP.get( + coordinator.data["sensors"][key].getUnit(), + IotaWattSensorEntityDescription("base_sensor"), + ), ) - entities = [entity] - async_add_entities(entities) - async_dispatcher_connect(hass, SIGNAL_ADD_DEVICE, async_new_entities) + async_add_entities(_create_entity(key) for key in coordinator.data["sensors"]) + @callback + def new_data_received(): + """Check for new sensors.""" + entities = [ + _create_entity(key) + for key in coordinator.data["sensors"] + if key not in created + ] + if entities: + async_add_entities(entities) -class IotaWattSensor(IotaWattEntity): - """Defines a IoTaWatt Energy Sensor.""" + coordinator.async_add_listener(new_data_received) - def __init__(self, coordinator, entity, mac_address, name): - """Initialize the sensor.""" - super().__init__( - coordinator=coordinator, entity=entity, mac_address=mac_address, name=name - ) - sensor = self.coordinator.data["sensors"][entity] - self._ent = entity - self._name = name - self._io_type = sensor.getType() - self._state = None - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_force_update = True - - unit = sensor.getUnit() - if unit == "Watts": - self._attr_unit_of_measurement = POWER_WATT - self._attr_device_class = DEVICE_CLASS_POWER - elif unit == "WattHours": - self._attr_unit_of_measurement = ENERGY_WATT_HOUR - self._attr_device_class = DEVICE_CLASS_ENERGY - elif unit == "Volts": - self._attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT - self._attr_device_class = DEVICE_CLASS_VOLTAGE - else: - self._attr_unit_of_measurement = unit +class IotaWattSensor(update_coordinator.CoordinatorEntity, RestoreEntity, SensorEntity): + """Defines a IoTaWatt Energy Sensor.""" - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - if self._io_type == "Input": - channel = self.coordinator.data["sensors"][self._ent].getChannel() - else: - channel = "N/A" + entity_description: IotaWattSensorEntityDescription + _attr_force_update = True - attrs = {"type": self._io_type, "channel": channel} + def __init__( + self, + coordinator, + key, + entity_description: IotaWattSensorEntityDescription, + ): + """Initialize the sensor.""" + super().__init__(coordinator=coordinator) - return attrs + self._key = key + data = self._sensor_data + self._accumulating = data.getUnit() == "WattHours" and not data.getFromStart() + self._accumulated_value = None + if data.getType() == "Input": + unit = data.getUnit() + self._name_suffix + self._attr_unique_id = ( + f"{data.hub_mac_address}-input-{data.getChannel()}-{unit}" + ) + if self._accumulating: + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self.entity_description = entity_description @property - def state(self): - """Return the state of the sensor.""" - return self.coordinator.data["sensors"][self._ent].getValue() + def _sensor_data(self) -> Sensor: + """Return sensor data.""" + return self.coordinator.data["sensors"][self._key] @property - def last_reset(self): - """Return the time when the sensor was last reset, if any.""" - last_reset = self.coordinator.data["sensors"][self._ent].getBegin() - if last_reset is None: - return None - return dt.parse_datetime(last_reset) + def _name_suffix(self) -> str: + return ".accumulated" if self._accumulating else "" @property - def name(self): - """Return the name of the sensor.""" - name = ( - "IoTaWatt " - + str(self._io_type) - + " " - + str(self.coordinator.data["sensors"][self._ent].getName()) - ) - return name + def name(self) -> str | None: + """Return name of the entity.""" + return self._sensor_data.getSourceName() + self._name_suffix @property - def unique_id(self) -> str: - """Return the Uniqie ID for the sensor.""" - return self.coordinator.data["sensors"][self._ent].getSensorID() + def device_info(self) -> entity.DeviceInfo | None: + """Return device info.""" + return { + "connections": { + (CONNECTION_NETWORK_MAC, self._sensor_data.hub_mac_address) + }, + "manufacturer": "IoTaWatt", + "model": "IoTaWatt", + } @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - if self._ent not in self.coordinator.data["sensors"]: - entity_registry.async_get(self.hass).async_remove(self.entity_id) + if self._key not in self.coordinator.data["sensors"]: + if self._attr_unique_id: + entity_registry.async_get(self.hass).async_remove(self.entity_id) + else: + self.hass.async_create_task(self.async_remove()) return - super()._handle_coordinator_update() \ No newline at end of file + if self._accumulating: + assert ( + self._accumulated_value is not None + ), "async_added_to_hass must have been called first" + self._accumulated_value += float(self._sensor_data.getValue()) + + super()._handle_coordinator_update() + + @property + def extra_state_attributes(self): + """Return the extra state attributes of the entity.""" + data = self._sensor_data + attrs = {"type": data.getType()} + if attrs["type"] == "Input": + attrs["channel"] = data.getChannel() + if self._accumulating: + attrs[ + ATTR_LAST_UPDATE + ] = self.coordinator.api.getLastUpdateTime().isoformat() + + return attrs + + async def async_added_to_hass(self): + """Load the last known state value of the entity if the accumulated type.""" + await super().async_added_to_hass() + if self._accumulating: + state = await self.async_get_last_state() + self._accumulated_value = 0.0 + if state: + try: + self._accumulated_value = float(state.state) + if ATTR_LAST_UPDATE in state.attributes: + self.coordinator.update_last_run( + dt.parse_datetime(state.attributes.get(ATTR_LAST_UPDATE)) + ) + except (ValueError) as err: + _LOGGER.warning("Could not restore last state: %s", err) + # Force a second update from the iotawatt to ensure that sensors are up to date. + await self.coordinator.request_refresh() + + @property + def native_value(self) -> entity.StateType: + """Return the state of the sensor.""" + if func := self.entity_description.value: + return func(self._sensor_data.getValue()) + + if not self._accumulating: + return self._sensor_data.getValue() + if self._accumulated_value is None: + return None + return round(self._accumulated_value, 1) diff --git a/custom_components/iotawatt/strings.json b/custom_components/iotawatt/strings.json index 13f3832..f21dfe0 100644 --- a/custom_components/iotawatt/strings.json +++ b/custom_components/iotawatt/strings.json @@ -1,10 +1,8 @@ { - "title": "iotawatt", "config": { "step": { "user": { "data": { - "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]" } }, @@ -20,9 +18,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/custom_components/iotawatt/translations/ca.json b/custom_components/iotawatt/translations/ca.json new file mode 100644 index 0000000..d6a771b --- /dev/null +++ b/custom_components/iotawatt/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "El dispositiu IoTawatt necessita autenticaci\u00f3. Introdueix el nom d'usuari i la contrasenya i fes clic al bot\u00f3 Envia." + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/cs.json b/custom_components/iotawatt/translations/cs.json new file mode 100644 index 0000000..4223dcf --- /dev/null +++ b/custom_components/iotawatt/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "auth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/de.json b/custom_components/iotawatt/translations/de.json new file mode 100644 index 0000000..b1dda29 --- /dev/null +++ b/custom_components/iotawatt/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Das IoTawatt-Ger\u00e4t erfordert eine Authentifizierung. Bitte gib den Benutzernamen und das Passwort ein und klicke auf die Schaltfl\u00e4che Senden." + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/en.json b/custom_components/iotawatt/translations/en.json index 70db1d5..679fc6c 100644 --- a/custom_components/iotawatt/translations/en.json +++ b/custom_components/iotawatt/translations/en.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", @@ -18,11 +15,9 @@ }, "user": { "data": { - "host": "Host", - "name": "Name" + "host": "Host" } } } - }, - "title": "iotawatt" + } } \ No newline at end of file diff --git a/custom_components/iotawatt/translations/et.json b/custom_components/iotawatt/translations/et.json new file mode 100644 index 0000000..786e73a --- /dev/null +++ b/custom_components/iotawatt/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Viga tuvastamisel", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "auth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "IoTawatt seade n\u00f5uab tuvastamist. Sisesta kasutajanimi ja salas\u00f5na ning kl\u00f5psa nuppu Edasta." + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/pl.json b/custom_components/iotawatt/translations/pl.json new file mode 100644 index 0000000..2ea6be8 --- /dev/null +++ b/custom_components/iotawatt/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "auth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/ru.json b/custom_components/iotawatt/translations/ru.json new file mode 100644 index 0000000..d004298 --- /dev/null +++ b/custom_components/iotawatt/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e IoTawatt \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c." + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/iotawatt/translations/zh-Hant.json b/custom_components/iotawatt/translations/zh-Hant.json new file mode 100644 index 0000000..d30fb42 --- /dev/null +++ b/custom_components/iotawatt/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "IoTawatt \u88dd\u7f6e\u9700\u8981\u8a8d\u8b49\uff0c\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3001\u4e26\u9ede\u9078\u50b3\u9001\u3002" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file