Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b856d2b10 | ||
|
|
dd6f079a92 | ||
|
|
cb57dc1e92 | ||
|
|
c6fde73e7a | ||
|
|
f704dfc5ce | ||
|
|
62e76b210c | ||
|
|
f6b88781ad |
@@ -24,6 +24,10 @@ from .const import (
|
|||||||
DATA_INTEGRATION_REQUEST,
|
DATA_INTEGRATION_REQUEST,
|
||||||
DATA_PUMP_ACTIVE,
|
DATA_PUMP_ACTIVE,
|
||||||
DATA_RENEWAL_REQUEST,
|
DATA_RENEWAL_REQUEST,
|
||||||
|
DATA_VENTILATION_MODE_DEHUMIDIFICATION,
|
||||||
|
DATA_VENTILATION_MODE_INTEGRATION,
|
||||||
|
DATA_VENTILATION_MODE_RENEWAL,
|
||||||
|
DATA_VENTILATION_MODE_VENTILATION,
|
||||||
DATA_VENTILATION_REQUEST,
|
DATA_VENTILATION_REQUEST,
|
||||||
DATA_ZONE_ACTIVITY,
|
DATA_ZONE_ACTIVITY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -42,6 +46,14 @@ ZONE_REQUEST_TYPES: dict[str, tuple[str, str]] = {
|
|||||||
"dehumidification_pump": (DATA_DEHUMIDIFICATION_PUMP, "Dehumidification pump"),
|
"dehumidification_pump": (DATA_DEHUMIDIFICATION_PUMP, "Dehumidification pump"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ventilation mode types with their data keys and display names
|
||||||
|
VENTILATION_MODE_TYPES: dict[str, tuple[str, str]] = {
|
||||||
|
"dehumidification": (DATA_VENTILATION_MODE_DEHUMIDIFICATION, "Dehumidification"),
|
||||||
|
"ventilation": (DATA_VENTILATION_MODE_VENTILATION, "Ventilation"),
|
||||||
|
"renewal": (DATA_VENTILATION_MODE_RENEWAL, "Renewal"),
|
||||||
|
"integration": (DATA_VENTILATION_MODE_INTEGRATION, "Integration"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -58,6 +70,10 @@ async def async_setup_entry(
|
|||||||
for pump_id in range(1, 9):
|
for pump_id in range(1, 9):
|
||||||
entities.append(RDZPumpActiveBinarySensor(coordinator, pump_id))
|
entities.append(RDZPumpActiveBinarySensor(coordinator, pump_id))
|
||||||
|
|
||||||
|
# Add ventilation mode binary sensors
|
||||||
|
for mode_type in VENTILATION_MODE_TYPES:
|
||||||
|
entities.append(RDZVentilationModeBinarySensor(coordinator, mode_type))
|
||||||
|
|
||||||
# Add zone pump binary sensors for each configured zone
|
# Add zone pump binary sensors for each configured zone
|
||||||
for zone_id_str, zone_data in zones_config.items():
|
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
|
zone_id = int(zone_id_str) if isinstance(zone_id_str, str) else zone_id_str
|
||||||
@@ -119,6 +135,55 @@ class RDZPumpActiveBinarySensor(CoordinatorEntity[RDZDataUpdateCoordinator], Bin
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class RDZVentilationModeBinarySensor(CoordinatorEntity[RDZDataUpdateCoordinator], BinarySensorEntity):
|
||||||
|
"""Representation of an RDZ HMI ventilation mode binary sensor.
|
||||||
|
|
||||||
|
Indicates whether a ventilation unit work mode is active.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_device_class = BinarySensorDeviceClass.RUNNING
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: RDZDataUpdateCoordinator,
|
||||||
|
mode_type: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the ventilation mode binary sensor entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coordinator: The data coordinator.
|
||||||
|
mode_type: The type of ventilation mode (key in VENTILATION_MODE_TYPES).
|
||||||
|
"""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._mode_type = mode_type
|
||||||
|
self._data_key, self._display_name = VENTILATION_MODE_TYPES[mode_type]
|
||||||
|
|
||||||
|
self._attr_unique_id = f"{DOMAIN}_{coordinator.client.host}_ventilation_mode_{mode_type}"
|
||||||
|
self._attr_name = self._display_name
|
||||||
|
|
||||||
|
# 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 ventilation mode is active."""
|
||||||
|
if self.coordinator.data is None:
|
||||||
|
return None
|
||||||
|
return self.coordinator.data.get(self._data_key)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class RDZZonePumpBinarySensor(CoordinatorEntity[RDZDataUpdateCoordinator], BinarySensorEntity):
|
class RDZZonePumpBinarySensor(CoordinatorEntity[RDZDataUpdateCoordinator], BinarySensorEntity):
|
||||||
"""Representation of an RDZ HMI zone pump binary sensor.
|
"""Representation of an RDZ HMI zone pump binary sensor.
|
||||||
|
|
||||||
|
|||||||
@@ -23,22 +23,43 @@ from .const import (
|
|||||||
CONF_ZONE_NAME,
|
CONF_ZONE_NAME,
|
||||||
CONF_ZONE_TYPE,
|
CONF_ZONE_TYPE,
|
||||||
CONF_ZONES,
|
CONF_ZONES,
|
||||||
|
DATA_CALCULATED_SETPOINTS,
|
||||||
DATA_SEASON,
|
DATA_SEASON,
|
||||||
DATA_SUMMER_SETPOINTS,
|
DATA_SUMMER_SETPOINTS,
|
||||||
DATA_TEMPERATURES,
|
DATA_TEMPERATURES,
|
||||||
DATA_WINTER_SETPOINTS,
|
DATA_WINTER_SETPOINTS,
|
||||||
DATA_ZONE_ACTIVITY,
|
DATA_ZONE_ACTIVITY,
|
||||||
|
DATA_ZONE_MODES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MAX_TEMP,
|
MAX_TEMP,
|
||||||
MIN_TEMP,
|
MIN_TEMP,
|
||||||
TEMP_STEP,
|
TEMP_STEP,
|
||||||
|
THERMOSTAT_TYPE_REAL,
|
||||||
THERMOSTAT_TYPE_UNCONFIGURED,
|
THERMOSTAT_TYPE_UNCONFIGURED,
|
||||||
THERMOSTAT_TYPE_VIRTUAL,
|
THERMOSTAT_TYPE_VIRTUAL,
|
||||||
|
ZONE_MODE_MAN,
|
||||||
|
ZONE_MODE_OFF,
|
||||||
|
ZONE_MODE_OPTIONS,
|
||||||
|
ZONE_MODE_PGM,
|
||||||
|
ZONE_MODE_PGM_MAN,
|
||||||
)
|
)
|
||||||
from .coordinator import RDZDataUpdateCoordinator
|
from .coordinator import RDZDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Preset modes that map to zone modes
|
||||||
|
PRESET_OFF = "Off"
|
||||||
|
PRESET_MAN = "Man"
|
||||||
|
PRESET_PGM = "Pgm"
|
||||||
|
PRESET_PGM_MAN = "Pgm/Man"
|
||||||
|
|
||||||
|
PRESET_MODE_TO_ZONE_MODE = {
|
||||||
|
PRESET_OFF: ZONE_MODE_OFF,
|
||||||
|
PRESET_MAN: ZONE_MODE_MAN,
|
||||||
|
PRESET_PGM: ZONE_MODE_PGM,
|
||||||
|
PRESET_PGM_MAN: ZONE_MODE_PGM_MAN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -66,6 +87,7 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
_attr_min_temp = MIN_TEMP
|
_attr_min_temp = MIN_TEMP
|
||||||
_attr_max_temp = MAX_TEMP
|
_attr_max_temp = MAX_TEMP
|
||||||
_attr_target_temperature_step = TEMP_STEP
|
_attr_target_temperature_step = TEMP_STEP
|
||||||
|
_attr_preset_modes = [PRESET_OFF, PRESET_MAN, PRESET_PGM, PRESET_PGM_MAN]
|
||||||
_enable_turn_on_off_backwards_compat = False
|
_enable_turn_on_off_backwards_compat = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -93,16 +115,19 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
|
|
||||||
# Set supported features and HVAC modes based on configuration
|
# Set supported features and HVAC modes based on configuration
|
||||||
if zone_type == THERMOSTAT_TYPE_UNCONFIGURED:
|
if zone_type == THERMOSTAT_TYPE_UNCONFIGURED:
|
||||||
self._attr_supported_features = ClimateEntityFeature(0)
|
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
|
||||||
self._attr_hvac_modes = [HVACMode.OFF]
|
self._attr_hvac_modes = [HVACMode.OFF]
|
||||||
elif zone_type == THERMOSTAT_TYPE_VIRTUAL:
|
elif zone_type == THERMOSTAT_TYPE_VIRTUAL:
|
||||||
# Virtual thermostats are read-only (synced from real thermostats)
|
# Virtual thermostats are read-only (synced from real thermostats)
|
||||||
self._attr_supported_features = ClimateEntityFeature(0)
|
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
|
||||||
self._attr_hvac_modes = [HVACMode.OFF]
|
self._attr_hvac_modes = [HVACMode.OFF]
|
||||||
else: # THERMOSTAT_TYPE_REAL
|
else: # THERMOSTAT_TYPE_REAL
|
||||||
# Real thermostats support temperature control
|
# Real thermostats support temperature control and preset mode
|
||||||
# HVAC mode is determined by season switch (read-only)
|
# HVAC mode is determined by season switch (read-only)
|
||||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
self._attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.PRESET_MODE
|
||||||
|
)
|
||||||
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL]
|
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL]
|
||||||
|
|
||||||
# Update device info
|
# Update device info
|
||||||
@@ -125,6 +150,26 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
else: # Winter or unknown
|
else: # Winter or unknown
|
||||||
return HVACMode.HEAT
|
return HVACMode.HEAT
|
||||||
|
|
||||||
|
def _get_effective_zone_id_for_preset(self) -> int:
|
||||||
|
"""Get the zone ID to use for preset operations based on season.
|
||||||
|
|
||||||
|
For real thermostats with linked virtual zone:
|
||||||
|
- Summer mode: use linked virtual zone ID (presets control cooling)
|
||||||
|
- Winter mode: use this zone ID (presets control heating)
|
||||||
|
|
||||||
|
For all other thermostats: use own zone ID.
|
||||||
|
"""
|
||||||
|
zone_type = self._zone_data.get(CONF_ZONE_TYPE, THERMOSTAT_TYPE_UNCONFIGURED)
|
||||||
|
|
||||||
|
if zone_type == THERMOSTAT_TYPE_REAL:
|
||||||
|
linked_virtual = self._zone_data.get(CONF_LINKED_VIRTUAL_ZONE)
|
||||||
|
if linked_virtual is not None:
|
||||||
|
current_mode = self._get_season_based_hvac_mode()
|
||||||
|
if current_mode == HVACMode.COOL: # Summer
|
||||||
|
return linked_virtual
|
||||||
|
|
||||||
|
return self._zone_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
@@ -135,19 +180,31 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the target temperature based on season (summer=cooling setpoint, winter=heating setpoint)."""
|
"""Return the target temperature based on zone mode and season.
|
||||||
|
|
||||||
|
In 'Man' mode: return manual setpoint (winter or summer based on season)
|
||||||
|
In other modes: return calculated setpoint from program
|
||||||
|
"""
|
||||||
if self.coordinator.data is None:
|
if self.coordinator.data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
current_mode = self._get_season_based_hvac_mode()
|
effective_zone_id = self._get_effective_zone_id_for_preset()
|
||||||
|
zone_modes = self.coordinator.data.get(DATA_ZONE_MODES, {})
|
||||||
|
current_zone_mode = zone_modes.get(effective_zone_id)
|
||||||
|
|
||||||
if current_mode == HVACMode.COOL:
|
# Only use manual setpoints in Man mode
|
||||||
|
if current_zone_mode == ZONE_MODE_MAN:
|
||||||
|
current_hvac_mode = self._get_season_based_hvac_mode()
|
||||||
|
if current_hvac_mode == HVACMode.COOL:
|
||||||
setpoints = self.coordinator.data.get(DATA_SUMMER_SETPOINTS, {})
|
setpoints = self.coordinator.data.get(DATA_SUMMER_SETPOINTS, {})
|
||||||
else: # HEAT
|
else:
|
||||||
setpoints = self.coordinator.data.get(DATA_WINTER_SETPOINTS, {})
|
setpoints = self.coordinator.data.get(DATA_WINTER_SETPOINTS, {})
|
||||||
|
|
||||||
return setpoints.get(self._zone_id)
|
return setpoints.get(self._zone_id)
|
||||||
|
|
||||||
|
# All other modes: use calculated setpoints from effective zone
|
||||||
|
calculated = self.coordinator.data.get(DATA_CALCULATED_SETPOINTS, {})
|
||||||
|
return calculated.get(effective_zone_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode:
|
def hvac_mode(self) -> HVACMode:
|
||||||
"""Return current HVAC mode based on season switch (read-only)."""
|
"""Return current HVAC mode based on season switch (read-only)."""
|
||||||
@@ -188,6 +245,33 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
|
|
||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_mode(self) -> str | None:
|
||||||
|
"""Return the current preset mode based on zone mode."""
|
||||||
|
if self.coordinator.data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
effective_zone_id = self._get_effective_zone_id_for_preset()
|
||||||
|
zone_modes = self.coordinator.data.get(DATA_ZONE_MODES, {})
|
||||||
|
mode_value = zone_modes.get(effective_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_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set the preset mode by writing to the appropriate zone mode register."""
|
||||||
|
mode_value = PRESET_MODE_TO_ZONE_MODE.get(preset_mode)
|
||||||
|
if mode_value is None:
|
||||||
|
_LOGGER.error("Invalid preset mode: %s", preset_mode)
|
||||||
|
return
|
||||||
|
|
||||||
|
effective_zone_id = self._get_effective_zone_id_for_preset()
|
||||||
|
success = await self.coordinator.async_set_zone_mode(effective_zone_id, mode_value)
|
||||||
|
if not success:
|
||||||
|
_LOGGER.error("Failed to set preset for zone %d", self._zone_id)
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""HVAC mode is read-only - determined by season switch."""
|
"""HVAC mode is read-only - determined by season switch."""
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -215,6 +299,18 @@ class RDZClimateEntity(CoordinatorEntity[RDZDataUpdateCoordinator], ClimateEntit
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if in Man mode using effective zone ID
|
||||||
|
if self.coordinator.data:
|
||||||
|
effective_zone_id = self._get_effective_zone_id_for_preset()
|
||||||
|
zone_modes = self.coordinator.data.get(DATA_ZONE_MODES, {})
|
||||||
|
if zone_modes.get(effective_zone_id) != ZONE_MODE_MAN:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Cannot set temperature for zone %d - not in manual mode. "
|
||||||
|
"Change preset to 'Man' first.",
|
||||||
|
self._zone_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
if temperature is None:
|
if temperature is None:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ REGISTER_DEHUMIDIFICATION_SETPOINT_COUNT = 64
|
|||||||
REGISTER_DEW_POINT_START = 2828
|
REGISTER_DEW_POINT_START = 2828
|
||||||
REGISTER_DEW_POINT_COUNT = 64
|
REGISTER_DEW_POINT_COUNT = 64
|
||||||
|
|
||||||
|
# Calculated setpoint registers (program-calculated target temperatures)
|
||||||
|
REGISTER_CALCULATED_SETPOINT_START = 2764
|
||||||
|
REGISTER_CALCULATED_SETPOINT_COUNT = 64
|
||||||
|
|
||||||
# Zone mode registers
|
# Zone mode registers
|
||||||
REGISTER_ZONE_MODE_START = 5301
|
REGISTER_ZONE_MODE_START = 5301
|
||||||
REGISTER_ZONE_MODE_COUNT = 64
|
REGISTER_ZONE_MODE_COUNT = 64
|
||||||
@@ -136,6 +140,12 @@ REGISTER_CALCULATED_WATER_TEMP_COUNT = 8
|
|||||||
# Pump active bitmask register
|
# Pump active bitmask register
|
||||||
REGISTER_PUMP_ACTIVE = 7615
|
REGISTER_PUMP_ACTIVE = 7615
|
||||||
|
|
||||||
|
# Ventilation mode registers (LSB = on/off)
|
||||||
|
REGISTER_VENTILATION_MODE_DEHUMIDIFICATION = 8201
|
||||||
|
REGISTER_VENTILATION_MODE_VENTILATION = 8202
|
||||||
|
REGISTER_VENTILATION_MODE_RENEWAL = 8203
|
||||||
|
REGISTER_VENTILATION_MODE_INTEGRATION = 8204
|
||||||
|
|
||||||
# Data keys for new features
|
# Data keys for new features
|
||||||
DATA_OUTSIDE_TEMP = "outside_temperature"
|
DATA_OUTSIDE_TEMP = "outside_temperature"
|
||||||
DATA_TIME_SETTINGS = "time_settings"
|
DATA_TIME_SETTINGS = "time_settings"
|
||||||
@@ -143,3 +153,8 @@ DATA_SYSTEM_ACTIVATION = "system_activation"
|
|||||||
DATA_DELIVERY_WATER_TEMPS = "delivery_water_temps"
|
DATA_DELIVERY_WATER_TEMPS = "delivery_water_temps"
|
||||||
DATA_CALCULATED_WATER_TEMPS = "calculated_water_temps"
|
DATA_CALCULATED_WATER_TEMPS = "calculated_water_temps"
|
||||||
DATA_PUMP_ACTIVE = "pump_active"
|
DATA_PUMP_ACTIVE = "pump_active"
|
||||||
|
DATA_CALCULATED_SETPOINTS = "calculated_setpoints"
|
||||||
|
DATA_VENTILATION_MODE_DEHUMIDIFICATION = "ventilation_mode_dehumidification"
|
||||||
|
DATA_VENTILATION_MODE_VENTILATION = "ventilation_mode_ventilation"
|
||||||
|
DATA_VENTILATION_MODE_RENEWAL = "ventilation_mode_renewal"
|
||||||
|
DATA_VENTILATION_MODE_INTEGRATION = "ventilation_mode_integration"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_ZONE_TYPE,
|
CONF_ZONE_TYPE,
|
||||||
CONF_ZONES,
|
CONF_ZONES,
|
||||||
|
DATA_CALCULATED_SETPOINTS,
|
||||||
DATA_CALCULATED_WATER_TEMPS,
|
DATA_CALCULATED_WATER_TEMPS,
|
||||||
DATA_DEHUMIDIFICATION_PUMP,
|
DATA_DEHUMIDIFICATION_PUMP,
|
||||||
DATA_DEHUMIDIFICATION_SETPOINTS,
|
DATA_DEHUMIDIFICATION_SETPOINTS,
|
||||||
@@ -33,6 +34,10 @@ from .const import (
|
|||||||
DATA_SYSTEM_ACTIVATION,
|
DATA_SYSTEM_ACTIVATION,
|
||||||
DATA_TEMPERATURES,
|
DATA_TEMPERATURES,
|
||||||
DATA_TIME_SETTINGS,
|
DATA_TIME_SETTINGS,
|
||||||
|
DATA_VENTILATION_MODE_DEHUMIDIFICATION,
|
||||||
|
DATA_VENTILATION_MODE_INTEGRATION,
|
||||||
|
DATA_VENTILATION_MODE_RENEWAL,
|
||||||
|
DATA_VENTILATION_MODE_VENTILATION,
|
||||||
DATA_VENTILATION_REQUEST,
|
DATA_VENTILATION_REQUEST,
|
||||||
DATA_WINTER_SETPOINTS,
|
DATA_WINTER_SETPOINTS,
|
||||||
DATA_ZONE_ACTIVITY,
|
DATA_ZONE_ACTIVITY,
|
||||||
@@ -83,6 +88,7 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
temperatures = await self.client.read_temperatures()
|
temperatures = await self.client.read_temperatures()
|
||||||
winter_setpoints = await self.client.read_winter_setpoints()
|
winter_setpoints = await self.client.read_winter_setpoints()
|
||||||
summer_setpoints = await self.client.read_summer_setpoints()
|
summer_setpoints = await self.client.read_summer_setpoints()
|
||||||
|
calculated_setpoints = await self.client.read_calculated_setpoints()
|
||||||
season = await self.client.read_season()
|
season = await self.client.read_season()
|
||||||
zone_activity = await self.client.read_zone_activity()
|
zone_activity = await self.client.read_zone_activity()
|
||||||
humidity = await self.client.read_humidity()
|
humidity = await self.client.read_humidity()
|
||||||
@@ -105,6 +111,9 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
integration_request = await self.client.read_integration_request()
|
integration_request = await self.client.read_integration_request()
|
||||||
dehumidification_pump = await self.client.read_dehumidification_pump()
|
dehumidification_pump = await self.client.read_dehumidification_pump()
|
||||||
|
|
||||||
|
# Read ventilation mode registers
|
||||||
|
ventilation_modes = await self.client.read_ventilation_modes()
|
||||||
|
|
||||||
if temperatures is None or winter_setpoints is None or summer_setpoints is None:
|
if temperatures is None or winter_setpoints is None or summer_setpoints is None:
|
||||||
raise UpdateFailed("Failed to read data from Modbus device")
|
raise UpdateFailed("Failed to read data from Modbus device")
|
||||||
|
|
||||||
@@ -118,6 +127,7 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
DATA_TEMPERATURES: temperatures,
|
DATA_TEMPERATURES: temperatures,
|
||||||
DATA_WINTER_SETPOINTS: winter_setpoints,
|
DATA_WINTER_SETPOINTS: winter_setpoints,
|
||||||
DATA_SUMMER_SETPOINTS: summer_setpoints,
|
DATA_SUMMER_SETPOINTS: summer_setpoints,
|
||||||
|
DATA_CALCULATED_SETPOINTS: calculated_setpoints,
|
||||||
DATA_SEASON: season, # True = summer, False = winter, None = unknown
|
DATA_SEASON: season, # True = summer, False = winter, None = unknown
|
||||||
DATA_ZONE_ACTIVITY: zone_activity, # dict of zone_id -> is_active
|
DATA_ZONE_ACTIVITY: zone_activity, # dict of zone_id -> is_active
|
||||||
DATA_HUMIDITY: humidity,
|
DATA_HUMIDITY: humidity,
|
||||||
@@ -137,6 +147,11 @@ class RDZDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
DATA_RENEWAL_REQUEST: renewal_request,
|
DATA_RENEWAL_REQUEST: renewal_request,
|
||||||
DATA_INTEGRATION_REQUEST: integration_request,
|
DATA_INTEGRATION_REQUEST: integration_request,
|
||||||
DATA_DEHUMIDIFICATION_PUMP: dehumidification_pump,
|
DATA_DEHUMIDIFICATION_PUMP: dehumidification_pump,
|
||||||
|
# Ventilation mode states
|
||||||
|
DATA_VENTILATION_MODE_DEHUMIDIFICATION: ventilation_modes.get("dehumidification") if ventilation_modes else None,
|
||||||
|
DATA_VENTILATION_MODE_VENTILATION: ventilation_modes.get("ventilation") if ventilation_modes else None,
|
||||||
|
DATA_VENTILATION_MODE_RENEWAL: ventilation_modes.get("renewal") if ventilation_modes else None,
|
||||||
|
DATA_VENTILATION_MODE_INTEGRATION: ventilation_modes.get("integration") if ventilation_modes else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pymodbus>=3.6.0"],
|
"requirements": ["pymodbus>=3.6.0"],
|
||||||
"version": "1.1.2"
|
"version": "1.1.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from .const import (
|
|||||||
COIL_SYSTEM_ACTIVATION_COUNT,
|
COIL_SYSTEM_ACTIVATION_COUNT,
|
||||||
REGISTER_ACTIVITY_COUNT,
|
REGISTER_ACTIVITY_COUNT,
|
||||||
REGISTER_ACTIVITY_START,
|
REGISTER_ACTIVITY_START,
|
||||||
|
REGISTER_CALCULATED_SETPOINT_COUNT,
|
||||||
|
REGISTER_CALCULATED_SETPOINT_START,
|
||||||
REGISTER_CALCULATED_WATER_TEMP_COUNT,
|
REGISTER_CALCULATED_WATER_TEMP_COUNT,
|
||||||
REGISTER_CALCULATED_WATER_TEMP_START,
|
REGISTER_CALCULATED_WATER_TEMP_START,
|
||||||
REGISTER_DEHUMIDIFICATION_PUMP_COUNT,
|
REGISTER_DEHUMIDIFICATION_PUMP_COUNT,
|
||||||
@@ -226,6 +228,20 @@ class RDZModbusClient:
|
|||||||
|
|
||||||
return {i: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
|
return {i: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
|
||||||
|
|
||||||
|
async def read_calculated_setpoints(self) -> dict[int, float] | None:
|
||||||
|
"""Read all calculated setpoint registers.
|
||||||
|
|
||||||
|
These are program-calculated target temperatures used in non-manual modes.
|
||||||
|
Values are stored as integer * 10 (e.g., 21.5°C = 215).
|
||||||
|
"""
|
||||||
|
registers = await self.read_registers(
|
||||||
|
REGISTER_CALCULATED_SETPOINT_START, REGISTER_CALCULATED_SETPOINT_COUNT
|
||||||
|
)
|
||||||
|
if registers is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {i: value / TEMP_SCALE_FACTOR for i, value in enumerate(registers)}
|
||||||
|
|
||||||
async def read_zone_activity(self) -> dict[int, bool] | None:
|
async def read_zone_activity(self) -> dict[int, bool] | None:
|
||||||
"""Read zone activity bitmasks and return dict of zone_id -> is_active.
|
"""Read zone activity bitmasks and return dict of zone_id -> is_active.
|
||||||
|
|
||||||
@@ -361,7 +377,10 @@ class RDZModbusClient:
|
|||||||
registers = await self.read_registers(REGISTER_OUTSIDE_TEMP, 1)
|
registers = await self.read_registers(REGISTER_OUTSIDE_TEMP, 1)
|
||||||
if registers is None:
|
if registers is None:
|
||||||
return None
|
return None
|
||||||
return registers[0] / TEMP_SCALE_FACTOR
|
raw = registers[0]
|
||||||
|
if raw > 32767:
|
||||||
|
raw -= 65536
|
||||||
|
return raw / TEMP_SCALE_FACTOR
|
||||||
|
|
||||||
async def read_time_settings(self) -> dict[str, int] | None:
|
async def read_time_settings(self) -> dict[str, int] | None:
|
||||||
"""Read time settings from registers 5009-5013.
|
"""Read time settings from registers 5009-5013.
|
||||||
@@ -542,3 +561,19 @@ class RDZModbusClient:
|
|||||||
return await self._read_zone_bitmask(
|
return await self._read_zone_bitmask(
|
||||||
REGISTER_DEHUMIDIFICATION_PUMP_START, REGISTER_DEHUMIDIFICATION_PUMP_COUNT
|
REGISTER_DEHUMIDIFICATION_PUMP_START, REGISTER_DEHUMIDIFICATION_PUMP_COUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def read_ventilation_modes(self) -> dict[str, bool] | None:
|
||||||
|
"""Read ventilation mode registers 8201-8204.
|
||||||
|
|
||||||
|
Returns dict with keys: dehumidification, ventilation, renewal, integration.
|
||||||
|
LSB (bit 0) of each register indicates on/off state.
|
||||||
|
"""
|
||||||
|
registers = await self.read_registers(8201, 4)
|
||||||
|
if registers is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"dehumidification": bool(registers[0] & 1),
|
||||||
|
"ventilation": bool(registers[1] & 1),
|
||||||
|
"renewal": bool(registers[2] & 1),
|
||||||
|
"integration": bool(registers[3] & 1),
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory, UnitOfTemperature
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@@ -23,6 +23,7 @@ from .const import (
|
|||||||
DATA_CALCULATED_WATER_TEMPS,
|
DATA_CALCULATED_WATER_TEMPS,
|
||||||
DATA_DELIVERY_WATER_TEMPS,
|
DATA_DELIVERY_WATER_TEMPS,
|
||||||
DATA_DEW_POINTS,
|
DATA_DEW_POINTS,
|
||||||
|
DATA_HUMIDITY,
|
||||||
DATA_OUTSIDE_TEMP,
|
DATA_OUTSIDE_TEMP,
|
||||||
DATA_SUMMER_SETPOINTS,
|
DATA_SUMMER_SETPOINTS,
|
||||||
DATA_WINTER_SETPOINTS,
|
DATA_WINTER_SETPOINTS,
|
||||||
@@ -58,6 +59,10 @@ async def async_setup_entry(
|
|||||||
entities.append(
|
entities.append(
|
||||||
RDZDewPointSensor(coordinator, zone_id, zone_data)
|
RDZDewPointSensor(coordinator, zone_id, zone_data)
|
||||||
)
|
)
|
||||||
|
# Create humidity sensor for each zone
|
||||||
|
entities.append(
|
||||||
|
RDZHumiditySensor(coordinator, zone_id, zone_data)
|
||||||
|
)
|
||||||
|
|
||||||
# System-level sensors
|
# System-level sensors
|
||||||
# Outside temperature sensor
|
# Outside temperature sensor
|
||||||
@@ -234,6 +239,81 @@ class RDZDewPointSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntit
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class RDZHumiditySensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntity):
|
||||||
|
"""Representation of an RDZ HMI humidity sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||||
|
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||||
|
_attr_native_unit_of_measurement = PERCENTAGE
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: RDZDataUpdateCoordinator,
|
||||||
|
zone_id: int,
|
||||||
|
zone_data: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the humidity 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}_humidity"
|
||||||
|
)
|
||||||
|
self._attr_translation_key = "humidity"
|
||||||
|
self._attr_name = "Humidity"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
humidity = self.coordinator.data.get(DATA_HUMIDITY, {})
|
||||||
|
return humidity.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):
|
class RDZOutsideTemperatureSensor(CoordinatorEntity[RDZDataUpdateCoordinator], SensorEntity):
|
||||||
"""Representation of an RDZ HMI outside temperature sensor."""
|
"""Representation of an RDZ HMI outside temperature sensor."""
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class RDZSeasonSwitch(CoordinatorEntity[RDZDataUpdateCoordinator], SwitchEntity)
|
|||||||
"""Representation of the RDZ HMI season switch."""
|
"""Representation of the RDZ HMI season switch."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_translation_key = "season"
|
_attr_name = "Summer Mode"
|
||||||
_attr_icon = "mdi:sun-snowflake-variant"
|
_attr_icon = "mdi:sun-snowflake-variant"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
Reference in New Issue
Block a user