diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 9abc9023617..abce143c760 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -5,7 +5,11 @@ from __future__ import annotations from typing import Any from xknx import XKNX -from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.devices import ( + Climate as XknxClimate, + ClimateMode as XknxClimateMode, + Device as XknxDevice, +) from xknx.dpt.dpt_20 import HVACControllerMode from homeassistant import config_entries @@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off and not self._device.is_on: return HVACMode.OFF if self._device.mode is not None and self._device.mode.supports_controller_mode: - hvac_mode = CONTROLLER_MODES.get( + return CONTROLLER_MODES.get( self._device.mode.controller_mode, 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 @property @@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off: if not ha_controller_modes: - ha_controller_modes.append(self.default_hvac_mode) + ha_controller_modes.append(self._last_hvac_mode) ha_controller_modes.append(HVACMode.OFF) hvac_modes = list(set(filter(None, ha_controller_modes))) - return hvac_modes if hvac_modes else [self.default_hvac_mode] + return ( + hvac_modes + if hvac_modes + else [self.hvac_mode] # mode read-only -> fall back to only current mode + ) @property def hvac_action(self) -> HVACAction | None: @@ -354,3 +359,13 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): self._device.mode.unregister_device_updated_cb(self.after_update_callback) self._device.mode.xknx.devices.async_remove(self._device.mode) await super().async_will_remove_from_hass() + + def after_update_callback(self, _device: XknxDevice) -> None: + """Call after device was updated.""" + if self._device.mode is not None and self._device.mode.supports_controller_mode: + hvac_mode = CONTROLLER_MODES.get( + self._device.mode.controller_mode, self.default_hvac_mode + ) + if hvac_mode is not HVACMode.OFF: + self._last_hvac_mode = hvac_mode + super().after_update_callback(_device) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6974ee300f5..9ecf687d6b9 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.0.0", + "xknx==3.1.0", "xknxproject==3.7.1", "knx-frontend==2024.8.9.225351" ], diff --git a/requirements_all.txt b/requirements_all.txt index c97d0fe527e..e19819dafde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2930,7 +2930,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f04c0425af9..575fd7757be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 77eeeef3559..9f198b48bd4 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -231,6 +231,90 @@ async def test_climate_hvac_mode( assert hass.states.get("climate.test").state == "cool" +async def test_climate_heat_cool_read_only( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + heat_cool_state_ga = "3/3/3" + 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_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "heat" + assert state.attributes["hvac_modes"] == ["heat"] + assert state.attributes["hvac_action"] == "heating" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "cool" + assert state.attributes["hvac_modes"] == ["cool"] + assert state.attributes["hvac_action"] == "cooling" + + +async def test_climate_heat_cool_read_only_on_off( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + on_off_ga = "2/2/2" + heat_cool_state_ga = "3/3/3" + 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: on_off_ga, + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "heat"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(on_off_ga, True) + state = hass.states.get("climate.test") + assert state.state == "cool" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "cooling" + + async def test_climate_preset_mode( hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry ) -> None: