Add files via upload
parent
b740c5359b
commit
e8a3f1e04d
59
README.md
59
README.md
|
|
@ -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: 5–24V 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` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue