From 97c37f8e7632b7621c04151ebb29299be677b342 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 28 Sep 2026 11:14:29 +0200 Subject: [PATCH] Fix Overkiz heat recovery ventilation crash without ventilation mode state (#183382) --- .../atlantic_heat_recovery_ventilation.py | 18 +- .../local_somfy_tahoma_switch_europe.json | 781 ++++++++++++++++++ .../overkiz/snapshots/test_climate.ambr | 89 ++ tests/components/overkiz/test_climate.py | 32 + 4 files changed, 916 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/overkiz/climate/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate/atlantic_heat_recovery_ventilation.py index d77d37ef6f37..0d2b6a0089f8 100644 --- a/homeassistant/components/overkiz/climate/atlantic_heat_recovery_ventilation.py +++ b/homeassistant/components/overkiz/climate/atlantic_heat_recovery_ventilation.py @@ -95,8 +95,11 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): return PRESET_MANUAL ventilation_mode = cast( - dict, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) + dict | None, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) ) + if ventilation_mode is None: + return None + prog = ventilation_mode.get(OverkizCommandParam.PROG) if prog == OverkizCommandParam.ON: @@ -140,9 +143,13 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): def fan_mode(self) -> str | None: """Return the fan setting.""" ventilation_mode = cast( - dict, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) + dict | None, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) + ) + cooling = ( + ventilation_mode.get(OverkizCommandParam.COOLING) + if ventilation_mode is not None + else None ) - cooling = ventilation_mode.get(OverkizCommandParam.COOLING) if cooling == OverkizCommandParam.ON: return FAN_BYPASS @@ -176,8 +183,11 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): ) -> None: """Execute ventilation mode command with all parameters.""" ventilation_mode = cast( - dict, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) + dict | None, self.device.states.get_value(OverkizState.IO_VENTILATION_MODE) ) + # The Local API can omit this state, and the command needs all its parameters + if ventilation_mode is None: + return if cooling: ventilation_mode[OverkizCommandParam.COOLING] = cooling diff --git a/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_switch_europe.json b/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_switch_europe.json index 365ad16d0d5e..b769d26ba30c 100644 --- a/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_switch_europe.json +++ b/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_switch_europe.json @@ -6554,6 +6554,787 @@ "widgetName": "DynamicExteriorVenetianBlind" }, "deviceURL": "io://1234-5678-6508/11292138" + }, + { + "type": 1, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#1", + "subsystemId": 1, + "available": true, + "definition": { + "type": "ACTUATOR", + "states": [ + { + "name": "io:SensorsErrorState" + }, + { + "name": "io:VentilationConfigurationModeState" + }, + { + "name": "io:AirDemandModeState" + }, + { + "name": "core:StatusState" + }, + { + "name": "io:VentilationModeState" + }, + { + "name": "io:EnergySavingState" + }, + { + "name": "io:CO2HistoryState" + }, + { + "name": "core:CommandLockLevelsState" + }, + { + "name": "core:TimeProgramState" + }, + { + "name": "core:OperatingStateState" + }, + { + "name": "io:InletEngineState" + }, + { + "name": "io:OutletEngineState" + }, + { + "name": "io:ProgCommandState" + }, + { + "name": "io:RemainingTimeBeforeFilterChangeState" + }, + { + "name": "core:DiscreteRSSILevelState" + }, + { + "name": "core:RSSILevelState" + }, + { + "name": "core:PriorityLockTimerState" + }, + { + "name": "io:PriorityLockLevelState" + }, + { + "name": "io:PriorityLockOriginatorState" + }, + { + "name": "core:AirDemandState" + }, + { + "name": "core:NameState" + } + ], + "commands": [ + { + "commandName": "refreshTimeProgram", + "nparams": 0 + }, + { + "commandName": "setTimeProgram", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "getName", + "nparams": 0 + }, + { + "commandName": "pairOneWayController", + "nparams": 1, + "paramsSig": "p1,*p2" + }, + { + "commandName": "setAirDemandMode", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "resetVentilation", + "nparams": 0, + "paramsSig": "*p1,*p2,*p3" + }, + { + "commandName": "advancedRefresh", + "nparams": 1, + "paramsSig": "p1,*p2" + }, + { + "commandName": "refreshVentilationState", + "nparams": 0 + }, + { + "commandName": "delayedStopIdentify", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "refreshVentilationConfigurationMode", + "nparams": 0 + }, + { + "commandName": "unpairOneWayController", + "nparams": 1, + "paramsSig": "p1,*p2" + }, + { + "commandName": "refreshSensorsState", + "nparams": 0 + }, + { + "commandName": "refreshEnergySaving", + "nparams": 0 + }, + { + "commandName": "refreshCO2History", + "nparams": 0 + }, + { + "commandName": "wink", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "addLockLevel", + "nparams": 1, + "paramsSig": "p1,*p2" + }, + { + "commandName": "stopIdentify", + "nparams": 0 + }, + { + "commandName": "startIdentify", + "nparams": 0 + }, + { + "commandName": "identify", + "nparams": 0 + }, + { + "commandName": "setName", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "setVentilationMode", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "resetLockLevels", + "nparams": 0 + }, + { + "commandName": "removeLockLevel", + "nparams": 1, + "paramsSig": "p1" + }, + { + "commandName": "unpairAllOneWayControllers", + "nparams": 0 + }, + { + "commandName": "setVentilationConfigurationMode", + "nparams": 1, + "paramsSig": "p1" + } + ], + "uiClass": "VentilationSystem", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "AtlanticHeatRecoveryVentilation" + }, + "controllableName": "io:AtlanticHeatRecoveryVentilationIOComponent", + "label": "Ventilation", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:CommandLockLevelsState", + "value": [], + "type": 11 + }, + { + "name": "core:DiscreteRSSILevelState", + "value": "good", + "type": 3 + }, + { + "name": "core:RSSILevelState", + "value": 100, + "type": 1 + }, + { + "name": "io:VentilationConfigurationModeState", + "value": "comfort", + "type": 3 + }, + { + "name": "core:AirDemandState", + "value": 100, + "type": 1 + }, + { + "name": "io:AirDemandModeState", + "value": "auto", + "type": 3 + }, + { + "name": "core:NameState", + "value": "Ventilation", + "type": 3 + }, + { + "name": "io:SensorsErrorState", + "value": { + "outlet": { + "exhaustAir": false, + "newAir": false + }, + "co2": false, + "outside": false, + "inlet": { + "exhaustAir": false, + "newAir": false + } + }, + "type": 11 + }, + { + "name": "core:OperatingStateState", + "value": 8, + "type": 1 + }, + { + "name": "io:InletEngineState", + "value": 114, + "type": 1 + }, + { + "name": "io:OutletEngineState", + "value": 111, + "type": 1 + }, + { + "name": "io:ProgCommandState", + "value": 0, + "type": 1 + }, + { + "name": "io:RemainingTimeBeforeFilterChangeState", + "value": 3262, + "type": 1 + }, + { + "name": "core:TimeProgramState", + "value": { + "daymode": { + "tuesday": "day-presence", + "monday": "day-presence", + "sunday": "day-presence", + "wednesday": "day-presence", + "thursday": "day-presence", + "friday": "day-presence", + "saturday": "day-presence" + }, + "programs": [ + { + "slots": { + "count": 2, + "definitions": [ + { + "mode": "away", + "start": "09:00" + }, + { + "mode": "presence", + "start": "00:00" + } + ] + } + }, + { + "slots": { + "count": 0, + "definitions": [] + } + } + ], + "lastRequest": "gui" + }, + "type": 11 + }, + { + "name": "io:CO2HistoryState", + "value": [ + 67, 70, 68, 68, 68, 69, 69, 70, 71, 72, 72, 72, 73, 73, 73, 73, 74, + 81, 80, 68, 72, 73, 70, 69, 68, 72, 68, 66, 68, 66, 64, 62, 64, 63, + 62, 62, 60, 58, 56, 54, 53, 52, 52, 56, 59, 63, 67, 69, 72, 73, 74, + 74, 75, 74, 74, 72, 72, 72, 72, 73, 73, 73, 74, 75, 76, 85, 72, 73, + 73, 70, 66, 64, 61, 59, 59, 57, 57, 53, 51, 50, 49, 48, 48, 47, 46, + 46, 48, 54, 58, 59, 58, 58, 60, 62, 63, 65, 65, 65, 64, 63, 63, 63, + 63, 63, 64, 64, 64, 65, 65, 66, 66, 67, 67, 70, 77, 77, 72, 67, 65, + 63, 52, 49, 47, 49, 52, 53, 53, 55, 59, 63, 60, 61, 54, 49, 49, 52, + 53, 59, 59, 60, 60, 60, 60, 60, 22 + ], + "type": 10 + }, + { + "name": "io:EnergySavingState", + "value": 314, + "type": 1 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#2", + "subsystemId": 2, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:CO2ConcentrationState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "AirSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "CO2Sensor" + }, + "controllableName": "io:CO2IOSystemDeviceSensor", + "label": "Ventilation (Ventilation#2)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:CO2ConcentrationState", + "value": 605, + "type": 1 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#3", + "subsystemId": 3, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:TemperatureState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "TemperatureSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:MeasuredValueType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "TemperatureSensor" + }, + "controllableName": "io:TemperatureInCelciusIOSystemDeviceSensor", + "label": "Ventilation (Ventilation#3)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:TemperatureState", + "value": -40, + "type": 1 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:MeasuredValueType", + "value": "core:TemperatureInCelcius", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#4", + "subsystemId": 4, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:TemperatureState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "TemperatureSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:MeasuredValueType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "TemperatureSensor" + }, + "controllableName": "io:TemperatureInCelciusIOSystemDeviceSensor", + "label": "Ventilation (Ventilation#4)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:TemperatureState", + "value": 19, + "type": 2 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:MeasuredValueType", + "value": "core:TemperatureInCelcius", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#5", + "subsystemId": 5, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:TemperatureState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "TemperatureSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:MeasuredValueType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "TemperatureSensor" + }, + "controllableName": "io:TemperatureInCelciusIOSystemDeviceSensor", + "label": "Ventilation (Ventilation#5)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:TemperatureState", + "value": 19.2, + "type": 2 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:MeasuredValueType", + "value": "core:TemperatureInCelcius", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#6", + "subsystemId": 6, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:TemperatureState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "TemperatureSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:MeasuredValueType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "TemperatureSensor" + }, + "controllableName": "io:TemperatureInCelciusIOSystemDeviceSensor", + "label": "Ventilation (Ventilation#6)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:TemperatureState", + "value": 23.1, + "type": 2 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:MeasuredValueType", + "value": "core:TemperatureInCelcius", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] + }, + { + "type": 2, + "synced": true, + "enabled": true, + "deviceURL": "io://1234-5678-6508/2840629#7", + "subsystemId": 7, + "available": true, + "definition": { + "type": "SENSOR", + "states": [ + { + "name": "core:TemperatureState" + }, + { + "name": "core:StatusState" + } + ], + "commands": [], + "uiClass": "TemperatureSensor", + "attributes": [ + { + "name": "core:Manufacturer" + }, + { + "name": "core:PowerSourceType" + }, + { + "name": "core:MeasuredValueType" + }, + { + "name": "core:FirmwareRevision" + } + ], + "widgetName": "TemperatureSensor" + }, + "controllableName": "io:TemperatureInCelciusIOSystemDeviceSensor", + "label": "Ventilation (Ventilation#7)", + "states": [ + { + "name": "core:StatusState", + "value": "available", + "type": 3 + }, + { + "name": "core:TemperatureState", + "value": 25, + "type": 2 + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "value": "Atlantic Group", + "type": 3 + }, + { + "name": "core:PowerSourceType", + "value": "mainSupply", + "type": 3 + }, + { + "name": "core:MeasuredValueType", + "value": "core:TemperatureInCelcius", + "type": 3 + }, + { + "name": "core:FirmwareRevision", + "value": "682008A", + "type": 3 + } + ] } ], "gateways": [ diff --git a/tests/components/overkiz/snapshots/test_climate.ambr b/tests/components/overkiz/snapshots/test_climate.ambr index a571a26126f4..8f7af9472dfa 100644 --- a/tests/components/overkiz/snapshots/test_climate.ambr +++ b/tests/components/overkiz/snapshots/test_climate.ambr @@ -832,3 +832,92 @@ 'state': 'heat', }) # --- +# name: test_climate_entities_snapshot[local_somfy_tahoma_switch_europe.json][climate.ventilation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'auto', + 'away', + 'home_boost', + 'kitchen_boost', + 'bypass_boost', + ]), + : list([ + , + ]), + : 35, + : 7, + : list([ + 'auto', + 'prog', + 'manual', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.ventilation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'overkiz', + 'unique_id': 'io://1234-5678-6508/2840629#1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entities_snapshot[local_somfy_tahoma_switch_europe.json][climate.ventilation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 19, + : 'auto', + : list([ + 'auto', + 'away', + 'home_boost', + 'kitchen_boost', + 'bypass_boost', + ]), + : 'Ventilation', + : list([ + , + ]), + : 35, + : 7, + : 'auto', + : list([ + 'auto', + 'prog', + 'manual', + ]), + : , + }), + 'context': , + 'entity_id': 'climate.ventilation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'fan_only', + }) +# --- diff --git a/tests/components/overkiz/test_climate.py b/tests/components/overkiz/test_climate.py index d032668a54a2..be536eae2385 100644 --- a/tests/components/overkiz/test_climate.py +++ b/tests/components/overkiz/test_climate.py @@ -12,6 +12,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, ATTR_HVAC_ACTION, ATTR_PRESET_MODE, HVACAction, @@ -69,12 +70,19 @@ THERMOSTAT_HEATING = FixtureDevice( "io://1234-5678-5010/386310#1", "climate.study_thermostat", ) +# io:AtlanticHeatRecoveryVentilationIOComponent without io:VentilationModeState value +HEAT_RECOVERY_VENTILATION = FixtureDevice( + "setup/local_somfy_tahoma_switch_europe.json", + "io://1234-5678-6508/2840629#1", + "climate.ventilation", +) SNAPSHOT_FIXTURES = [ VALVE, COZYTOUCH, YUTAKI_ZONE_1, THERMOSTAT_HEATING, + HEAT_RECOVERY_VENTILATION, ] @@ -390,3 +398,27 @@ async def test_thermostat_heating_set_preset_mode( command_name="setDerogation", parameters=parameters, ) + + +@pytest.mark.parametrize( + ("service", "service_data"), + [ + pytest.param("set_fan_mode", {ATTR_FAN_MODE: "bypass_boost"}, id="fan_mode"), + pytest.param("set_preset_mode", {ATTR_PRESET_MODE: "prog"}, id="preset_mode"), + ], +) +async def test_heat_recovery_ventilation_without_ventilation_mode( + hass: HomeAssistant, + setup_overkiz_integration: SetupOverkizIntegration, + service: str, + service_data: dict[str, str], +) -> None: + """Test setting modes does not fail without an io:VentilationModeState value.""" + await setup_overkiz_integration(fixture=HEAT_RECOVERY_VENTILATION.fixture) + + await hass.services.async_call( + "climate", + service, + {"entity_id": HEAT_RECOVERY_VENTILATION.entity_id, **service_data}, + blocking=True, + )