diff --git a/simplipy/device/__init__.py b/simplipy/device/__init__.py index b4bf677a..f1bd551b 100644 --- a/simplipy/device/__init__.py +++ b/simplipy/device/__init__.py @@ -84,7 +84,9 @@ def name(self) -> str: Returns: The device name. """ - return cast(str, self._system.sensor_data[self._serial]["name"]) + return cast( + str, self._system.sensor_data[self._serial].get("name", self._serial) + ) @property def serial(self) -> str: @@ -93,7 +95,9 @@ def serial(self) -> str: Returns: The device serial number. """ - return cast(str, self._system.sensor_data[self._serial]["serial"]) + return cast( + str, self._system.sensor_data[self._serial].get("serial", self._serial) + ) @property def type(self) -> DeviceTypes: @@ -146,7 +150,9 @@ def error(self) -> bool: """ return cast( bool, - self._system.sensor_data[self._serial]["status"].get("malfunction", False), + self._system.sensor_data[self._serial] + .get("status", {}) + .get("malfunction", False), ) @property @@ -156,7 +162,12 @@ def low_battery(self) -> bool: Returns: The device's low battery status. """ - return cast(bool, self._system.sensor_data[self._serial]["flags"]["lowBattery"]) + return cast( + bool, + self._system.sensor_data[self._serial] + .get("flags", {}) + .get("lowBattery", False), + ) @property def offline(self) -> bool: @@ -165,7 +176,12 @@ def offline(self) -> bool: Returns: The device's offline status. """ - return cast(bool, self._system.sensor_data[self._serial]["flags"]["offline"]) + return cast( + bool, + self._system.sensor_data[self._serial] + .get("flags", {}) + .get("offline", False), + ) @property def settings(self) -> dict[str, Any]: @@ -176,7 +192,9 @@ def settings(self) -> dict[str, Any]: Returns: A settings dictionary. """ - return cast(dict[str, Any], self._system.sensor_data[self._serial]["setting"]) + return cast( + dict[str, Any], self._system.sensor_data[self._serial].get("setting", {}) + ) def as_dict(self) -> dict[str, Any]: """Return dictionary version of this device. diff --git a/tests/sensor/test_base.py b/tests/sensor/test_base.py index b67134ba..fbd643ed 100644 --- a/tests/sensor/test_base.py +++ b/tests/sensor/test_base.py @@ -27,3 +27,25 @@ async def test_properties_base( assert sensor.type == DeviceTypes.KEYPAD aresponses.assert_plan_strictly_followed() + + +@pytest.mark.asyncio +async def test_properties_base_missing_name_falls_back_to_serial( + aresponses: ResponsesMockServer, + authenticated_simplisafe_server_v3: ResponsesMockServer, +) -> None: + """Test that name falls back to serial when the API omits the 'name' field. + + Regression test for home-assistant/core#172599. + """ + async with authenticated_simplisafe_server_v3, aiohttp.ClientSession() as session: + simplisafe = await API.async_from_auth( + TEST_AUTHORIZATION_CODE, TEST_CODE_VERIFIER, session=session + ) + systems = await simplisafe.async_get_systems() + system = systems[TEST_SYSTEM_ID] + sensor = system.sensors["825"] + del system.sensor_data["825"]["name"] + assert sensor.name == "825" + + aresponses.assert_plan_strictly_followed() diff --git a/tests/sensor/test_v3.py b/tests/sensor/test_v3.py index 4e5c8400..23e87cba 100644 --- a/tests/sensor/test_v3.py +++ b/tests/sensor/test_v3.py @@ -44,3 +44,33 @@ async def test_properties_v3( assert siren.temperature == 42 aresponses.assert_plan_strictly_followed() + + +@pytest.mark.asyncio +async def test_properties_v3_missing_fields_return_defaults( + aresponses: ResponsesMockServer, + authenticated_simplisafe_server_v3: ResponsesMockServer, +) -> None: + """Test that V3 properties return safe defaults when API fields are absent. + + Regression test for home-assistant/core#172599. + """ + async with authenticated_simplisafe_server_v3, aiohttp.ClientSession() as session: + simplisafe = await API.async_from_auth( + TEST_AUTHORIZATION_CODE, TEST_CODE_VERIFIER, session=session + ) + systems = await simplisafe.async_get_systems() + system = systems[TEST_SYSTEM_ID] + sensor: SensorV3 = cast(SensorV3, system.sensors["825"]) + + # Remove optional nested fields to simulate a sparse API response + system.sensor_data["825"].pop("status", None) + system.sensor_data["825"].pop("flags", None) + system.sensor_data["825"].pop("setting", None) + + assert not sensor.error + assert not sensor.low_battery + assert not sensor.offline + assert sensor.settings == {} + + aresponses.assert_plan_strictly_followed()