From 8fd3d0bb44fe235cede78648bb2f213e5676cd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Tue, 28 Apr 2026 20:30:53 +0200 Subject: [PATCH] Fix nobo_hub KeyError when a zone or component is removed (#169378) --- homeassistant/components/nobo_hub/climate.py | 5 +++++ homeassistant/components/nobo_hub/select.py | 7 +++++++ homeassistant/components/nobo_hub/sensor.py | 5 +++++ tests/components/nobo_hub/test_climate.py | 13 ++++++++++++- tests/components/nobo_hub/test_select.py | 13 ++++++++++++- tests/components/nobo_hub/test_sensor.py | 13 ++++++++++++- 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 4246c8a8db0..9415618de75 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -145,6 +145,11 @@ class NoboZone(NoboBaseEntity, ClimateEntity): @callback def _read_state(self) -> None: """Read the current state from the hub. These are only local calls.""" + if self._id not in self._nobo.zones: + # Zone removed via the Nobø app; mark unavailable. + self._attr_available = False + return + self._attr_available = True state = self._nobo.get_current_zone_mode(self._id, dt_util.now()) self._attr_hvac_mode = HVACMode.AUTO self._attr_preset_mode = PRESET_NONE diff --git a/homeassistant/components/nobo_hub/select.py b/homeassistant/components/nobo_hub/select.py index 4512f95892b..4e11f049b55 100644 --- a/homeassistant/components/nobo_hub/select.py +++ b/homeassistant/components/nobo_hub/select.py @@ -94,6 +94,7 @@ class NoboGlobalSelector(NoboBaseEntity, SelectEntity): @callback def _read_state(self) -> None: + """Read the current state from the hub. These are only local calls.""" for override in self._nobo.overrides.values(): if override["target_type"] == nobo.API.OVERRIDE_TARGET_GLOBAL: self._attr_current_option = self._modes[override["mode"]] @@ -136,6 +137,12 @@ class NoboProfileSelector(NoboBaseEntity, SelectEntity): @callback def _read_state(self) -> None: + """Read the current state from the hub. These are only local calls.""" + if self._id not in self._nobo.zones: + # Zone removed via the Nobø app; mark unavailable. + self._attr_available = False + return + self._attr_available = True self._profiles = { profile["week_profile_id"]: profile["name"].replace("\xa0", " ") for profile in self._nobo.week_profiles.values() diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 642de720b8b..0c8c9bc2b43 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -71,6 +71,11 @@ class NoboTemperatureSensor(NoboBaseEntity, SensorEntity): @callback def _read_state(self) -> None: """Read the current state from the hub. This is a local call.""" + if self._id not in self._nobo.components: + # Component removed via the Nobø app; mark unavailable. + self._attr_available = False + return + self._attr_available = True value = self._nobo.get_current_component_temperature(self._id) if value is None: self._attr_native_value = None diff --git a/tests/components/nobo_hub/test_climate.py b/tests/components/nobo_hub/test_climate.py index 26f4329b51a..e1db4c59c9b 100644 --- a/tests/components/nobo_hub/test_climate.py +++ b/tests/components/nobo_hub/test_climate.py @@ -26,7 +26,7 @@ from homeassistant.components.nobo_hub.const import ( CONF_OVERRIDE_TYPE, OVERRIDE_TYPE_NOW, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -186,6 +186,17 @@ async def test_set_preset_with_override_type_now( ) +@pytest.mark.usefixtures("init_integration") +async def test_zone_removed_marks_unavailable( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, +) -> None: + """A zone removed via the Nobø app must not crash and goes unavailable.""" + mock_nobo_hub.zones.pop("1") + await fire_hub_update(hass, mock_nobo_hub) + assert hass.states.get(CLIMATE_ENTITY).state == STATE_UNAVAILABLE + + @pytest.mark.usefixtures("init_integration") async def test_set_temperature_updates_zone( hass: HomeAssistant, diff --git a/tests/components/nobo_hub/test_select.py b/tests/components/nobo_hub/test_select.py index 774ebb5f116..7564b27dca9 100644 --- a/tests/components/nobo_hub/test_select.py +++ b/tests/components/nobo_hub/test_select.py @@ -11,7 +11,7 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -136,3 +136,14 @@ async def test_week_profile_push_update( mock_nobo_hub.zones["1"]["week_profile_id"] = "1" await fire_hub_update(hass, mock_nobo_hub) assert hass.states.get(PROFILE_ENTITY).state == "Weekend" + + +@pytest.mark.usefixtures("init_integration") +async def test_zone_removed_marks_week_profile_unavailable( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, +) -> None: + """A zone removed via the Nobø app must not crash and goes unavailable.""" + mock_nobo_hub.zones.pop("1") + await fire_hub_update(hass, mock_nobo_hub) + assert hass.states.get(PROFILE_ENTITY).state == STATE_UNAVAILABLE diff --git a/tests/components/nobo_hub/test_sensor.py b/tests/components/nobo_hub/test_sensor.py index 8a2ed740e50..3db22018646 100644 --- a/tests/components/nobo_hub/test_sensor.py +++ b/tests/components/nobo_hub/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -55,3 +55,14 @@ async def test_temperature_push_update( mock_nobo_hub.get_current_component_temperature.return_value = "19.3" await fire_hub_update(hass, mock_nobo_hub) assert hass.states.get(TEMPERATURE_ENTITY).state == "19.3" + + +@pytest.mark.usefixtures("init_integration") +async def test_component_removed_marks_unavailable( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, +) -> None: + """A component removed via the Nobø app must not crash and goes unavailable.""" + mock_nobo_hub.components.pop("200000059091") + await fire_hub_update(hass, mock_nobo_hub) + assert hass.states.get(TEMPERATURE_ENTITY).state == STATE_UNAVAILABLE