From edc6e3e2f9e74739f38e2160eeeac37f9ffe97b1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 4 Feb 2024 14:35:08 -0700 Subject: [PATCH] Bump `aionotion` to 2024.02.0 (#109577) --- homeassistant/components/notion/__init__.py | 102 +++++++++--------- .../components/notion/binary_sensor.py | 6 +- .../components/notion/config_flow.py | 4 +- homeassistant/components/notion/manifest.json | 2 +- homeassistant/components/notion/model.py | 2 +- homeassistant/components/notion/sensor.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/notion/conftest.py | 34 ++++-- .../notion/fixtures/bridge_data.json | 32 +----- .../notion/fixtures/listener_data.json | 49 ++------- .../notion/fixtures/sensor_data.json | 10 +- tests/components/notion/test_diagnostics.py | 78 +++++--------- 13 files changed, 134 insertions(+), 197 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 406acd6aabd..bd83b192a69 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -4,22 +4,15 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, field from datetime import timedelta -import logging -import traceback from typing import Any from uuid import UUID from aionotion import async_get_client -from aionotion.bridge.models import Bridge, BridgeAllResponse +from aionotion.bridge.models import Bridge from aionotion.errors import InvalidCredentialsError, NotionError -from aionotion.sensor.models import ( - Listener, - ListenerAllResponse, - ListenerKind, - Sensor, - SensorAllResponse, -) -from aionotion.user.models import UserPreferences, UserPreferencesResponse +from aionotion.listener.models import Listener, ListenerKind +from aionotion.sensor.models import Sensor +from aionotion.user.models import UserPreferences from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -112,36 +105,35 @@ class NotionData: # Define a user preferences response object: user_preferences: UserPreferences | None = field(default=None) - def update_data_from_response( - self, - response: BridgeAllResponse - | ListenerAllResponse - | SensorAllResponse - | UserPreferencesResponse, - ) -> None: - """Update data from an aionotion response.""" - if isinstance(response, BridgeAllResponse): - for bridge in response.bridges: - # If a new bridge is discovered, register it: - if bridge.id not in self.bridges: - _async_register_new_bridge(self.hass, self.entry, bridge) - self.bridges[bridge.id] = bridge - elif isinstance(response, ListenerAllResponse): - self.listeners = {listener.id: listener for listener in response.listeners} - elif isinstance(response, SensorAllResponse): - self.sensors = {sensor.uuid: sensor for sensor in response.sensors} - elif isinstance(response, UserPreferencesResponse): - self.user_preferences = response.user_preferences + def update_bridges(self, bridges: list[Bridge]) -> None: + """Update the bridges.""" + for bridge in bridges: + # If a new bridge is discovered, register it: + if bridge.id not in self.bridges: + _async_register_new_bridge(self.hass, self.entry, bridge) + self.bridges[bridge.id] = bridge + + def update_listeners(self, listeners: list[Listener]) -> None: + """Update the listeners.""" + self.listeners = {listener.id: listener for listener in listeners} + + def update_sensors(self, sensors: list[Sensor]) -> None: + """Update the sensors.""" + self.sensors = {sensor.uuid: sensor for sensor in sensors} + + def update_user_preferences(self, user_preferences: UserPreferences) -> None: + """Update the user preferences.""" + self.user_preferences = user_preferences def asdict(self) -> dict[str, Any]: """Represent this dataclass (and its Pydantic contents) as a dict.""" data: dict[str, Any] = { - DATA_BRIDGES: [bridge.dict() for bridge in self.bridges.values()], - DATA_LISTENERS: [listener.dict() for listener in self.listeners.values()], - DATA_SENSORS: [sensor.dict() for sensor in self.sensors.values()], + DATA_BRIDGES: [item.to_dict() for item in self.bridges.values()], + DATA_LISTENERS: [item.to_dict() for item in self.listeners.values()], + DATA_SENSORS: [item.to_dict() for item in self.sensors.values()], } if self.user_preferences: - data[DATA_USER_PREFERENCES] = self.user_preferences.dict() + data[DATA_USER_PREFERENCES] = self.user_preferences.to_dict() return data @@ -156,7 +148,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: client = await async_get_client( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + session=session, + use_legacy_auth=True, ) except InvalidCredentialsError as err: raise ConfigEntryAuthFailed("Invalid username and/or password") from err @@ -166,34 +161,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update() -> NotionData: """Get the latest data from the Notion API.""" data = NotionData(hass=hass, entry=entry) - tasks = { - DATA_BRIDGES: client.bridge.async_all(), - DATA_LISTENERS: client.sensor.async_listeners(), - DATA_SENSORS: client.sensor.async_all(), - DATA_USER_PREFERENCES: client.user.async_preferences(), - } - results = await asyncio.gather(*tasks.values(), return_exceptions=True) - for attr, result in zip(tasks, results): + try: + async with asyncio.TaskGroup() as tg: + bridges = tg.create_task(client.bridge.async_all()) + listeners = tg.create_task(client.listener.async_all()) + sensors = tg.create_task(client.sensor.async_all()) + user_preferences = tg.create_task(client.user.async_preferences()) + except BaseExceptionGroup as err: + result = err.exceptions[0] if isinstance(result, InvalidCredentialsError): raise ConfigEntryAuthFailed( "Invalid username and/or password" ) from result if isinstance(result, NotionError): raise UpdateFailed( - f"There was a Notion error while updating {attr}: {result}" + f"There was a Notion error while updating: {result}" ) from result if isinstance(result, Exception): - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) + LOGGER.debug( + "There was an unknown error while updating: %s", + result, + exc_info=result, + ) raise UpdateFailed( - f"There was an unknown error while updating {attr}: {result}" + f"There was an unknown error while updating: {result}" ) from result if isinstance(result, BaseException): raise result from None - data.update_data_from_response(result) # type: ignore[arg-type] - + data.update_bridges(bridges.result()) + data.update_listeners(listeners.result()) + data.update_sensors(sensors.result()) + data.update_user_preferences(user_preferences.result()) return data coordinator = DataUpdateCoordinator( @@ -232,7 +232,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: listener for listener in coordinator.data.listeners.values() if listener.sensor_id == sensor.uuid - and listener.listener_kind == TASK_TYPE_TO_LISTENER_MAP[task_type] + and listener.definition_id == TASK_TYPE_TO_LISTENER_MAP[task_type].value ) return {"new_unique_id": listener.id} diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 8e4d5927152..dfa6dc5ec06 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Literal -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -123,7 +123,7 @@ async def async_setup_entry( ) for listener_id, listener in coordinator.data.listeners.items() for description in BINARY_SENSOR_DESCRIPTIONS - if description.listener_kind == listener.listener_kind + if description.listener_kind.value == listener.definition_id and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -138,6 +138,6 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" if not self.listener.insights.primary.value: - LOGGER.warning("Unknown listener structure: %s", self.listener.dict()) + LOGGER.warning("Unknown listener structure: %s", self.listener) return False return self.listener.insights.primary.value == self.entity_description.on_state diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 1e4adab2910..2ed83adeb08 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -38,7 +38,9 @@ async def async_validate_credentials( errors = {} try: - await async_get_client(username, password, session=session) + await async_get_client( + username, password, session=session, use_legacy_auth=True + ) except InvalidCredentialsError: errors["base"] = "invalid_auth" except NotionError as err: diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index f23a082df35..662114742bd 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aionotion"], - "requirements": ["aionotion==2023.05.5"] + "requirements": ["aionotion==2024.02.0"] } diff --git a/homeassistant/components/notion/model.py b/homeassistant/components/notion/model.py index a774bfdfad3..059ea551b09 100644 --- a/homeassistant/components/notion/model.py +++ b/homeassistant/components/notion/model.py @@ -1,7 +1,7 @@ """Define Notion model mixins.""" from dataclasses import dataclass -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 1d2c81addfa..f5439895ac9 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,7 +1,7 @@ """Support for Notion sensors.""" from dataclasses import dataclass -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind from homeassistant.components.sensor import ( SensorDeviceClass, @@ -59,7 +59,7 @@ async def async_setup_entry( ) for listener_id, listener in coordinator.data.listeners.items() for description in SENSOR_DESCRIPTIONS - if description.listener_kind == listener.listener_kind + if description.listener_kind.value == listener.definition_id and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -71,7 +71,7 @@ class NotionSensor(NotionEntity, SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" - if self.listener.listener_kind == ListenerKind.TEMPERATURE: + if self.listener.definition_id == ListenerKind.TEMPERATURE.value: if not self.coordinator.data.user_preferences: return None if self.coordinator.data.user_preferences.celsius_enabled: @@ -84,7 +84,7 @@ class NotionSensor(NotionEntity, SensorEntity): """Return the value reported by the sensor.""" if not self.listener.status_localized: return None - if self.listener.listener_kind == ListenerKind.TEMPERATURE: + if self.listener.definition_id == ListenerKind.TEMPERATURE.value: # The Notion API only returns a localized string for temperature (e.g. # "70°"); we simply remove the degree symbol: return self.listener.status_localized.state[:-1] diff --git a/requirements_all.txt b/requirements_all.txt index fb563af478f..3cda8a4f523 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2023.05.5 +aionotion==2024.02.0 # homeassistant.components.oncue aiooncue==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cb24852bac..086133cbb25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2023.05.5 +aionotion==2024.02.0 # homeassistant.components.oncue aiooncue==0.3.5 diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 81d69158e82..3623782429f 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -3,9 +3,10 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch -from aionotion.bridge.models import BridgeAllResponse -from aionotion.sensor.models import ListenerAllResponse, SensorAllResponse -from aionotion.user.models import UserPreferencesResponse +from aionotion.bridge.models import Bridge +from aionotion.listener.models import Listener +from aionotion.sensor.models import Sensor +from aionotion.user.models import UserPreferences import pytest from homeassistant.components.notion import DOMAIN @@ -32,17 +33,32 @@ def client_fixture(data_bridge, data_listener, data_sensor, data_user_preference """Define a fixture for an aionotion client.""" return Mock( bridge=Mock( - async_all=AsyncMock(return_value=BridgeAllResponse.parse_obj(data_bridge)) + async_all=AsyncMock( + return_value=[ + Bridge.from_dict(bridge) for bridge in data_bridge["base_stations"] + ] + ) + ), + listener=Mock( + async_all=AsyncMock( + return_value=[ + Listener.from_dict(listener) + for listener in data_listener["listeners"] + ] + ) ), sensor=Mock( - async_all=AsyncMock(return_value=SensorAllResponse.parse_obj(data_sensor)), - async_listeners=AsyncMock( - return_value=ListenerAllResponse.parse_obj(data_listener) - ), + async_all=AsyncMock( + return_value=[ + Sensor.from_dict(sensor) for sensor in data_sensor["sensors"] + ] + ) ), user=Mock( async_preferences=AsyncMock( - return_value=UserPreferencesResponse.parse_obj(data_user_preferences) + return_value=UserPreferences.from_dict( + data_user_preferences["user_preferences"] + ) ) ), ) diff --git a/tests/components/notion/fixtures/bridge_data.json b/tests/components/notion/fixtures/bridge_data.json index 05bd8859e7e..d8a0feead69 100644 --- a/tests/components/notion/fixtures/bridge_data.json +++ b/tests/components/notion/fixtures/bridge_data.json @@ -2,31 +2,7 @@ "base_stations": [ { "id": 12345, - "name": "Bridge 1", - "mode": "home", - "hardware_id": "0x0000000000000000", - "hardware_revision": 4, - "firmware_version": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" - }, - "missing_at": null, - "created_at": "2019-06-27T00:18:44.337Z", - "updated_at": "2023-03-19T03:20:16.061Z", - "system_id": 11111, - "firmware": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" - }, - "links": { - "system": 11111 - } - }, - { - "id": 67890, - "name": "Bridge 2", + "name": "Laundry Closet", "mode": "home", "hardware_id": "0x0000000000000000", "hardware_revision": 4, @@ -37,15 +13,15 @@ }, "missing_at": null, "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2023-01-02T19:09:58.251Z", - "system_id": 11111, + "updated_at": "2023-12-12T22:33:01.073Z", + "system_id": 12345, "firmware": { "wifi": "0.121.0", "wifi_app": "3.3.0", "silabs": "1.1.2" }, "links": { - "system": 11111 + "system": 12345 } } ] diff --git a/tests/components/notion/fixtures/listener_data.json b/tests/components/notion/fixtures/listener_data.json index 6d59dde76df..af692795f1b 100644 --- a/tests/components/notion/fixtures/listener_data.json +++ b/tests/components/notion/fixtures/listener_data.json @@ -2,56 +2,27 @@ "listeners": [ { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 4, - "created_at": "2019-06-28T22:12:49.651Z", + "definition_id": 24, + "created_at": "2019-06-17T03:29:45.722Z", "type": "sensor", - "model_version": "2.1", + "model_version": "1.0", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_leak", - "data_received_at": "2022-03-20T08:00:29.763Z" - }, "status_localized": { - "state": "No Leak", - "description": "Mar 20 at 2:00am" + "state": "Idle", + "description": "Jun 18 at 12:17am" }, "insights": { "primary": { "origin": { - "type": "Sensor", - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": "Sensor" }, - "value": "no_leak", - "data_received_at": "2022-03-20T08:00:29.763Z" + "value": "idle", + "data_received_at": "2023-06-18T06:17:00.697Z" } }, "configuration": {}, - "pro_monitoring_status": "eligible" - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 7, - "created_at": "2019-07-10T22:40:48.847Z", - "type": "sensor", - "model_version": "3.1", - "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516Z" - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm" - }, - "insights": { - "primary": { - "origin": {}, - "value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516Z" - } - }, - "configuration": {}, - "pro_monitoring_status": "eligible" + "pro_monitoring_status": "ineligible" } ] } diff --git a/tests/components/notion/fixtures/sensor_data.json b/tests/components/notion/fixtures/sensor_data.json index 9f0d0fe2e03..56989cbb157 100644 --- a/tests/components/notion/fixtures/sensor_data.json +++ b/tests/components/notion/fixtures/sensor_data.json @@ -20,12 +20,12 @@ "firmware_version": "1.1.2", "device_key": "0x0000000000000000", "encryption_key": true, - "installed_at": "2019-06-28T22:12:51.209Z", - "calibrated_at": "2023-03-07T19:51:56.838Z", - "last_reported_at": "2023-04-19T18:09:40.479Z", + "installed_at": "2019-06-17T03:30:27.766Z", + "calibrated_at": "2024-01-19T00:38:15.372Z", + "last_reported_at": "2024-01-21T00:00:46.705Z", "missing_at": null, - "updated_at": "2023-03-28T13:33:33.801Z", - "created_at": "2019-06-28T22:12:20.256Z", + "updated_at": "2024-01-19T00:38:16.856Z", + "created_at": "2019-06-17T03:29:45.506Z", "signal_strength": 4, "firmware": { "status": "valid" diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 07a67cb1429..a2b829281f8 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -33,31 +33,7 @@ async def test_entry_diagnostics( "bridges": [ { "id": 12345, - "name": "Bridge 1", - "mode": "home", - "hardware_id": REDACTED, - "hardware_revision": 4, - "firmware_version": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2", - "ti": None, - }, - "missing_at": None, - "created_at": "2019-06-27T00:18:44.337000+00:00", - "updated_at": "2023-03-19T03:20:16.061000+00:00", - "system_id": 11111, - "firmware": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2", - "ti": None, - }, - "links": {"system": 11111}, - }, - { - "id": 67890, - "name": "Bridge 2", + "name": "Laundry Closet", "mode": "home", "hardware_id": REDACTED, "hardware_revision": 4, @@ -69,45 +45,41 @@ async def test_entry_diagnostics( }, "missing_at": None, "created_at": "2019-04-30T01:43:50.497000+00:00", - "updated_at": "2023-01-02T19:09:58.251000+00:00", - "system_id": 11111, + "updated_at": "2023-12-12T22:33:01.073000+00:00", + "system_id": 12345, "firmware": { "wifi": "0.121.0", "wifi_app": "3.3.0", "silabs": "1.1.2", "ti": None, }, - "links": {"system": 11111}, - }, + "links": {"system": 12345}, + } ], "listeners": [ { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "listener_kind": { - "__type": "", - "repr": "", - }, - "created_at": "2019-07-10T22:40:48.847000+00:00", - "device_type": "sensor", - "model_version": "3.1", + "definition_id": 24, + "created_at": "2019-06-17T03:29:45.722000+00:00", + "model_version": "1.0", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "status_localized": { + "state": "Idle", + "description": "Jun 18 at 12:17am", + }, "insights": { "primary": { - "origin": {"type": None, "id": None}, - "value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516000+00:00", + "origin": { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": "Sensor", + }, + "value": "idle", + "data_received_at": "2023-06-18T06:17:00.697000+00:00", } }, "configuration": {}, - "pro_monitoring_status": "eligible", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516000+00:00", - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm", - }, + "pro_monitoring_status": "ineligible", + "device_type": "sensor", } ], "sensors": [ @@ -125,12 +97,12 @@ async def test_entry_diagnostics( "firmware_version": "1.1.2", "device_key": REDACTED, "encryption_key": True, - "installed_at": "2019-06-28T22:12:51.209000+00:00", - "calibrated_at": "2023-03-07T19:51:56.838000+00:00", - "last_reported_at": "2023-04-19T18:09:40.479000+00:00", + "installed_at": "2019-06-17T03:30:27.766000+00:00", + "calibrated_at": "2024-01-19T00:38:15.372000+00:00", + "last_reported_at": "2024-01-21T00:00:46.705000+00:00", "missing_at": None, - "updated_at": "2023-03-28T13:33:33.801000+00:00", - "created_at": "2019-06-28T22:12:20.256000+00:00", + "updated_at": "2024-01-19T00:38:16.856000+00:00", + "created_at": "2019-06-17T03:29:45.506000+00:00", "signal_strength": 4, "firmware": {"status": "valid"}, "surface_type": None,