1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Support KNX climate entity configuration from UI (#154162)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Matthias Alphart
2025-10-25 23:50:14 +02:00
committed by GitHub
parent 278f32285a
commit fc9313f7ef
9 changed files with 1530 additions and 95 deletions

View File

@@ -12,6 +12,7 @@ from xknx.devices import (
)
from xknx.devices.fan import FanSpeedMode
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
from xknx.remote_value.remote_value_setpoint_shift import SetpointShiftMode
from homeassistant import config_entries
from homeassistant.components.climate import (
@@ -34,13 +35,53 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.typing import ConfigType
from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .const import (
CONF_SYNC_STATE,
CONTROLLER_MODES,
CURRENT_HVAC_ACTIONS,
DOMAIN,
KNX_MODULE_KEY,
ClimateConf,
)
from .entity import (
KnxUiEntity,
KnxUiEntityPlatformController,
KnxYamlEntity,
_KnxEntityBase,
)
from .knx_module import KNXModule
from .schema import ClimateSchema
from .storage.const import (
CONF_ENTITY,
CONF_GA_ACTIVE,
CONF_GA_CONTROLLER_MODE,
CONF_GA_CONTROLLER_STATUS,
CONF_GA_FAN_SPEED,
CONF_GA_FAN_SWING,
CONF_GA_FAN_SWING_HORIZONTAL,
CONF_GA_HEAT_COOL,
CONF_GA_HUMIDITY_CURRENT,
CONF_GA_ON_OFF,
CONF_GA_OP_MODE_COMFORT,
CONF_GA_OP_MODE_ECO,
CONF_GA_OP_MODE_PROTECTION,
CONF_GA_OP_MODE_STANDBY,
CONF_GA_OPERATION_MODE,
CONF_GA_SETPOINT_SHIFT,
CONF_GA_TEMPERATURE_CURRENT,
CONF_GA_TEMPERATURE_TARGET,
CONF_GA_VALVE,
CONF_IGNORE_AUTO_MODE,
CONF_TARGET_TEMPERATURE,
)
from .storage.entity_store_schema import ConfClimateFanSpeedMode, ConfSetpointShiftMode
from .storage.util import ConfigExtractor
ATTR_COMMAND_VALUE = "command_value"
CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()}
@@ -53,12 +94,30 @@ async def async_setup_entry(
) -> None:
"""Set up climate(s) for KNX platform."""
knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.CLIMATE]
async_add_entities(
KNXClimate(knx_module, entity_config) for entity_config in config
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.CLIMATE,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiClimate,
),
)
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.CLIMATE):
entities.extend(
KnxYamlClimate(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(Platform.CLIMATE):
entities.extend(
KnxUiClimate(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)
def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
"""Return a KNX Climate device to be used within XKNX."""
@@ -99,8 +158,8 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
group_address_heat_cool_state=config.get(
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS
),
operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES),
controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES),
operation_modes=config.get(ClimateConf.OPERATION_MODES),
controller_modes=config.get(ClimateConf.CONTROLLER_MODES),
)
return XknxClimate(
@@ -120,24 +179,24 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS
),
setpoint_shift_mode=config.get(ClimateSchema.CONF_SETPOINT_SHIFT_MODE),
setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX],
setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN],
temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP],
setpoint_shift_max=config[ClimateConf.SETPOINT_SHIFT_MAX],
setpoint_shift_min=config[ClimateConf.SETPOINT_SHIFT_MIN],
temperature_step=config[ClimateConf.TEMPERATURE_STEP],
group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS),
group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS),
on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
on_off_invert=config[ClimateConf.ON_OFF_INVERT],
group_address_active_state=config.get(ClimateSchema.CONF_ACTIVE_STATE_ADDRESS),
group_address_command_value_state=config.get(
ClimateSchema.CONF_COMMAND_VALUE_STATE_ADDRESS
),
min_temp=config.get(ClimateSchema.CONF_MIN_TEMP),
max_temp=config.get(ClimateSchema.CONF_MAX_TEMP),
min_temp=config.get(ClimateConf.MIN_TEMP),
max_temp=config.get(ClimateConf.MAX_TEMP),
mode=climate_mode,
group_address_fan_speed=config.get(ClimateSchema.CONF_FAN_SPEED_ADDRESS),
group_address_fan_speed_state=config.get(
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS
),
fan_speed_mode=config[ClimateSchema.CONF_FAN_SPEED_MODE],
fan_speed_mode=config[ClimateConf.FAN_SPEED_MODE],
group_address_swing=config.get(ClimateSchema.CONF_SWING_ADDRESS),
group_address_swing_state=config.get(ClimateSchema.CONF_SWING_STATE_ADDRESS),
group_address_horizontal_swing=config.get(
@@ -152,91 +211,195 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
)
class KNXClimate(KnxYamlEntity, ClimateEntity):
def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClimate:
"""Return a KNX Climate device to be used within XKNX from UI config."""
sync_state = conf.get(CONF_SYNC_STATE)
op_modes: list[str | HVACOperationMode] = list(HVACOperationMode)
if conf.get(CONF_IGNORE_AUTO_MODE):
op_modes.remove(HVACOperationMode.AUTO)
climate_mode = XknxClimateMode(
xknx,
name=f"{name} Mode",
group_address_operation_mode=conf.get_write(CONF_GA_OPERATION_MODE),
group_address_operation_mode_state=conf.get_state_and_passive(
CONF_GA_OPERATION_MODE
),
group_address_operation_mode_comfort=conf.get_write_and_passive(
CONF_GA_OP_MODE_COMFORT
),
group_address_operation_mode_economy=conf.get_write_and_passive(
CONF_GA_OP_MODE_ECO
),
group_address_operation_mode_protection=conf.get_write_and_passive(
CONF_GA_OP_MODE_PROTECTION
),
group_address_operation_mode_standby=conf.get_write_and_passive(
CONF_GA_OP_MODE_STANDBY
),
group_address_controller_status=conf.get_write(CONF_GA_CONTROLLER_STATUS),
group_address_controller_status_state=conf.get_state_and_passive(
CONF_GA_CONTROLLER_STATUS
),
group_address_controller_mode=conf.get_write(CONF_GA_CONTROLLER_MODE),
group_address_controller_mode_state=conf.get_state_and_passive(
CONF_GA_CONTROLLER_MODE
),
group_address_heat_cool=conf.get_write(CONF_GA_HEAT_COOL),
group_address_heat_cool_state=conf.get_state_and_passive(CONF_GA_HEAT_COOL),
sync_state=sync_state,
operation_modes=op_modes,
)
sps_mode = None
if _sps_dpt := conf.get_dpt(CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT):
sps_mode = (
SetpointShiftMode.DPT6010
if _sps_dpt == ConfSetpointShiftMode.COUNT
else SetpointShiftMode.DPT9002
)
_fan_speed_dpt = conf.get_dpt(CONF_GA_FAN_SPEED)
fan_speed_mode = (
FanSpeedMode.STEP
if _fan_speed_dpt == ConfClimateFanSpeedMode.STEPS
else FanSpeedMode.PERCENT
)
return XknxClimate(
xknx,
name=name,
group_address_temperature=conf.get_state_and_passive(
CONF_GA_TEMPERATURE_CURRENT
),
group_address_target_temperature=conf.get_write(
CONF_TARGET_TEMPERATURE, CONF_GA_TEMPERATURE_TARGET
),
group_address_target_temperature_state=conf.get_state_and_passive(
CONF_TARGET_TEMPERATURE, CONF_GA_TEMPERATURE_TARGET
),
group_address_setpoint_shift=conf.get_write(
CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT
),
group_address_setpoint_shift_state=conf.get_state_and_passive(
CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT
),
setpoint_shift_mode=sps_mode,
setpoint_shift_max=conf.get(
CONF_TARGET_TEMPERATURE, ClimateConf.SETPOINT_SHIFT_MAX, default=6
),
setpoint_shift_min=conf.get(
CONF_TARGET_TEMPERATURE, ClimateConf.SETPOINT_SHIFT_MIN, default=-6
),
temperature_step=conf.get(
CONF_TARGET_TEMPERATURE, ClimateConf.TEMPERATURE_STEP, default=0.1
),
group_address_on_off=conf.get_write(CONF_GA_ON_OFF),
group_address_on_off_state=conf.get_state_and_passive(CONF_GA_ON_OFF),
on_off_invert=conf.get(ClimateConf.ON_OFF_INVERT, default=False),
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
sync_state=sync_state,
min_temp=conf.get(ClimateConf.MIN_TEMP),
max_temp=conf.get(ClimateConf.MAX_TEMP),
mode=climate_mode,
group_address_fan_speed=conf.get_write(CONF_GA_FAN_SPEED),
group_address_fan_speed_state=conf.get_state_and_passive(CONF_GA_FAN_SPEED),
fan_speed_mode=fan_speed_mode,
group_address_humidity_state=conf.get_state_and_passive(
CONF_GA_HUMIDITY_CURRENT
),
group_address_swing=conf.get_write(CONF_GA_FAN_SWING),
group_address_swing_state=conf.get_state_and_passive(CONF_GA_FAN_SWING),
group_address_horizontal_swing=conf.get_write(CONF_GA_FAN_SWING_HORIZONTAL),
group_address_horizontal_swing_state=conf.get_state_and_passive(
CONF_GA_FAN_SWING_HORIZONTAL
),
)
class _KnxClimate(ClimateEntity, _KnxEntityBase):
"""Representation of a KNX climate device."""
_device: XknxClimate
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "knx_climate"
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of a KNX climate device."""
super().__init__(
knx_module=knx_module,
device=_create_climate(knx_module.xknx, config),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
default_hvac_mode: HVACMode
_last_hvac_mode: HVACMode
fan_zero_mode: str
_fan_modes_percentages: list[int]
def _init_from_device_config(
self,
device: XknxClimate,
default_hvac_mode: HVACMode,
fan_max_step: int,
fan_zero_mode: str,
) -> None:
"""Set attributes that depend on device config."""
self.default_hvac_mode = default_hvac_mode
# non-OFF HVAC mode to be used when turning on the device without on_off address
self._last_hvac_mode = self.default_hvac_mode
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
if self._device.supports_on_off:
if device.supports_on_off:
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
if (
self._device.mode is not None
and len(self._device.mode.controller_modes) >= 2
and HVACControllerMode.OFF in self._device.mode.controller_modes
device.mode is not None
and len(device.mode.controller_modes) >= 2
and HVACControllerMode.OFF in device.mode.controller_modes
):
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
if (
self._device.mode is not None
and self._device.mode.operation_modes # empty list when not writable
device.mode is not None
and device.mode.operation_modes # empty list when not writable
):
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_preset_modes = [
mode.name.lower() for mode in self._device.mode.operation_modes
mode.name.lower() for mode in device.mode.operation_modes
]
fan_max_step = config[ClimateSchema.CONF_FAN_MAX_STEP]
self.fan_zero_mode = fan_zero_mode
self._fan_modes_percentages = [
int(100 * i / fan_max_step) for i in range(fan_max_step + 1)
]
self.fan_zero_mode: str = config[ClimateSchema.CONF_FAN_ZERO_MODE]
if self._device.fan_speed is not None and self._device.fan_speed.initialized:
if device.fan_speed is not None and device.fan_speed.initialized:
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
if fan_max_step == 3:
self._attr_fan_modes = [
self.fan_zero_mode,
fan_zero_mode,
FAN_LOW,
FAN_MEDIUM,
FAN_HIGH,
]
elif fan_max_step == 2:
self._attr_fan_modes = [self.fan_zero_mode, FAN_LOW, FAN_HIGH]
self._attr_fan_modes = [fan_zero_mode, FAN_LOW, FAN_HIGH]
elif fan_max_step == 1:
self._attr_fan_modes = [self.fan_zero_mode, FAN_ON]
elif self._device.fan_speed_mode == FanSpeedMode.STEP:
self._attr_fan_modes = [self.fan_zero_mode] + [
self._attr_fan_modes = [fan_zero_mode, FAN_ON]
elif device.fan_speed_mode == FanSpeedMode.STEP:
self._attr_fan_modes = [fan_zero_mode] + [
str(i) for i in range(1, fan_max_step + 1)
]
else:
self._attr_fan_modes = [self.fan_zero_mode] + [
self._attr_fan_modes = [fan_zero_mode] + [
f"{percentage}%" for percentage in self._fan_modes_percentages[1:]
]
if self._device.swing.initialized:
if device.swing.initialized:
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
self._attr_swing_modes = [SWING_ON, SWING_OFF]
if self._device.horizontal_swing.initialized:
if device.horizontal_swing.initialized:
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
self._attr_swing_horizontal_modes = [SWING_ON, SWING_OFF]
self._attr_target_temperature_step = self._device.temperature_step
self._attr_unique_id = (
f"{self._device.temperature.group_address_state}_"
f"{self._device.target_temperature.group_address_state}_"
f"{self._device.target_temperature.group_address}_"
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
)
self.default_hvac_mode: HVACMode = config[
ClimateSchema.CONF_DEFAULT_CONTROLLER_MODE
]
# non-OFF HVAC mode to be used when turning on the device without on_off address
self._last_hvac_mode: HVACMode = self.default_hvac_mode
self._attr_target_temperature_step = device.temperature_step
@property
def current_temperature(self) -> float | None:
@@ -475,3 +638,63 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
super().after_update_callback(device)
class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
"""Representation of a KNX climate device configured from YAML."""
_device: XknxClimate
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of a KNX climate device."""
super().__init__(
knx_module=knx_module,
device=_create_climate(knx_module.xknx, config),
)
default_hvac_mode: HVACMode = config[ClimateConf.DEFAULT_CONTROLLER_MODE]
fan_max_step = config[ClimateConf.FAN_MAX_STEP]
fan_zero_mode: str = config[ClimateConf.FAN_ZERO_MODE]
self._init_from_device_config(
device=self._device,
default_hvac_mode=default_hvac_mode,
fan_max_step=fan_max_step,
fan_zero_mode=fan_zero_mode,
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = (
f"{self._device.temperature.group_address_state}_"
f"{self._device.target_temperature.group_address_state}_"
f"{self._device.target_temperature.group_address}_"
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
)
class KnxUiClimate(_KnxClimate, KnxUiEntity):
"""Representation of a KNX climate device configured from the UI."""
_device: XknxClimate
def __init__(
self, knx_module: KNXModule, unique_id: str, config: ConfigType
) -> None:
"""Initialize of a KNX climate device."""
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
self._device = _create_climate_ui(
knx_module.xknx, knx_conf, config[CONF_ENTITY][CONF_NAME]
)
default_hvac_mode = HVACMode(knx_conf.get(ClimateConf.DEFAULT_CONTROLLER_MODE))
fan_max_step = knx_conf.get(ClimateConf.FAN_MAX_STEP)
fan_zero_mode = knx_conf.get(ClimateConf.FAN_ZERO_MODE)
self._init_from_device_config(
device=self._device,
default_hvac_mode=default_hvac_mode,
fan_max_step=fan_max_step,
fan_zero_mode=fan_zero_mode,
)

View File

@@ -160,6 +160,7 @@ SUPPORTED_PLATFORMS_YAML: Final = {
SUPPORTED_PLATFORMS_UI: Final = {
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SWITCH,
@@ -193,3 +194,23 @@ class CoverConf:
INVERT_UPDOWN: Final = "invert_updown"
INVERT_POSITION: Final = "invert_position"
INVERT_ANGLE: Final = "invert_angle"
class ClimateConf:
"""Common config keys for climate."""
MIN_TEMP: Final = "min_temp"
MAX_TEMP: Final = "max_temp"
TEMPERATURE_STEP: Final = "temperature_step"
SETPOINT_SHIFT_MAX: Final = "setpoint_shift_max"
SETPOINT_SHIFT_MIN: Final = "setpoint_shift_min"
ON_OFF_INVERT: Final = "on_off_invert"
OPERATION_MODES: Final = "operation_modes"
CONTROLLER_MODES: Final = "controller_modes"
DEFAULT_CONTROLLER_MODE: Final = "default_controller_mode"
FAN_MAX_STEP: Final = "fan_max_step"
FAN_SPEED_MODE: Final = "fan_speed_mode"
FAN_ZERO_MODE: Final = "fan_zero_mode"

View File

@@ -56,6 +56,7 @@ from .const import (
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
KNX_ADDRESS,
ClimateConf,
ColorTempModes,
CoverConf,
FanZeroMode,
@@ -306,10 +307,7 @@ class ClimateSchema(KNXPlatformSchema):
CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address"
CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address"
CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode"
CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max"
CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min"
CONF_TEMPERATURE_ADDRESS = "temperature_address"
CONF_TEMPERATURE_STEP = "temperature_step"
CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address"
CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address"
CONF_OPERATION_MODE_ADDRESS = "operation_mode_address"
@@ -327,19 +325,10 @@ class ClimateSchema(KNXPlatformSchema):
CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address"
CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address"
CONF_OPERATION_MODES = "operation_modes"
CONF_CONTROLLER_MODES = "controller_modes"
CONF_DEFAULT_CONTROLLER_MODE = "default_controller_mode"
CONF_ON_OFF_ADDRESS = "on_off_address"
CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
CONF_ON_OFF_INVERT = "on_off_invert"
CONF_MIN_TEMP = "min_temp"
CONF_MAX_TEMP = "max_temp"
CONF_FAN_SPEED_ADDRESS = "fan_speed_address"
CONF_FAN_SPEED_STATE_ADDRESS = "fan_speed_state_address"
CONF_FAN_MAX_STEP = "fan_max_step"
CONF_FAN_SPEED_MODE = "fan_speed_mode"
CONF_FAN_ZERO_MODE = "fan_zero_mode"
CONF_HUMIDITY_STATE_ADDRESS = "humidity_state_address"
CONF_SWING_ADDRESS = "swing_address"
CONF_SWING_STATE_ADDRESS = "swing_state_address"
@@ -359,13 +348,13 @@ class ClimateSchema(KNXPlatformSchema):
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(
CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
ClimateConf.SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
): vol.All(int, vol.Range(min=0, max=32)),
vol.Optional(
CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
ClimateConf.SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
): vol.All(int, vol.Range(min=-32, max=0)),
vol.Optional(
CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
ClimateConf.TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
): vol.All(float, vol.Range(min=0, max=2)),
vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator,
vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator,
@@ -408,29 +397,29 @@ class ClimateSchema(KNXPlatformSchema):
vol.Optional(CONF_ON_OFF_ADDRESS): ga_list_validator,
vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_list_validator,
vol.Optional(
CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
ClimateConf.ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
): cv.boolean,
vol.Optional(CONF_OPERATION_MODES): vol.All(
vol.Optional(ClimateConf.OPERATION_MODES): vol.All(
cv.ensure_list,
[backwards_compatible_xknx_climate_enum_member(HVACOperationMode)],
),
vol.Optional(CONF_CONTROLLER_MODES): vol.All(
vol.Optional(ClimateConf.CONTROLLER_MODES): vol.All(
cv.ensure_list,
[backwards_compatible_xknx_climate_enum_member(HVACControllerMode)],
),
vol.Optional(
CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
ClimateConf.DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
): vol.Coerce(HVACMode),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(ClimateConf.MIN_TEMP): vol.Coerce(float),
vol.Optional(ClimateConf.MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_FAN_SPEED_ADDRESS): ga_list_validator,
vol.Optional(CONF_FAN_SPEED_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_FAN_MAX_STEP, default=3): cv.byte,
vol.Optional(ClimateConf.FAN_MAX_STEP, default=3): cv.byte,
vol.Optional(
CONF_FAN_SPEED_MODE, default=DEFAULT_FAN_SPEED_MODE
ClimateConf.FAN_SPEED_MODE, default=DEFAULT_FAN_SPEED_MODE
): vol.All(vol.Upper, cv.enum(FanSpeedMode)),
vol.Optional(CONF_FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce(
vol.Optional(ClimateConf.FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce(
FanZeroMode
),
vol.Optional(CONF_SWING_ADDRESS): ga_list_validator,

View File

@@ -14,6 +14,28 @@ CONF_DPT: Final = "dpt"
CONF_GA_SENSOR: Final = "ga_sensor"
CONF_GA_SWITCH: Final = "ga_switch"
# Climate
CONF_GA_TEMPERATURE_CURRENT: Final = "ga_temperature_current"
CONF_GA_HUMIDITY_CURRENT: Final = "ga_humidity_current"
CONF_TARGET_TEMPERATURE: Final = "target_temperature"
CONF_GA_TEMPERATURE_TARGET: Final = "ga_temperature_target"
CONF_GA_SETPOINT_SHIFT: Final = "ga_setpoint_shift"
CONF_GA_ACTIVE: Final = "ga_active"
CONF_GA_VALVE: Final = "ga_valve"
CONF_GA_OPERATION_MODE: Final = "ga_operation_mode"
CONF_IGNORE_AUTO_MODE: Final = "ignore_auto_mode"
CONF_GA_OP_MODE_COMFORT: Final = "ga_operation_mode_comfort"
CONF_GA_OP_MODE_ECO: Final = "ga_operation_mode_economy"
CONF_GA_OP_MODE_STANDBY: Final = "ga_operation_mode_standby"
CONF_GA_OP_MODE_PROTECTION: Final = "ga_operation_mode_protection"
CONF_GA_HEAT_COOL: Final = "ga_heat_cool"
CONF_GA_ON_OFF: Final = "ga_on_off"
CONF_GA_CONTROLLER_MODE: Final = "ga_controller_mode"
CONF_GA_CONTROLLER_STATUS: Final = "ga_controller_status"
CONF_GA_FAN_SPEED: Final = "ga_fan_speed"
CONF_GA_FAN_SWING: Final = "ga_fan_swing"
CONF_GA_FAN_SWING_HORIZONTAL: Final = "ga_fan_swing_horizontal"
# Cover
CONF_GA_UP_DOWN: Final = "ga_up_down"
CONF_GA_STOP: Final = "ga_stop"

View File

@@ -4,6 +4,7 @@ from enum import StrEnum, unique
import voluptuous as vol
from homeassistant.components.climate import HVACMode
from homeassistant.const import (
CONF_ENTITY_CATEGORY,
CONF_ENTITY_ID,
@@ -24,8 +25,10 @@ from ..const import (
CONF_SYNC_STATE,
DOMAIN,
SUPPORTED_PLATFORMS_UI,
ClimateConf,
ColorTempModes,
CoverConf,
FanZeroMode,
)
from .const import (
CONF_COLOR,
@@ -34,27 +37,47 @@ from .const import (
CONF_DATA,
CONF_DEVICE_INFO,
CONF_ENTITY,
CONF_GA_ACTIVE,
CONF_GA_ANGLE,
CONF_GA_BLUE_BRIGHTNESS,
CONF_GA_BLUE_SWITCH,
CONF_GA_BRIGHTNESS,
CONF_GA_COLOR,
CONF_GA_COLOR_TEMP,
CONF_GA_CONTROLLER_MODE,
CONF_GA_CONTROLLER_STATUS,
CONF_GA_FAN_SPEED,
CONF_GA_FAN_SWING,
CONF_GA_FAN_SWING_HORIZONTAL,
CONF_GA_GREEN_BRIGHTNESS,
CONF_GA_GREEN_SWITCH,
CONF_GA_HEAT_COOL,
CONF_GA_HUE,
CONF_GA_HUMIDITY_CURRENT,
CONF_GA_ON_OFF,
CONF_GA_OP_MODE_COMFORT,
CONF_GA_OP_MODE_ECO,
CONF_GA_OP_MODE_PROTECTION,
CONF_GA_OP_MODE_STANDBY,
CONF_GA_OPERATION_MODE,
CONF_GA_POSITION_SET,
CONF_GA_POSITION_STATE,
CONF_GA_RED_BRIGHTNESS,
CONF_GA_RED_SWITCH,
CONF_GA_SATURATION,
CONF_GA_SENSOR,
CONF_GA_SETPOINT_SHIFT,
CONF_GA_STEP,
CONF_GA_STOP,
CONF_GA_SWITCH,
CONF_GA_TEMPERATURE_CURRENT,
CONF_GA_TEMPERATURE_TARGET,
CONF_GA_UP_DOWN,
CONF_GA_VALVE,
CONF_GA_WHITE_BRIGHTNESS,
CONF_GA_WHITE_SWITCH,
CONF_IGNORE_AUTO_MODE,
CONF_TARGET_TEMPERATURE,
)
from .knx_selector import (
AllSerializeFirst,
@@ -313,8 +336,151 @@ SWITCH_KNX_SCHEMA = vol.Schema(
},
)
@unique
class ConfSetpointShiftMode(StrEnum):
"""Enum for setpoint shift mode."""
COUNT = "6.010"
FLOAT = "9.002"
@unique
class ConfClimateFanSpeedMode(StrEnum):
"""Enum for climate fan speed mode."""
PERCENTAGE = "5.001"
STEPS = "5.010"
CLIMATE_KNX_SCHEMA = vol.Schema(
{
vol.Required(CONF_GA_TEMPERATURE_CURRENT): GASelector(
write=False, state_required=True, valid_dpt="9.001"
),
vol.Optional(CONF_GA_HUMIDITY_CURRENT): GASelector(
write=False, valid_dpt="9.002"
),
vol.Required(CONF_TARGET_TEMPERATURE): GroupSelect(
GroupSelectOption(
translation_key="group_direct_temp",
schema={
vol.Required(CONF_GA_TEMPERATURE_TARGET): GASelector(
write_required=True, valid_dpt="9.001"
),
vol.Required(
ClimateConf.MIN_TEMP, default=7
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=-20, max=80, step=1, unit_of_measurement="°C"
)
),
vol.Required(
ClimateConf.MAX_TEMP, default=28
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=100, step=1, unit_of_measurement="°C"
)
),
vol.Required(
ClimateConf.TEMPERATURE_STEP, default=0.1
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0.1, max=2, step=0.1, unit_of_measurement="K"
),
),
},
),
GroupSelectOption(
translation_key="group_setpoint_shift",
schema={
vol.Required(CONF_GA_TEMPERATURE_TARGET): GASelector(
write=False, state_required=True, valid_dpt="9.001"
),
vol.Required(CONF_GA_SETPOINT_SHIFT): GASelector(
write_required=True,
state_required=True,
dpt=ConfSetpointShiftMode,
),
vol.Required(
ClimateConf.SETPOINT_SHIFT_MIN, default=-6
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=-32, max=0, step=1, unit_of_measurement="K"
)
),
vol.Required(
ClimateConf.SETPOINT_SHIFT_MAX, default=6
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=32, step=1, unit_of_measurement="K"
)
),
vol.Required(
ClimateConf.TEMPERATURE_STEP, default=0.1
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0.1, max=2, step=0.1, unit_of_measurement="K"
),
),
},
),
collapsible=False,
),
"section_activity": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_ACTIVE): GASelector(write=False, valid_dpt="1"),
vol.Optional(CONF_GA_VALVE): GASelector(write=False, valid_dpt="5.001"),
"section_operation_mode": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_OPERATION_MODE): GASelector(valid_dpt="20.102"),
vol.Optional(CONF_IGNORE_AUTO_MODE): selector.BooleanSelector(),
"section_operation_mode_individual": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_OP_MODE_COMFORT): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_OP_MODE_ECO): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_OP_MODE_STANDBY): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_OP_MODE_PROTECTION): GASelector(
state=False, valid_dpt="1"
),
"section_heat_cool": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_HEAT_COOL): GASelector(valid_dpt="1.100"),
"section_on_off": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_ON_OFF): GASelector(valid_dpt="1"),
vol.Optional(ClimateConf.ON_OFF_INVERT): selector.BooleanSelector(),
"section_controller_mode": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_CONTROLLER_MODE): GASelector(valid_dpt="20.105"),
vol.Optional(CONF_GA_CONTROLLER_STATUS): GASelector(write=False),
vol.Required(
ClimateConf.DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=list(HVACMode),
translation_key="component.climate.selector.hvac_mode",
)
),
"section_fan": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_FAN_SPEED): GASelector(dpt=ConfClimateFanSpeedMode),
vol.Required(ClimateConf.FAN_MAX_STEP, default=3): AllSerializeFirst(
selector.NumberSelector(
selector.NumberSelectorConfig(min=1, max=100, step=1)
),
vol.Coerce(int),
),
vol.Required(
ClimateConf.FAN_ZERO_MODE, default=FanZeroMode.OFF
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=list(FanZeroMode),
translation_key="component.knx.config_panel.entities.create.climate.knx.fan_zero_mode",
)
),
vol.Optional(CONF_GA_FAN_SWING): GASelector(valid_dpt="1"),
vol.Optional(CONF_GA_FAN_SWING_HORIZONTAL): GASelector(valid_dpt="1"),
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
},
)
KNX_SCHEMA_FOR_PLATFORM = {
Platform.BINARY_SENSOR: BINARY_SENSOR_KNX_SCHEMA,
Platform.CLIMATE: CLIMATE_KNX_SCHEMA,
Platform.COVER: COVER_KNX_SCHEMA,
Platform.LIGHT: LIGHT_KNX_SCHEMA,
Platform.SWITCH: SWITCH_KNX_SCHEMA,

View File

@@ -412,6 +412,164 @@
}
}
},
"climate": {
"description": "The KNX climate platform is used as an interface to heating actuators, HVAC gateways, etc.",
"knx": {
"ga_temperature_current": {
"label": "Current temperature"
},
"ga_humidity_current": {
"label": "Current humidity"
},
"target_temperature": {
"title": "Target temperature",
"description": "Set the target temperature.",
"options": {
"group_direct_temp": {
"label": "Absolute setpoint",
"description": "Set the target temperature by an absolute value."
},
"group_setpoint_shift": {
"label": "Setpoint shift",
"description": "Shift the target temperature from a base setpoint."
}
},
"ga_temperature_target": {
"label": "Target temperature",
"description": "Current absolute target temperature."
},
"min_temp": {
"label": "Minimum temperature",
"description": "Minimum temperature that can be set."
},
"max_temp": {
"label": "Maximum temperature",
"description": "Maximum temperature that can be set."
},
"temperature_step": {
"label": "Temperature step",
"description": "Smallest step size to change the temperature. For setpoint shift configureations this sets the scale factor of the shift value."
},
"ga_setpoint_shift": {
"label": "Setpoint shift",
"description": "Target temperature deviation from a base setpoint."
},
"setpoint_shift_min": {
"label": "Minimum setpoint shift",
"description": "Lowest allowed deviation from the base setpoint."
},
"setpoint_shift_max": {
"label": "Maximum setpoint shift",
"description": "Highest allowed deviation from the base setpoint."
}
},
"section_activity": {
"title": "Activity",
"description": "Determine if the device is active or idle."
},
"ga_active": {
"label": "Active",
"description": "Binary value indicating if the device is active or idle. If configured, this takes precedence over valve position."
},
"ga_valve": {
"label": "Valve position",
"description": "Current control value / valve position in percent. `0` sets the climate entity to idle."
},
"section_operation_mode": {
"title": "Operation mode",
"description": "Set the preset mode of the device."
},
"ga_operation_mode": {
"label": "Operation mode",
"description": "Current operation mode."
},
"ignore_auto_mode": {
"label": "Ignore auto mode",
"description": "Enable when your controller doesn't support `auto` mode. It will be ignored by the integration then."
},
"section_operation_mode_individual": {
"title": "Individual operation modes",
"description": "Set the preset mode of the device using individual group addresses."
},
"ga_operation_mode_comfort": {
"label": "Comfort mode"
},
"ga_operation_mode_economy": {
"label": "Economy mode"
},
"ga_operation_mode_standby": {
"label": "Standby mode"
},
"ga_operation_mode_protection": {
"label": "Building protection mode"
},
"section_heat_cool": {
"title": "Heating/Cooling",
"description": "Set whether the device is in heating or cooling mode."
},
"ga_heat_cool": {
"label": "Heating/Cooling"
},
"section_on_off": {
"title": "On/Off",
"description": "Turn the device on or off."
},
"ga_on_off": {
"label": "On/Off"
},
"on_off_invert": {
"label": "[%key:component::knx::config_panel::entities::create::binary_sensor::knx::invert::label%]",
"description": "[%key:component::knx::config_panel::entities::create::binary_sensor::knx::invert::description%]"
},
"section_controller_mode": {
"title": "Controller mode",
"description": "Set the mode of the climate device."
},
"ga_controller_mode": {
"label": "Controller mode"
},
"ga_controller_status": {
"label": "Controller status",
"description": "HVAC controller mode and preset status. Eberle Status octet (KNX AN 097/07 rev 3) non-standardized DPT."
},
"default_controller_mode": {
"label": "Default mode",
"description": "Climate mode to be set on initialization."
},
"section_fan": {
"title": "Fan",
"description": "Configuration for fan control (AC units)."
},
"ga_fan_speed": {
"label": "Fan speed",
"description": "Set the current fan speed.",
"options": {
"5_001": "Percent",
"5_010": "Steps"
}
},
"fan_max_step": {
"label": "Fan steps",
"description": "The maximum amount of steps for the fan."
},
"fan_zero_mode": {
"label": "Zero fan speed mode",
"description": "Set the mode that represents fan speed `0`.",
"options": {
"off": "[%key:common::state::off%]",
"auto": "[%key:common::state::auto%]"
}
},
"ga_fan_swing": {
"label": "Fan swing",
"description": "Toggle (vertical) fan swing mode. Use this if only one direction is supported."
},
"ga_fan_swing_horizontal": {
"label": "Fan horizontal swing",
"description": "Toggle horizontal fan swing mode."
}
}
},
"cover": {
"description": "The KNX cover platform is used as an interface to shutter actuators.",
"knx": {

View File

@@ -0,0 +1,136 @@
{
"version": 2,
"minor_version": 2,
"key": "knx/config_store.json",
"data": {
"entities": {
"climate": {
"knx_es_01K76NGZRMJA74CBRQF9KXNPE8": {
"entity": {
"name": "direct_indi-op_heat-cool",
"device_info": null,
"entity_category": null
},
"knx": {
"ga_temperature_current": {
"state": "0/0/1",
"passive": []
},
"default_controller_mode": "cool",
"fan_max_step": 4,
"fan_zero_mode": "auto",
"sync_state": true,
"target_temperature": {
"ga_temperature_target": {
"write": "0/1/1",
"state": "0/1/2",
"passive": []
},
"min_temp": 10.0,
"max_temp": 24.0,
"temperature_step": 0.1
},
"ga_operation_mode_comfort": {
"write": "0/2/1",
"passive": []
},
"ga_operation_mode_protection": {
"write": "0/2/4",
"passive": []
},
"ga_operation_mode_economy": {
"write": "0/2/2",
"passive": []
},
"ga_operation_mode_standby": {
"write": "0/2/3",
"passive": []
},
"ga_heat_cool": {
"write": "0/3/1",
"state": "0/3/2",
"passive": []
},
"ga_on_off": {
"write": "0/4/1",
"state": "0/4/2",
"passive": []
},
"on_off_invert": true
}
},
"knx_es_01K76NGZRMJA74CBRQF9KXNPE9": {
"entity": {
"name": "sps_op-mode_contr-mode",
"device_info": null,
"entity_category": null
},
"knx": {
"ga_temperature_current": {
"state": "1/0/1",
"passive": []
},
"ga_humidity_current": {
"state": "1/0/2",
"passive": []
},
"target_temperature": {
"ga_temperature_target": {
"state": "1/1/0",
"passive": []
},
"ga_setpoint_shift": {
"write": "1/1/1",
"dpt": "9.002",
"state": "1/1/2",
"passive": []
},
"setpoint_shift_min": -8.0,
"setpoint_shift_max": 8.0,
"temperature_step": 0.5
},
"ga_active": {
"state": "1/1/3",
"passive": []
},
"ga_valve": {
"state": "1/1/4",
"passive": []
},
"ignore_auto_mode": true,
"ga_operation_mode": {
"write": "1/2/1",
"state": "1/2/2",
"passive": []
},
"ga_controller_mode": {
"write": "1/3/1",
"state": "1/3/2",
"passive": []
},
"default_controller_mode": "heat",
"ga_fan_speed": {
"write": "1/4/1",
"dpt": "5.001",
"state": "1/4/2",
"passive": []
},
"fan_max_step": 4,
"fan_zero_mode": "auto",
"ga_fan_swing": {
"write": "1/4/3",
"state": "1/4/4",
"passive": []
},
"ga_fan_swing_horizontal": {
"write": "1/4/5",
"state": "1/4/6",
"passive": []
},
"sync_state": "init"
}
}
}
}
}
}

View File

@@ -89,6 +89,643 @@
'type': 'result',
})
# ---
# name: test_knx_get_schema[climate]
dict({
'id': 1,
'result': list([
dict({
'name': 'ga_temperature_current',
'options': dict({
'passive': True,
'state': dict({
'required': True,
}),
'validDPTs': list([
dict({
'main': 9,
'sub': 1,
}),
]),
'write': False,
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_humidity_current',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 9,
'sub': 2,
}),
]),
'write': False,
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'collapsible': False,
'name': 'target_temperature',
'required': True,
'schema': list([
dict({
'schema': list([
dict({
'name': 'ga_temperature_target',
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 9,
'sub': 1,
}),
]),
'write': dict({
'required': True,
}),
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'default': 7,
'name': 'min_temp',
'required': True,
'selector': dict({
'number': dict({
'max': 80.0,
'min': -20.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': '°C',
}),
}),
'type': 'ha_selector',
}),
dict({
'default': 28,
'name': 'max_temp',
'required': True,
'selector': dict({
'number': dict({
'max': 100.0,
'min': 0.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': '°C',
}),
}),
'type': 'ha_selector',
}),
dict({
'default': 0.1,
'name': 'temperature_step',
'required': True,
'selector': dict({
'number': dict({
'max': 2.0,
'min': 0.1,
'mode': 'slider',
'step': 0.1,
'unit_of_measurement': 'K',
}),
}),
'type': 'ha_selector',
}),
]),
'translation_key': 'group_direct_temp',
'type': 'knx_group_select_option',
}),
dict({
'schema': list([
dict({
'name': 'ga_temperature_target',
'options': dict({
'passive': True,
'state': dict({
'required': True,
}),
'validDPTs': list([
dict({
'main': 9,
'sub': 1,
}),
]),
'write': False,
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_setpoint_shift',
'options': dict({
'dptSelect': list([
dict({
'dpt': dict({
'main': 6,
'sub': 10,
}),
'translation_key': '6_010',
'value': '6.010',
}),
dict({
'dpt': dict({
'main': 9,
'sub': 2,
}),
'translation_key': '9_002',
'value': '9.002',
}),
]),
'passive': True,
'state': dict({
'required': True,
}),
'write': dict({
'required': True,
}),
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'default': -6,
'name': 'setpoint_shift_min',
'required': True,
'selector': dict({
'number': dict({
'max': 0.0,
'min': -32.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': 'K',
}),
}),
'type': 'ha_selector',
}),
dict({
'default': 6,
'name': 'setpoint_shift_max',
'required': True,
'selector': dict({
'number': dict({
'max': 32.0,
'min': 0.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': 'K',
}),
}),
'type': 'ha_selector',
}),
dict({
'default': 0.1,
'name': 'temperature_step',
'required': True,
'selector': dict({
'number': dict({
'max': 2.0,
'min': 0.1,
'mode': 'slider',
'step': 0.1,
'unit_of_measurement': 'K',
}),
}),
'type': 'ha_selector',
}),
]),
'translation_key': 'group_setpoint_shift',
'type': 'knx_group_select_option',
}),
]),
'type': 'knx_group_select',
}),
dict({
'collapsible': True,
'name': 'section_activity',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_active',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': False,
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_valve',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 5,
'sub': 1,
}),
]),
'write': False,
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'collapsible': True,
'name': 'section_operation_mode',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_operation_mode',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 20,
'sub': 102,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ignore_auto_mode',
'optional': True,
'required': False,
'selector': dict({
'boolean': dict({
}),
}),
'type': 'ha_selector',
}),
dict({
'collapsible': True,
'name': 'section_operation_mode_individual',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_operation_mode_comfort',
'optional': True,
'options': dict({
'passive': True,
'state': False,
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_operation_mode_economy',
'optional': True,
'options': dict({
'passive': True,
'state': False,
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_operation_mode_standby',
'optional': True,
'options': dict({
'passive': True,
'state': False,
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_operation_mode_protection',
'optional': True,
'options': dict({
'passive': True,
'state': False,
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'collapsible': True,
'name': 'section_heat_cool',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_heat_cool',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 1,
'sub': 100,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'collapsible': True,
'name': 'section_on_off',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_on_off',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'on_off_invert',
'optional': True,
'required': False,
'selector': dict({
'boolean': dict({
}),
}),
'type': 'ha_selector',
}),
dict({
'collapsible': True,
'name': 'section_controller_mode',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_controller_mode',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 20,
'sub': 105,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_controller_status',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'write': False,
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'default': 'heat',
'name': 'default_controller_mode',
'required': True,
'selector': dict({
'select': dict({
'custom_value': False,
'multiple': False,
'options': list([
'off',
'heat',
'cool',
'heat_cool',
'auto',
'dry',
'fan_only',
]),
'sort': False,
'translation_key': 'component.climate.selector.hvac_mode',
}),
}),
'type': 'ha_selector',
}),
dict({
'collapsible': True,
'name': 'section_fan',
'required': False,
'type': 'knx_section_flat',
}),
dict({
'name': 'ga_fan_speed',
'optional': True,
'options': dict({
'dptSelect': list([
dict({
'dpt': dict({
'main': 5,
'sub': 1,
}),
'translation_key': '5_001',
'value': '5.001',
}),
dict({
'dpt': dict({
'main': 5,
'sub': 10,
}),
'translation_key': '5_010',
'value': '5.010',
}),
]),
'passive': True,
'state': dict({
'required': False,
}),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'default': 3,
'name': 'fan_max_step',
'required': True,
'selector': dict({
'number': dict({
'max': 100.0,
'min': 1.0,
'mode': 'slider',
'step': 1.0,
}),
}),
'type': 'ha_selector',
}),
dict({
'default': 'off',
'name': 'fan_zero_mode',
'required': True,
'selector': dict({
'select': dict({
'custom_value': False,
'multiple': False,
'options': list([
'off',
'auto',
]),
'sort': False,
'translation_key': 'component.knx.config_panel.entities.create.climate.knx.fan_zero_mode',
}),
}),
'type': 'ha_selector',
}),
dict({
'name': 'ga_fan_swing',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'name': 'ga_fan_swing_horizontal',
'optional': True,
'options': dict({
'passive': True,
'state': dict({
'required': False,
}),
'validDPTs': list([
dict({
'main': 1,
'sub': None,
}),
]),
'write': dict({
'required': False,
}),
}),
'required': False,
'type': 'knx_group_address',
}),
dict({
'allow_false': False,
'default': True,
'name': 'sync_state',
'optional': True,
'required': False,
'type': 'knx_sync_state',
}),
]),
'success': True,
'type': 'result',
})
# ---
# name: test_knx_get_schema[cover]
dict({
'id': 1,

View File

@@ -3,16 +3,19 @@
import pytest
from homeassistant.components.climate import HVACMode
from homeassistant.components.knx.const import ClimateConf
from homeassistant.components.knx.schema import ClimateSchema
from homeassistant.const import CONF_NAME, STATE_IDLE
from homeassistant.const import CONF_NAME, STATE_IDLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from . import KnxEntityGenerator
from .conftest import KNXTestKit
from tests.common import async_capture_events
RAW_FLOAT_MINUS_1_0 = (0x87, 0x9C)
RAW_FLOAT_20_0 = (0x07, 0xD0)
RAW_FLOAT_21_0 = (0x0C, 0x1A)
RAW_FLOAT_22_0 = (0x0C, 0x4C)
@@ -158,7 +161,7 @@ async def test_climate_hvac_mode(
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: controller_mode_ga,
ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_OPERATION_MODES: ["Auto"],
ClimateConf.OPERATION_MODES: ["Auto"],
}
| (
{
@@ -452,8 +455,8 @@ async def test_fan_speed_3_steps(hass: HomeAssistant, knx: KNXTestKit) -> None:
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "step",
ClimateSchema.CONF_FAN_MAX_STEP: 3,
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_MAX_STEP: 3,
}
}
)
@@ -508,8 +511,8 @@ async def test_fan_speed_2_steps(hass: HomeAssistant, knx: KNXTestKit) -> None:
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "step",
ClimateSchema.CONF_FAN_MAX_STEP: 2,
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_MAX_STEP: 2,
}
}
)
@@ -561,8 +564,8 @@ async def test_fan_speed_1_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "step",
ClimateSchema.CONF_FAN_MAX_STEP: 1,
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_MAX_STEP: 1,
}
}
)
@@ -604,8 +607,8 @@ async def test_fan_speed_5_steps(hass: HomeAssistant, knx: KNXTestKit) -> None:
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "step",
ClimateSchema.CONF_FAN_MAX_STEP: 5,
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_MAX_STEP: 5,
}
}
)
@@ -660,7 +663,7 @@ async def test_fan_speed_percentage(hass: HomeAssistant, knx: KNXTestKit) -> Non
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "percent",
ClimateConf.FAN_SPEED_MODE: "percent",
}
}
)
@@ -725,8 +728,8 @@ async def test_fan_speed_percentage_4_steps(
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_SPEED_MODE: "percent",
ClimateSchema.CONF_FAN_MAX_STEP: 4,
ClimateConf.FAN_SPEED_MODE: "percent",
ClimateConf.FAN_MAX_STEP: 4,
}
}
)
@@ -785,9 +788,9 @@ async def test_fan_speed_zero_mode_auto(hass: HomeAssistant, knx: KNXTestKit) ->
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_FAN_MAX_STEP: 3,
ClimateSchema.CONF_FAN_SPEED_MODE: "step",
ClimateSchema.CONF_FAN_ZERO_MODE: "auto",
ClimateConf.FAN_MAX_STEP: 3,
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_ZERO_MODE: "auto",
}
}
)
@@ -938,3 +941,83 @@ async def test_horizontal_swing(hass: HomeAssistant, knx: KNXTestKit) -> None:
)
await knx.assert_write("1/2/6", False)
knx.assert_state("climate.test", HVACMode.HEAT, swing_horizontal_mode="off")
async def test_climate_ui_create(
hass: HomeAssistant,
knx: KNXTestKit,
create_ui_entity: KnxEntityGenerator,
) -> None:
"""Test creating a climate entity."""
await knx.setup_integration()
await create_ui_entity(
platform=Platform.CLIMATE,
entity_data={"name": "test"},
knx_data={
"ga_temperature_current": {"state": "0/0/1"},
"target_temperature": {
"ga_temperature_target": {"write": "1/1/1", "state": "1/1/2"},
},
"sync_state": True,
},
)
# created entity sends read-request to KNX bus
await knx.assert_read("0/0/1", response=RAW_FLOAT_20_0)
await knx.assert_read("1/1/2", response=RAW_FLOAT_20_0)
knx.assert_state("climate.test", HVACMode.HEAT)
async def test_climate_ui_load(knx: KNXTestKit) -> None:
"""Test loading climate entities from storage."""
await knx.setup_integration(config_store_fixture="config_store_climate.json")
# direct_indi-op_heat-cool
await knx.assert_read(
"0/0/1", response=RAW_FLOAT_20_0, ignore_order=True
) # current
await knx.assert_read("0/1/2", response=RAW_FLOAT_20_0, ignore_order=True) # target
await knx.assert_read(
"0/4/2", response=False, ignore_order=True
) # on_off - inverted
await knx.assert_read("0/3/2", response=True, ignore_order=True) # heat-cool
await knx.assert_read("0/2/1", response=True, ignore_order=True) # comfort
await knx.assert_read("0/2/2", response=False, ignore_order=True) # eco
await knx.assert_read("0/2/3", response=False, ignore_order=True) # standby
await knx.assert_read("0/2/4", response=False, ignore_order=True) # protection
# sps_op-mode_contr-mode
await knx.assert_read(
"1/0/1", response=RAW_FLOAT_20_0, ignore_order=True
) # current
await knx.assert_read("1/1/0", response=RAW_FLOAT_21_0, ignore_order=True) # target
await knx.assert_read(
"1/1/2", response=RAW_FLOAT_MINUS_1_0, ignore_order=True
) # shift
await knx.assert_read("1/1/3", response=True, ignore_order=True) # active
await knx.assert_read("1/1/4", response=(0x22,), ignore_order=True) # valve
await knx.assert_read("1/4/2", response=(0x22,), ignore_order=True) # fan speed
await knx.assert_read("1/4/4", response=False, ignore_order=True) # swing vertical
await knx.assert_read(
"1/4/6", response=False, ignore_order=True
) # swing horizontal
await knx.assert_read(
"1/0/2", response=RAW_FLOAT_20_0, ignore_order=True
) # humidity
await knx.assert_read(
"1/2/2", # operation mode
response=(0x01,), # comfort
ignore_order=True,
)
await knx.assert_read(
"1/3/2", # controller mode
response=(0x03,), # cool
ignore_order=True,
)
knx.assert_state(
"climate.direct_indi_op_heat_cool",
HVACMode.HEAT,
)
knx.assert_state(
"climate.sps_op_mode_contr_mode",
HVACMode.COOL,
)