mirror of
https://github.com/home-assistant/core.git
synced 2026-08-27 10:12:31 -05:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
320 lines
11 KiB
Python
320 lines
11 KiB
Python
"""Support for the Flexit climate platform."""
|
|
|
|
import logging
|
|
from typing import Any, override
|
|
|
|
from flexit_modbus import MAX_TEMPERATURE, MIN_TEMPERATURE, FanMode, SystemActivity
|
|
from modbus_connection import ModbusError
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.climate import (
|
|
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA,
|
|
ClimateEntity,
|
|
ClimateEntityFeature,
|
|
HVACAction,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.components.modbus import ModbusHub, get_hub
|
|
from homeassistant.const import (
|
|
ATTR_TEMPERATURE,
|
|
CONF_NAME,
|
|
CONF_SLAVE,
|
|
DEVICE_DEFAULT_NAME,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
|
from homeassistant.helpers.entity_platform import (
|
|
AddConfigEntryEntitiesCallback,
|
|
AddEntitiesCallback,
|
|
)
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
|
|
from . import FlexitConfigEntry
|
|
from .const import DOMAIN
|
|
from .coordinator import FlexitDataCoordinator
|
|
from .entity import FlexitEntity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PARALLEL_UPDATES = 0
|
|
|
|
CALL_TYPE_REGISTER_HOLDING = "holding"
|
|
CALL_TYPE_REGISTER_INPUT = "input"
|
|
CALL_TYPE_WRITE_REGISTER = "write_register"
|
|
DEFAULT_HUB = "modbus_hub"
|
|
|
|
CONF_HUB = "hub"
|
|
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
|
vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)),
|
|
vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string,
|
|
}
|
|
)
|
|
|
|
FLEXIT_TO_HA_FAN_MODE = {
|
|
FanMode.OFF: "Off",
|
|
FanMode.LOW: "Low",
|
|
FanMode.MEDIUM: "Medium",
|
|
FanMode.HIGH: "High",
|
|
}
|
|
HA_TO_FLEXIT_FAN_MODE = {value: key for key, value in FLEXIT_TO_HA_FAN_MODE.items()}
|
|
|
|
FLEXIT_TO_HA_HVAC_ACTION = {
|
|
SystemActivity.OFF: HVACAction.OFF,
|
|
SystemActivity.FAN: HVACAction.FAN,
|
|
SystemActivity.HEAT_RECOVERY: HVACAction.IDLE,
|
|
SystemActivity.COOLING: HVACAction.COOLING,
|
|
SystemActivity.HEATING: HVACAction.HEATING,
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: FlexitConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Flexit climate platform."""
|
|
async_add_entities([FlexitClimate(entry.runtime_data)])
|
|
|
|
|
|
async def async_setup_platform(
|
|
hass: HomeAssistant,
|
|
config: ConfigType,
|
|
async_add_entities: AddEntitiesCallback,
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
) -> None:
|
|
"""Set up the deprecated YAML configuration."""
|
|
ir.async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
"deprecated_yaml_no_import",
|
|
breaks_in_ha_version="2027.3.0",
|
|
is_fixable=False,
|
|
is_persistent=False,
|
|
issue_domain=DOMAIN,
|
|
severity=ir.IssueSeverity.WARNING,
|
|
translation_key="deprecated_yaml_no_import",
|
|
translation_placeholders={"domain": DOMAIN, "integration_title": "Flexit"},
|
|
)
|
|
modbus_slave = config.get(CONF_SLAVE)
|
|
name = config.get(CONF_NAME)
|
|
hub = get_hub(hass, config[CONF_HUB])
|
|
async_add_entities([Flexit(hub, modbus_slave, name)], True)
|
|
|
|
|
|
class Flexit(ClimateEntity):
|
|
"""Representation of a Flexit AC unit."""
|
|
|
|
_attr_fan_modes = ["Off", "Low", "Medium", "High"]
|
|
_attr_hvac_mode = HVACMode.COOL
|
|
_attr_hvac_modes = [HVACMode.COOL]
|
|
_attr_supported_features = (
|
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
|
)
|
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
def __init__(
|
|
self, hub: ModbusHub, modbus_slave: int | None, name: str | None
|
|
) -> None:
|
|
"""Initialize the unit."""
|
|
self._hub = hub
|
|
self._attr_name = name
|
|
self._slave = modbus_slave
|
|
self._attr_fan_mode = None
|
|
self._filter_hours: int | None = None
|
|
self._filter_alarm: int | None = None
|
|
self._heat_recovery: int | None = None
|
|
self._heater_enabled: int | None = None
|
|
self._heating: int | None = None
|
|
self._cooling: int | None = None
|
|
self._alarm = False
|
|
self._outdoor_air_temp: float | None = None
|
|
|
|
async def async_update(self) -> None:
|
|
"""Update unit attributes."""
|
|
self._attr_target_temperature = await self._async_read_temp_from_register(
|
|
CALL_TYPE_REGISTER_HOLDING, 8
|
|
)
|
|
self._attr_current_temperature = await self._async_read_temp_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 9
|
|
)
|
|
res = await self._async_read_int16_from_register(CALL_TYPE_REGISTER_HOLDING, 17)
|
|
if self.fan_modes and res < len(self.fan_modes):
|
|
self._attr_fan_mode = self.fan_modes[res]
|
|
self._filter_hours = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 8
|
|
)
|
|
# # Mechanical heat recovery, 0-100%
|
|
self._heat_recovery = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 14
|
|
)
|
|
# # Heater active 0-100%
|
|
self._heating = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 15
|
|
)
|
|
# # Cooling active 0-100%
|
|
self._cooling = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 13
|
|
)
|
|
# # Filter alarm 0/1
|
|
self._filter_alarm = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 27
|
|
)
|
|
# # Heater enabled or not. Does not mean it's necessarily heating
|
|
self._heater_enabled = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 28
|
|
)
|
|
self._outdoor_air_temp = await self._async_read_temp_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 11
|
|
)
|
|
|
|
actual_air_speed = await self._async_read_int16_from_register(
|
|
CALL_TYPE_REGISTER_INPUT, 48
|
|
)
|
|
|
|
if self._heating:
|
|
self._attr_hvac_action = HVACAction.HEATING
|
|
elif self._cooling:
|
|
self._attr_hvac_action = HVACAction.COOLING
|
|
elif self._heat_recovery:
|
|
self._attr_hvac_action = HVACAction.IDLE
|
|
elif actual_air_speed:
|
|
self._attr_hvac_action = HVACAction.FAN
|
|
else:
|
|
self._attr_hvac_action = HVACAction.OFF
|
|
|
|
@property
|
|
@override
|
|
def extra_state_attributes(self) -> dict[str, Any]:
|
|
"""Return device specific state attributes."""
|
|
return {
|
|
"filter_hours": self._filter_hours,
|
|
"filter_alarm": self._filter_alarm,
|
|
"heat_recovery": self._heat_recovery,
|
|
"heating": self._heating,
|
|
"heater_enabled": self._heater_enabled,
|
|
"cooling": self._cooling,
|
|
"outdoor_air_temp": self._outdoor_air_temp,
|
|
}
|
|
|
|
@override
|
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
"""Set new target temperature."""
|
|
if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
|
_LOGGER.error("Received invalid temperature")
|
|
return
|
|
|
|
if await self._async_write_int16_to_register(8, int(target_temperature * 10)):
|
|
self._attr_target_temperature = target_temperature
|
|
else:
|
|
_LOGGER.error("Modbus error setting target temperature to Flexit")
|
|
|
|
@override
|
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Set new fan mode."""
|
|
if self.fan_modes and await self._async_write_int16_to_register(
|
|
17, self.fan_modes.index(fan_mode)
|
|
):
|
|
self._attr_fan_mode = fan_mode
|
|
else:
|
|
_LOGGER.error("Modbus error setting fan mode to Flexit")
|
|
|
|
async def _async_read_int16_from_register(
|
|
self, register_type: str, register: int
|
|
) -> int:
|
|
"""Read register using the Modbus hub slave."""
|
|
result = await self._hub.async_pb_call(self._slave, register, 1, register_type)
|
|
if result is None:
|
|
_LOGGER.error("Error reading value from Flexit modbus adapter")
|
|
return -1
|
|
|
|
return int(result.registers[0])
|
|
|
|
async def _async_read_temp_from_register(
|
|
self, register_type: str, register: int
|
|
) -> float:
|
|
result = float(
|
|
await self._async_read_int16_from_register(register_type, register)
|
|
)
|
|
if not result:
|
|
return -1
|
|
return result / 10.0
|
|
|
|
async def _async_write_int16_to_register(self, register: int, value: int) -> bool:
|
|
result = await self._hub.async_pb_call(
|
|
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
|
|
)
|
|
if not result:
|
|
return False
|
|
return True
|
|
|
|
|
|
class FlexitClimate(FlexitEntity, ClimateEntity):
|
|
"""Representation of a Flexit AC unit."""
|
|
|
|
_attr_name = None
|
|
_attr_fan_modes = list(HA_TO_FLEXIT_FAN_MODE)
|
|
_attr_hvac_mode = HVACMode.HEAT_COOL
|
|
_attr_hvac_modes = [HVACMode.HEAT_COOL]
|
|
_attr_supported_features = (
|
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
|
)
|
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
_attr_min_temp = MIN_TEMPERATURE
|
|
_attr_max_temp = MAX_TEMPERATURE
|
|
|
|
def __init__(self, coordinator: FlexitDataCoordinator) -> None:
|
|
"""Initialize the unit."""
|
|
assert coordinator.config_entry is not None
|
|
super().__init__(coordinator)
|
|
self._attr_unique_id = coordinator.config_entry.entry_id
|
|
self._set_attr()
|
|
|
|
@override
|
|
def _handle_coordinator_update(self) -> None:
|
|
"""Handle entity update."""
|
|
self._set_attr()
|
|
super()._handle_coordinator_update()
|
|
|
|
def _set_attr(self) -> None:
|
|
device = self.coordinator.device
|
|
measurements = device.measurements
|
|
|
|
fan_mode = device.fan_mode
|
|
activity = device.activity
|
|
|
|
self._attr_target_temperature = device.target_temperature
|
|
self._attr_current_temperature = measurements.supply_air_temperature
|
|
self._attr_fan_mode = (
|
|
FLEXIT_TO_HA_FAN_MODE.get(fan_mode) if fan_mode is not None else None
|
|
)
|
|
self._attr_hvac_action = (
|
|
FLEXIT_TO_HA_HVAC_ACTION.get(activity) if activity is not None else None
|
|
)
|
|
|
|
@override
|
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
"""Set new target temperature."""
|
|
target_temperature = kwargs[ATTR_TEMPERATURE]
|
|
try:
|
|
await self.coordinator.device.async_set_target_temperature(
|
|
target_temperature
|
|
)
|
|
except ModbusError as err:
|
|
raise HomeAssistantError("Failed to set target temperature") from err
|
|
await self.coordinator.async_request_refresh()
|
|
|
|
@override
|
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Set new fan mode."""
|
|
try:
|
|
await self.coordinator.device.async_set_fan_mode(
|
|
HA_TO_FLEXIT_FAN_MODE[fan_mode]
|
|
)
|
|
except ModbusError as err:
|
|
raise HomeAssistantError("Failed to set fan mode") from err
|
|
await self.coordinator.async_request_refresh()
|