mirror of
https://github.com/home-assistant/core.git
synced 2026-07-10 08:15:14 +01:00
bb964ccd95
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
"""Fan platform for the Duco integration."""
|
|
|
|
import logging
|
|
|
|
from duco_connectivity.exceptions import DucoError, DucoRateLimitError
|
|
from duco_connectivity.models import Node, NodeType, VentilationState
|
|
|
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.util.percentage import percentage_to_ordered_list_item
|
|
|
|
from .const import DOMAIN
|
|
from .coordinator import DucoConfigEntry, DucoCoordinator
|
|
from .entity import DucoEntity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
# Permanent speed states ordered low → high.
|
|
ORDERED_NAMED_FAN_SPEEDS: list[VentilationState] = [
|
|
VentilationState.CNT1,
|
|
VentilationState.CNT2,
|
|
VentilationState.CNT3,
|
|
]
|
|
|
|
PRESET_AUTO = "auto"
|
|
|
|
# Upper-bound percentages for 3 speed levels: 33 / 66 / 100.
|
|
# Using upper bounds guarantees that reading a percentage back and writing it
|
|
# again always round-trips to the same Duco state.
|
|
_SPEED_LEVEL_PERCENTAGES: list[int] = [
|
|
(i + 1) * 100 // len(ORDERED_NAMED_FAN_SPEEDS)
|
|
for i, _ in enumerate(ORDERED_NAMED_FAN_SPEEDS)
|
|
]
|
|
|
|
# Maps every active Duco state (including timed MAN variants) to its
|
|
# display percentage so externally-set timed modes show the correct level.
|
|
_STATE_TO_PERCENTAGE: dict[VentilationState, int] = {
|
|
VentilationState.CNT1: _SPEED_LEVEL_PERCENTAGES[0],
|
|
VentilationState.MAN1: _SPEED_LEVEL_PERCENTAGES[0],
|
|
VentilationState.MAN1x2: _SPEED_LEVEL_PERCENTAGES[0],
|
|
VentilationState.MAN1x3: _SPEED_LEVEL_PERCENTAGES[0],
|
|
VentilationState.CNT2: _SPEED_LEVEL_PERCENTAGES[1],
|
|
VentilationState.MAN2: _SPEED_LEVEL_PERCENTAGES[1],
|
|
VentilationState.MAN2x2: _SPEED_LEVEL_PERCENTAGES[1],
|
|
VentilationState.MAN2x3: _SPEED_LEVEL_PERCENTAGES[1],
|
|
VentilationState.CNT3: _SPEED_LEVEL_PERCENTAGES[2],
|
|
VentilationState.MAN3: _SPEED_LEVEL_PERCENTAGES[2],
|
|
VentilationState.MAN3x2: _SPEED_LEVEL_PERCENTAGES[2],
|
|
VentilationState.MAN3x3: _SPEED_LEVEL_PERCENTAGES[2],
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: DucoConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up Duco fan entities."""
|
|
coordinator = entry.runtime_data
|
|
|
|
# BOX is always node 1 and is never dynamically added
|
|
# or removed, so no listener needed.
|
|
async_add_entities(
|
|
DucoVentilationFanEntity(coordinator, node)
|
|
for node in coordinator.data.nodes.values()
|
|
if node.general.node_type == NodeType.BOX
|
|
)
|
|
|
|
|
|
class DucoVentilationFanEntity(DucoEntity, FanEntity):
|
|
"""Fan entity for the ventilation control of a Duco node."""
|
|
|
|
_attr_translation_key = "ventilation"
|
|
_attr_name = None
|
|
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE
|
|
_attr_preset_modes = [PRESET_AUTO]
|
|
_attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
|
|
|
|
def __init__(self, coordinator: DucoCoordinator, node: Node) -> None:
|
|
"""Initialize the fan entity."""
|
|
super().__init__(coordinator, node)
|
|
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{node.node_id}"
|
|
|
|
@property
|
|
def percentage(self) -> int | None:
|
|
"""Return the current speed as a percentage, or None when in AUTO mode."""
|
|
node = self._node
|
|
if node.ventilation is None:
|
|
return None
|
|
return _STATE_TO_PERCENTAGE.get(node.ventilation.state)
|
|
|
|
@property
|
|
def preset_mode(self) -> str | None:
|
|
"""Return the current preset mode (auto when Duco controls, else None)."""
|
|
node = self._node
|
|
if node.ventilation is None:
|
|
return None
|
|
if node.ventilation.state not in _STATE_TO_PERCENTAGE:
|
|
return PRESET_AUTO
|
|
return None
|
|
|
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Set preset mode: 'auto' hands control back to Duco."""
|
|
self._valid_preset_mode_or_raise(preset_mode)
|
|
await self._async_set_state(VentilationState.AUTO)
|
|
|
|
async def async_set_percentage(self, percentage: int) -> None:
|
|
"""Set the fan speed as a percentage (maps to low/medium/high)."""
|
|
if percentage == 0:
|
|
await self._async_set_state(VentilationState.AUTO)
|
|
return
|
|
state = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
|
|
await self._async_set_state(state)
|
|
|
|
async def _async_set_state(self, state: VentilationState) -> None:
|
|
"""Send the ventilation state to the device and refresh coordinator."""
|
|
try:
|
|
await self.coordinator.client.async_set_ventilation_state(
|
|
self._node_id, state
|
|
)
|
|
except DucoRateLimitError as err:
|
|
_LOGGER.warning("Duco write rate limit exceeded for node %s", self._node_id)
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="rate_limit_exceeded",
|
|
) from err
|
|
except DucoError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="failed_to_set_state",
|
|
translation_placeholders={"error": repr(err)},
|
|
) from err
|
|
await self.coordinator.async_refresh()
|