Add files via upload

main
nero150 2026-03-18 08:34:33 +01:00 committed by GitHub
parent b740c5359b
commit e8a3f1e04d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 239 additions and 81 deletions

View File

@ -35,7 +35,7 @@ Elektroměr posílá DLMS/COSEM PUSH zprávy každých **60 sekund**. Integrace
## Instalace přes HACS ## Instalace přes HACS
1. Otevři HACS → **Integrace** → tři tečky vpravo nahoře → **Vlastní repozitáře** 1. Otevři HACS → **Integrace** → tři tečky vpravo nahoře → **Vlastní repozitáře**
2. Přidej URL tohoto repozitáře, kategorie: **Integration** (https://github.com/nero150/CEZ_rele_box) 2. Přidej URL tohoto repozitáře, kategorie: **Integration**
3. Najdi „XT211 HAN" a nainstaluj 3. Najdi „XT211 HAN" a nainstaluj
4. Restartuj Home Assistant 4. Restartuj Home Assistant
5. **Nastavení → Zařízení a služby → Přidat integraci → XT211 HAN** 5. **Nastavení → Zařízení a služby → Přidat integraci → XT211 HAN**
@ -73,22 +73,49 @@ Napájení USR-DR134: 524V DC (např. z USB adaptéru přes step-up, nebo 12V
--- ---
## Dostupné senzory ## Dostupné entity
| Název | OBIS kód | Jednotka | ### 📊 Výkon (W) okamžité hodnoty
|-------|----------|----------| | Název entity | OBIS kód |
| Active Power Consumption | `1-0:1.7.0.255` | W | |---|---|
| Active Power Delivery | `1-0:2.7.0.255` | W | | Active Power Consumption | `1-0:1.7.0.255` |
| Active Power L1 | `1-0:21.7.0.255` | W | | Active Power Consumption L1 | `1-0:21.7.0.255` |
| Active Power L2 | `1-0:41.7.0.255` | W | | Active Power Consumption L2 | `1-0:41.7.0.255` |
| Active Power L3 | `1-0:61.7.0.255` | W | | Active Power Consumption L3 | `1-0:61.7.0.255` |
| Energy Consumed | `1-0:1.8.0.255` | kWh | | Active Power Delivery | `1-0:2.7.0.255` |
| Energy Consumed T1 | `1-0:1.8.1.255` | kWh | | Active Power Delivery L1 | `1-0:22.7.0.255` |
| Energy Consumed T2 | `1-0:1.8.2.255` | kWh | | Active Power Delivery L2 | `1-0:42.7.0.255` |
| Energy Delivered | `1-0:2.8.0.255` | kWh | | Active Power Delivery L3 | `1-0:62.7.0.255` |
| Serial Number | `0-0:96.1.1.255` | | | Limiter Value | `0-0:17.0.0.255` |
| Current Tariff | `0-0:96.14.0.255` | |
| Disconnector Status | `0-0:96.3.10.255` | | ### ⚡ Energie (kWh) kumulativní
| Název entity | OBIS kód |
|---|---|
| Energy Consumed | `1-0:1.8.0.255` |
| Energy Consumed T1 | `1-0:1.8.1.255` |
| Energy Consumed T2 | `1-0:1.8.2.255` |
| Energy Consumed T3 | `1-0:1.8.3.255` |
| Energy Consumed T4 | `1-0:1.8.4.255` |
| Energy Delivered | `1-0:2.8.0.255` |
| Energy Delivered T1 | `1-0:2.8.1.255` |
| Energy Delivered T2 | `1-0:2.8.2.255` |
| Energy Delivered T3 | `1-0:2.8.3.255` |
| Energy Delivered T4 | `1-0:2.8.4.255` |
### 🔀 Binární senzory (zapnuto/vypnuto)
| Název entity | OBIS kód |
|---|---|
| Disconnector Status | `0-0:96.3.10.255` |
| Relay R1 Status | `0-1:96.3.10.255` |
| Relay R2 Status | `0-2:96.3.10.255` |
| Relay R3 Status | `0-3:96.3.10.255` |
| Relay R4 Status | `0-4:96.3.10.255` |
### 📋 Diagnostika (text)
| Název entity | OBIS kód |
|---|---|
| Serial Number | `0-0:96.1.1.255` |
| Current Tariff | `0-0:96.14.0.255` |
--- ---

View File

@ -436,23 +436,41 @@ class DLMSParser:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
OBIS_DESCRIPTIONS: dict[str, dict] = { OBIS_DESCRIPTIONS: dict[str, dict] = {
"0-0:96.1.1.255": {"name": "Serial Number", "unit": "", "class": "text"}, # --- Diagnostic / text ---
"0-0:96.3.10.255": {"name": "Disconnector Status", "unit": "", "class": "binary"}, "0-0:96.1.1.255": {"name": "Serial Number", "unit": "", "class": "text"},
"0-0:96.14.0.255": {"name": "Current Tariff", "unit": "", "class": "text"}, "0-0:96.14.0.255": {"name": "Current Tariff", "unit": "", "class": "text"},
"1-0:1.7.0.255": {"name": "Active Power Consumption", "unit": "W", "class": "power"}, "0-0:17.0.0.255": {"name": "Limiter Value", "unit": "W", "class": "power"},
"1-0:2.7.0.255": {"name": "Active Power Delivery", "unit": "W", "class": "power"},
"1-0:21.7.0.255": {"name": "Active Power L1", "unit": "W", "class": "power"}, # --- Binary (disconnector / relays) ---
"1-0:41.7.0.255": {"name": "Active Power L2", "unit": "W", "class": "power"}, "0-0:96.3.10.255": {"name": "Disconnector Status", "unit": "", "class": "binary"},
"1-0:61.7.0.255": {"name": "Active Power L3", "unit": "W", "class": "power"}, "0-1:96.3.10.255": {"name": "Relay R1 Status", "unit": "", "class": "binary"},
"1-0:1.8.0.255": {"name": "Energy Consumed", "unit": "Wh", "class": "energy"}, "0-2:96.3.10.255": {"name": "Relay R2 Status", "unit": "", "class": "binary"},
"1-0:1.8.1.255": {"name": "Energy Consumed T1", "unit": "Wh", "class": "energy"}, "0-3:96.3.10.255": {"name": "Relay R3 Status", "unit": "", "class": "binary"},
"1-0:1.8.2.255": {"name": "Energy Consumed T2", "unit": "Wh", "class": "energy"}, "0-4:96.3.10.255": {"name": "Relay R4 Status", "unit": "", "class": "binary"},
"1-0:1.8.3.255": {"name": "Energy Consumed T3", "unit": "Wh", "class": "energy"},
"1-0:1.8.4.255": {"name": "Energy Consumed T4", "unit": "Wh", "class": "energy"}, # --- Instant power consumption (odběr) ---
"1-0:2.8.0.255": {"name": "Energy Delivered", "unit": "Wh", "class": "energy"}, "1-0:1.7.0.255": {"name": "Active Power Consumption", "unit": "W", "class": "power"},
"0-1:96.3.10.255": {"name": "Relay R1 Status", "unit": "", "class": "binary"}, "1-0:21.7.0.255": {"name": "Active Power Consumption L1", "unit": "W", "class": "power"},
"0-2:96.3.10.255": {"name": "Relay R2 Status", "unit": "", "class": "binary"}, "1-0:41.7.0.255": {"name": "Active Power Consumption L2", "unit": "W", "class": "power"},
"0-3:96.3.10.255": {"name": "Relay R3 Status", "unit": "", "class": "binary"}, "1-0:61.7.0.255": {"name": "Active Power Consumption L3", "unit": "W", "class": "power"},
"0-4:96.3.10.255": {"name": "Relay R4 Status", "unit": "", "class": "binary"},
"0-0:17.0.0.255": {"name": "Limiter Value", "unit": "W", "class": "power"}, # --- Instant power delivery (dodávka / FVE) ---
"1-0:2.7.0.255": {"name": "Active Power Delivery", "unit": "W", "class": "power"},
"1-0:22.7.0.255": {"name": "Active Power Delivery L1", "unit": "W", "class": "power"},
"1-0:42.7.0.255": {"name": "Active Power Delivery L2", "unit": "W", "class": "power"},
"1-0:62.7.0.255": {"name": "Active Power Delivery L3", "unit": "W", "class": "power"},
# --- Energy consumption (odběr kWh) ---
"1-0:1.8.0.255": {"name": "Energy Consumed", "unit": "Wh", "class": "energy"},
"1-0:1.8.1.255": {"name": "Energy Consumed T1", "unit": "Wh", "class": "energy"},
"1-0:1.8.2.255": {"name": "Energy Consumed T2", "unit": "Wh", "class": "energy"},
"1-0:1.8.3.255": {"name": "Energy Consumed T3", "unit": "Wh", "class": "energy"},
"1-0:1.8.4.255": {"name": "Energy Consumed T4", "unit": "Wh", "class": "energy"},
# --- Energy delivery (dodávka kWh, FVE přetoky) ---
"1-0:2.8.0.255": {"name": "Energy Delivered", "unit": "Wh", "class": "energy"},
"1-0:2.8.1.255": {"name": "Energy Delivered T1", "unit": "Wh", "class": "energy"},
"1-0:2.8.2.255": {"name": "Energy Delivered T2", "unit": "Wh", "class": "energy"},
"1-0:2.8.3.255": {"name": "Energy Delivered T3", "unit": "Wh", "class": "energy"},
"1-0:2.8.4.255": {"name": "Energy Delivered T4", "unit": "Wh", "class": "energy"},
} }

View File

@ -1,10 +1,20 @@
"""Sensor platform for XT211 HAN integration.""" """Sensor platform for XT211 HAN integration.
Registers three types of entities:
- Numeric sensors (power, energy)
- Text sensors (serial number, tariff, limiter)
- Binary sensors (disconnector, relays)
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -13,6 +23,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
EntityCategory,
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
) )
@ -27,7 +38,7 @@ from .dlms_parser import OBIS_DESCRIPTIONS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Map OBIS "class" strings → HA SensorDeviceClass + StateClass + unit # Map OBIS "class" → HA SensorDeviceClass + StateClass + unit
SENSOR_META: dict[str, dict] = { SENSOR_META: dict[str, dict] = {
"power": { "power": {
"device_class": SensorDeviceClass.POWER, "device_class": SensorDeviceClass.POWER,
@ -37,7 +48,7 @@ SENSOR_META: dict[str, dict] = {
"energy": { "energy": {
"device_class": SensorDeviceClass.ENERGY, "device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL_INCREASING, "state_class": SensorStateClass.TOTAL_INCREASING,
"unit": UnitOfEnergy.WATT_HOUR, "unit": UnitOfEnergy.KILO_WATT_HOUR,
}, },
"sensor": { "sensor": {
"device_class": None, "device_class": None,
@ -46,8 +57,26 @@ SENSOR_META: dict[str, dict] = {
}, },
} }
# OBIS codes that are NOT numeric sensors (text / binary) handled separately # OBIS codes that send text values (not numeric)
NON_SENSOR_CLASSES = {"text", "binary"} TEXT_OBIS = {"0-0:96.1.1.255", "0-0:96.14.0.255"}
# OBIS codes that are binary (on/off)
BINARY_OBIS = {
"0-0:96.3.10.255",
"0-1:96.3.10.255",
"0-2:96.3.10.255",
"0-3:96.3.10.255",
"0-4:96.3.10.255",
}
def _device_info(entry: ConfigEntry) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=entry.data.get(CONF_NAME, "XT211 HAN"),
manufacturer="Sagemcom",
model="XT211 AMM",
)
async def async_setup_entry( async def async_setup_entry(
@ -55,46 +84,52 @@ async def async_setup_entry(
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up XT211 HAN sensors from a config entry.""" """Set up all XT211 HAN entities from a config entry."""
coordinator: XT211Coordinator = hass.data[DOMAIN][entry.entry_id] coordinator: XT211Coordinator = hass.data[DOMAIN][entry.entry_id]
# We create entities for all known OBIS codes upfront. entities: list = []
# Unknown codes that arrive later will be added dynamically.
entities: list[XT211SensorEntity] = []
registered_obis: set[str] = set() registered_obis: set[str] = set()
for obis, meta in OBIS_DESCRIPTIONS.items(): for obis, meta in OBIS_DESCRIPTIONS.items():
if meta.get("class") in NON_SENSOR_CLASSES:
continue
entities.append(
XT211SensorEntity(coordinator, entry, obis, meta)
)
registered_obis.add(obis) registered_obis.add(obis)
if obis in BINARY_OBIS:
entities.append(XT211BinarySensorEntity(coordinator, entry, obis, meta))
elif obis in TEXT_OBIS:
entities.append(XT211TextSensorEntity(coordinator, entry, obis, meta))
else:
entities.append(XT211SensorEntity(coordinator, entry, obis, meta))
async_add_entities(entities) async_add_entities(entities)
@callback # Dynamically register any unknown OBIS codes that arrive at runtime
def _handle_new_obis(obis: str, data: dict) -> None:
"""Dynamically add sensor for a previously unknown OBIS code."""
if obis in registered_obis:
return
if data.get("class") in NON_SENSOR_CLASSES:
return
registered_obis.add(obis)
async_add_entities([XT211SensorEntity(coordinator, entry, obis, data)])
# Subscribe to coordinator updates to detect new OBIS codes
@callback @callback
def _on_update() -> None: def _on_update() -> None:
if coordinator.data: if not coordinator.data:
for obis, data in coordinator.data.items(): return
_handle_new_obis(obis, data) new: list = []
for obis, data in coordinator.data.items():
if obis in registered_obis:
continue
registered_obis.add(obis)
_LOGGER.info("XT211: discovered new OBIS code %s adding entity", obis)
if obis in BINARY_OBIS:
new.append(XT211BinarySensorEntity(coordinator, entry, obis, data))
elif obis in TEXT_OBIS:
new.append(XT211TextSensorEntity(coordinator, entry, obis, data))
else:
new.append(XT211SensorEntity(coordinator, entry, obis, data))
if new:
async_add_entities(new)
coordinator.async_add_listener(_on_update) coordinator.async_add_listener(_on_update)
# ---------------------------------------------------------------------------
# Numeric sensor
# ---------------------------------------------------------------------------
class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity): class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
"""A single numeric sensor entity backed by an OBIS code.""" """Numeric sensor (power / energy / generic)."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -107,7 +142,6 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
) -> None: ) -> None:
super().__init__(coordinator) super().__init__(coordinator)
self._obis = obis self._obis = obis
self._meta = meta
self._entry = entry self._entry = entry
sensor_type = meta.get("class", "sensor") sensor_type = meta.get("class", "sensor")
@ -118,33 +152,20 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
self._attr_device_class = sm["device_class"] self._attr_device_class = sm["device_class"]
self._attr_state_class = sm["state_class"] self._attr_state_class = sm["state_class"]
self._attr_native_unit_of_measurement = sm["unit"] or meta.get("unit") self._attr_native_unit_of_measurement = sm["unit"] or meta.get("unit")
self._wh_to_kwh = (sensor_type == "energy")
# Energy sensors: convert Wh → kWh for HA Energy dashboard
if sensor_type == "energy":
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
self._wh_to_kwh = True
else:
self._wh_to_kwh = False
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
return DeviceInfo( return _device_info(self._entry)
identifiers={(DOMAIN, self._entry.entry_id)},
name=self._entry.data.get(CONF_NAME, "XT211 HAN"),
manufacturer="Sagemcom",
model="XT211 AMM",
)
@property @property
def native_value(self) -> float | None: def native_value(self) -> float | None:
if self.coordinator.data is None: if not self.coordinator.data:
return None return None
obj = self.coordinator.data.get(self._obis) obj = self.coordinator.data.get(self._obis)
if obj is None: if obj is None:
return None return None
raw = obj.get("value") raw = obj.get("value")
if raw is None:
return None
try: try:
val = float(raw) val = float(raw)
if self._wh_to_kwh: if self._wh_to_kwh:
@ -156,3 +177,95 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
@property @property
def available(self) -> bool: def available(self) -> bool:
return self.coordinator.connected and self.coordinator.data is not None return self.coordinator.connected and self.coordinator.data is not None
# ---------------------------------------------------------------------------
# Text sensor
# ---------------------------------------------------------------------------
class XT211TextSensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
"""Text sensor (serial number, tariff)."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self,
coordinator: XT211Coordinator,
entry: ConfigEntry,
obis: str,
meta: dict,
) -> None:
super().__init__(coordinator)
self._obis = obis
self._entry = entry
self._attr_unique_id = f"{entry.entry_id}_{obis}"
self._attr_name = meta.get("name", obis)
self._attr_device_class = None
self._attr_state_class = None
self._attr_native_unit_of_measurement = None
@property
def device_info(self) -> DeviceInfo:
return _device_info(self._entry)
@property
def native_value(self) -> str | None:
if not self.coordinator.data:
return None
obj = self.coordinator.data.get(self._obis)
if obj is None:
return None
val = obj.get("value")
return str(val) if val is not None else None
@property
def available(self) -> bool:
return self.coordinator.connected and self.coordinator.data is not None
# ---------------------------------------------------------------------------
# Binary sensor
# ---------------------------------------------------------------------------
class XT211BinarySensorEntity(CoordinatorEntity[XT211Coordinator], BinarySensorEntity):
"""Binary sensor (disconnector / relay status)."""
_attr_has_entity_name = True
_attr_device_class = BinarySensorDeviceClass.PLUG
def __init__(
self,
coordinator: XT211Coordinator,
entry: ConfigEntry,
obis: str,
meta: dict,
) -> None:
super().__init__(coordinator)
self._obis = obis
self._entry = entry
self._attr_unique_id = f"{entry.entry_id}_{obis}"
self._attr_name = meta.get("name", obis)
@property
def device_info(self) -> DeviceInfo:
return _device_info(self._entry)
@property
def is_on(self) -> bool | None:
if not self.coordinator.data:
return None
obj = self.coordinator.data.get(self._obis)
if obj is None:
return None
val = obj.get("value")
if isinstance(val, bool):
return val
try:
return int(val) != 0
except (TypeError, ValueError):
return None
@property
def available(self) -> bool:
return self.coordinator.connected and self.coordinator.data is not None