diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 8daddf2..0000000 --- a/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN - -async def async_setup(hass: HomeAssistant, config: ConfigType): - return True - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - await hass.config_entries.async_forward_entry_setups(entry, ["sensor"]) - return True - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - return await hass.config_entries.async_unload_platforms(entry, ["sensor"]) diff --git a/config_flow.py b/config_flow.py index 956374f..4253e5d 100644 --- a/config_flow.py +++ b/config_flow.py @@ -1,21 +1,93 @@ -from homeassistant import config_entries +"""Config flow for Linky Tariff integration.""" +from __future__ import annotations + +import logging import voluptuous as vol -from .const import DOMAIN, CONF_IEEE, CONF_INTERVAL, DEFAULT_INTERVAL +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr + +from .const import ( + DOMAIN, + CONF_IEEE, + CONF_POLL_INTERVAL, + DEFAULT_POLL_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + +async def async_get_devices(hass): + """Get ZHA devices.""" + device_registry = dr.async_get(hass) + return [ + entry + for entry in device_registry.devices.values() + if entry.via_device_id is not None and "zha" in entry.via_device_id + ] + +class LinkyTariffConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Linky Tariff.""" + + VERSION = 1 -class LinkyTarifConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): + """Handle the initial step.""" errors = {} + devices = await async_get_devices(self.hass) + + if not devices: + return self.async_abort(reason="no_zha_devices") if user_input is not None: - return self.async_create_entry(title="Linky Tarif", data={ - CONF_IEEE: user_input[CONF_IEEE], - }, options={ - CONF_INTERVAL: user_input.get(CONF_INTERVAL, DEFAULT_INTERVAL) - }) + await self.async_set_unique_id(user_input[CONF_IEEE]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Linky Tariff", data=user_input) - schema = vol.Schema({ - vol.Required(CONF_IEEE): str, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): int, - }) - return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + device_options = { + device.id: f"{device.name_by_user or device.name} ({device.id})" + for device in devices + } + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({ + vol.Required(CONF_IEEE): vol.In(device_options), + vol.Optional( + CONF_POLL_INTERVAL, + default=DEFAULT_POLL_INTERVAL + ): int, + }), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return LinkyTariffOptionsFlow(config_entry) + +class LinkyTariffOptionsFlow(config_entries.OptionsFlow): + """Handle options flow for Linky Tariff.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema({ + vol.Optional( + CONF_POLL_INTERVAL, + default=self.config_entry.options.get( + CONF_POLL_INTERVAL, DEFAULT_POLL_INTERVAL + ), + ): int, + }), + ) \ No newline at end of file diff --git a/const.py b/const.py index 62de2dc..c287837 100644 --- a/const.py +++ b/const.py @@ -1,4 +1,13 @@ +"""Constants for the Linky Tariff integration.""" +from homeassistant.const import Platform + DOMAIN = "linky_tarif" +PLATFORMS = [Platform.SENSOR] + CONF_IEEE = "ieee" -CONF_INTERVAL = "interval" -DEFAULT_INTERVAL = 300 +CONF_POLL_INTERVAL = "poll_interval" +DEFAULT_POLL_INTERVAL = 60 + +CLUSTER_ID = 0xFF66 +ATTRIBUTE_ID = 0x0010 +ENDPOINT_ID = 1 \ No newline at end of file diff --git a/init.py b/init.py new file mode 100644 index 0000000..bcc0367 --- /dev/null +++ b/init.py @@ -0,0 +1,94 @@ +"""The Linky Tariff integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IEEE, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + DOMAIN, + CONF_POLL_INTERVAL, + CLUSTER_ID, + ATTRIBUTE_ID, + ENDPOINT_ID, + PLATFORMS, +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Linky Tariff from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + coordinator = LinkyTariffCoordinator( + hass, + entry.data[CONF_IEEE], + entry.options.get(CONF_POLL_INTERVAL, entry.data.get(CONF_POLL_INTERVAL, 60)), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(async_update_options)) + + return True + +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 + +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + +class LinkyTariffCoordinator(DataUpdateCoordinator): + """Class to manage fetching Linky Tariff data.""" + + def __init__(self, hass: HomeAssistant, ieee: str, poll_interval: int) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=poll_interval, + ) + self.ieee = ieee + self._attr = None + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch data from ZHA device.""" + try: + result = await self.hass.services.async_call( + "zha_toolkit", + "attr_read", + { + "ieee": self.ieee, + "endpoint_id": ENDPOINT_ID, + "cluster_id": CLUSTER_ID, + "attribute_id": ATTRIBUTE_ID, + "manufacturer": None, + }, + blocking=True, + return_response=True, + ) + + if result and "value" in result: + return { + "value": result["value"], + "last_update": self.hass.config.time_utc(), + } + return {"value": "unknown", "last_update": self.hass.config.time_utc()} + + except Exception as ex: + _LOGGER.error("Error reading Linky tariff: %s", ex) + return {"value": "error", "last_update": self.hass.config.time_utc()} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 3077084..cbbff27 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,12 @@ { "domain": "linky_tarif", - "name": "Linky Tarif Sensor", + "name": "Linky Tariff Period", "version": "1.0.0", "config_flow": true, "documentation": "https://git.flavien.ovh/flavien/linky_tarif", + "codeowners": ["@flavien"], "requirements": [], - "dependencies": ["zha"] + "iot_class": "local_polling", + "loggers": ["zha_toolkit"], + "integration_type": "device" } \ No newline at end of file diff --git a/sensor.py b/sensor.py index 19ca0ff..a6b752b 100644 --- a/sensor.py +++ b/sensor.py @@ -1,44 +1,46 @@ +"""Sensor platform for Linky Tariff integration.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .coordinator import LinkyTarifCoordinator -from .const import DOMAIN, CONF_IEEE, CONF_INTERVAL +from .const import DOMAIN -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback): - coordinator = LinkyTarifCoordinator( - hass, - ieee=entry.data[CONF_IEEE], - interval=entry.options.get(CONF_INTERVAL, 300), - ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Linky Tariff sensor.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([LinkyTariffSensor(coordinator)]) - try: - await coordinator.async_refresh() - if coordinator.last_update_success is False: - raise ConfigEntryNotReady - except Exception: - raise ConfigEntryNotReady +class LinkyTariffSensor(CoordinatorEntity, SensorEntity): + """Representation of a Linky Tariff sensor.""" - async_add_entities([LinkyTarifSensor(coordinator)]) + _attr_icon = "mdi:lightning-bolt" + _attr_has_entity_name = True + _attr_name = "Linky Tariff Period" -class LinkyTarifSensor(SensorEntity): - def __init__(self, coordinator: LinkyTarifCoordinator): - self.coordinator = coordinator - self._attr_unique_id = f"linky_tarif_{coordinator._ieee}" - self._attr_name = "Linky Tarif Period" - self._attr_icon = "mdi:flash" - self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, coordinator._ieee)}) + def __init__(self, coordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.ieee}_tariff_period" + self._attr_device_info = { + "identifiers": {(DOMAIN, coordinator.ieee)}, + } @property - def native_value(self): - return self.coordinator.data.get("tariff") if self.coordinator.data else None + def native_value(self) -> str: + """Return the state of the sensor.""" + return self.coordinator.data["value"] @property - def extra_state_attributes(self): - return {"last_update": self.coordinator.data.get("last_update")} if self.coordinator.data else {} - - async def async_update(self): - await self.coordinator.async_request_refresh() + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return { + "last_update": self.coordinator.data["last_update"], + } \ No newline at end of file diff --git a/translations/en.json b/translations/en.json index ae15dc2..ab79430 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2,13 +2,15 @@ "config": { "step": { "user": { - "title": "Configure Linky Tarif", - "description": "Enter the IEEE address of your ZLinky_TIC device.", + "title": "Configure Linky Tariff", "data": { - "ieee": "Device IEEE address", - "interval": "Polling interval (seconds)" + "ieee": "ZHA Device", + "poll_interval": "Polling Interval (seconds)" } } + }, + "abort": { + "no_zha_devices": "No ZHA devices found. Please set up ZHA first." } } -} +} \ No newline at end of file