Add `async_turn_on/off` methods for KNX climate entities (#117882)

Add async_turn_on/off methods for KNX climate entities
pull/116734/head^2
Matthias Alphart 2024-05-21 23:54:43 +02:00 committed by GitHub
parent c2b3bf3fb9
commit b94735a445
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 155 additions and 24 deletions

View File

@ -141,11 +141,20 @@ class KNXClimate(KnxEntity, ClimateEntity):
"""Initialize of a KNX climate device.""" """Initialize of a KNX climate device."""
super().__init__(_create_climate(xknx, config)) super().__init__(_create_climate(xknx, config))
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_supported_features = ( self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON
)
if self._device.supports_on_off: if self._device.supports_on_off:
self._attr_supported_features |= ClimateEntityFeature.TURN_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
):
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
if self.preset_modes: if self.preset_modes:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_target_temperature_step = self._device.temperature_step self._attr_target_temperature_step = self._device.temperature_step
@ -158,6 +167,8 @@ class KNXClimate(KnxEntity, ClimateEntity):
self.default_hvac_mode: HVACMode = config[ self.default_hvac_mode: HVACMode = config[
ClimateSchema.CONF_DEFAULT_CONTROLLER_MODE 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
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
@ -181,6 +192,34 @@ class KNXClimate(KnxEntity, ClimateEntity):
temp = self._device.target_temperature_max temp = self._device.target_temperature_max
return temp if temp is not None else super().max_temp return temp if temp is not None else super().max_temp
async def async_turn_on(self) -> None:
"""Turn the entity on."""
if self._device.supports_on_off:
await self._device.turn_on()
self.async_write_ha_state()
return
if self._device.mode is not None and self._device.mode.supports_controller_mode:
knx_controller_mode = HVACControllerMode(
CONTROLLER_MODES_INV.get(self._last_hvac_mode)
)
await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state()
async def async_turn_off(self) -> None:
"""Turn the entity off."""
if self._device.supports_on_off:
await self._device.turn_off()
self.async_write_ha_state()
return
if (
self._device.mode is not None
and HVACControllerMode.OFF in self._device.mode.controller_modes
):
await self._device.mode.set_controller_mode(HVACControllerMode.OFF)
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
@ -194,9 +233,12 @@ class KNXClimate(KnxEntity, ClimateEntity):
if self._device.supports_on_off and not self._device.is_on: if self._device.supports_on_off and not self._device.is_on:
return HVACMode.OFF return HVACMode.OFF
if self._device.mode is not None and self._device.mode.supports_controller_mode: if self._device.mode is not None and self._device.mode.supports_controller_mode:
return CONTROLLER_MODES.get( hvac_mode = CONTROLLER_MODES.get(
self._device.mode.controller_mode.value, self.default_hvac_mode self._device.mode.controller_mode.value, self.default_hvac_mode
) )
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
return hvac_mode
return self.default_hvac_mode return self.default_hvac_mode
@property @property
@ -234,21 +276,23 @@ class KNXClimate(KnxEntity, ClimateEntity):
return None return None
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set operation mode.""" """Set controller mode."""
if self._device.supports_on_off and hvac_mode == HVACMode.OFF: if self._device.mode is not None and self._device.mode.supports_controller_mode:
await self._device.turn_off() knx_controller_mode = HVACControllerMode(
else: CONTROLLER_MODES_INV.get(hvac_mode)
if self._device.supports_on_off and not self._device.is_on: )
await self._device.turn_on() if knx_controller_mode in self._device.mode.controller_modes:
if (
self._device.mode is not None
and self._device.mode.supports_controller_mode
):
knx_controller_mode = HVACControllerMode(
CONTROLLER_MODES_INV.get(hvac_mode)
)
await self._device.mode.set_controller_mode(knx_controller_mode) await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state() self.async_write_ha_state()
return
if self._device.supports_on_off:
if hvac_mode == HVACMode.OFF:
await self._device.turn_off()
elif not self._device.is_on:
# for default hvac mode, otherwise above would have triggered
await self._device.turn_on()
self.async_write_ha_state()
@property @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:

View File

@ -1,5 +1,7 @@
"""Test KNX climate.""" """Test KNX climate."""
import pytest
from homeassistant.components.climate import PRESET_ECO, PRESET_SLEEP, HVACMode from homeassistant.components.climate import PRESET_ECO, PRESET_SLEEP, 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
@ -52,6 +54,94 @@ async def test_climate_basic_temperature_set(
assert len(events) == 1 assert len(events) == 1
@pytest.mark.parametrize("heat_cool", [False, True])
async def test_climate_on_off(
hass: HomeAssistant, knx: KNXTestKit, heat_cool: bool
) -> None:
"""Test KNX climate on/off."""
await knx.setup_integration(
{
ClimateSchema.PLATFORM: {
CONF_NAME: "test",
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8",
ClimateSchema.CONF_ON_OFF_STATE_ADDRESS: "1/2/9",
}
| (
{
ClimateSchema.CONF_HEAT_COOL_ADDRESS: "1/2/10",
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11",
}
if heat_cool
else {}
)
}
)
await hass.async_block_till_done()
# read heat/cool state
if heat_cool:
await knx.assert_read("1/2/11")
await knx.receive_response("1/2/11", 0) # cool
# read temperature state
await knx.assert_read("1/2/3")
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
# read target temperature state
await knx.assert_read("1/2/5")
await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
# read on/off state
await knx.assert_read("1/2/9")
await knx.receive_response("1/2/9", 1)
# turn off
await hass.services.async_call(
"climate",
"turn_off",
{"entity_id": "climate.test"},
blocking=True,
)
await knx.assert_write("1/2/8", 0)
assert hass.states.get("climate.test").state == "off"
# turn on
await hass.services.async_call(
"climate",
"turn_on",
{"entity_id": "climate.test"},
blocking=True,
)
await knx.assert_write("1/2/8", 1)
if heat_cool:
# does not fall back to default hvac mode after turn_on
assert hass.states.get("climate.test").state == "cool"
else:
assert hass.states.get("climate.test").state == "heat"
# set hvac mode to off triggers turn_off if no controller_mode is available
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
blocking=True,
)
await knx.assert_write("1/2/8", 0)
# set hvac mode to heat
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT},
blocking=True,
)
if heat_cool:
# only set new hvac_mode without changing on/off - actuator shall handle that
await knx.assert_write("1/2/10", 1)
else:
await knx.assert_write("1/2/8", 1)
async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
"""Test KNX climate hvac mode.""" """Test KNX climate hvac mode."""
await knx.setup_integration( await knx.setup_integration(
@ -68,7 +158,6 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
} }
} }
) )
async_capture_events(hass, "state_changed")
await hass.async_block_till_done() await hass.async_block_till_done()
# read states state updater # read states state updater
@ -82,14 +171,14 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_read("1/2/5") await knx.assert_read("1/2/5")
await knx.receive_response("1/2/5", RAW_FLOAT_22_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
# turn hvac off # turn hvac mode to off
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_hvac_mode", "set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/8", False) await knx.assert_write("1/2/6", (0x06,))
# turn hvac on # turn hvac on
await hass.services.async_call( await hass.services.async_call(
@ -98,7 +187,6 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/8", True)
await knx.assert_write("1/2/6", (0x01,)) await knx.assert_write("1/2/6", (0x01,))
@ -182,7 +270,6 @@ async def test_update_entity(hass: HomeAssistant, knx: KNXTestKit) -> None:
) )
assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done() await hass.async_block_till_done()
async_capture_events(hass, "state_changed")
await hass.async_block_till_done() await hass.async_block_till_done()
# read states state updater # read states state updater