Rename KNX Climate preset modes according to specification (#123964)

* Rename KNX Climate preset modes according to specification

* change icon for "standby"
This commit is contained in:
Matthias Alphart 2024-08-15 08:37:10 +02:00 committed by GitHub
parent 9911aa4ede
commit b042ebe4ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 59 deletions

View file

@ -10,11 +10,10 @@ from xknx.devices import (
ClimateMode as XknxClimateMode, ClimateMode as XknxClimateMode,
Device as XknxDevice, Device as XknxDevice,
) )
from xknx.dpt.dpt_20 import HVACControllerMode from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.climate import ( from homeassistant.components.climate import (
PRESET_AWAY,
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
HVACAction, HVACAction,
@ -32,19 +31,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import KNXModule from . import KNXModule
from .const import ( from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DATA_KNX_CONFIG, DOMAIN
CONTROLLER_MODES,
CURRENT_HVAC_ACTIONS,
DATA_KNX_CONFIG,
DOMAIN,
PRESET_MODES,
)
from .knx_entity import KnxYamlEntity from .knx_entity import KnxYamlEntity
from .schema import ClimateSchema from .schema import ClimateSchema
ATTR_COMMAND_VALUE = "command_value" ATTR_COMMAND_VALUE = "command_value"
CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()}
PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()}
async def async_setup_entry( async def async_setup_entry(
@ -142,6 +134,7 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
_device: XknxClimate _device: XknxClimate
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "knx_climate"
_enable_turn_on_off_backwards_compatibility = False _enable_turn_on_off_backwards_compatibility = False
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
@ -165,8 +158,14 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
) )
if self.preset_modes: if (
self._device.mode is not None
and self._device.mode.operation_modes # empty list when not writable
):
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_preset_modes = [
mode.name.lower() for mode in self._device.mode.operation_modes
]
self._attr_target_temperature_step = self._device.temperature_step self._attr_target_temperature_step = self._device.temperature_step
self._attr_unique_id = ( self._attr_unique_id = (
f"{self._device.temperature.group_address_state}_" f"{self._device.temperature.group_address_state}_"
@ -309,32 +308,18 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
Requires ClimateEntityFeature.PRESET_MODE. Requires ClimateEntityFeature.PRESET_MODE.
""" """
if self._device.mode is not None and self._device.mode.supports_operation_mode: if self._device.mode is not None and self._device.mode.supports_operation_mode:
return PRESET_MODES.get(self._device.mode.operation_mode, PRESET_AWAY) return self._device.mode.operation_mode.name.lower()
return None return None
@property
def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires ClimateEntityFeature.PRESET_MODE.
"""
if self._device.mode is None:
return None
presets = [
PRESET_MODES.get(operation_mode)
for operation_mode in self._device.mode.operation_modes
]
return list(filter(None, presets))
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
if ( if (
self._device.mode is not None self._device.mode is not None
and self._device.mode.supports_operation_mode and self._device.mode.operation_modes # empty list when not writable
and (knx_operation_mode := PRESET_MODES_INV.get(preset_mode)) is not None
): ):
await self._device.mode.set_operation_mode(knx_operation_mode) await self._device.mode.set_operation_mode(
HVACOperationMode[preset_mode.upper()]
)
self.async_write_ha_state() self.async_write_ha_state()
@property @property

View file

@ -6,18 +6,10 @@ from collections.abc import Awaitable, Callable
from enum import Enum from enum import Enum
from typing import Final, TypedDict from typing import Final, TypedDict
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode from xknx.dpt.dpt_20 import HVACControllerMode
from xknx.telegram import Telegram from xknx.telegram import Telegram
from homeassistant.components.climate import ( from homeassistant.components.climate import HVACAction, HVACMode
PRESET_AWAY,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
PRESET_SLEEP,
HVACAction,
HVACMode,
)
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN: Final = "knx" DOMAIN: Final = "knx"
@ -174,12 +166,3 @@ CURRENT_HVAC_ACTIONS: Final = {
HVACMode.FAN_ONLY: HVACAction.FAN, HVACMode.FAN_ONLY: HVACAction.FAN,
HVACMode.DRY: HVACAction.DRYING, HVACMode.DRY: HVACAction.DRYING,
} }
PRESET_MODES: Final = {
# Map DPT 20.102 HVAC operating modes to HA presets
HVACOperationMode.AUTO: PRESET_NONE,
HVACOperationMode.BUILDING_PROTECTION: PRESET_ECO,
HVACOperationMode.ECONOMY: PRESET_SLEEP,
HVACOperationMode.STANDBY: PRESET_AWAY,
HVACOperationMode.COMFORT: PRESET_COMFORT,
}

View file

@ -1,5 +1,19 @@
{ {
"entity": { "entity": {
"climate": {
"knx_climate": {
"state_attributes": {
"preset_mode": {
"state": {
"comfort": "mdi:sofa",
"standby": "mdi:home-export-outline",
"economy": "mdi:leaf",
"building_protection": "mdi:sun-snowflake-variant"
}
}
}
}
},
"sensor": { "sensor": {
"individual_address": { "individual_address": {
"default": "mdi:router-network" "default": "mdi:router-network"

View file

@ -267,6 +267,22 @@
} }
}, },
"entity": { "entity": {
"climate": {
"knx_climate": {
"state_attributes": {
"preset_mode": {
"name": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::name%]",
"state": {
"auto": "Auto",
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
"standby": "Standby",
"economy": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
"building_protection": "Building protection"
}
}
}
}
},
"sensor": { "sensor": {
"individual_address": { "individual_address": {
"name": "[%key:component::knx::config::step::routing::data::individual_address%]" "name": "[%key:component::knx::config::step::routing::data::individual_address%]"

View file

@ -2,7 +2,7 @@
import pytest import pytest
from homeassistant.components.climate import PRESET_ECO, PRESET_SLEEP, HVACMode from homeassistant.components.climate import HVACMode
from homeassistant.components.knx.schema import ClimateSchema from homeassistant.components.knx.schema import ClimateSchema
from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.const import CONF_NAME, STATE_IDLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -331,7 +331,6 @@ async def test_climate_preset_mode(
} }
} }
) )
events = async_capture_events(hass, "state_changed")
# StateUpdater initialize state # StateUpdater initialize state
# StateUpdater semaphore allows 2 concurrent requests # StateUpdater semaphore allows 2 concurrent requests
@ -340,30 +339,28 @@ async def test_climate_preset_mode(
await knx.receive_response("1/2/3", RAW_FLOAT_21_0) await knx.receive_response("1/2/3", RAW_FLOAT_21_0)
await knx.receive_response("1/2/5", RAW_FLOAT_22_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
await knx.assert_read("1/2/7") await knx.assert_read("1/2/7")
await knx.receive_response("1/2/7", (0x01,)) await knx.receive_response("1/2/7", (0x01,)) # comfort
events.clear()
knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="comfort")
# set preset mode # set preset mode
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_preset_mode", "set_preset_mode",
{"entity_id": "climate.test", "preset_mode": PRESET_ECO}, {"entity_id": "climate.test", "preset_mode": "building_protection"},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/6", (0x04,)) await knx.assert_write("1/2/6", (0x04,))
assert len(events) == 1 knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="building_protection")
events.pop()
# set preset mode # set preset mode
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_preset_mode", "set_preset_mode",
{"entity_id": "climate.test", "preset_mode": PRESET_SLEEP}, {"entity_id": "climate.test", "preset_mode": "economy"},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/6", (0x03,)) await knx.assert_write("1/2/6", (0x03,))
assert len(events) == 1 knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="economy")
events.pop()
assert len(knx.xknx.devices) == 2 assert len(knx.xknx.devices) == 2
assert len(knx.xknx.devices[0].device_updated_cbs) == 2 assert len(knx.xknx.devices[0].device_updated_cbs) == 2