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
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
4. Restartuj Home Assistant
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 |
|-------|----------|----------|
| Active Power Consumption | `1-0:1.7.0.255` | W |
| Active Power Delivery | `1-0:2.7.0.255` | W |
| Active Power L1 | `1-0:21.7.0.255` | W |
| Active Power L2 | `1-0:41.7.0.255` | W |
| Active Power L3 | `1-0:61.7.0.255` | W |
| Energy Consumed | `1-0:1.8.0.255` | kWh |
| Energy Consumed T1 | `1-0:1.8.1.255` | kWh |
| Energy Consumed T2 | `1-0:1.8.2.255` | kWh |
| Energy Delivered | `1-0:2.8.0.255` | kWh |
| Serial Number | `0-0:96.1.1.255` | |
| Current Tariff | `0-0:96.14.0.255` | |
| Disconnector Status | `0-0:96.3.10.255` | |
### 📊 Výkon (W) okamžité hodnoty
| Název entity | OBIS kód |
|---|---|
| Active Power Consumption | `1-0:1.7.0.255` |
| Active Power Consumption L1 | `1-0:21.7.0.255` |
| Active Power Consumption L2 | `1-0:41.7.0.255` |
| Active Power Consumption L3 | `1-0:61.7.0.255` |
| Active Power Delivery | `1-0:2.7.0.255` |
| Active Power Delivery L1 | `1-0:22.7.0.255` |
| Active Power Delivery L2 | `1-0:42.7.0.255` |
| Active Power Delivery L3 | `1-0:62.7.0.255` |
| Limiter Value | `0-0:17.0.0.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] = {
"0-0:96.1.1.255": {"name": "Serial Number", "unit": "", "class": "text"},
"0-0:96.3.10.255": {"name": "Disconnector Status", "unit": "", "class": "binary"},
"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"},
"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"},
"1-0:41.7.0.255": {"name": "Active Power L2", "unit": "W", "class": "power"},
"1-0:61.7.0.255": {"name": "Active Power L3", "unit": "W", "class": "power"},
"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"},
"1-0:2.8.0.255": {"name": "Energy Delivered", "unit": "Wh", "class": "energy"},
"0-1:96.3.10.255": {"name": "Relay R1 Status", "unit": "", "class": "binary"},
"0-2:96.3.10.255": {"name": "Relay R2 Status", "unit": "", "class": "binary"},
"0-3:96.3.10.255": {"name": "Relay R3 Status", "unit": "", "class": "binary"},
"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"},
# --- Diagnostic / text ---
"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:17.0.0.255": {"name": "Limiter Value", "unit": "W", "class": "power"},
# --- Binary (disconnector / relays) ---
"0-0:96.3.10.255": {"name": "Disconnector Status", "unit": "", "class": "binary"},
"0-1:96.3.10.255": {"name": "Relay R1 Status", "unit": "", "class": "binary"},
"0-2:96.3.10.255": {"name": "Relay R2 Status", "unit": "", "class": "binary"},
"0-3:96.3.10.255": {"name": "Relay R3 Status", "unit": "", "class": "binary"},
"0-4:96.3.10.255": {"name": "Relay R4 Status", "unit": "", "class": "binary"},
# --- Instant power consumption (odběr) ---
"1-0:1.7.0.255": {"name": "Active Power Consumption", "unit": "W", "class": "power"},
"1-0:21.7.0.255": {"name": "Active Power Consumption L1", "unit": "W", "class": "power"},
"1-0:41.7.0.255": {"name": "Active Power Consumption L2", "unit": "W", "class": "power"},
"1-0:61.7.0.255": {"name": "Active Power Consumption L3", "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
import logging
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@ -13,6 +23,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
)
@ -27,7 +38,7 @@ from .dlms_parser import OBIS_DESCRIPTIONS
_LOGGER = logging.getLogger(__name__)
# Map OBIS "class" strings → HA SensorDeviceClass + StateClass + unit
# Map OBIS "class" → HA SensorDeviceClass + StateClass + unit
SENSOR_META: dict[str, dict] = {
"power": {
"device_class": SensorDeviceClass.POWER,
@ -37,7 +48,7 @@ SENSOR_META: dict[str, dict] = {
"energy": {
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL_INCREASING,
"unit": UnitOfEnergy.WATT_HOUR,
"unit": UnitOfEnergy.KILO_WATT_HOUR,
},
"sensor": {
"device_class": None,
@ -46,8 +57,26 @@ SENSOR_META: dict[str, dict] = {
},
}
# OBIS codes that are NOT numeric sensors (text / binary) handled separately
NON_SENSOR_CLASSES = {"text", "binary"}
# OBIS codes that send text values (not numeric)
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(
@ -55,46 +84,52 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> 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]
# We create entities for all known OBIS codes upfront.
# Unknown codes that arrive later will be added dynamically.
entities: list[XT211SensorEntity] = []
entities: list = []
registered_obis: set[str] = set()
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)
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)
@callback
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
# Dynamically register any unknown OBIS codes that arrive at runtime
@callback
def _on_update() -> None:
if coordinator.data:
for obis, data in coordinator.data.items():
_handle_new_obis(obis, data)
if not coordinator.data:
return
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)
# ---------------------------------------------------------------------------
# Numeric sensor
# ---------------------------------------------------------------------------
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
@ -107,7 +142,6 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
) -> None:
super().__init__(coordinator)
self._obis = obis
self._meta = meta
self._entry = entry
sensor_type = meta.get("class", "sensor")
@ -118,33 +152,20 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
self._attr_device_class = sm["device_class"]
self._attr_state_class = sm["state_class"]
self._attr_native_unit_of_measurement = sm["unit"] or meta.get("unit")
# 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
self._wh_to_kwh = (sensor_type == "energy")
@property
def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
name=self._entry.data.get(CONF_NAME, "XT211 HAN"),
manufacturer="Sagemcom",
model="XT211 AMM",
)
return _device_info(self._entry)
@property
def native_value(self) -> float | None:
if self.coordinator.data is None:
if not self.coordinator.data:
return None
obj = self.coordinator.data.get(self._obis)
if obj is None:
return None
raw = obj.get("value")
if raw is None:
return None
try:
val = float(raw)
if self._wh_to_kwh:
@ -156,3 +177,95 @@ class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
@property
def available(self) -> bool:
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