From c6d846453d9be19f931c2c766fc34d39fb78454f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 21 Apr 2023 17:52:57 -0600 Subject: [PATCH] Bump `aionotion` to 2023.04.2 to address imminent API change (#91786) * Bump `aionotion` to 2023.04.0 * Bump `aionotion` to 2023.04.2 to address imminent API change * Clean migration * Reduce blast area * Fix tests * Better naming --- homeassistant/components/notion/__init__.py | 168 ++++++++++++++---- .../components/notion/binary_sensor.py | 54 +++--- homeassistant/components/notion/const.py | 3 +- .../components/notion/diagnostics.py | 11 +- homeassistant/components/notion/manifest.json | 2 +- homeassistant/components/notion/model.py | 11 ++ homeassistant/components/notion/sensor.py | 39 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/notion/conftest.py | 39 ++-- .../notion/fixtures/bridge_data.json | 38 +++- .../notion/fixtures/listener_data.json | 55 ++++++ .../notion/fixtures/sensor_data.json | 62 ++----- .../components/notion/fixtures/task_data.json | 86 --------- tests/components/notion/test_diagnostics.py | 152 ++++++++-------- 15 files changed, 410 insertions(+), 314 deletions(-) create mode 100644 homeassistant/components/notion/model.py create mode 100644 tests/components/notion/fixtures/listener_data.json delete mode 100644 tests/components/notion/fixtures/task_data.json diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index eaa3f55e56c..7ff05d2a75e 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -2,13 +2,17 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass, field, fields 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 from aionotion.errors import InvalidCredentialsError, NotionError +from aionotion.sensor.models import Listener, ListenerKind, Sensor from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -18,6 +22,7 @@ from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, + entity_registry as er, ) from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -26,7 +31,20 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DOMAIN, LOGGER +from .const import ( + DOMAIN, + LOGGER, + SENSOR_BATTERY, + SENSOR_DOOR, + SENSOR_GARAGE_DOOR, + SENSOR_LEAK, + SENSOR_MISSING, + SENSOR_SAFE, + SENSOR_SLIDING, + SENSOR_SMOKE_CO, + SENSOR_TEMPERATURE, + SENSOR_WINDOW_HINGED, +) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -37,6 +55,51 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +# Define a map of old-API task types to new-API listener types: +TASK_TYPE_TO_LISTENER_MAP: dict[str, ListenerKind] = { + SENSOR_BATTERY: ListenerKind.BATTERY, + SENSOR_DOOR: ListenerKind.DOOR, + SENSOR_GARAGE_DOOR: ListenerKind.GARAGE_DOOR, + SENSOR_LEAK: ListenerKind.LEAK_STATUS, + SENSOR_MISSING: ListenerKind.CONNECTED, + SENSOR_SAFE: ListenerKind.SAFE, + SENSOR_SLIDING: ListenerKind.SLIDING_DOOR_OR_WINDOW, + SENSOR_SMOKE_CO: ListenerKind.SMOKE, + SENSOR_TEMPERATURE: ListenerKind.TEMPERATURE, + SENSOR_WINDOW_HINGED: ListenerKind.HINGED_WINDOW, +} + + +@callback +def is_uuid(value: str) -> bool: + """Return whether a string is a valid UUID.""" + try: + UUID(value) + except ValueError: + return False + return True + + +@dataclass +class NotionData: + """Define a manager class for Notion data.""" + + # Define a dict of bridges, indexed by bridge ID (an integer): + bridges: dict[int, Bridge] = field(default_factory=dict) + + # Define a dict of listeners, indexed by listener UUID (a string): + listeners: dict[str, Listener] = field(default_factory=dict) + + # Define a dict of sensors, indexed by sensor UUID (a string): + sensors: dict[str, Sensor] = field(default_factory=dict) + + def asdict(self) -> dict[str, Any]: + """Represent this dataclass (and its Pydantic contents) as a dict.""" + return { + field.name: [obj.dict() for obj in getattr(self, field.name).values()] + for field in fields(self) + } + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notion as a config entry.""" @@ -56,13 +119,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except NotionError as err: raise ConfigEntryNotReady("Config entry failed to load") from err - async def async_update() -> dict[str, dict[str, Any]]: + async def async_update() -> NotionData: """Get the latest data from the Notion API.""" - data: dict[str, dict[str, Any]] = {"bridges": {}, "sensors": {}, "tasks": {}} + data = NotionData() tasks = { "bridges": client.bridge.async_all(), + "listeners": client.sensor.async_listeners(), "sensors": client.sensor.async_all(), - "tasks": client.task.async_all(), } results = await asyncio.gather(*tasks.values(), return_exceptions=True) @@ -83,10 +146,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from result for item in result: - if attr == "bridges" and item["id"] not in data["bridges"]: + if attr == "bridges": # If a new bridge is discovered, register it: - _async_register_new_bridge(hass, item, entry) - data[attr][item["id"]] = item + if item.id not in data.bridges: + _async_register_new_bridge(hass, item, entry) + data.bridges[item.id] = item + elif attr == "listeners": + data.listeners[item.id] = item + else: + data.sensors[item.uuid] = item return data @@ -102,6 +170,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + @callback + def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: + """Migrate Notion entity entries. + + This migration focuses on unique IDs, which have changed because of a Notion API + change: + + Old Format: _ + New Format: + """ + if is_uuid(entry.unique_id): + # If the unique ID is already a UUID, we don't need to migrate it: + return None + + sensor_id_str, task_type = entry.unique_id.split("_", 1) + sensor = next( + sensor + for sensor in coordinator.data.sensors.values() + if sensor.id == int(sensor_id_str) + ) + listener = next( + 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] + ) + + return {"new_unique_id": listener.id} + + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -118,22 +216,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_register_new_bridge( - hass: HomeAssistant, bridge: dict, entry: ConfigEntry + hass: HomeAssistant, bridge: Bridge, entry: ConfigEntry ) -> None: """Register a new bridge.""" - if name := bridge["name"]: + if name := bridge.name: bridge_name = name.capitalize() else: - bridge_name = bridge["id"] + bridge_name = str(bridge.id) device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers={(DOMAIN, bridge["hardware_id"])}, + identifiers={(DOMAIN, bridge.hardware_id)}, manufacturer="Silicon Labs", - model=bridge["hardware_revision"], + model=str(bridge.hardware_revision), name=bridge_name, - sw_version=bridge["firmware_version"]["wifi"], + sw_version=bridge.firmware_version.wifi, ) @@ -145,7 +243,7 @@ class NotionEntity(CoordinatorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - task_id: str, + listener_id: str, sensor_id: str, bridge_id: str, system_id: str, @@ -154,25 +252,23 @@ class NotionEntity(CoordinatorEntity): """Initialize the entity.""" super().__init__(coordinator) - bridge = self.coordinator.data["bridges"].get(bridge_id, {}) - sensor = self.coordinator.data["sensors"][sensor_id] + bridge = self.coordinator.data.bridges.get(bridge_id, {}) + sensor = self.coordinator.data.sensors[sensor_id] self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, sensor["hardware_id"])}, + identifiers={(DOMAIN, sensor.hardware_id)}, manufacturer="Silicon Labs", - model=sensor["hardware_revision"], - name=str(sensor["name"]).capitalize(), - sw_version=sensor["firmware_version"], - via_device=(DOMAIN, bridge.get("hardware_id")), + model=sensor.hardware_revision, + name=str(sensor.name).capitalize(), + sw_version=sensor.firmware_version, + via_device=(DOMAIN, bridge.hardware_id), ) self._attr_extra_state_attributes = {} - self._attr_unique_id = ( - f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' - ) + self._attr_unique_id = listener_id self._bridge_id = bridge_id + self._listener_id = listener_id self._sensor_id = sensor_id self._system_id = system_id - self._task_id = task_id self.entity_description = description @property @@ -180,7 +276,7 @@ class NotionEntity(CoordinatorEntity): """Return True if entity is available.""" return ( self.coordinator.last_update_success - and self._task_id in self.coordinator.data["tasks"] + and self._listener_id in self.coordinator.data.listeners ) @callback @@ -189,27 +285,23 @@ class NotionEntity(CoordinatorEntity): Sensors can move to other bridges based on signal strength, etc. """ - sensor = self.coordinator.data["sensors"][self._sensor_id] + sensor = self.coordinator.data.sensors[self._sensor_id] # If the sensor's bridge ID is the same as what we had before or if it points # to a bridge that doesn't exist (which can happen due to a Notion API bug), # return immediately: if ( - self._bridge_id == sensor["bridge"]["id"] - or sensor["bridge"]["id"] not in self.coordinator.data["bridges"] + self._bridge_id == sensor.bridge.id + or sensor.bridge.id not in self.coordinator.data.bridges ): return - self._bridge_id = sensor["bridge"]["id"] + self._bridge_id = sensor.bridge.id device_registry = dr.async_get(self.hass) - this_device = device_registry.async_get_device( - {(DOMAIN, sensor["hardware_id"])} - ) - bridge = self.coordinator.data["bridges"][self._bridge_id] - bridge_device = device_registry.async_get_device( - {(DOMAIN, bridge["hardware_id"])} - ) + this_device = device_registry.async_get_device({(DOMAIN, sensor.hardware_id)}) + bridge = self.coordinator.data.bridges[self._bridge_id] + bridge_device = device_registry.async_get_device({(DOMAIN, bridge.hardware_id)}) if not bridge_device or not this_device: return @@ -226,7 +318,7 @@ class NotionEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" - if self._task_id in self.coordinator.data["tasks"]: + if self._listener_id in self.coordinator.data.listeners: self._async_update_bridge_id() self._async_update_from_latest_data() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index f5d40b2a9de..bd2de303d2d 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import Literal +from aionotion.sensor.models import ListenerKind + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -26,9 +28,9 @@ from .const import ( SENSOR_SAFE, SENSOR_SLIDING, SENSOR_SMOKE_CO, - SENSOR_WINDOW_HINGED_HORIZONTAL, - SENSOR_WINDOW_HINGED_VERTICAL, + SENSOR_WINDOW_HINGED, ) +from .model import NotionEntityDescriptionMixin @dataclass @@ -40,7 +42,9 @@ class NotionBinarySensorDescriptionMixin: @dataclass class NotionBinarySensorDescription( - BinarySensorEntityDescription, NotionBinarySensorDescriptionMixin + BinarySensorEntityDescription, + NotionBinarySensorDescriptionMixin, + NotionEntityDescriptionMixin, ): """Describe a Notion binary sensor.""" @@ -51,24 +55,28 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Low battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, + listener_kind=ListenerKind.BATTERY, on_state="critical", ), NotionBinarySensorDescription( key=SENSOR_DOOR, name="Door", device_class=BinarySensorDeviceClass.DOOR, + listener_kind=ListenerKind.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, name="Garage door", device_class=BinarySensorDeviceClass.GARAGE_DOOR, + listener_kind=ListenerKind.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, name="Leak detector", device_class=BinarySensorDeviceClass.MOISTURE, + listener_kind=ListenerKind.LEAK_STATUS, on_state="leak", ), NotionBinarySensorDescription( @@ -76,36 +84,34 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Missing", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, + listener_kind=ListenerKind.CONNECTED, on_state="not_missing", ), NotionBinarySensorDescription( key=SENSOR_SAFE, name="Safe", device_class=BinarySensorDeviceClass.DOOR, + listener_kind=ListenerKind.SAFE, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SLIDING, name="Sliding door/window", device_class=BinarySensorDeviceClass.DOOR, + listener_kind=ListenerKind.SLIDING_DOOR_OR_WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, name="Smoke/Carbon monoxide detector", device_class=BinarySensorDeviceClass.SMOKE, + listener_kind=ListenerKind.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( - key=SENSOR_WINDOW_HINGED_HORIZONTAL, + key=SENSOR_WINDOW_HINGED, name="Hinged window", - device_class=BinarySensorDeviceClass.WINDOW, - on_state="open", - ), - NotionBinarySensorDescription( - key=SENSOR_WINDOW_HINGED_VERTICAL, - name="Hinged window", - device_class=BinarySensorDeviceClass.WINDOW, + listener_kind=ListenerKind.HINGED_WINDOW, on_state="open", ), ) @@ -121,16 +127,16 @@ async def async_setup_entry( [ NotionBinarySensor( coordinator, - task_id, - sensor["id"], - sensor["bridge"]["id"], - sensor["system_id"], + listener_id, + sensor.uuid, + sensor.bridge.id, + sensor.system_id, description, ) - for task_id, task in coordinator.data["tasks"].items() + for listener_id, listener in coordinator.data.listeners.items() for description in BINARY_SENSOR_DESCRIPTIONS - if description.key == task["task_type"] - and (sensor := coordinator.data["sensors"][task["sensor_id"]]) + if description.listener_kind == listener.listener_kind + and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -143,14 +149,14 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self._task_id] + listener = self.coordinator.data.listeners[self._listener_id] - if "value" in task["status"]: - state = task["status"]["value"] - elif task["status"].get("insights", {}).get("primary"): - state = task["status"]["insights"]["primary"]["to_state"] + if listener.status.trigger_value: + state = listener.status.trigger_value + elif listener.insights.primary.value: + state = listener.insights.primary.value else: - LOGGER.warning("Unknown data payload: %s", task["status"]) + LOGGER.warning("Unknown listener structure: %s", listener) state = None self._attr_is_on = self.entity_description.on_state == state diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 339d3020734..5e89767d0e0 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -13,5 +13,4 @@ SENSOR_SAFE = "safe" SENSOR_SLIDING = "sliding" SENSOR_SMOKE_CO = "alarm" SENSOR_TEMPERATURE = "temperature" -SENSOR_WINDOW_HINGED_HORIZONTAL = "window_hinged_horizontal" -SENSOR_WINDOW_HINGED_VERTICAL = "window_hinged_vertical" +SENSOR_WINDOW_HINGED = "window_hinged" diff --git a/homeassistant/components/notion/diagnostics.py b/homeassistant/components/notion/diagnostics.py index 9b0a070897c..ea243c43b33 100644 --- a/homeassistant/components/notion/diagnostics.py +++ b/homeassistant/components/notion/diagnostics.py @@ -35,7 +35,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return { - "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "data": async_redact_data(coordinator.data, TO_REDACT), - } + return async_redact_data( + { + "entry": entry.as_dict(), + "data": coordinator.data.asdict(), + }, + TO_REDACT, + ) diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index a2a01ca113b..7eb2ef6bba3 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==3.0.2"] + "requirements": ["aionotion==2023.04.2"] } diff --git a/homeassistant/components/notion/model.py b/homeassistant/components/notion/model.py new file mode 100644 index 00000000000..0999df3abdb --- /dev/null +++ b/homeassistant/components/notion/model.py @@ -0,0 +1,11 @@ +"""Define Notion model mixins.""" +from dataclasses import dataclass + +from aionotion.sensor.models import ListenerKind + + +@dataclass +class NotionEntityDescriptionMixin: + """Define an description mixin Notion entities.""" + + listener_kind: ListenerKind diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 7881780c4ed..687c13055b9 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,4 +1,8 @@ """Support for Notion sensors.""" +from dataclasses import dataclass + +from aionotion.sensor.models import ListenerKind + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -12,14 +16,22 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE +from .model import NotionEntityDescriptionMixin + + +@dataclass +class NotionSensorDescription(SensorEntityDescription, NotionEntityDescriptionMixin): + """Describe a Notion sensor.""" + SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( + NotionSensorDescription( key=SENSOR_TEMPERATURE, name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, + listener_kind=ListenerKind.TEMPERATURE, ), ) @@ -34,16 +46,16 @@ async def async_setup_entry( [ NotionSensor( coordinator, - task_id, - sensor["id"], - sensor["bridge"]["id"], - sensor["system_id"], + listener_id, + sensor.uuid, + sensor.bridge.id, + sensor.system_id, description, ) - for task_id, task in coordinator.data["tasks"].items() + for listener_id, listener in coordinator.data.listeners.items() for description in SENSOR_DESCRIPTIONS - if description.key == task["task_type"] - and (sensor := coordinator.data["sensors"][task["sensor_id"]]) + if description.listener_kind == listener.listener_kind + and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -54,13 +66,12 @@ class NotionSensor(NotionEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self._task_id] + listener = self.coordinator.data.listeners[self._listener_id] - if task["task_type"] == SENSOR_TEMPERATURE: - self._attr_native_value = round(float(task["status"]["value"]), 1) + if listener.listener_kind == ListenerKind.TEMPERATURE: + self._attr_native_value = round(listener.status.temperature, 1) else: LOGGER.error( - "Unknown task type: %s: %s", - self.coordinator.data["sensors"][self._sensor_id], - task["task_type"], + "Unknown listener type for sensor %s", + self.coordinator.data.sensors[self._sensor_id], ) diff --git a/requirements_all.txt b/requirements_all.txt index 3fb10ac5d96..d1d494c099e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aionanoleaf==0.2.1 aionotify==0.2.0 # homeassistant.components.notion -aionotion==3.0.2 +aionotion==2023.04.2 # homeassistant.components.oncue aiooncue==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16c70ec63ab..f23a49ac1c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==3.0.2 +aionotion==2023.04.2 # homeassistant.components.oncue aiooncue==0.3.4 diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 7484e8a997f..75eeda70300 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -3,10 +3,13 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch +from aionotion.bridge.models import Bridge +from aionotion.sensor.models import Listener, Sensor import pytest from homeassistant.components.notion import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -24,17 +27,29 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="client") -def client_fixture(data_bridge, data_sensor, data_task): +def client_fixture(data_bridge, data_listener, data_sensor): """Define a fixture for an aionotion client.""" return Mock( - bridge=Mock(async_all=AsyncMock(return_value=data_bridge)), - sensor=Mock(async_all=AsyncMock(return_value=data_sensor)), - task=Mock(async_all=AsyncMock(return_value=data_task)), + bridge=Mock( + async_all=AsyncMock( + return_value=[Bridge.parse_obj(bridge) for bridge in data_bridge] + ) + ), + sensor=Mock( + async_all=AsyncMock( + return_value=[Sensor.parse_obj(sensor) for sensor in data_sensor] + ), + async_listeners=AsyncMock( + return_value=[ + Listener.parse_obj(listener) for listener in data_listener + ] + ), + ), ) @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture(hass: HomeAssistant, config): """Define a config entry fixture.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=TEST_USERNAME, data=config) entry.add_to_hass(hass) @@ -56,18 +71,18 @@ def data_bridge_fixture(): return json.loads(load_fixture("bridge_data.json", "notion")) +@pytest.fixture(name="data_listener", scope="package") +def data_listener_fixture(): + """Define listener data.""" + return json.loads(load_fixture("listener_data.json", "notion")) + + @pytest.fixture(name="data_sensor", scope="package") def data_sensor_fixture(): """Define sensor data.""" return json.loads(load_fixture("sensor_data.json", "notion")) -@pytest.fixture(name="data_task", scope="package") -def data_task_fixture(): - """Define task data.""" - return json.loads(load_fixture("task_data.json", "notion")) - - @pytest.fixture(name="get_client") def get_client_fixture(client): """Define a fixture to mock the async_get_client method.""" @@ -88,7 +103,7 @@ async def mock_aionotion_fixture(client): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_aionotion): +async def setup_config_entry_fixture(hass: HomeAssistant, config_entry, mock_aionotion): """Define a fixture to set up notion.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/notion/fixtures/bridge_data.json b/tests/components/notion/fixtures/bridge_data.json index c865dd18bb3..008967ece86 100644 --- a/tests/components/notion/fixtures/bridge_data.json +++ b/tests/components/notion/fixtures/bridge_data.json @@ -1,26 +1,50 @@ [ { "id": 12345, - "name": null, + "name": "Bridge 1", "mode": "home", - "hardware_id": "0x1234567890abcdef", + "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", + "mode": "home", + "hardware_id": "0x0000000000000000", "hardware_revision": 4, "firmware_version": { "wifi": "0.121.0", "wifi_app": "3.3.0", - "silabs": "1.0.1" + "silabs": "1.1.2" }, "missing_at": null, "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2019-04-30T01:44:43.749Z", - "system_id": 12345, + "updated_at": "2023-01-02T19:09:58.251Z", + "system_id": 11111, "firmware": { "wifi": "0.121.0", "wifi_app": "3.3.0", - "silabs": "1.0.1" + "silabs": "1.1.2" }, "links": { - "system": 12345 + "system": 11111 } } ] diff --git a/tests/components/notion/fixtures/listener_data.json b/tests/components/notion/fixtures/listener_data.json new file mode 100644 index 00000000000..bd49aab89db --- /dev/null +++ b/tests/components/notion/fixtures/listener_data.json @@ -0,0 +1,55 @@ +[ + { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "definition_id": 4, + "created_at": "2019-06-28T22:12:49.651Z", + "type": "sensor", + "model_version": "2.1", + "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" + }, + "insights": { + "primary": { + "origin": { + "type": "Sensor", + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "value": "no_leak", + "data_received_at": "2022-03-20T08:00:29.763Z" + } + }, + "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" + } +] diff --git a/tests/components/notion/fixtures/sensor_data.json b/tests/components/notion/fixtures/sensor_data.json index e631f856207..e042daf6ddd 100644 --- a/tests/components/notion/fixtures/sensor_data.json +++ b/tests/components/notion/fixtures/sensor_data.json @@ -7,64 +7,28 @@ "email": "user@email.com" }, "bridge": { - "id": 12345, - "hardware_id": "0x1234567890abcdef" + "id": 67890, + "hardware_id": "0x0000000000000000" }, "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Bathroom Sensor", + "name": "Sensor 1", "location_id": 123456, "system_id": 12345, - "hardware_id": "0x1234567890abcdef", - "firmware_version": "1.1.2", + "hardware_id": "0x0000000000000000", "hardware_revision": 5, - "device_key": "0x1234567890abcdef", - "encryption_key": true, - "installed_at": "2019-04-30T01:57:34.443Z", - "calibrated_at": "2019-04-30T01:57:35.651Z", - "last_reported_at": "2019-04-30T02:20:04.821Z", - "missing_at": null, - "updated_at": "2019-04-30T01:57:36.129Z", - "created_at": "2019-04-30T01:56:45.932Z", - "signal_strength": 5, - "links": { - "location": 123456 - }, - "lqi": 0, - "rssi": -46, - "surface_type": null - }, - { - "id": 132462, - "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "user": { - "id": 12345, - "email": "user@email.com" - }, - "bridge": { - "id": 12345, - "hardware_id": "0x1234567890abcdef" - }, - "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Living Room Sensor", - "location_id": 123456, - "system_id": 12345, - "hardware_id": "0x1234567890abcdef", "firmware_version": "1.1.2", - "hardware_revision": 5, - "device_key": "0x1234567890abcdef", + "device_key": "0x0000000000000000", "encryption_key": true, - "installed_at": "2019-04-30T01:45:56.169Z", - "calibrated_at": "2019-04-30T01:46:06.256Z", - "last_reported_at": "2019-04-30T02:20:04.829Z", + "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", "missing_at": null, - "updated_at": "2019-04-30T01:46:07.717Z", - "created_at": "2019-04-30T01:45:14.148Z", - "signal_strength": 5, - "links": { - "location": 123456 + "updated_at": "2023-03-28T13:33:33.801Z", + "created_at": "2019-06-28T22:12:20.256Z", + "signal_strength": 4, + "firmware": { + "status": "valid" }, - "lqi": 0, - "rssi": -30, "surface_type": null } ] diff --git a/tests/components/notion/fixtures/task_data.json b/tests/components/notion/fixtures/task_data.json deleted file mode 100644 index a56d734fb77..00000000000 --- a/tests/components/notion/fixtures/task_data.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "missing", - "sensor_data": [], - "status": { - "value": "not_missing", - "received_at": "2020-11-11T21:18:06.613Z" - }, - "created_at": "2020-11-11T21:18:06.613Z", - "updated_at": "2020-11-11T21:18:06.617Z", - "sensor_id": 525993, - "model_version": "2.0", - "configuration": {}, - "links": { - "sensor": 525993 - } - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "leak", - "sensor_data": [], - "status": { - "insights": { - "primary": { - "from_state": null, - "to_state": "no_leak", - "data_received_at": "2020-11-11T21:19:13.755Z", - "origin": {} - } - } - }, - "created_at": "2020-11-11T21:19:13.755Z", - "updated_at": "2020-11-11T21:19:13.764Z", - "sensor_id": 525993, - "model_version": "2.1", - "configuration": {}, - "links": { - "sensor": 525993 - } - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "temperature", - "sensor_data": [], - "status": { - "value": "20.991287231445312", - "received_at": "2021-01-27T15:18:49.996Z" - }, - "created_at": "2020-11-11T21:19:13.856Z", - "updated_at": "2020-11-11T21:19:13.865Z", - "sensor_id": 525993, - "model_version": "2.1", - "configuration": { - "lower": 15.56, - "upper": 29.44, - "offset": 0 - }, - "links": { - "sensor": 525993 - } - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "low_battery", - "sensor_data": [], - "status": { - "insights": { - "primary": { - "from_state": null, - "to_state": "high", - "data_received_at": "2020-11-17T18:40:27.024Z", - "origin": {} - } - } - }, - "created_at": "2020-11-17T18:40:27.024Z", - "updated_at": "2020-11-17T18:40:27.033Z", - "sensor_id": 525993, - "model_version": "4.1", - "configuration": {}, - "links": { - "sensor": 525993 - } - } -] diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index bc28a98b875..7062778e812 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -1,5 +1,6 @@ """Test Notion diagnostics.""" from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.notion import DOMAIN from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -17,7 +18,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 1, - "domain": "notion", + "domain": DOMAIN, "title": REDACTED, "data": {"username": REDACTED, "password": REDACTED}, "options": {}, @@ -28,106 +29,107 @@ async def test_entry_diagnostics( "disabled_by": None, }, "data": { - "bridges": { - "12345": { + "bridges": [ + { "id": 12345, - "name": None, + "name": "Bridge 1", "mode": "home", "hardware_id": REDACTED, "hardware_revision": 4, "firmware_version": { + "silabs": "1.1.2", "wifi": "0.121.0", "wifi_app": "3.3.0", - "silabs": "1.0.1", }, "missing_at": None, - "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2019-04-30T01:44:43.749Z", - "system_id": 12345, + "created_at": "2019-06-27T00:18:44.337000+00:00", + "updated_at": "2023-03-19T03:20:16.061000+00:00", + "system_id": 11111, "firmware": { + "silabs": "1.1.2", "wifi": "0.121.0", "wifi_app": "3.3.0", - "silabs": "1.0.1", }, - "links": {"system": 12345}, + "links": {"system": 11111}, + }, + { + "id": 67890, + "name": "Bridge 2", + "mode": "home", + "hardware_id": REDACTED, + "hardware_revision": 4, + "firmware_version": { + "silabs": "1.1.2", + "wifi": "0.121.0", + "wifi_app": "3.3.0", + }, + "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, + "firmware": { + "silabs": "1.1.2", + "wifi": "0.121.0", + "wifi_app": "3.3.0", + }, + "links": {"system": 11111}, + }, + ], + "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", + "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "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", + }, + "insights": { + "primary": { + "origin": {"type": None, "id": None}, + "value": "no_alarm", + "data_received_at": "2019-06-28T22:12:49.516000+00:00", + } + }, + "configuration": {}, + "pro_monitoring_status": "eligible", } - }, - "sensors": { - "123456": { + ], + "sensors": [ + { "id": 123456, "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "user": {"id": 12345, "email": REDACTED}, - "bridge": {"id": 12345, "hardware_id": REDACTED}, + "bridge": {"id": 67890, "hardware_id": REDACTED}, "last_bridge_hardware_id": REDACTED, - "name": "Bathroom Sensor", + "name": "Sensor 1", "location_id": 123456, "system_id": 12345, "hardware_id": REDACTED, - "firmware_version": "1.1.2", "hardware_revision": 5, + "firmware_version": "1.1.2", "device_key": REDACTED, "encryption_key": True, - "installed_at": "2019-04-30T01:57:34.443Z", - "calibrated_at": "2019-04-30T01:57:35.651Z", - "last_reported_at": "2019-04-30T02:20:04.821Z", + "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", "missing_at": None, - "updated_at": "2019-04-30T01:57:36.129Z", - "created_at": "2019-04-30T01:56:45.932Z", - "signal_strength": 5, - "links": {"location": 123456}, - "lqi": 0, - "rssi": -46, + "updated_at": "2023-03-28T13:33:33.801000+00:00", + "created_at": "2019-06-28T22:12:20.256000+00:00", + "signal_strength": 4, + "firmware": {"status": "valid"}, "surface_type": None, - }, - "132462": { - "id": 132462, - "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "user": {"id": 12345, "email": REDACTED}, - "bridge": {"id": 12345, "hardware_id": REDACTED}, - "last_bridge_hardware_id": REDACTED, - "name": "Living Room Sensor", - "location_id": 123456, - "system_id": 12345, - "hardware_id": REDACTED, - "firmware_version": "1.1.2", - "hardware_revision": 5, - "device_key": REDACTED, - "encryption_key": True, - "installed_at": "2019-04-30T01:45:56.169Z", - "calibrated_at": "2019-04-30T01:46:06.256Z", - "last_reported_at": "2019-04-30T02:20:04.829Z", - "missing_at": None, - "updated_at": "2019-04-30T01:46:07.717Z", - "created_at": "2019-04-30T01:45:14.148Z", - "signal_strength": 5, - "links": {"location": 123456}, - "lqi": 0, - "rssi": -30, - "surface_type": None, - }, - }, - "tasks": { - "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "low_battery", - "sensor_data": [], - "status": { - "insights": { - "primary": { - "from_state": None, - "to_state": "high", - "data_received_at": "2020-11-17T18:40:27.024Z", - "origin": {}, - } - } - }, - "created_at": "2020-11-17T18:40:27.024Z", - "updated_at": "2020-11-17T18:40:27.033Z", - "sensor_id": 525993, - "model_version": "4.1", - "configuration": {}, - "links": {"sensor": 525993}, } - }, + ], }, }