From f85beb29e177f640cbb236724376de76cec3ffa1 Mon Sep 17 00:00:00 2001 From: Flavien Haas Date: Fri, 25 Apr 2025 20:01:50 +0200 Subject: [PATCH] first commit --- __init__.py | 16 +++++++++++++++ config_flow.py | 21 ++++++++++++++++++++ const.py | 4 ++++ coordinator.py | 46 +++++++++++++++++++++++++++++++++++++++++++ icon.png | Bin 0 -> 1459 bytes manifest.json | 10 ++++++++++ sensor.py | 36 +++++++++++++++++++++++++++++++++ strings.json | 13 ++++++++++++ translations/en.json | 14 +++++++++++++ 9 files changed, 160 insertions(+) create mode 100644 __init__.py create mode 100644 config_flow.py create mode 100644 const.py create mode 100644 coordinator.py create mode 100644 icon.png create mode 100644 manifest.json create mode 100644 sensor.py create mode 100644 strings.json create mode 100644 translations/en.json diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ed00a87 --- /dev/null +++ b/__init__.py @@ -0,0 +1,16 @@ +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): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..956374f --- /dev/null +++ b/config_flow.py @@ -0,0 +1,21 @@ +from homeassistant import config_entries +import voluptuous as vol + +from .const import DOMAIN, CONF_IEEE, CONF_INTERVAL, DEFAULT_INTERVAL + +class LinkyTarifConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + async def async_step_user(self, user_input=None): + errors = {} + + 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) + }) + + 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) diff --git a/const.py b/const.py new file mode 100644 index 0000000..62de2dc --- /dev/null +++ b/const.py @@ -0,0 +1,4 @@ +DOMAIN = "linky_tarif" +CONF_IEEE = "ieee" +CONF_INTERVAL = "interval" +DEFAULT_INTERVAL = 300 diff --git a/coordinator.py b/coordinator.py new file mode 100644 index 0000000..2cbec9f --- /dev/null +++ b/coordinator.py @@ -0,0 +1,46 @@ +from datetime import timedelta +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from zigpy import types +import logging + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +class LinkyTarifCoordinator(DataUpdateCoordinator): + def __init__(self, hass: HomeAssistant, ieee: str, interval: int): + super().__init__( + hass, + _LOGGER, + name="Linky Tarif Coordinator", + update_interval=timedelta(seconds=interval), + ) + self._ieee = ieee + + async def _async_update_data(self): + zha_storage = self.hass.data["zha"] + app_ctrl = zha_storage.gateway.application + + device = app_ctrl.get_device(types.EUI64.convert(self._ieee)) + if not device: + raise UpdateFailed(f"Device with IEEE {self._ieee} not found") + + endpoint = device.endpoints.get(1) + if not endpoint: + raise UpdateFailed("Endpoint 1 not found") + + cluster = endpoint[0xFF66] + if not cluster: + raise UpdateFailed("Cluster 0xFF66 not found") + + try: + res = await cluster.read_attributes([0x0010]) + value = res.get(0x0010) + return { + "tariff": value, + "last_update": self.hass.helpers.event.dt_util.utcnow().isoformat(), + } + except Exception as e: + raise UpdateFailed(f"Error reading tariff: {e}") diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2064f84f9428ee467c04cb85dc3ad3471209f8 GIT binary patch literal 1459 zcmY*ZYfuwc6ux`il!Xw<0z!~i?BWA!b%7`jI3_|^4dqpJkg+YqR%5h7O|?Y?OgGR9 zVpKp;M$uvDR2{__kPdc4Ocl@}h)HoAwW-Dulq!!{VF*~Dd(&V2wda24+;hJ3?K$^g zM%r3|*IX|EfIzWMz8(OKGciCw?$axjMghPpP{`M8%#!+fGQ&OgcOm_0pna!)Xw zP}vfycxXL)m4En_p+7gCSG(+_`Cwzu%eRMezGaxnk%494NCa!m&Y6}@2qY1p-c#9} z{?(hqCK`%Qfd$*}P9d=3G6~jg{>%apc20#C_;DB=Pg$x#*#Z{oWm4qWNy9S)t43y* zCh$QC@+IqKdk|0_1#>+)--SEGe?{V^b1jv4M%DGYOq`}DS`RKp!&E}Ko>6*Z{wlm} zA)=B!J#P#{)l=>fqYDs{_p~%`K0D_c|4Ay~iF8`rNKnS3sMOq6pCLJr`^kcsIuA^k z3aazNpVWfPzNKnE%>N7giLa?xZ+eMm93|E3z?ac5+?VSjqspgaGl`69Qk{UVZ+Tj( zO$Uee1deho#lRe2)1=hVFNk7I$P1 z(dzDY|I^YoF-SXazb?k)G`vr+(&)7^5UDpMLi-gH$^Dz29p;rYNru-!6ON#=*dSQ|RU)5i=cLcdeAeY&(a?C`Je2iS{N$3OezhmgiH zU>o~8MGkCShb_mqv+2VHf+R7&!c5fG!~DXwlhVgFz870KJlOlfsU0c;9r>Wc5)yO6Y12@p84Qi23DKKh-*N;csq_eOUb(tAdJ5+d@*}dZ_4C{ z2r=izTLfVbXOx#-dB{O)(?8tl1o`OVD2D!}lO-! zpiB*2XpET)^k>=I4sKMQGUXiFD;Cuz4%qg(R@l zkQb8BI2ST?LM$Y4)-j{ZVaeEx3GazpMN_^72$|dB$|6c-;?u9#`Z=|bvKF7W;jIvaq zt5tg02Pmie@!=RO>L!ZPkr26z{UhY)ih<8iF(agm?qW$I-F}p-;WjZS2K7@u6`}2X T$5v}G_ZS03QkvY5pe+6u%RUjD literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..a0407b7 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "linky_tarif", + "name": "Linky Tarif Sensor", + "version": "1.0.0", + "config_flow": true, + "documentation": "https://github.com/yourusername/linky_tarif", + "requirements": [], + "dependencies": ["zha"], + "codeowners": ["@yourusername"] +} diff --git a/sensor.py b/sensor.py new file mode 100644 index 0000000..f15e159 --- /dev/null +++ b/sensor.py @@ -0,0 +1,36 @@ +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 .coordinator import LinkyTarifCoordinator +from .const import DOMAIN, CONF_IEEE, CONF_INTERVAL + +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), + ) + await coordinator.async_config_entry_first_refresh() + async_add_entities([LinkyTarifSensor(coordinator)]) + +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)}) + + @property + def native_value(self): + return self.coordinator.data.get("tariff") if self.coordinator.data else None + + @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() diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..eac31d7 --- /dev/null +++ b/strings.json @@ -0,0 +1,13 @@ +{ + "domain": "linky_tarif", + "name": "Linky Tarif", + "config_flow": { + "title": "Linky Tarif", + "description": "Monitors Linky energy meter tariff periods using ZHA.", + "step": { + "user": { + "title": "Configure Linky Tarif" + } + } + } +} diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..ae15dc2 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Linky Tarif", + "description": "Enter the IEEE address of your ZLinky_TIC device.", + "data": { + "ieee": "Device IEEE address", + "interval": "Polling interval (seconds)" + } + } + } + } +}