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
This commit is contained in:
Aaron Bach 2023-04-21 17:52:57 -06:00 committed by GitHub
parent 4de124cdd5
commit c6d846453d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 410 additions and 314 deletions

View file

@ -2,13 +2,17 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from dataclasses import dataclass, field, fields
from datetime import timedelta from datetime import timedelta
import logging import logging
import traceback import traceback
from typing import Any from typing import Any
from uuid import UUID
from aionotion import async_get_client from aionotion import async_get_client
from aionotion.bridge.models import Bridge
from aionotion.errors import InvalidCredentialsError, NotionError from aionotion.errors import InvalidCredentialsError, NotionError
from aionotion.sensor.models import Listener, ListenerKind, Sensor
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
@ -18,6 +22,7 @@ from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
config_validation as cv, config_validation as cv,
device_registry as dr, device_registry as dr,
entity_registry as er,
) )
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@ -26,7 +31,20 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed, 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] 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) 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Notion as a config entry.""" """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: except NotionError as err:
raise ConfigEntryNotReady("Config entry failed to load") from 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.""" """Get the latest data from the Notion API."""
data: dict[str, dict[str, Any]] = {"bridges": {}, "sensors": {}, "tasks": {}} data = NotionData()
tasks = { tasks = {
"bridges": client.bridge.async_all(), "bridges": client.bridge.async_all(),
"listeners": client.sensor.async_listeners(),
"sensors": client.sensor.async_all(), "sensors": client.sensor.async_all(),
"tasks": client.task.async_all(),
} }
results = await asyncio.gather(*tasks.values(), return_exceptions=True) 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 ) from result
for item in 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: # If a new bridge is discovered, register it:
_async_register_new_bridge(hass, item, entry) if item.id not in data.bridges:
data[attr][item["id"]] = item _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 return data
@ -102,6 +170,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator 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: <sensor_id>_<task_type>
New Format: <listener_uuid>
"""
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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@ -118,22 +216,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@callback @callback
def _async_register_new_bridge( def _async_register_new_bridge(
hass: HomeAssistant, bridge: dict, entry: ConfigEntry hass: HomeAssistant, bridge: Bridge, entry: ConfigEntry
) -> None: ) -> None:
"""Register a new bridge.""" """Register a new bridge."""
if name := bridge["name"]: if name := bridge.name:
bridge_name = name.capitalize() bridge_name = name.capitalize()
else: else:
bridge_name = bridge["id"] bridge_name = str(bridge.id)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, bridge["hardware_id"])}, identifiers={(DOMAIN, bridge.hardware_id)},
manufacturer="Silicon Labs", manufacturer="Silicon Labs",
model=bridge["hardware_revision"], model=str(bridge.hardware_revision),
name=bridge_name, name=bridge_name,
sw_version=bridge["firmware_version"]["wifi"], sw_version=bridge.firmware_version.wifi,
) )
@ -145,7 +243,7 @@ class NotionEntity(CoordinatorEntity):
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator,
task_id: str, listener_id: str,
sensor_id: str, sensor_id: str,
bridge_id: str, bridge_id: str,
system_id: str, system_id: str,
@ -154,25 +252,23 @@ class NotionEntity(CoordinatorEntity):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
bridge = self.coordinator.data["bridges"].get(bridge_id, {}) bridge = self.coordinator.data.bridges.get(bridge_id, {})
sensor = self.coordinator.data["sensors"][sensor_id] sensor = self.coordinator.data.sensors[sensor_id]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, sensor["hardware_id"])}, identifiers={(DOMAIN, sensor.hardware_id)},
manufacturer="Silicon Labs", manufacturer="Silicon Labs",
model=sensor["hardware_revision"], model=sensor.hardware_revision,
name=str(sensor["name"]).capitalize(), name=str(sensor.name).capitalize(),
sw_version=sensor["firmware_version"], sw_version=sensor.firmware_version,
via_device=(DOMAIN, bridge.get("hardware_id")), via_device=(DOMAIN, bridge.hardware_id),
) )
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._attr_unique_id = ( self._attr_unique_id = listener_id
f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}'
)
self._bridge_id = bridge_id self._bridge_id = bridge_id
self._listener_id = listener_id
self._sensor_id = sensor_id self._sensor_id = sensor_id
self._system_id = system_id self._system_id = system_id
self._task_id = task_id
self.entity_description = description self.entity_description = description
@property @property
@ -180,7 +276,7 @@ class NotionEntity(CoordinatorEntity):
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
self.coordinator.last_update_success self.coordinator.last_update_success
and self._task_id in self.coordinator.data["tasks"] and self._listener_id in self.coordinator.data.listeners
) )
@callback @callback
@ -189,27 +285,23 @@ class NotionEntity(CoordinatorEntity):
Sensors can move to other bridges based on signal strength, etc. 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 # 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), # to a bridge that doesn't exist (which can happen due to a Notion API bug),
# return immediately: # return immediately:
if ( if (
self._bridge_id == sensor["bridge"]["id"] self._bridge_id == sensor.bridge.id
or sensor["bridge"]["id"] not in self.coordinator.data["bridges"] or sensor.bridge.id not in self.coordinator.data.bridges
): ):
return return
self._bridge_id = sensor["bridge"]["id"] self._bridge_id = sensor.bridge.id
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
this_device = device_registry.async_get_device( this_device = device_registry.async_get_device({(DOMAIN, sensor.hardware_id)})
{(DOMAIN, sensor["hardware_id"])} bridge = self.coordinator.data.bridges[self._bridge_id]
) bridge_device = device_registry.async_get_device({(DOMAIN, bridge.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: if not bridge_device or not this_device:
return return
@ -226,7 +318,7 @@ class NotionEntity(CoordinatorEntity):
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Respond to a DataUpdateCoordinator update.""" """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_bridge_id()
self._async_update_from_latest_data() self._async_update_from_latest_data()

View file

@ -4,6 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal from typing import Literal
from aionotion.sensor.models import ListenerKind
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
@ -26,9 +28,9 @@ from .const import (
SENSOR_SAFE, SENSOR_SAFE,
SENSOR_SLIDING, SENSOR_SLIDING,
SENSOR_SMOKE_CO, SENSOR_SMOKE_CO,
SENSOR_WINDOW_HINGED_HORIZONTAL, SENSOR_WINDOW_HINGED,
SENSOR_WINDOW_HINGED_VERTICAL,
) )
from .model import NotionEntityDescriptionMixin
@dataclass @dataclass
@ -40,7 +42,9 @@ class NotionBinarySensorDescriptionMixin:
@dataclass @dataclass
class NotionBinarySensorDescription( class NotionBinarySensorDescription(
BinarySensorEntityDescription, NotionBinarySensorDescriptionMixin BinarySensorEntityDescription,
NotionBinarySensorDescriptionMixin,
NotionEntityDescriptionMixin,
): ):
"""Describe a Notion binary sensor.""" """Describe a Notion binary sensor."""
@ -51,24 +55,28 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Low battery", name="Low battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
listener_kind=ListenerKind.BATTERY,
on_state="critical", on_state="critical",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_DOOR, key=SENSOR_DOOR,
name="Door", name="Door",
device_class=BinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR,
listener_kind=ListenerKind.DOOR,
on_state="open", on_state="open",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_GARAGE_DOOR, key=SENSOR_GARAGE_DOOR,
name="Garage door", name="Garage door",
device_class=BinarySensorDeviceClass.GARAGE_DOOR, device_class=BinarySensorDeviceClass.GARAGE_DOOR,
listener_kind=ListenerKind.GARAGE_DOOR,
on_state="open", on_state="open",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_LEAK, key=SENSOR_LEAK,
name="Leak detector", name="Leak detector",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
listener_kind=ListenerKind.LEAK_STATUS,
on_state="leak", on_state="leak",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
@ -76,36 +84,34 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Missing", name="Missing",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
listener_kind=ListenerKind.CONNECTED,
on_state="not_missing", on_state="not_missing",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_SAFE, key=SENSOR_SAFE,
name="Safe", name="Safe",
device_class=BinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR,
listener_kind=ListenerKind.SAFE,
on_state="open", on_state="open",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_SLIDING, key=SENSOR_SLIDING,
name="Sliding door/window", name="Sliding door/window",
device_class=BinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR,
listener_kind=ListenerKind.SLIDING_DOOR_OR_WINDOW,
on_state="open", on_state="open",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_SMOKE_CO, key=SENSOR_SMOKE_CO,
name="Smoke/Carbon monoxide detector", name="Smoke/Carbon monoxide detector",
device_class=BinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE,
listener_kind=ListenerKind.SMOKE,
on_state="alarm", on_state="alarm",
), ),
NotionBinarySensorDescription( NotionBinarySensorDescription(
key=SENSOR_WINDOW_HINGED_HORIZONTAL, key=SENSOR_WINDOW_HINGED,
name="Hinged window", name="Hinged window",
device_class=BinarySensorDeviceClass.WINDOW, listener_kind=ListenerKind.HINGED_WINDOW,
on_state="open",
),
NotionBinarySensorDescription(
key=SENSOR_WINDOW_HINGED_VERTICAL,
name="Hinged window",
device_class=BinarySensorDeviceClass.WINDOW,
on_state="open", on_state="open",
), ),
) )
@ -121,16 +127,16 @@ async def async_setup_entry(
[ [
NotionBinarySensor( NotionBinarySensor(
coordinator, coordinator,
task_id, listener_id,
sensor["id"], sensor.uuid,
sensor["bridge"]["id"], sensor.bridge.id,
sensor["system_id"], sensor.system_id,
description, 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 for description in BINARY_SENSOR_DESCRIPTIONS
if description.key == task["task_type"] if description.listener_kind == listener.listener_kind
and (sensor := coordinator.data["sensors"][task["sensor_id"]]) and (sensor := coordinator.data.sensors[listener.sensor_id])
] ]
) )
@ -143,14 +149,14 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity):
@callback @callback
def _async_update_from_latest_data(self) -> None: def _async_update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor.""" """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"]: if listener.status.trigger_value:
state = task["status"]["value"] state = listener.status.trigger_value
elif task["status"].get("insights", {}).get("primary"): elif listener.insights.primary.value:
state = task["status"]["insights"]["primary"]["to_state"] state = listener.insights.primary.value
else: else:
LOGGER.warning("Unknown data payload: %s", task["status"]) LOGGER.warning("Unknown listener structure: %s", listener)
state = None state = None
self._attr_is_on = self.entity_description.on_state == state self._attr_is_on = self.entity_description.on_state == state

View file

@ -13,5 +13,4 @@ SENSOR_SAFE = "safe"
SENSOR_SLIDING = "sliding" SENSOR_SLIDING = "sliding"
SENSOR_SMOKE_CO = "alarm" SENSOR_SMOKE_CO = "alarm"
SENSOR_TEMPERATURE = "temperature" SENSOR_TEMPERATURE = "temperature"
SENSOR_WINDOW_HINGED_HORIZONTAL = "window_hinged_horizontal" SENSOR_WINDOW_HINGED = "window_hinged"
SENSOR_WINDOW_HINGED_VERTICAL = "window_hinged_vertical"

View file

@ -35,7 +35,10 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return { return async_redact_data(
"entry": async_redact_data(entry.as_dict(), TO_REDACT), {
"data": async_redact_data(coordinator.data, TO_REDACT), "entry": entry.as_dict(),
} "data": coordinator.data.asdict(),
},
TO_REDACT,
)

View file

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aionotion"], "loggers": ["aionotion"],
"requirements": ["aionotion==3.0.2"] "requirements": ["aionotion==2023.04.2"]
} }

View file

@ -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

View file

@ -1,4 +1,8 @@
"""Support for Notion sensors.""" """Support for Notion sensors."""
from dataclasses import dataclass
from aionotion.sensor.models import ListenerKind
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -12,14 +16,22 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import NotionEntity from . import NotionEntity
from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE
from .model import NotionEntityDescriptionMixin
@dataclass
class NotionSensorDescription(SensorEntityDescription, NotionEntityDescriptionMixin):
"""Describe a Notion sensor."""
SENSOR_DESCRIPTIONS = ( SENSOR_DESCRIPTIONS = (
SensorEntityDescription( NotionSensorDescription(
key=SENSOR_TEMPERATURE, key=SENSOR_TEMPERATURE,
name="Temperature", name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
listener_kind=ListenerKind.TEMPERATURE,
), ),
) )
@ -34,16 +46,16 @@ async def async_setup_entry(
[ [
NotionSensor( NotionSensor(
coordinator, coordinator,
task_id, listener_id,
sensor["id"], sensor.uuid,
sensor["bridge"]["id"], sensor.bridge.id,
sensor["system_id"], sensor.system_id,
description, description,
) )
for task_id, task in coordinator.data["tasks"].items() for listener_id, listener in coordinator.data.listeners.items()
for description in SENSOR_DESCRIPTIONS for description in SENSOR_DESCRIPTIONS
if description.key == task["task_type"] if description.listener_kind == listener.listener_kind
and (sensor := coordinator.data["sensors"][task["sensor_id"]]) and (sensor := coordinator.data.sensors[listener.sensor_id])
] ]
) )
@ -54,13 +66,12 @@ class NotionSensor(NotionEntity, SensorEntity):
@callback @callback
def _async_update_from_latest_data(self) -> None: def _async_update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor.""" """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: if listener.listener_kind == ListenerKind.TEMPERATURE:
self._attr_native_value = round(float(task["status"]["value"]), 1) self._attr_native_value = round(listener.status.temperature, 1)
else: else:
LOGGER.error( LOGGER.error(
"Unknown task type: %s: %s", "Unknown listener type for sensor %s",
self.coordinator.data["sensors"][self._sensor_id], self.coordinator.data.sensors[self._sensor_id],
task["task_type"],
) )

View file

@ -223,7 +223,7 @@ aionanoleaf==0.2.1
aionotify==0.2.0 aionotify==0.2.0
# homeassistant.components.notion # homeassistant.components.notion
aionotion==3.0.2 aionotion==2023.04.2
# homeassistant.components.oncue # homeassistant.components.oncue
aiooncue==0.3.4 aiooncue==0.3.4

View file

@ -204,7 +204,7 @@ aiomusiccast==0.14.8
aionanoleaf==0.2.1 aionanoleaf==0.2.1
# homeassistant.components.notion # homeassistant.components.notion
aionotion==3.0.2 aionotion==2023.04.2
# homeassistant.components.oncue # homeassistant.components.oncue
aiooncue==0.3.4 aiooncue==0.3.4

View file

@ -3,10 +3,13 @@ from collections.abc import Generator
import json import json
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aionotion.bridge.models import Bridge
from aionotion.sensor.models import Listener, Sensor
import pytest import pytest
from homeassistant.components.notion import DOMAIN from homeassistant.components.notion import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture
@ -24,17 +27,29 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
@pytest.fixture(name="client") @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.""" """Define a fixture for an aionotion client."""
return Mock( return Mock(
bridge=Mock(async_all=AsyncMock(return_value=data_bridge)), bridge=Mock(
sensor=Mock(async_all=AsyncMock(return_value=data_sensor)), async_all=AsyncMock(
task=Mock(async_all=AsyncMock(return_value=data_task)), 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") @pytest.fixture(name="config_entry")
def config_entry_fixture(hass, config): def config_entry_fixture(hass: HomeAssistant, config):
"""Define a config entry fixture.""" """Define a config entry fixture."""
entry = MockConfigEntry(domain=DOMAIN, unique_id=TEST_USERNAME, data=config) entry = MockConfigEntry(domain=DOMAIN, unique_id=TEST_USERNAME, data=config)
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -56,18 +71,18 @@ def data_bridge_fixture():
return json.loads(load_fixture("bridge_data.json", "notion")) 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") @pytest.fixture(name="data_sensor", scope="package")
def data_sensor_fixture(): def data_sensor_fixture():
"""Define sensor data.""" """Define sensor data."""
return json.loads(load_fixture("sensor_data.json", "notion")) 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") @pytest.fixture(name="get_client")
def get_client_fixture(client): def get_client_fixture(client):
"""Define a fixture to mock the async_get_client method.""" """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") @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.""" """Define a fixture to set up notion."""
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -1,26 +1,50 @@
[ [
{ {
"id": 12345, "id": 12345,
"name": null, "name": "Bridge 1",
"mode": "home", "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, "hardware_revision": 4,
"firmware_version": { "firmware_version": {
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.0", "wifi_app": "3.3.0",
"silabs": "1.0.1" "silabs": "1.1.2"
}, },
"missing_at": null, "missing_at": null,
"created_at": "2019-04-30T01:43:50.497Z", "created_at": "2019-04-30T01:43:50.497Z",
"updated_at": "2019-04-30T01:44:43.749Z", "updated_at": "2023-01-02T19:09:58.251Z",
"system_id": 12345, "system_id": 11111,
"firmware": { "firmware": {
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.0", "wifi_app": "3.3.0",
"silabs": "1.0.1" "silabs": "1.1.2"
}, },
"links": { "links": {
"system": 12345 "system": 11111
} }
} }
] ]

View file

@ -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"
}
]

View file

@ -7,64 +7,28 @@
"email": "user@email.com" "email": "user@email.com"
}, },
"bridge": { "bridge": {
"id": 12345, "id": 67890,
"hardware_id": "0x1234567890abcdef" "hardware_id": "0x0000000000000000"
}, },
"last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Bathroom Sensor", "name": "Sensor 1",
"location_id": 123456, "location_id": 123456,
"system_id": 12345, "system_id": 12345,
"hardware_id": "0x1234567890abcdef", "hardware_id": "0x0000000000000000",
"firmware_version": "1.1.2",
"hardware_revision": 5, "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", "firmware_version": "1.1.2",
"hardware_revision": 5, "device_key": "0x0000000000000000",
"device_key": "0x1234567890abcdef",
"encryption_key": true, "encryption_key": true,
"installed_at": "2019-04-30T01:45:56.169Z", "installed_at": "2019-06-28T22:12:51.209Z",
"calibrated_at": "2019-04-30T01:46:06.256Z", "calibrated_at": "2023-03-07T19:51:56.838Z",
"last_reported_at": "2019-04-30T02:20:04.829Z", "last_reported_at": "2023-04-19T18:09:40.479Z",
"missing_at": null, "missing_at": null,
"updated_at": "2019-04-30T01:46:07.717Z", "updated_at": "2023-03-28T13:33:33.801Z",
"created_at": "2019-04-30T01:45:14.148Z", "created_at": "2019-06-28T22:12:20.256Z",
"signal_strength": 5, "signal_strength": 4,
"links": { "firmware": {
"location": 123456 "status": "valid"
}, },
"lqi": 0,
"rssi": -30,
"surface_type": null "surface_type": null
} }
] ]

View file

@ -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
}
}
]

View file

@ -1,5 +1,6 @@
"""Test Notion diagnostics.""" """Test Notion diagnostics."""
from homeassistant.components.diagnostics import REDACTED from homeassistant.components.diagnostics import REDACTED
from homeassistant.components.notion import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
@ -17,7 +18,7 @@ async def test_entry_diagnostics(
"entry": { "entry": {
"entry_id": config_entry.entry_id, "entry_id": config_entry.entry_id,
"version": 1, "version": 1,
"domain": "notion", "domain": DOMAIN,
"title": REDACTED, "title": REDACTED,
"data": {"username": REDACTED, "password": REDACTED}, "data": {"username": REDACTED, "password": REDACTED},
"options": {}, "options": {},
@ -28,106 +29,107 @@ async def test_entry_diagnostics(
"disabled_by": None, "disabled_by": None,
}, },
"data": { "data": {
"bridges": { "bridges": [
"12345": { {
"id": 12345, "id": 12345,
"name": None, "name": "Bridge 1",
"mode": "home", "mode": "home",
"hardware_id": REDACTED, "hardware_id": REDACTED,
"hardware_revision": 4, "hardware_revision": 4,
"firmware_version": { "firmware_version": {
"silabs": "1.1.2",
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.0", "wifi_app": "3.3.0",
"silabs": "1.0.1",
}, },
"missing_at": None, "missing_at": None,
"created_at": "2019-04-30T01:43:50.497Z", "created_at": "2019-06-27T00:18:44.337000+00:00",
"updated_at": "2019-04-30T01:44:43.749Z", "updated_at": "2023-03-19T03:20:16.061000+00:00",
"system_id": 12345, "system_id": 11111,
"firmware": { "firmware": {
"silabs": "1.1.2",
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.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": "<enum 'ListenerKind'>",
"repr": "<ListenerKind.SMOKE: 7>",
},
"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": { "sensors": [
"123456": { {
"id": 123456, "id": 123456,
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {"id": 12345, "email": REDACTED}, "user": {"id": 12345, "email": REDACTED},
"bridge": {"id": 12345, "hardware_id": REDACTED}, "bridge": {"id": 67890, "hardware_id": REDACTED},
"last_bridge_hardware_id": REDACTED, "last_bridge_hardware_id": REDACTED,
"name": "Bathroom Sensor", "name": "Sensor 1",
"location_id": 123456, "location_id": 123456,
"system_id": 12345, "system_id": 12345,
"hardware_id": REDACTED, "hardware_id": REDACTED,
"firmware_version": "1.1.2",
"hardware_revision": 5, "hardware_revision": 5,
"firmware_version": "1.1.2",
"device_key": REDACTED, "device_key": REDACTED,
"encryption_key": True, "encryption_key": True,
"installed_at": "2019-04-30T01:57:34.443Z", "installed_at": "2019-06-28T22:12:51.209000+00:00",
"calibrated_at": "2019-04-30T01:57:35.651Z", "calibrated_at": "2023-03-07T19:51:56.838000+00:00",
"last_reported_at": "2019-04-30T02:20:04.821Z", "last_reported_at": "2023-04-19T18:09:40.479000+00:00",
"missing_at": None, "missing_at": None,
"updated_at": "2019-04-30T01:57:36.129Z", "updated_at": "2023-03-28T13:33:33.801000+00:00",
"created_at": "2019-04-30T01:56:45.932Z", "created_at": "2019-06-28T22:12:20.256000+00:00",
"signal_strength": 5, "signal_strength": 4,
"links": {"location": 123456}, "firmware": {"status": "valid"},
"lqi": 0,
"rssi": -46,
"surface_type": None, "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},
} }
}, ],
}, },
} }