From 058393ef3a83282648daf7a6bf274874048ab0e1 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 24 Apr 2024 09:10:42 -0500 Subject: [PATCH] Move unavailable status to separate binary sensor (#27) Also, workaround bug in device tracker component & entity registry that can cause entities to not appear at restart. --- custom_components/google_maps/__init__.py | 33 +++++++++-- .../google_maps/binary_sensor.py | 56 +++++++++++++++++++ .../google_maps/device_tracker.py | 5 ++ .../google_maps/gm_loc_sharing.py | 2 +- custom_components/google_maps/helpers.py | 2 + custom_components/google_maps/manifest.json | 6 +- 6 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 custom_components/google_maps/binary_sensor.py diff --git a/custom_components/google_maps/__init__.py b/custom_components/google_maps/__init__.py index c96b054..ed5b72d 100644 --- a/custom_components/google_maps/__init__.py +++ b/custom_components/google_maps/__init__.py @@ -7,16 +7,40 @@ from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.typing import ConfigType from .const import CONF_COOKIES_FILE, CONF_CREATE_ACCT_ENTITY, DOMAIN, NAME_PREFIX from .coordinator import GMDataUpdateCoordinator, GMIntegData from .helpers import ConfigID, ConfigUniqueIDs, cookies_file_path _LOGGER = logging.getLogger(__name__) -_PLATFORMS = [Platform.DEVICE_TRACKER] +_PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] + + +async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: + """Set up integration.""" + hass.data[DOMAIN] = GMIntegData(ConfigUniqueIDs(hass)) + ent_reg = er.async_get(hass) + + async def device_work_around(_: Event) -> None: + """Work around for device tracker component deleting devices. + + The device tracker component level code, at startup, deletes devices that are + associated only with device_tracker entities. Not only that, it will delete + those device_tracker entities from the entity registry as well. So, when HA + shuts down, remove references to devices from our device_tracker entity registry + entries. They'll get set back up automatically the next time our config is + loaded (i.e., setup.) + """ + for c_entry in hass.config_entries.async_entries(DOMAIN): + for r_entry in er.async_entries_for_config_entry(ent_reg, c_entry.entry_id): + ent_reg.async_update_entity(r_entry.entity_id, device_id=None) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device_work_around) + return True async def entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -55,8 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] create_acct_entity = entry.options[CONF_CREATE_ACCT_ENTITY] - if not (gmi_data := cast(GMIntegData | None, hass.data.get(DOMAIN))): - hass.data[DOMAIN] = gmi_data = GMIntegData(ConfigUniqueIDs(hass)) + gmi_data = cast(GMIntegData, hass.data.get(DOMAIN)) # For "account person", unique ID is username (which is also returned in person.id.) ent_reg = er.async_get(hass) diff --git a/custom_components/google_maps/binary_sensor.py b/custom_components/google_maps/binary_sensor.py new file mode 100644 index 0000000..5066d53 --- /dev/null +++ b/custom_components/google_maps/binary_sensor.py @@ -0,0 +1,56 @@ +"""Google Maps binary sensor.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION, DOMAIN, NAME_PREFIX +from .coordinator import GMDataUpdateCoordinator, GMIntegData +from .helpers import ConfigID + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the binary sensor platform.""" + cid = cast(ConfigID, entry.entry_id) + gmi_data = cast(GMIntegData, hass.data[DOMAIN]) + coordinator = gmi_data.coordinators[cid] + + async_add_entities([GoogleMapsBinarySensor(coordinator)]) + + +class GoogleMapsBinarySensor( + CoordinatorEntity[GMDataUpdateCoordinator], BinarySensorEntity +): + """Google Maps Binary Sensor.""" + + _attr_attribution = ATTRIBUTION + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + + def __init__(self, coordinator: GMDataUpdateCoordinator) -> None: + """Initialize Google Maps Binary Sensor.""" + super().__init__(coordinator) + entry = coordinator.config_entry + self._attr_unique_id = entry.entry_id + self._attr_name = f"{NAME_PREFIX} {entry.title} online" + self._attr_is_on = super().available + + @property + def available(self) -> bool: + """Return if entity is available.""" + return True + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = super().available + super()._handle_coordinator_update() diff --git a/custom_components/google_maps/device_tracker.py b/custom_components/google_maps/device_tracker.py index a30b721..0cf728a 100644 --- a/custom_components/google_maps/device_tracker.py +++ b/custom_components/google_maps/device_tracker.py @@ -294,6 +294,11 @@ def entity_picture(self) -> str | None: return None return self._misc.entity_picture + @property + def available(self) -> bool: + """Return if entity is available.""" + return True + @property def force_update(self) -> bool: """Return True if state updates should be forced.""" diff --git a/custom_components/google_maps/gm_loc_sharing.py b/custom_components/google_maps/gm_loc_sharing.py index 9dccd84..96c8291 100644 --- a/custom_components/google_maps/gm_loc_sharing.py +++ b/custom_components/google_maps/gm_loc_sharing.py @@ -198,7 +198,7 @@ def cookies_expiration(self) -> datetime | None: expirations: list[int] = [] for name in _VALID_COOKIE_NAMES: if (data := cookie_data.get(name)) and (expiration := data[0]): - expirations.append(expiration) + expirations.append(expiration) # noqa: PERF401 if not expirations: return None return datetime.fromtimestamp(min(expirations)).astimezone() diff --git a/custom_components/google_maps/helpers.py b/custom_components/google_maps/helpers.py index f01e216..13275c4 100644 --- a/custom_components/google_maps/helpers.py +++ b/custom_components/google_maps/helpers.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any, NewType, Self, cast +from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.restore_state import ExtraStoredData @@ -185,6 +186,7 @@ def __init__(self, hass: HomeAssistant) -> None: cfg_uids = { cast(UniqueID, ent.unique_id) for ent in er.async_entries_for_config_entry(ent_reg, cid) + if ent.domain == DT_DOMAIN } self._all_uids.update(cfg_uids) self._cfg_uids[cid] = cfg_uids diff --git a/custom_components/google_maps/manifest.json b/custom_components/google_maps/manifest.json index 18ad467..f937647 100644 --- a/custom_components/google_maps/manifest.json +++ b/custom_components/google_maps/manifest.json @@ -4,10 +4,10 @@ "codeowners": ["@pnbruckner"], "config_flow": true, "dependencies": ["file_upload"], - "documentation": "https://github.com/pnbruckner/ha-google-maps/blob/1.3.1/README.md", + "documentation": "https://github.com/pnbruckner/ha-google-maps/blob/1.3.2b3/README.md", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/pnbruckner/ha-google-maps/issues", "loggers": [], - "requirements": [], - "version": "1.3.1" + "requirements": ["locationsharinglib==5.0.1"], + "version": "1.3.2b3" }