mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add a coordinator to Point (#126775)
* Add a coordinator to Point * Fix * Fix * Fix * Fix * Fix * Fix
This commit is contained in:
parent
5106548f2c
commit
9c869fa701
@ -1,7 +1,5 @@
|
|||||||
"""Support for Minut Point."""
|
"""Support for Minut Point."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -29,26 +27,18 @@ from homeassistant.helpers import (
|
|||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .const import (
|
from .const import CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, SIGNAL_WEBHOOK
|
||||||
CONF_WEBHOOK_URL,
|
from .coordinator import PointDataUpdateCoordinator
|
||||||
DOMAIN,
|
|
||||||
EVENT_RECEIVED,
|
|
||||||
POINT_DISCOVERY_NEW,
|
|
||||||
SCAN_INTERVAL,
|
|
||||||
SIGNAL_UPDATE_ENTITY,
|
|
||||||
SIGNAL_WEBHOOK,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||||
|
|
||||||
type PointConfigEntry = ConfigEntry[PointData]
|
type PointConfigEntry = ConfigEntry[PointDataUpdateCoordinator]
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -131,9 +121,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> boo
|
|||||||
|
|
||||||
point_session = PointSession(auth)
|
point_session = PointSession(auth)
|
||||||
|
|
||||||
client = MinutPointClient(hass, entry, point_session)
|
coordinator = PointDataUpdateCoordinator(hass, point_session)
|
||||||
hass.async_create_task(client.update())
|
|
||||||
entry.runtime_data = PointData(client)
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await async_setup_webhook(hass, entry, point_session)
|
await async_setup_webhook(hass, entry, point_session)
|
||||||
await hass.config_entries.async_forward_entry_setups(
|
await hass.config_entries.async_forward_entry_setups(
|
||||||
@ -176,7 +168,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bo
|
|||||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||||
entry, [*PLATFORMS, Platform.ALARM_CONTROL_PANEL]
|
entry, [*PLATFORMS, Platform.ALARM_CONTROL_PANEL]
|
||||||
):
|
):
|
||||||
session: PointSession = entry.runtime_data.client
|
session = entry.runtime_data.point
|
||||||
if CONF_WEBHOOK_ID in entry.data:
|
if CONF_WEBHOOK_ID in entry.data:
|
||||||
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||||
await session.remove_webhook()
|
await session.remove_webhook()
|
||||||
@ -197,87 +189,3 @@ async def handle_webhook(
|
|||||||
data["webhook_id"] = webhook_id
|
data["webhook_id"] = webhook_id
|
||||||
async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get("hook_id"))
|
async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get("hook_id"))
|
||||||
hass.bus.async_fire(EVENT_RECEIVED, data)
|
hass.bus.async_fire(EVENT_RECEIVED, data)
|
||||||
|
|
||||||
|
|
||||||
class MinutPointClient:
|
|
||||||
"""Get the latest data and update the states."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, hass: HomeAssistant, config_entry: ConfigEntry, session: PointSession
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Minut data object."""
|
|
||||||
self._known_devices: set[str] = set()
|
|
||||||
self._known_homes: set[str] = set()
|
|
||||||
self._hass = hass
|
|
||||||
self._config_entry = config_entry
|
|
||||||
self._is_available = True
|
|
||||||
self._client = session
|
|
||||||
|
|
||||||
async_track_time_interval(self._hass, self.update, SCAN_INTERVAL)
|
|
||||||
|
|
||||||
async def update(self, *args):
|
|
||||||
"""Periodically poll the cloud for current state."""
|
|
||||||
await self._sync()
|
|
||||||
|
|
||||||
async def _sync(self):
|
|
||||||
"""Update local list of devices."""
|
|
||||||
if not await self._client.update():
|
|
||||||
self._is_available = False
|
|
||||||
_LOGGER.warning("Device is unavailable")
|
|
||||||
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._is_available = True
|
|
||||||
for home_id in self._client.homes:
|
|
||||||
if home_id not in self._known_homes:
|
|
||||||
async_dispatcher_send(
|
|
||||||
self._hass,
|
|
||||||
POINT_DISCOVERY_NEW.format(Platform.ALARM_CONTROL_PANEL),
|
|
||||||
home_id,
|
|
||||||
)
|
|
||||||
self._known_homes.add(home_id)
|
|
||||||
for device in self._client.devices:
|
|
||||||
if device.device_id not in self._known_devices:
|
|
||||||
for platform in PLATFORMS:
|
|
||||||
async_dispatcher_send(
|
|
||||||
self._hass,
|
|
||||||
POINT_DISCOVERY_NEW.format(platform),
|
|
||||||
device.device_id,
|
|
||||||
)
|
|
||||||
self._known_devices.add(device.device_id)
|
|
||||||
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
|
|
||||||
|
|
||||||
def device(self, device_id):
|
|
||||||
"""Return device representation."""
|
|
||||||
return self._client.device(device_id)
|
|
||||||
|
|
||||||
def is_available(self, device_id):
|
|
||||||
"""Return device availability."""
|
|
||||||
if not self._is_available:
|
|
||||||
return False
|
|
||||||
return device_id in self._client.device_ids
|
|
||||||
|
|
||||||
async def remove_webhook(self):
|
|
||||||
"""Remove the session webhook."""
|
|
||||||
return await self._client.remove_webhook()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def homes(self):
|
|
||||||
"""Return known homes."""
|
|
||||||
return self._client.homes
|
|
||||||
|
|
||||||
async def async_alarm_disarm(self, home_id):
|
|
||||||
"""Send alarm disarm command."""
|
|
||||||
return await self._client.alarm_disarm(home_id)
|
|
||||||
|
|
||||||
async def async_alarm_arm(self, home_id):
|
|
||||||
"""Send alarm arm command."""
|
|
||||||
return await self._client.alarm_arm(home_id)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PointData:
|
|
||||||
"""Point Data."""
|
|
||||||
|
|
||||||
client: MinutPointClient
|
|
||||||
entry_lock: asyncio.Lock = asyncio.Lock()
|
|
||||||
|
@ -2,23 +2,22 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pypoint import PointSession
|
||||||
|
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
AlarmControlPanelState,
|
AlarmControlPanelState,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import MinutPointClient
|
from . import PointConfigEntry
|
||||||
from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK
|
from .const import DOMAIN as POINT_DOMAIN, SIGNAL_WEBHOOK
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,21 +31,20 @@ EVENT_MAP = {
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: PointConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Point's alarm_control_panel based on a config entry."""
|
"""Set up a Point's alarm_control_panel based on a config entry."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
async def async_discover_home(home_id):
|
def async_discover_home(home_id: str) -> None:
|
||||||
"""Discover and add a discovered home."""
|
"""Discover and add a discovered home."""
|
||||||
client = config_entry.runtime_data.client
|
async_add_entities([MinutPointAlarmControl(coordinator.point, home_id)])
|
||||||
async_add_entities([MinutPointAlarmControl(client, home_id)], True)
|
|
||||||
|
|
||||||
async_dispatcher_connect(
|
coordinator.new_home_callback = async_discover_home
|
||||||
hass,
|
|
||||||
POINT_DISCOVERY_NEW.format(ALARM_CONTROL_PANEL_DOMAIN, POINT_DOMAIN),
|
for home_id in coordinator.point.homes:
|
||||||
async_discover_home,
|
async_discover_home(home_id)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MinutPointAlarmControl(AlarmControlPanelEntity):
|
class MinutPointAlarmControl(AlarmControlPanelEntity):
|
||||||
@ -55,12 +53,11 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
|
|||||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||||
_attr_code_arm_required = False
|
_attr_code_arm_required = False
|
||||||
|
|
||||||
def __init__(self, point_client: MinutPointClient, home_id: str) -> None:
|
def __init__(self, point: PointSession, home_id: str) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._client = point_client
|
self._client = point
|
||||||
self._home_id = home_id
|
self._home_id = home_id
|
||||||
self._async_unsub_hook_dispatcher_connect: Callable[[], None] | None = None
|
self._home = point.homes[self._home_id]
|
||||||
self._home = point_client.homes[self._home_id]
|
|
||||||
|
|
||||||
self._attr_name = self._home["name"]
|
self._attr_name = self._home["name"]
|
||||||
self._attr_unique_id = f"point.{home_id}"
|
self._attr_unique_id = f"point.{home_id}"
|
||||||
@ -73,16 +70,10 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
|
|||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added to HOme Assistant."""
|
"""Call when entity is added to HOme Assistant."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect(
|
self.async_on_remove(
|
||||||
self.hass, SIGNAL_WEBHOOK, self._webhook_event
|
async_dispatcher_connect(self.hass, SIGNAL_WEBHOOK, self._webhook_event)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Disconnect dispatcher listener when removed."""
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
if self._async_unsub_hook_dispatcher_connect:
|
|
||||||
self._async_unsub_hook_dispatcher_connect()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _webhook_event(self, data, webhook):
|
def _webhook_event(self, data, webhook):
|
||||||
"""Process new event from the webhook."""
|
"""Process new event from the webhook."""
|
||||||
@ -107,12 +98,12 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
|
|||||||
|
|
||||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
status = await self._client.async_alarm_disarm(self._home_id)
|
status = await self._client.alarm_disarm(self._home_id)
|
||||||
if status:
|
if status:
|
||||||
self._home["alarm_status"] = "off"
|
self._home["alarm_status"] = "off"
|
||||||
|
|
||||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
status = await self._client.async_alarm_arm(self._home_id)
|
status = await self._client.alarm_arm(self._home_id)
|
||||||
if status:
|
if status:
|
||||||
self._home["alarm_status"] = "on"
|
self._home["alarm_status"] = "on"
|
||||||
|
@ -3,26 +3,27 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pypoint import EVENTS
|
from pypoint import EVENTS
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK
|
from . import PointConfigEntry
|
||||||
|
from .const import SIGNAL_WEBHOOK
|
||||||
|
from .coordinator import PointDataUpdateCoordinator
|
||||||
from .entity import MinutPointEntity
|
from .entity import MinutPointEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEVICES = {
|
DEVICES: dict[str, Any] = {
|
||||||
"alarm": {"icon": "mdi:alarm-bell"},
|
"alarm": {"icon": "mdi:alarm-bell"},
|
||||||
"battery": {"device_class": BinarySensorDeviceClass.BATTERY},
|
"battery": {"device_class": BinarySensorDeviceClass.BATTERY},
|
||||||
"button_press": {"icon": "mdi:gesture-tap-button"},
|
"button_press": {"icon": "mdi:gesture-tap-button"},
|
||||||
@ -42,69 +43,60 @@ DEVICES = {
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: PointConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Point's binary sensors based on a config entry."""
|
"""Set up a Point's binary sensors based on a config entry."""
|
||||||
|
|
||||||
async def async_discover_sensor(device_id):
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
def async_discover_sensor(device_id: str) -> None:
|
||||||
"""Discover and add a discovered sensor."""
|
"""Discover and add a discovered sensor."""
|
||||||
client = config_entry.runtime_data.client
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
(
|
MinutPointBinarySensor(coordinator, device_id, device_name)
|
||||||
MinutPointBinarySensor(client, device_id, device_name)
|
|
||||||
for device_name in DEVICES
|
for device_name in DEVICES
|
||||||
if device_name in EVENTS
|
if device_name in EVENTS
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_dispatcher_connect(
|
coordinator.new_device_callbacks.append(async_discover_sensor)
|
||||||
hass,
|
|
||||||
POINT_DISCOVERY_NEW.format(BINARY_SENSOR_DOMAIN, POINT_DOMAIN),
|
async_add_entities(
|
||||||
async_discover_sensor,
|
MinutPointBinarySensor(coordinator, device_id, device_name)
|
||||||
|
for device_name in DEVICES
|
||||||
|
if device_name in EVENTS
|
||||||
|
for device_id in coordinator.point.device_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity):
|
class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity):
|
||||||
"""The platform class required by Home Assistant."""
|
"""The platform class required by Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, point_client, device_id, device_name):
|
def __init__(
|
||||||
|
self, coordinator: PointDataUpdateCoordinator, device_id: str, key: str
|
||||||
|
) -> None:
|
||||||
"""Initialize the binary sensor."""
|
"""Initialize the binary sensor."""
|
||||||
super().__init__(
|
self._attr_device_class = DEVICES[key].get("device_class", key)
|
||||||
point_client,
|
super().__init__(coordinator, device_id)
|
||||||
device_id,
|
self._device_name = key
|
||||||
DEVICES[device_name].get("device_class", device_name),
|
self._events = EVENTS[key]
|
||||||
)
|
self._attr_unique_id = f"point.{device_id}-{key}"
|
||||||
self._device_name = device_name
|
self._attr_icon = DEVICES[key].get("icon")
|
||||||
self._async_unsub_hook_dispatcher_connect = None
|
|
||||||
self._events = EVENTS[device_name]
|
|
||||||
self._attr_unique_id = f"point.{device_id}-{device_name}"
|
|
||||||
self._attr_icon = DEVICES[self._device_name].get("icon")
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added to HOme Assistant."""
|
"""Call when entity is added to HOme Assistant."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect(
|
self.async_on_remove(
|
||||||
self.hass, SIGNAL_WEBHOOK, self._webhook_event
|
async_dispatcher_connect(self.hass, SIGNAL_WEBHOOK, self._webhook_event)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Disconnect dispatcher listener when removed."""
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
if self._async_unsub_hook_dispatcher_connect:
|
|
||||||
self._async_unsub_hook_dispatcher_connect()
|
|
||||||
|
|
||||||
async def _update_callback(self):
|
|
||||||
"""Update the value of the sensor."""
|
"""Update the value of the sensor."""
|
||||||
if not self.is_updated:
|
|
||||||
return
|
|
||||||
if self.device_class == BinarySensorDeviceClass.CONNECTIVITY:
|
if self.device_class == BinarySensorDeviceClass.CONNECTIVITY:
|
||||||
# connectivity is the other way around.
|
# connectivity is the other way around.
|
||||||
self._attr_is_on = self._events[0] not in self.device.ongoing_events
|
self._attr_is_on = self._events[0] not in self.device.ongoing_events
|
||||||
else:
|
else:
|
||||||
self._attr_is_on = self._events[0] in self.device.ongoing_events
|
self._attr_is_on = self._events[0] in self.device.ongoing_events
|
||||||
self.async_write_ha_state()
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _webhook_event(self, data, webhook):
|
def _webhook_event(self, data, webhook):
|
||||||
|
70
homeassistant/components/point/coordinator.py
Normal file
70
homeassistant/components/point/coordinator.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""Define a data update coordinator for Point."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pypoint import PointSession
|
||||||
|
from tempora.utc import fromtimestamp
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util.dt import parse_datetime
|
||||||
|
|
||||||
|
from .const import DOMAIN, SCAN_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||||
|
"""Class to manage fetching Point data from the API."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, point: PointSession) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
self.point = point
|
||||||
|
self.device_updates: dict[str, datetime] = {}
|
||||||
|
self._known_devices: set[str] = set()
|
||||||
|
self._known_homes: set[str] = set()
|
||||||
|
self.new_home_callback: Callable[[str], None] | None = None
|
||||||
|
self.new_device_callbacks: list[Callable[[str], None]] = []
|
||||||
|
self.data: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||||
|
if not await self.point.update():
|
||||||
|
raise UpdateFailed("Failed to fetch data from Point")
|
||||||
|
|
||||||
|
if new_homes := set(self.point.homes) - self._known_homes:
|
||||||
|
_LOGGER.debug("Found new homes: %s", new_homes)
|
||||||
|
for home_id in new_homes:
|
||||||
|
if self.new_home_callback:
|
||||||
|
self.new_home_callback(home_id)
|
||||||
|
self._known_homes.update(new_homes)
|
||||||
|
|
||||||
|
device_ids = {device.device_id for device in self.point.devices}
|
||||||
|
if new_devices := device_ids - self._known_devices:
|
||||||
|
_LOGGER.debug("Found new devices: %s", new_devices)
|
||||||
|
for device_id in new_devices:
|
||||||
|
for callback in self.new_device_callbacks:
|
||||||
|
callback(device_id)
|
||||||
|
self._known_devices.update(new_devices)
|
||||||
|
|
||||||
|
for device in self.point.devices:
|
||||||
|
last_updated = parse_datetime(device.last_update)
|
||||||
|
if (
|
||||||
|
not last_updated
|
||||||
|
or device.device_id not in self.device_updates
|
||||||
|
or self.device_updates[device.device_id] < last_updated
|
||||||
|
):
|
||||||
|
self.device_updates[device.device_id] = last_updated or fromtimestamp(0)
|
||||||
|
self.data[device.device_id] = {
|
||||||
|
k: await device.sensor(k)
|
||||||
|
for k in ("temperature", "humidity", "sound_pressure")
|
||||||
|
}
|
||||||
|
return self.data
|
@ -2,31 +2,27 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pypoint import Device, PointSession
|
||||||
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.util.dt import as_local
|
||||||
from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp
|
|
||||||
|
|
||||||
from .const import DOMAIN, SIGNAL_UPDATE_ENTITY
|
from .const import DOMAIN
|
||||||
|
from .coordinator import PointDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MinutPointEntity(Entity):
|
class MinutPointEntity(CoordinatorEntity[PointDataUpdateCoordinator]):
|
||||||
"""Base Entity used by the sensors."""
|
"""Base Entity used by the sensors."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
def __init__(self, coordinator: PointDataUpdateCoordinator, device_id: str) -> None:
|
||||||
|
|
||||||
def __init__(self, point_client, device_id, device_class) -> None:
|
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._async_unsub_dispatcher_connect = None
|
super().__init__(coordinator)
|
||||||
self._client = point_client
|
self.device_id = device_id
|
||||||
self._id = device_id
|
|
||||||
self._name = self.device.name
|
self._name = self.device.name
|
||||||
self._attr_device_class = device_class
|
|
||||||
self._updated = utc_from_timestamp(0)
|
|
||||||
self._attr_unique_id = f"point.{device_id}-{device_class}"
|
|
||||||
device = self.device.device
|
device = self.device.device
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, device["device_mac"])},
|
connections={(dr.CONNECTION_NETWORK_MAC, device["device_mac"])},
|
||||||
@ -37,59 +33,32 @@ class MinutPointEntity(Entity):
|
|||||||
sw_version=device["firmware"]["installed"],
|
sw_version=device["firmware"]["installed"],
|
||||||
via_device=(DOMAIN, device["home"]),
|
via_device=(DOMAIN, device["home"]),
|
||||||
)
|
)
|
||||||
if device_class:
|
if self.device_class:
|
||||||
self._attr_name = f"{self._name} {device_class.capitalize()}"
|
self._attr_name = f"{self._name} {self.device_class.capitalize()}"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return string representation of device."""
|
|
||||||
return f"MinutPoint {self.name}"
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
|
||||||
"""Call when entity is added to hass."""
|
|
||||||
_LOGGER.debug("Created device %s", self)
|
|
||||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
|
||||||
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback
|
|
||||||
)
|
|
||||||
await self._update_callback()
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Disconnect dispatcher listener when removed."""
|
|
||||||
if self._async_unsub_dispatcher_connect:
|
|
||||||
self._async_unsub_dispatcher_connect()
|
|
||||||
|
|
||||||
async def _update_callback(self):
|
async def _update_callback(self):
|
||||||
"""Update the value of the sensor."""
|
"""Update the value of the sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> PointSession:
|
||||||
|
"""Return the client object."""
|
||||||
|
return self.coordinator.point
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return true if device is not offline."""
|
"""Return true if device is not offline."""
|
||||||
return self._client.is_available(self.device_id)
|
return super().available and self.device_id in self.client.device_ids
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device(self):
|
def device(self) -> Device:
|
||||||
"""Return the representation of the device."""
|
"""Return the representation of the device."""
|
||||||
return self._client.device(self.device_id)
|
return self.client.device(self.device_id)
|
||||||
|
|
||||||
@property
|
|
||||||
def device_id(self):
|
|
||||||
"""Return the id of the device."""
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self):
|
||||||
"""Return status of device."""
|
"""Return status of device."""
|
||||||
attrs = self.device.device_status
|
attrs = self.device.device_status
|
||||||
attrs["last_heard_from"] = as_local(self.last_update).strftime(
|
attrs["last_heard_from"] = as_local(
|
||||||
"%Y-%m-%d %H:%M:%S"
|
self.coordinator.device_updates[self.device_id]
|
||||||
)
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@property
|
|
||||||
def is_updated(self):
|
|
||||||
"""Return true if sensor have been updated."""
|
|
||||||
return self.last_update > self._updated
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_update(self):
|
|
||||||
"""Return the last_update time for the device."""
|
|
||||||
return parse_datetime(self.device.last_update)
|
|
||||||
|
@ -5,19 +5,17 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import PERCENTAGE, UnitOfSoundPressure, UnitOfTemperature
|
from homeassistant.const import PERCENTAGE, UnitOfSoundPressure, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.dt import parse_datetime
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW
|
from . import PointConfigEntry
|
||||||
|
from .coordinator import PointDataUpdateCoordinator
|
||||||
from .entity import MinutPointEntity
|
from .entity import MinutPointEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -37,7 +35,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key="sound",
|
key="sound_pressure",
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||||
@ -47,26 +45,26 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: PointConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Point's sensors based on a config entry."""
|
"""Set up a Point's sensors based on a config entry."""
|
||||||
|
|
||||||
async def async_discover_sensor(device_id):
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
def async_discover_sensor(device_id: str) -> None:
|
||||||
"""Discover and add a discovered sensor."""
|
"""Discover and add a discovered sensor."""
|
||||||
client = config_entry.runtime_data.client
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
MinutPointSensor(coordinator, device_id, description)
|
||||||
MinutPointSensor(client, device_id, description)
|
|
||||||
for description in SENSOR_TYPES
|
for description in SENSOR_TYPES
|
||||||
],
|
|
||||||
True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_dispatcher_connect(
|
coordinator.new_device_callbacks.append(async_discover_sensor)
|
||||||
hass,
|
|
||||||
POINT_DISCOVERY_NEW.format(SENSOR_DOMAIN, POINT_DOMAIN),
|
async_add_entities(
|
||||||
async_discover_sensor,
|
MinutPointSensor(coordinator, device_id, description)
|
||||||
|
for device_id in coordinator.data
|
||||||
|
for description in SENSOR_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -74,16 +72,17 @@ class MinutPointSensor(MinutPointEntity, SensorEntity):
|
|||||||
"""The platform class required by Home Assistant."""
|
"""The platform class required by Home Assistant."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, point_client, device_id, description: SensorEntityDescription
|
self,
|
||||||
|
coordinator: PointDataUpdateCoordinator,
|
||||||
|
device_id: str,
|
||||||
|
description: SensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(point_client, device_id, description.device_class)
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
super().__init__(coordinator, device_id)
|
||||||
|
self._attr_unique_id = f"point.{device_id}-{description.key}"
|
||||||
|
|
||||||
async def _update_callback(self):
|
@property
|
||||||
"""Update the value of the sensor."""
|
def native_value(self) -> StateType:
|
||||||
_LOGGER.debug("Update sensor value for %s", self)
|
"""Return the state of the sensor."""
|
||||||
if self.is_updated:
|
return self.coordinator.data[self.device_id].get(self.entity_description.key)
|
||||||
self._attr_native_value = await self.device.sensor(self.device_class)
|
|
||||||
self._updated = parse_datetime(self.device.last_update)
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user