1 Commits
13 changed files with 1275 additions and 9 deletions
+58 -6
View File
@@ -46,8 +46,12 @@ When you change the target temperature, it writes to the appropriate setpoint ba
│ __init__.py Entry point, platform setup │
│ coordinator.py Data polling, setpoint syncing │
│ climate.py Climate entities (thermostats) │
│ switch.py Season switch entity │
│ sensor.py Setpoint diagnostic sensors │
│ humidifier.py Dehumidifier entities (humidity ctrl) │
│ switch.py Season + system activation switches │
│ select.py Zone mode select entities │
│ sensor.py Setpoint, temperature, dew point │
│ binary_sensor.py Pump active sensors │
│ number.py Time setting entities │
│ modbus_client.py Async Modbus TCP communication │
│ config_flow.py UI configuration wizard │
│ const.py Constants and register addresses │
@@ -68,21 +72,42 @@ When you change the target temperature, it writes to the appropriate setpoint ba
| `__init__.py` | Integration setup, platform loading, lifecycle management |
| `coordinator.py` | `RDZDataUpdateCoordinator` - polls Modbus every 30s, syncs linked thermostats |
| `climate.py` | `RDZClimateEntity` - thermostat entities with season-based HVAC mode |
| `switch.py` | `RDZSeasonSwitch` - global summer/winter mode control |
| `sensor.py` | `RDZSetpointSensor` - diagnostic sensors showing both setpoints |
| `humidifier.py` | `RDZDehumidifierEntity` - dehumidifier control with humidity setpoints |
| `switch.py` | `RDZSeasonSwitch` + `RDZSystemActivationSwitch` - season and system control |
| `select.py` | `RDZZoneModeSelect` - zone mode selection (Off/Man/Pgm/Pgm-Man) |
| `sensor.py` | `RDZSetpointSensor`, `RDZDewPointSensor`, `RDZOutsideTemperatureSensor`, `RDZWaterTemperatureSensor` |
| `binary_sensor.py` | `RDZPumpActiveBinarySensor` - pump running status |
| `number.py` | `RDZTimeSettingNumber` - system time configuration |
| `modbus_client.py` | `RDZModbusClient` - async Modbus TCP wrapper with connection management |
| `config_flow.py` | Configuration UI for adding/configuring the integration |
| `const.py` | Constants: register addresses, defaults, config keys |
## Modbus Register Map
### Zone Registers
| Register/Coil | Address | Description |
|---------------|---------|-------------|
| Coil 2 | 2 | Season (0=winter, 1=summer) |
| Registers 300-363 | 300 + zone_id | Winter setpoints (value * 10) |
| Registers 364-427 | 364 + zone_id | Summer setpoints (value * 10) |
| Registers 428-491 | 428 + zone_id | Dehumidification setpoints (raw %, no scaling) |
| Registers 2700-2763 | 2700 + zone_id | Current temperatures (value * 10, read-only) |
| Registers 2828-2891 | 2828 + zone_id | Dew point temperatures (value * 10, read-only) |
| Registers 2892-2895 | 2892 + (zone_id / 16) | Zone activity bitmasks (16-bit, read-only) |
| Registers 5301-5364 | 5301 + zone_id | Zone mode (0=Off, 1=Man, 2=Pgm, 3=Pgm/Man) |
| Registers 7701-7764 | 7701 + zone_id | Current humidity (value * 10, read-only) |
### System Registers
| Register/Coil | Address | Description |
|---------------|---------|-------------|
| Coils 100-107 | 100 + (system_id - 1) | System activation (1-8) |
| Register 2600 | 2600 | Outside temperature (value * 10, read-only) |
| Registers 2650-2657 | 2650 + (system_id - 1) | Delivery water temperatures 1-8 (value * 10, read-only) |
| Registers 2658-2665 | 2658 + (system_id - 1) | Calculated water temperatures 1-8 (value * 10, read-only) |
| Registers 5009-5013 | 5009-5013 | Time settings: Day, Month, Year, Hour, Minute |
| Register 7615 | 7615 | Pump active bitmask (bit 0 = pump 1, read-only) |
Temperature values are stored as integers with a scale factor of 10 (e.g., 21.5°C = 215).
@@ -96,6 +121,16 @@ Registers 2892-2895 contain 16-bit bitmasks indicating which zones are actively
Bit = 1: Zone is actively heating/cooling
Bit = 0: Zone is idle
### Pump Active Bitmask
Register 7615 contains an 8-bit bitmask indicating which pumps are currently running:
- Bit 0: Pump 1 active
- Bit 1: Pump 2 active
- ...
- Bit 7: Pump 8 active
Bit = 1: Pump is running
Bit = 0: Pump is idle
## Data Flow
### Reading Data (Coordinator)
@@ -103,8 +138,18 @@ Bit = 0: Zone is idle
- All temperature registers (2700-2763)
- Winter setpoints (300-363)
- Summer setpoints (364-427)
- Dehumidification setpoints (428-491)
- Humidity values (7701-7764)
- Dew point temperatures (2828-2891)
- Zone modes (5301-5364)
- Season coil (2)
- Zone activity bitmasks (2892-2895)
- Outside temperature (2600)
- Time settings (5009-5013)
- System activation coils (100-107)
- Delivery water temperatures (2650-2657)
- Calculated water temperatures (2658-2665)
- Pump active bitmask (7615)
2. Data stored in `coordinator.data` dictionary
3. Entities update via `_handle_coordinator_update()`
@@ -135,9 +180,16 @@ For each configured zone:
- 1x Climate entity (thermostat)
- 1x Winter setpoint sensor (diagnostic)
- 1x Summer setpoint sensor (diagnostic)
- 1x Dew point sensor (diagnostic)
System-level:
- 1x Season switch
System-level (on the "RDZ HMI System" device):
- 1x Season switch (summer/winter mode)
- 8x System activation switches (System 1-8)
- 1x Outside temperature sensor
- 8x Delivery water temperature sensors (1-8)
- 8x Calculated water temperature sensors (1-8)
- 8x Pump active binary sensors (1-8)
- 5x Time setting number entities (Day, Month, Year, Hour, Minute)
## Configuration
+24
View File
@@ -11,6 +11,11 @@ A custom Home Assistant integration for controlling RDZ Graphic Tablet (HMI) the
- Global season switch to toggle between heating and cooling
- Automatic setpoint synchronization between real and virtual thermostats
- Real-time temperature monitoring
- Outside temperature sensor
- Water temperature monitoring (delivery and calculated temperatures for 8 systems)
- Pump status monitoring (8 pumps)
- System activation control (8 systems)
- System time configuration (day, month, year, hour, minute)
## Prerequisites
@@ -125,6 +130,25 @@ When you link a real thermostat to a virtual one:
- The virtual thermostat's setpoint stays synchronized automatically
- This allows you to use the physical thermostat dial to control cooling temperature
### System-Level Entities
The integration creates a "RDZ HMI System" device with the following entities:
**Sensors:**
- **Outside Temperature**: Current outdoor temperature from the system
- **Delivery Water Temperature 1-8**: Actual water temperatures being delivered by each system
- **Calculated Water Temperature 1-8**: Target water temperatures calculated by each system
**Binary Sensors:**
- **Pump 1-8 Active**: Shows whether each circulation pump is currently running
**Switches:**
- **Season Switch**: Controls global heating/cooling mode
- **System 1-8**: Enable/disable each heating/cooling system
**Number Entities:**
- **Day, Month, Year, Hour, Minute**: Configure the RDZ system's internal clock
## Troubleshooting
### Thermostats show "Unavailable"
@@ -13,7 +13,15 @@ from .coordinator import RDZDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -0,0 +1,87 @@
"""Binary sensor platform for RDZ HMI Control integration."""
from __future__ import annotations
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DATA_PUMP_ACTIVE,
DOMAIN,
)
from .coordinator import RDZDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up RDZ HMI binary sensor entities from a config entry."""
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
entities: list[BinarySensorEntity] = []
# Add pump active binary sensors (1-8)
for pump_id in range(1, 9):
entities.append(RDZPumpActiveBinarySensor(coordinator, pump_id))
async_add_entities(entities)
class RDZPumpActiveBinarySensor(CoordinatorEntity[RDZDataUpdateCoordinator], BinarySensorEntity):
"""Representation of an RDZ HMI pump active binary sensor."""
_attr_has_entity_name = True
_attr_device_class = BinarySensorDeviceClass.RUNNING
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
pump_id: int,
) -> None:
"""Initialize the pump active binary sensor entity.
Args:
coordinator: The data coordinator.
pump_id: The pump ID (1-8).
"""
super().__init__(coordinator)
self._pump_id = pump_id
self._attr_unique_id = f"{DOMAIN}_{coordinator.client.host}_pump_{pump_id}_active"
self._attr_translation_key = f"pump_{pump_id}_active"
self._attr_name = f"Pump {pump_id} active"
# Place on the System device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.client.host}_system")},
name="RDZ HMI System",
manufacturer="RDZ",
model="HMI Control System",
sw_version="1.0",
)
@property
def is_on(self) -> bool | None:
"""Return True if the pump is active."""
if self.coordinator.data is None:
return None
pump_active = self.coordinator.data.get(DATA_PUMP_ACTIVE, {})
return pump_active.get(self._pump_id)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()
@@ -23,6 +23,35 @@ COIL_SEASON = 2 # 0 = winter, 1 = summer
REGISTER_ACTIVITY_START = 2892
REGISTER_ACTIVITY_COUNT = 4
# Humidity registers
REGISTER_HUMIDITY_START = 7701
REGISTER_HUMIDITY_COUNT = 64
# Summer dehumidification setpoint registers
REGISTER_DEHUMIDIFICATION_SETPOINT_START = 428
REGISTER_DEHUMIDIFICATION_SETPOINT_COUNT = 64
# Dew point registers
REGISTER_DEW_POINT_START = 2828
REGISTER_DEW_POINT_COUNT = 64
# Zone mode registers
REGISTER_ZONE_MODE_START = 5301
REGISTER_ZONE_MODE_COUNT = 64
# Zone mode values
ZONE_MODE_OFF = 0
ZONE_MODE_MAN = 1
ZONE_MODE_PGM = 2
ZONE_MODE_PGM_MAN = 3
ZONE_MODE_OPTIONS = {
ZONE_MODE_OFF: "Off",
ZONE_MODE_MAN: "Man",
ZONE_MODE_PGM: "Pgm",
ZONE_MODE_PGM_MAN: "Pgm/Man",
}
# Config keys
CONF_HOST = "host"
CONF_PORT = "port"
@@ -46,6 +75,10 @@ DATA_WINTER_SETPOINTS = "winter_setpoints"
DATA_SUMMER_SETPOINTS = "summer_setpoints"
DATA_SEASON = "season" # True = summer, False = winter
DATA_ZONE_ACTIVITY = "zone_activity" # dict[int, bool] - zone_id -> is_active
DATA_HUMIDITY = "humidity"
DATA_DEHUMIDIFICATION_SETPOINTS = "dehumidification_setpoints"
DATA_DEW_POINTS = "dew_points"
DATA_ZONE_MODES = "zone_modes"
# Temperature scale factor (Modbus returns values * 10)
TEMP_SCALE_FACTOR = 10.0
@@ -54,3 +87,38 @@ TEMP_SCALE_FACTOR = 10.0
MIN_TEMP = 5.0
MAX_TEMP = 35.0
TEMP_STEP = 0.5
# Humidity limits
MIN_HUMIDITY = 30
MAX_HUMIDITY = 90
# Outside temperature register
REGISTER_OUTSIDE_TEMP = 2600
# Time settings registers (Day, Month, Year, Hour, Minute)
REGISTER_TIME_DAY = 5009
REGISTER_TIME_MONTH = 5010
REGISTER_TIME_YEAR = 5011
REGISTER_TIME_HOUR = 5012
REGISTER_TIME_MINUTE = 5013
# System activation coils
COIL_SYSTEM_ACTIVATION_START = 100
COIL_SYSTEM_ACTIVATION_COUNT = 8
# Water temperature registers
REGISTER_DELIVERY_WATER_TEMP_START = 2650
REGISTER_DELIVERY_WATER_TEMP_COUNT = 8
REGISTER_CALCULATED_WATER_TEMP_START = 2658
REGISTER_CALCULATED_WATER_TEMP_COUNT = 8
# Pump active bitmask register
REGISTER_PUMP_ACTIVE = 7615
# Data keys for new features
DATA_OUTSIDE_TEMP = "outside_temperature"
DATA_TIME_SETTINGS = "time_settings"
DATA_SYSTEM_ACTIVATION = "system_activation"
DATA_DELIVERY_WATER_TEMPS = "delivery_water_temps"
DATA_CALCULATED_WATER_TEMPS = "calculated_water_temps"
DATA_PUMP_ACTIVE = "pump_active"
@@ -17,11 +17,21 @@ from .const import (
CONF_PORT,
CONF_ZONE_TYPE,
CONF_ZONES,
DATA_CALCULATED_WATER_TEMPS,
DATA_DEHUMIDIFICATION_SETPOINTS,
DATA_DELIVERY_WATER_TEMPS,
DATA_DEW_POINTS,
DATA_HUMIDITY,
DATA_OUTSIDE_TEMP,
DATA_PUMP_ACTIVE,
DATA_SEASON,
DATA_SUMMER_SETPOINTS,
DATA_SYSTEM_ACTIVATION,
DATA_TEMPERATURES,
DATA_TIME_SETTINGS,
DATA_WINTER_SETPOINTS,
DATA_ZONE_ACTIVITY,
DATA_ZONE_MODES,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
@@ -70,6 +80,18 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
summer_setpoints = await self.client.read_summer_setpoints()
season = await self.client.read_season()
zone_activity = await self.client.read_zone_activity()
humidity = await self.client.read_humidity()
dehumidification_setpoints = await self.client.read_dehumidification_setpoints()
dew_points = await self.client.read_dew_points()
zone_modes = await self.client.read_zone_modes()
# Read new system-level data
outside_temp = await self.client.read_outside_temperature()
time_settings = await self.client.read_time_settings()
system_activation = await self.client.read_system_activation()
delivery_water_temps = await self.client.read_delivery_water_temps()
calculated_water_temps = await self.client.read_calculated_water_temps()
pump_active = await self.client.read_pump_active()
if temperatures is None or winter_setpoints is None or summer_setpoints is None:
raise UpdateFailed("Failed to read data from Modbus device")
@@ -86,6 +108,17 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
DATA_SUMMER_SETPOINTS: summer_setpoints,
DATA_SEASON: season, # True = summer, False = winter, None = unknown
DATA_ZONE_ACTIVITY: zone_activity, # dict of zone_id -> is_active
DATA_HUMIDITY: humidity,
DATA_DEHUMIDIFICATION_SETPOINTS: dehumidification_setpoints,
DATA_DEW_POINTS: dew_points,
DATA_ZONE_MODES: zone_modes,
# New system-level data
DATA_OUTSIDE_TEMP: outside_temp,
DATA_TIME_SETTINGS: time_settings,
DATA_SYSTEM_ACTIVATION: system_activation,
DATA_DELIVERY_WATER_TEMPS: delivery_water_temps,
DATA_CALCULATED_WATER_TEMPS: calculated_water_temps,
DATA_PUMP_ACTIVE: pump_active,
}
except Exception as ex:
@@ -177,6 +210,64 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return success
async def async_set_dehumidification_setpoint(
self, zone_id: int, humidity: float
) -> bool:
"""Set the dehumidification setpoint for a zone."""
success = await self.client.write_dehumidification_setpoint(zone_id, humidity)
if success:
await self.async_request_refresh()
return success
async def async_set_zone_mode(self, zone_id: int, mode: int) -> bool:
"""Set the zone mode for a zone."""
success = await self.client.write_zone_mode(zone_id, mode)
if success:
await self.async_request_refresh()
return success
async def async_set_system_activation(self, system_id: int, is_on: bool) -> bool:
"""Set system activation for a system (1-8)."""
success = await self.client.write_system_activation(system_id, is_on)
if success:
await self.async_request_refresh()
return success
async def async_set_time_day(self, day: int) -> bool:
"""Set the day value."""
success = await self.client.write_time_day(day)
if success:
await self.async_request_refresh()
return success
async def async_set_time_month(self, month: int) -> bool:
"""Set the month value."""
success = await self.client.write_time_month(month)
if success:
await self.async_request_refresh()
return success
async def async_set_time_year(self, year: int) -> bool:
"""Set the year value."""
success = await self.client.write_time_year(year)
if success:
await self.async_request_refresh()
return success
async def async_set_time_hour(self, hour: int) -> bool:
"""Set the hour value."""
success = await self.client.write_time_hour(hour)
if success:
await self.async_request_refresh()
return success
async def async_set_time_minute(self, minute: int) -> bool:
"""Set the minute value."""
success = await self.client.write_time_minute(minute)
if success:
await self.async_request_refresh()
return success
async def async_shutdown(self) -> None:
"""Disconnect from the Modbus device."""
await self.client.disconnect()
@@ -0,0 +1,174 @@
"""Humidifier platform for RDZ HMI Control integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.humidifier import (
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
CONF_ZONE_NAME,
CONF_ZONES,
DATA_DEHUMIDIFICATION_SETPOINTS,
DATA_HUMIDITY,
DOMAIN,
MAX_HUMIDITY,
MIN_HUMIDITY,
)
from .coordinator import RDZDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up RDZ HMI dehumidifier entities from a config entry."""
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
zones_config = config_entry.data.get(CONF_ZONES, {})
entities = []
for zone_id_str, zone_data in zones_config.items():
zone_id = int(zone_id_str) if isinstance(zone_id_str, str) else zone_id_str
entities.append(RDZDehumidifierEntity(coordinator, zone_id, zone_data))
async_add_entities(entities)
class RDZDehumidifierEntity(
CoordinatorEntity[RDZDataUpdateCoordinator], HumidifierEntity
):
"""Representation of an RDZ HMI dehumidifier entity."""
_attr_has_entity_name = True
_attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER
_attr_supported_features = HumidifierEntityFeature(0)
_attr_min_humidity = MIN_HUMIDITY
_attr_max_humidity = MAX_HUMIDITY
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
zone_id: int,
zone_data: dict[str, Any],
) -> None:
"""Initialize the dehumidifier entity."""
super().__init__(coordinator)
self._zone_id = zone_id
self._zone_data = zone_data
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.client.host}_{zone_id}_dehumidifier"
)
self._attr_translation_key = "dehumidifier"
self._attr_name = "Dehumidifier"
self._update_device_info()
def _update_device_info(self) -> None:
"""Update device info from zone data."""
zones_config = self.coordinator.config_entry.data.get(CONF_ZONES, {})
zone_data = zones_config.get(str(self._zone_id), self._zone_data)
self._zone_data = zone_data
zone_name = zone_data.get(CONF_ZONE_NAME, f"Zone {self._zone_id}")
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self.coordinator.client.host}_{self._zone_id}")},
name=zone_name,
manufacturer="RDZ",
model="HMI Thermostat",
sw_version="1.0",
)
@property
def is_on(self) -> bool:
"""Return True if the dehumidifier is on.
The dehumidifier is always considered 'on' as it's system-controlled.
"""
return True
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
if self.coordinator.data is None:
return None
humidity_data = self.coordinator.data.get(DATA_HUMIDITY, {})
return humidity_data.get(self._zone_id)
@property
def target_humidity(self) -> float | None:
"""Return the target humidity (dehumidification setpoint)."""
if self.coordinator.data is None:
return None
setpoints = self.coordinator.data.get(DATA_DEHUMIDIFICATION_SETPOINTS, {})
return setpoints.get(self._zone_id)
async def async_set_humidity(self, humidity: int) -> None:
"""Set the target humidity (dehumidification setpoint)."""
success = await self.coordinator.async_set_dehumidification_setpoint(
self._zone_id, float(humidity)
)
if not success:
_LOGGER.error(
"Failed to set dehumidification setpoint for zone %d", self._zone_id
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the dehumidifier.
The dehumidifier is system-controlled, so this is a no-op.
"""
_LOGGER.debug(
"Dehumidifier turn_on called for zone %d, but system is auto-controlled",
self._zone_id,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the dehumidifier.
The dehumidifier is system-controlled, so this is a no-op.
"""
_LOGGER.debug(
"Dehumidifier turn_off called for zone %d, but system is auto-controlled",
self._zone_id,
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_device_info()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# Listen for config entry updates
self.async_on_remove(
self.coordinator.config_entry.add_update_listener(
self._async_config_entry_updated
)
)
async def _async_config_entry_updated(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle config entry update."""
self._update_device_info()
self.async_write_ha_state()
@@ -8,14 +8,35 @@ from pymodbus.exceptions import ModbusException
from .const import (
COIL_SEASON,
COIL_SYSTEM_ACTIVATION_START,
COIL_SYSTEM_ACTIVATION_COUNT,
REGISTER_ACTIVITY_COUNT,
REGISTER_ACTIVITY_START,
REGISTER_CALCULATED_WATER_TEMP_COUNT,
REGISTER_CALCULATED_WATER_TEMP_START,
REGISTER_DEHUMIDIFICATION_SETPOINT_COUNT,
REGISTER_DEHUMIDIFICATION_SETPOINT_START,
REGISTER_DELIVERY_WATER_TEMP_COUNT,
REGISTER_DELIVERY_WATER_TEMP_START,
REGISTER_DEW_POINT_COUNT,
REGISTER_DEW_POINT_START,
REGISTER_HUMIDITY_COUNT,
REGISTER_HUMIDITY_START,
REGISTER_OUTSIDE_TEMP,
REGISTER_PUMP_ACTIVE,
REGISTER_TEMP_START,
REGISTER_TEMP_COUNT,
REGISTER_TIME_DAY,
REGISTER_TIME_HOUR,
REGISTER_TIME_MINUTE,
REGISTER_TIME_MONTH,
REGISTER_TIME_YEAR,
REGISTER_WINTER_SETPOINT_START,
REGISTER_WINTER_SETPOINT_COUNT,
REGISTER_SUMMER_SETPOINT_START,
REGISTER_SUMMER_SETPOINT_COUNT,
REGISTER_ZONE_MODE_COUNT,
REGISTER_ZONE_MODE_START,
TEMP_SCALE_FACTOR,
)
@@ -231,6 +252,81 @@ class RDZModbusClient:
value = int(temperature * TEMP_SCALE_FACTOR)
return await self.write_register(address, value)
async def read_humidity(self) -> dict[int, float] | None:
"""Read all humidity registers and return dict of zone_id -> humidity %.
Values are stored as integer * 10 (e.g., 55.0% = 550).
"""
registers = await self.read_registers(
REGISTER_HUMIDITY_START, REGISTER_HUMIDITY_COUNT
)
if registers is None:
return None
return {i: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
async def read_dehumidification_setpoints(self) -> dict[int, int] | None:
"""Read all dehumidification setpoint registers.
Values are stored as raw percentage (e.g., 50% = 50).
"""
registers = await self.read_registers(
REGISTER_DEHUMIDIFICATION_SETPOINT_START,
REGISTER_DEHUMIDIFICATION_SETPOINT_COUNT,
)
if registers is None:
return None
return {i: value for i, value in enumerate(registers)}
async def write_dehumidification_setpoint(
self, zone_id: int, humidity: int
) -> bool:
"""Write a dehumidification setpoint for a zone.
Args:
zone_id: The zone ID (0-63).
humidity: The target humidity percentage (e.g., 50).
"""
address = REGISTER_DEHUMIDIFICATION_SETPOINT_START + zone_id
return await self.write_register(address, humidity)
async def read_dew_points(self) -> dict[int, float] | None:
"""Read all dew point registers.
Values are stored as integer * 10 (e.g., 15.5°C = 155).
"""
registers = await self.read_registers(
REGISTER_DEW_POINT_START, REGISTER_DEW_POINT_COUNT
)
if registers is None:
return None
return {i: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
async def read_zone_modes(self) -> dict[int, int] | None:
"""Read all zone mode registers.
Returns dict of zone_id -> mode (0=Off, 1=Man, 2=Pgm, 3=Pgm/Man).
"""
registers = await self.read_registers(
REGISTER_ZONE_MODE_START, REGISTER_ZONE_MODE_COUNT
)
if registers is None:
return None
return {i: value for i, value in enumerate(registers)}
async def write_zone_mode(self, zone_id: int, mode: int) -> bool:
"""Write a zone mode.
Args:
zone_id: The zone ID (0-63).
mode: The mode value (0=Off, 1=Man, 2=Pgm, 3=Pgm/Man).
"""
address = REGISTER_ZONE_MODE_START + zone_id
return await self.write_register(address, mode)
async def discover_zones(self) -> list[int]:
"""Discover active zones by reading temperature registers."""
temperatures = await self.read_temperatures()
@@ -246,3 +342,129 @@ class RDZModbusClient:
# Try reading temperature registers as a connection test
result = await self.read_registers(REGISTER_TEMP_START, 1)
return result is not None
async def read_outside_temperature(self) -> float | None:
"""Read the outside temperature from register 2600.
Value is stored as integer * 10 (e.g., 21.5°C = 215).
"""
registers = await self.read_registers(REGISTER_OUTSIDE_TEMP, 1)
if registers is None:
return None
return registers[0] / TEMP_SCALE_FACTOR
async def read_time_settings(self) -> dict[str, int] | None:
"""Read time settings from registers 5009-5013.
Returns dict with keys: day, month, year, hour, minute
"""
registers = await self.read_registers(REGISTER_TIME_DAY, 5)
if registers is None:
return None
return {
"day": registers[0],
"month": registers[1],
"year": registers[2],
"hour": registers[3],
"minute": registers[4],
}
async def write_time_day(self, day: int) -> bool:
"""Write the day value to register 5009."""
return await self.write_register(REGISTER_TIME_DAY, day)
async def write_time_month(self, month: int) -> bool:
"""Write the month value to register 5010."""
return await self.write_register(REGISTER_TIME_MONTH, month)
async def write_time_year(self, year: int) -> bool:
"""Write the year value to register 5011."""
return await self.write_register(REGISTER_TIME_YEAR, year)
async def write_time_hour(self, hour: int) -> bool:
"""Write the hour value to register 5012."""
return await self.write_register(REGISTER_TIME_HOUR, hour)
async def write_time_minute(self, minute: int) -> bool:
"""Write the minute value to register 5013."""
return await self.write_register(REGISTER_TIME_MINUTE, minute)
async def read_coils(self, address: int, count: int) -> list[bool] | None:
"""Read multiple coils from the Modbus device."""
async with self._lock:
if not await self._connect_unlocked():
return None
try:
result = await self._client.read_coils(address=address, count=count)
if result.isError():
_LOGGER.error("Error reading coils at %d: %s", address, result)
return None
return list(result.bits[:count])
except ModbusException as ex:
_LOGGER.error("Modbus exception reading coils at %d: %s", address, ex)
return None
except Exception as ex:
_LOGGER.error("Unexpected error reading coils at %d: %s", address, ex)
return None
async def read_system_activation(self) -> dict[int, bool] | None:
"""Read system activation coils 100-107.
Returns dict of system_id (1-8) -> is_on (True/False).
"""
coils = await self.read_coils(COIL_SYSTEM_ACTIVATION_START, COIL_SYSTEM_ACTIVATION_COUNT)
if coils is None:
return None
return {i + 1: coils[i] for i in range(COIL_SYSTEM_ACTIVATION_COUNT)}
async def write_system_activation(self, system_id: int, is_on: bool) -> bool:
"""Write a system activation coil.
Args:
system_id: The system ID (1-8).
is_on: Whether to activate (True) or deactivate (False).
"""
if not 1 <= system_id <= 8:
_LOGGER.error("Invalid system_id %d, must be 1-8", system_id)
return False
address = COIL_SYSTEM_ACTIVATION_START + (system_id - 1)
return await self.write_coil(address, is_on)
async def read_delivery_water_temps(self) -> dict[int, float] | None:
"""Read delivery water temperatures from registers 2650-2657.
Returns dict of system_id (1-8) -> temperature.
Values are stored as integer * 10.
"""
registers = await self.read_registers(
REGISTER_DELIVERY_WATER_TEMP_START, REGISTER_DELIVERY_WATER_TEMP_COUNT
)
if registers is None:
return None
return {i + 1: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
async def read_calculated_water_temps(self) -> dict[int, float] | None:
"""Read calculated water temperatures from registers 2658-2665.
Returns dict of system_id (1-8) -> temperature.
Values are stored as integer * 10.
"""
registers = await self.read_registers(
REGISTER_CALCULATED_WATER_TEMP_START, REGISTER_CALCULATED_WATER_TEMP_COUNT
)
if registers is None:
return None
return {i + 1: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
async def read_pump_active(self) -> dict[int, bool] | None:
"""Read pump active status from register 7615 bitmask.
Returns dict of pump_id (1-8) -> is_active (True/False).
Bit 0 = pump 1, bit 7 = pump 8.
"""
registers = await self.read_registers(REGISTER_PUMP_ACTIVE, 1)
if registers is None:
return None
bitmask = registers[0]
return {i + 1: bool(bitmask & (1 << i)) for i in range(8)}
+129
View File
@@ -0,0 +1,129 @@
"""Number platform for RDZ HMI Control integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.number import NumberEntity, NumberMode
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DATA_TIME_SETTINGS,
DOMAIN,
)
from .coordinator import RDZDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
# Time setting configurations: (key, name, min, max)
TIME_SETTINGS = [
("day", "Day", 1, 31),
("month", "Month", 1, 12),
("year", "Year", 2000, 2099),
("hour", "Hour", 0, 23),
("minute", "Minute", 0, 59),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up RDZ HMI number entities from a config entry."""
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
entities: list[NumberEntity] = []
# Add time setting number entities
for key, name, min_val, max_val in TIME_SETTINGS:
entities.append(
RDZTimeSettingNumber(coordinator, key, name, min_val, max_val)
)
async_add_entities(entities)
class RDZTimeSettingNumber(CoordinatorEntity[RDZDataUpdateCoordinator], NumberEntity):
"""Representation of an RDZ HMI time setting number entity."""
_attr_has_entity_name = True
_attr_mode = NumberMode.BOX
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
setting_key: str,
setting_name: str,
min_value: int,
max_value: int,
) -> None:
"""Initialize the time setting number entity.
Args:
coordinator: The data coordinator.
setting_key: The key in the time settings dict (day, month, year, hour, minute).
setting_name: The display name for the entity.
min_value: The minimum allowed value.
max_value: The maximum allowed value.
"""
super().__init__(coordinator)
self._setting_key = setting_key
self._attr_unique_id = f"{DOMAIN}_{coordinator.client.host}_time_{setting_key}"
self._attr_translation_key = f"time_{setting_key}"
self._attr_name = setting_name
self._attr_native_min_value = min_value
self._attr_native_max_value = max_value
self._attr_native_step = 1
# Place on the System device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.client.host}_system")},
name="RDZ HMI System",
manufacturer="RDZ",
model="HMI Control System",
sw_version="1.0",
)
@property
def native_value(self) -> float | None:
"""Return the current value."""
if self.coordinator.data is None:
return None
time_settings = self.coordinator.data.get(DATA_TIME_SETTINGS, {})
if time_settings is None:
return None
return time_settings.get(self._setting_key)
async def async_set_native_value(self, value: float) -> None:
"""Set the value."""
int_value = int(value)
# Call the appropriate setter method on the coordinator
setter_methods = {
"day": self.coordinator.async_set_time_day,
"month": self.coordinator.async_set_time_month,
"year": self.coordinator.async_set_time_year,
"hour": self.coordinator.async_set_time_hour,
"minute": self.coordinator.async_set_time_minute,
}
setter = setter_methods.get(self._setting_key)
if setter is None:
_LOGGER.error("Unknown time setting key: %s", self._setting_key)
return
success = await setter(int_value)
if not success:
_LOGGER.error("Failed to set time %s to %d", self._setting_key, int_value)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()
+140
View File
@@ -0,0 +1,140 @@
"""Select platform for RDZ HMI Control integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
CONF_ZONE_NAME,
CONF_ZONES,
DATA_ZONE_MODES,
DOMAIN,
ZONE_MODE_MAN,
ZONE_MODE_OFF,
ZONE_MODE_OPTIONS,
ZONE_MODE_PGM,
ZONE_MODE_PGM_MAN,
)
from .coordinator import RDZDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
# Reverse mapping from option string to mode value
ZONE_MODE_VALUES = {v: k for k, v in ZONE_MODE_OPTIONS.items()}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up RDZ HMI zone mode select entities from a config entry."""
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
zones_config = config_entry.data.get(CONF_ZONES, {})
entities = []
for zone_id_str, zone_data in zones_config.items():
zone_id = int(zone_id_str) if isinstance(zone_id_str, str) else zone_id_str
entities.append(RDZZoneModeSelect(coordinator, zone_id, zone_data))
async_add_entities(entities)
class RDZZoneModeSelect(CoordinatorEntity[RDZDataUpdateCoordinator], SelectEntity):
"""Representation of an RDZ HMI zone mode select entity."""
_attr_has_entity_name = True
_attr_options = list(ZONE_MODE_OPTIONS.values())
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
zone_id: int,
zone_data: dict[str, Any],
) -> None:
"""Initialize the zone mode select entity."""
super().__init__(coordinator)
self._zone_id = zone_id
self._zone_data = zone_data
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.client.host}_{zone_id}_zone_mode"
)
self._attr_translation_key = "zone_mode"
self._attr_name = "Zone mode"
self._update_device_info()
def _update_device_info(self) -> None:
"""Update device info from zone data."""
zones_config = self.coordinator.config_entry.data.get(CONF_ZONES, {})
zone_data = zones_config.get(str(self._zone_id), self._zone_data)
self._zone_data = zone_data
zone_name = zone_data.get(CONF_ZONE_NAME, f"Zone {self._zone_id}")
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self.coordinator.client.host}_{self._zone_id}")},
name=zone_name,
manufacturer="RDZ",
model="HMI Thermostat",
sw_version="1.0",
)
@property
def current_option(self) -> str | None:
"""Return the currently selected option."""
if self.coordinator.data is None:
return None
zone_modes = self.coordinator.data.get(DATA_ZONE_MODES, {})
mode_value = zone_modes.get(self._zone_id)
if mode_value is None:
return None
return ZONE_MODE_OPTIONS.get(mode_value, ZONE_MODE_OPTIONS[ZONE_MODE_OFF])
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
mode_value = ZONE_MODE_VALUES.get(option)
if mode_value is None:
_LOGGER.error("Invalid zone mode option: %s", option)
return
success = await self.coordinator.async_set_zone_mode(self._zone_id, mode_value)
if not success:
_LOGGER.error("Failed to set zone mode for zone %d", self._zone_id)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_device_info()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# Listen for config entry updates
self.async_on_remove(
self.coordinator.config_entry.add_update_listener(
self._async_config_entry_updated
)
)
async def _async_config_entry_updated(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle config entry update."""
self._update_device_info()
self.async_write_ha_state()
+203 -1
View File
@@ -20,6 +20,10 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
CONF_ZONE_NAME,
CONF_ZONES,
DATA_CALCULATED_WATER_TEMPS,
DATA_DELIVERY_WATER_TEMPS,
DATA_DEW_POINTS,
DATA_OUTSIDE_TEMP,
DATA_SUMMER_SETPOINTS,
DATA_WINTER_SETPOINTS,
DOMAIN,
@@ -38,7 +42,9 @@ async def async_setup_entry(
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
zones_config = config_entry.data.get(CONF_ZONES, {})
entities = []
entities: list[SensorEntity] = []
# Zone-level sensors
for zone_id_str, zone_data in zones_config.items():
zone_id = int(zone_id_str) if isinstance(zone_id_str, str) else zone_id_str
# Create winter and summer setpoint sensors for each zone
@@ -48,6 +54,19 @@ async def async_setup_entry(
entities.append(
RDZSetpointSensor(coordinator, zone_id, zone_data, "summer")
)
# Create dew point sensor for each zone
entities.append(
RDZDewPointSensor(coordinator, zone_id, zone_data)
)
# System-level sensors
# Outside temperature sensor
entities.append(RDZOutsideTemperatureSensor(coordinator))
# Water temperature sensors (1-8 for each type)
for system_id in range(1, 9):
entities.append(RDZWaterTemperatureSensor(coordinator, system_id, "delivery"))
entities.append(RDZWaterTemperatureSensor(coordinator, system_id, "calculated"))
async_add_entities(entities)
@@ -137,3 +156,186 @@ class RDZSetpointSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntit
"""Handle config entry update."""
self._update_device_info()
self.async_write_ha_state()
class RDZDewPointSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntity):
"""Representation of an RDZ HMI dew point sensor."""
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
zone_id: int,
zone_data: dict[str, Any],
) -> None:
"""Initialize the dew point sensor entity."""
super().__init__(coordinator)
self._zone_id = zone_id
self._zone_data = zone_data
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.client.host}_{zone_id}_dew_point"
)
self._attr_translation_key = "dew_point"
self._attr_name = "Dew point"
self._update_device_info()
def _update_device_info(self) -> None:
"""Update device info from zone data."""
zones_config = self.coordinator.config_entry.data.get(CONF_ZONES, {})
zone_data = zones_config.get(str(self._zone_id), self._zone_data)
self._zone_data = zone_data
zone_name = zone_data.get(CONF_ZONE_NAME, f"Zone {self._zone_id}")
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self.coordinator.client.host}_{self._zone_id}")},
name=zone_name,
manufacturer="RDZ",
model="HMI Thermostat",
sw_version="1.0",
)
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
if self.coordinator.data is None:
return None
dew_points = self.coordinator.data.get(DATA_DEW_POINTS, {})
return dew_points.get(self._zone_id)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_device_info()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# Listen for config entry updates
self.async_on_remove(
self.coordinator.config_entry.add_update_listener(self._async_config_entry_updated)
)
async def _async_config_entry_updated(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle config entry update."""
self._update_device_info()
self.async_write_ha_state()
class RDZOutsideTemperatureSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntity):
"""Representation of an RDZ HMI outside temperature sensor."""
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_translation_key = "outside_temperature"
_attr_name = "Outside temperature"
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
) -> None:
"""Initialize the outside temperature sensor entity."""
super().__init__(coordinator)
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.client.host}_outside_temperature"
)
# Place on the System device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.client.host}_system")},
name="RDZ HMI System",
manufacturer="RDZ",
model="HMI Control System",
sw_version="1.0",
)
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
if self.coordinator.data is None:
return None
return self.coordinator.data.get(DATA_OUTSIDE_TEMP)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()
class RDZWaterTemperatureSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntity):
"""Representation of an RDZ HMI water temperature sensor."""
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
system_id: int,
temp_type: str,
) -> None:
"""Initialize the water temperature sensor entity.
Args:
coordinator: The data coordinator.
system_id: The system ID (1-8).
temp_type: Either "delivery" or "calculated".
"""
super().__init__(coordinator)
self._system_id = system_id
self._temp_type = temp_type
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.client.host}_{temp_type}_water_temp_{system_id}"
)
if temp_type == "delivery":
self._attr_translation_key = f"delivery_water_temp_{system_id}"
self._attr_name = f"Delivery water temperature {system_id}"
else:
self._attr_translation_key = f"calculated_water_temp_{system_id}"
self._attr_name = f"Calculated water temperature {system_id}"
# Place on the System device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.client.host}_system")},
name="RDZ HMI System",
manufacturer="RDZ",
model="HMI Control System",
sw_version="1.0",
)
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
if self.coordinator.data is None:
return None
if self._temp_type == "delivery":
temps = self.coordinator.data.get(DATA_DELIVERY_WATER_TEMPS, {})
else:
temps = self.coordinator.data.get(DATA_CALCULATED_WATER_TEMPS, {})
return temps.get(self._system_id)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()
+65 -1
View File
@@ -14,6 +14,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DATA_SEASON,
DATA_SYSTEM_ACTIVATION,
DOMAIN,
)
from .coordinator import RDZDataUpdateCoordinator
@@ -29,10 +30,14 @@ async def async_setup_entry(
"""Set up RDZ HMI switch entities from a config entry."""
coordinator: RDZDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = [
entities: list[SwitchEntity] = [
RDZSeasonSwitch(coordinator),
]
# Add system activation switches (1-8)
for system_id in range(1, 9):
entities.append(RDZSystemActivationSwitch(coordinator, system_id))
async_add_entities(entities)
@@ -91,6 +96,65 @@ class RDZSeasonSwitch(CoordinatorEntity[RDZDataUpdateCoordinator], SwitchEntity)
if not success:
_LOGGER.error("Failed to set season to winter")
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()
class RDZSystemActivationSwitch(CoordinatorEntity[RDZDataUpdateCoordinator], SwitchEntity):
"""Representation of an RDZ HMI system activation switch."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: RDZDataUpdateCoordinator,
system_id: int,
) -> None:
"""Initialize the system activation switch entity.
Args:
coordinator: The data coordinator.
system_id: The system ID (1-8).
"""
super().__init__(coordinator)
self._system_id = system_id
self._attr_unique_id = f"{DOMAIN}_{coordinator.client.host}_system_{system_id}"
self._attr_translation_key = f"system_{system_id}"
self._attr_name = f"System {system_id}"
self._attr_icon = "mdi:power"
# Place on the System device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.client.host}_system")},
name="RDZ HMI System",
manufacturer="RDZ",
model="HMI Control System",
sw_version="1.0",
)
@property
def is_on(self) -> bool | None:
"""Return True if the system is active."""
if self.coordinator.data is None:
return None
activation = self.coordinator.data.get(DATA_SYSTEM_ACTIVATION, {})
return activation.get(self._system_id)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the system."""
success = await self.coordinator.async_set_system_activation(self._system_id, True)
if not success:
_LOGGER.error("Failed to activate system %d", self._system_id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the system."""
success = await self.coordinator.async_set_system_activation(self._system_id, False)
if not success:
_LOGGER.error("Failed to deactivate system %d", self._system_id)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
+5
View File
@@ -0,0 +1,5 @@
{
"name": "RDZ HMI Control",
"render_readme": true,
"homeassistant": "2024.1.0"
}