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 import asyncio
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
import logging
import traceback
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
from aionotion import async_get_client 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.errors import InvalidCredentialsError, NotionError
from aionotion.sensor.models import ( from aionotion.listener.models import Listener, ListenerKind
Listener, from aionotion.sensor.models import Sensor
ListenerAllResponse, from aionotion.user.models import UserPreferences
ListenerKind,
Sensor,
SensorAllResponse,
)
from aionotion.user.models import UserPreferences, UserPreferencesResponse
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
@ -112,36 +105,35 @@ class NotionData:
# Define a user preferences response object: # Define a user preferences response object:
user_preferences: UserPreferences | None = field(default=None) user_preferences: UserPreferences | None = field(default=None)
def update_data_from_response( def update_bridges(self, bridges: list[Bridge]) -> None:
self, """Update the bridges."""
response: BridgeAllResponse for bridge in bridges:
| ListenerAllResponse # If a new bridge is discovered, register it:
| SensorAllResponse if bridge.id not in self.bridges:
| UserPreferencesResponse, _async_register_new_bridge(self.hass, self.entry, bridge)
) -> None: self.bridges[bridge.id] = bridge
"""Update data from an aionotion response."""
if isinstance(response, BridgeAllResponse): def update_listeners(self, listeners: list[Listener]) -> None:
for bridge in response.bridges: """Update the listeners."""
# If a new bridge is discovered, register it: self.listeners = {listener.id: listener for listener in listeners}
if bridge.id not in self.bridges:
_async_register_new_bridge(self.hass, self.entry, bridge) def update_sensors(self, sensors: list[Sensor]) -> None:
self.bridges[bridge.id] = bridge """Update the sensors."""
elif isinstance(response, ListenerAllResponse): self.sensors = {sensor.uuid: sensor for sensor in sensors}
self.listeners = {listener.id: listener for listener in response.listeners}
elif isinstance(response, SensorAllResponse): def update_user_preferences(self, user_preferences: UserPreferences) -> None:
self.sensors = {sensor.uuid: sensor for sensor in response.sensors} """Update the user preferences."""
elif isinstance(response, UserPreferencesResponse): self.user_preferences = user_preferences
self.user_preferences = response.user_preferences
def asdict(self) -> dict[str, Any]: def asdict(self) -> dict[str, Any]:
"""Represent this dataclass (and its Pydantic contents) as a dict.""" """Represent this dataclass (and its Pydantic contents) as a dict."""
data: dict[str, Any] = { data: dict[str, Any] = {
DATA_BRIDGES: [bridge.dict() for bridge in self.bridges.values()], DATA_BRIDGES: [item.to_dict() for item in self.bridges.values()],
DATA_LISTENERS: [listener.dict() for listener in self.listeners.values()], DATA_LISTENERS: [item.to_dict() for item in self.listeners.values()],
DATA_SENSORS: [sensor.dict() for sensor in self.sensors.values()], DATA_SENSORS: [item.to_dict() for item in self.sensors.values()],
} }
if self.user_preferences: if self.user_preferences:
data[DATA_USER_PREFERENCES] = self.user_preferences.dict() data[DATA_USER_PREFERENCES] = self.user_preferences.to_dict()
return data return data
@ -156,7 +148,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
client = await async_get_client( 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: except InvalidCredentialsError as err:
raise ConfigEntryAuthFailed("Invalid username and/or password") from 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: async def async_update() -> NotionData:
"""Get the latest data from the Notion API.""" """Get the latest data from the Notion API."""
data = NotionData(hass=hass, entry=entry) 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) try:
for attr, result in zip(tasks, results): 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): if isinstance(result, InvalidCredentialsError):
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(
"Invalid username and/or password" "Invalid username and/or password"
) from result ) from result
if isinstance(result, NotionError): if isinstance(result, NotionError):
raise UpdateFailed( raise UpdateFailed(
f"There was a Notion error while updating {attr}: {result}" f"There was a Notion error while updating: {result}"
) from result ) from result
if isinstance(result, Exception): if isinstance(result, Exception):
if LOGGER.isEnabledFor(logging.DEBUG): LOGGER.debug(
LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) "There was an unknown error while updating: %s",
result,
exc_info=result,
)
raise UpdateFailed( raise UpdateFailed(
f"There was an unknown error while updating {attr}: {result}" f"There was an unknown error while updating: {result}"
) from result ) from result
if isinstance(result, BaseException): if isinstance(result, BaseException):
raise result from None 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 return data
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
@ -232,7 +232,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
listener listener
for listener in coordinator.data.listeners.values() for listener in coordinator.data.listeners.values()
if listener.sensor_id == sensor.uuid 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} return {"new_unique_id": listener.id}

View File

@ -4,7 +4,7 @@ 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 aionotion.listener.models import ListenerKind
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -123,7 +123,7 @@ async def async_setup_entry(
) )
for listener_id, listener in coordinator.data.listeners.items() for listener_id, listener in coordinator.data.listeners.items()
for description in BINARY_SENSOR_DESCRIPTIONS 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]) and (sensor := coordinator.data.sensors[listener.sensor_id])
] ]
) )
@ -138,6 +138,6 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity):
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
if not self.listener.insights.primary.value: 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 False
return self.listener.insights.primary.value == self.entity_description.on_state return self.listener.insights.primary.value == self.entity_description.on_state

View File

@ -38,7 +38,9 @@ async def async_validate_credentials(
errors = {} errors = {}
try: try:
await async_get_client(username, password, session=session) await async_get_client(
username, password, session=session, use_legacy_auth=True
)
except InvalidCredentialsError: except InvalidCredentialsError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except NotionError as err: except NotionError as err:

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==2023.05.5"] "requirements": ["aionotion==2024.02.0"]
} }

View File

@ -1,7 +1,7 @@
"""Define Notion model mixins.""" """Define Notion model mixins."""
from dataclasses import dataclass from dataclasses import dataclass
from aionotion.sensor.models import ListenerKind from aionotion.listener.models import ListenerKind
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)

View File

@ -1,7 +1,7 @@
"""Support for Notion sensors.""" """Support for Notion sensors."""
from dataclasses import dataclass from dataclasses import dataclass
from aionotion.sensor.models import ListenerKind from aionotion.listener.models import ListenerKind
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -59,7 +59,7 @@ async def async_setup_entry(
) )
for listener_id, listener in coordinator.data.listeners.items() for listener_id, listener in coordinator.data.listeners.items()
for description in SENSOR_DESCRIPTIONS 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]) and (sensor := coordinator.data.sensors[listener.sensor_id])
] ]
) )
@ -71,7 +71,7 @@ class NotionSensor(NotionEntity, SensorEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor.""" """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: if not self.coordinator.data.user_preferences:
return None return None
if self.coordinator.data.user_preferences.celsius_enabled: if self.coordinator.data.user_preferences.celsius_enabled:
@ -84,7 +84,7 @@ class NotionSensor(NotionEntity, SensorEntity):
"""Return the value reported by the sensor.""" """Return the value reported by the sensor."""
if not self.listener.status_localized: if not self.listener.status_localized:
return None 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. # The Notion API only returns a localized string for temperature (e.g.
# "70°"); we simply remove the degree symbol: # "70°"); we simply remove the degree symbol:
return self.listener.status_localized.state[:-1] return self.listener.status_localized.state[:-1]

View File

@ -309,7 +309,7 @@ aiomusiccast==0.14.8
aionanoleaf==0.2.1 aionanoleaf==0.2.1
# homeassistant.components.notion # homeassistant.components.notion
aionotion==2023.05.5 aionotion==2024.02.0
# homeassistant.components.oncue # homeassistant.components.oncue
aiooncue==0.3.5 aiooncue==0.3.5

View File

@ -282,7 +282,7 @@ aiomusiccast==0.14.8
aionanoleaf==0.2.1 aionanoleaf==0.2.1
# homeassistant.components.notion # homeassistant.components.notion
aionotion==2023.05.5 aionotion==2024.02.0
# homeassistant.components.oncue # homeassistant.components.oncue
aiooncue==0.3.5 aiooncue==0.3.5

View File

@ -3,9 +3,10 @@ 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 BridgeAllResponse from aionotion.bridge.models import Bridge
from aionotion.sensor.models import ListenerAllResponse, SensorAllResponse from aionotion.listener.models import Listener
from aionotion.user.models import UserPreferencesResponse from aionotion.sensor.models import Sensor
from aionotion.user.models import UserPreferences
import pytest import pytest
from homeassistant.components.notion import DOMAIN 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.""" """Define a fixture for an aionotion client."""
return Mock( return Mock(
bridge=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( sensor=Mock(
async_all=AsyncMock(return_value=SensorAllResponse.parse_obj(data_sensor)), async_all=AsyncMock(
async_listeners=AsyncMock( return_value=[
return_value=ListenerAllResponse.parse_obj(data_listener) Sensor.from_dict(sensor) for sensor in data_sensor["sensors"]
), ]
)
), ),
user=Mock( user=Mock(
async_preferences=AsyncMock( 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": [ "base_stations": [
{ {
"id": 12345, "id": 12345,
"name": "Bridge 1", "name": "Laundry Closet",
"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",
"mode": "home", "mode": "home",
"hardware_id": "0x0000000000000000", "hardware_id": "0x0000000000000000",
"hardware_revision": 4, "hardware_revision": 4,
@ -37,15 +13,15 @@
}, },
"missing_at": null, "missing_at": null,
"created_at": "2019-04-30T01:43:50.497Z", "created_at": "2019-04-30T01:43:50.497Z",
"updated_at": "2023-01-02T19:09:58.251Z", "updated_at": "2023-12-12T22:33:01.073Z",
"system_id": 11111, "system_id": 12345,
"firmware": { "firmware": {
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.0", "wifi_app": "3.3.0",
"silabs": "1.1.2" "silabs": "1.1.2"
}, },
"links": { "links": {
"system": 11111 "system": 12345
} }
} }
] ]

View File

@ -2,56 +2,27 @@
"listeners": [ "listeners": [
{ {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"definition_id": 4, "definition_id": 24,
"created_at": "2019-06-28T22:12:49.651Z", "created_at": "2019-06-17T03:29:45.722Z",
"type": "sensor", "type": "sensor",
"model_version": "2.1", "model_version": "1.0",
"sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": {
"trigger_value": "no_leak",
"data_received_at": "2022-03-20T08:00:29.763Z"
},
"status_localized": { "status_localized": {
"state": "No Leak", "state": "Idle",
"description": "Mar 20 at 2:00am" "description": "Jun 18 at 12:17am"
}, },
"insights": { "insights": {
"primary": { "primary": {
"origin": { "origin": {
"type": "Sensor", "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "type": "Sensor"
}, },
"value": "no_leak", "value": "idle",
"data_received_at": "2022-03-20T08:00:29.763Z" "data_received_at": "2023-06-18T06:17:00.697Z"
} }
}, },
"configuration": {}, "configuration": {},
"pro_monitoring_status": "eligible" "pro_monitoring_status": "ineligible"
},
{
"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

@ -20,12 +20,12 @@
"firmware_version": "1.1.2", "firmware_version": "1.1.2",
"device_key": "0x0000000000000000", "device_key": "0x0000000000000000",
"encryption_key": true, "encryption_key": true,
"installed_at": "2019-06-28T22:12:51.209Z", "installed_at": "2019-06-17T03:30:27.766Z",
"calibrated_at": "2023-03-07T19:51:56.838Z", "calibrated_at": "2024-01-19T00:38:15.372Z",
"last_reported_at": "2023-04-19T18:09:40.479Z", "last_reported_at": "2024-01-21T00:00:46.705Z",
"missing_at": null, "missing_at": null,
"updated_at": "2023-03-28T13:33:33.801Z", "updated_at": "2024-01-19T00:38:16.856Z",
"created_at": "2019-06-28T22:12:20.256Z", "created_at": "2019-06-17T03:29:45.506Z",
"signal_strength": 4, "signal_strength": 4,
"firmware": { "firmware": {
"status": "valid" "status": "valid"

View File

@ -33,31 +33,7 @@ async def test_entry_diagnostics(
"bridges": [ "bridges": [
{ {
"id": 12345, "id": 12345,
"name": "Bridge 1", "name": "Laundry Closet",
"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",
"mode": "home", "mode": "home",
"hardware_id": REDACTED, "hardware_id": REDACTED,
"hardware_revision": 4, "hardware_revision": 4,
@ -69,45 +45,41 @@ async def test_entry_diagnostics(
}, },
"missing_at": None, "missing_at": None,
"created_at": "2019-04-30T01:43:50.497000+00:00", "created_at": "2019-04-30T01:43:50.497000+00:00",
"updated_at": "2023-01-02T19:09:58.251000+00:00", "updated_at": "2023-12-12T22:33:01.073000+00:00",
"system_id": 11111, "system_id": 12345,
"firmware": { "firmware": {
"wifi": "0.121.0", "wifi": "0.121.0",
"wifi_app": "3.3.0", "wifi_app": "3.3.0",
"silabs": "1.1.2", "silabs": "1.1.2",
"ti": None, "ti": None,
}, },
"links": {"system": 11111}, "links": {"system": 12345},
}, }
], ],
"listeners": [ "listeners": [
{ {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"listener_kind": { "definition_id": 24,
"__type": "<enum 'ListenerKind'>", "created_at": "2019-06-17T03:29:45.722000+00:00",
"repr": "<ListenerKind.SMOKE: 7>", "model_version": "1.0",
},
"created_at": "2019-07-10T22:40:48.847000+00:00",
"device_type": "sensor",
"model_version": "3.1",
"sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status_localized": {
"state": "Idle",
"description": "Jun 18 at 12:17am",
},
"insights": { "insights": {
"primary": { "primary": {
"origin": {"type": None, "id": None}, "origin": {
"value": "no_alarm", "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"data_received_at": "2019-06-28T22:12:49.516000+00:00", "type": "Sensor",
},
"value": "idle",
"data_received_at": "2023-06-18T06:17:00.697000+00:00",
} }
}, },
"configuration": {}, "configuration": {},
"pro_monitoring_status": "eligible", "pro_monitoring_status": "ineligible",
"status": { "device_type": "sensor",
"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",
},
} }
], ],
"sensors": [ "sensors": [
@ -125,12 +97,12 @@ async def test_entry_diagnostics(
"firmware_version": "1.1.2", "firmware_version": "1.1.2",
"device_key": REDACTED, "device_key": REDACTED,
"encryption_key": True, "encryption_key": True,
"installed_at": "2019-06-28T22:12:51.209000+00:00", "installed_at": "2019-06-17T03:30:27.766000+00:00",
"calibrated_at": "2023-03-07T19:51:56.838000+00:00", "calibrated_at": "2024-01-19T00:38:15.372000+00:00",
"last_reported_at": "2023-04-19T18:09:40.479000+00:00", "last_reported_at": "2024-01-21T00:00:46.705000+00:00",
"missing_at": None, "missing_at": None,
"updated_at": "2023-03-28T13:33:33.801000+00:00", "updated_at": "2024-01-19T00:38:16.856000+00:00",
"created_at": "2019-06-28T22:12:20.256000+00:00", "created_at": "2019-06-17T03:29:45.506000+00:00",
"signal_strength": 4, "signal_strength": 4,
"firmware": {"status": "valid"}, "firmware": {"status": "valid"},
"surface_type": None, "surface_type": None,