Bump aionotion to 2024.02.0 (#109577)

This commit is contained in:
Aaron Bach 2024-02-04 14:35:08 -07:00 committed by GitHub
parent 770119c8ad
commit edc6e3e2f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 134 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]
)
)
),
)

View File

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

View File

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

View File

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

View File

@ -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": "<enum 'ListenerKind'>",
"repr": "<ListenerKind.SMOKE: 7>",
},
"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,