Skip to content

Commit

Permalink
Add reload service & misc improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
pnbruckner committed Jan 10, 2024
1 parent 592fd33 commit 8daf7d8
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 62 deletions.
77 changes: 46 additions & 31 deletions custom_components/composite/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
"""Composite Device Tracker."""
from __future__ import annotations

import asyncio
from collections.abc import Coroutine
import logging
from typing import Any, cast

import voluptuous as vol

from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_ID,
CONF_NAME,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify

from .config_flow import split_conf
from .const import (
CONF_ALL_STATES,
CONF_DEFAULT_OPTIONS,
Expand Down Expand Up @@ -159,39 +168,45 @@ def _defaults(config: dict) -> dict:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up composite integration."""

# Get all existing composite config entries.
cfg_entries = {
cast(str, entry.data[CONF_ID]): entry
for entry in hass.config_entries.async_entries(DOMAIN)
}

# For each tracker config, see if it conflicts with a known_devices.yaml entry.
# If not, update the config entry if one already exists for it in case the config
# has changed, or create a new config entry if one did not already exist.
tracker_configs: list[dict[str, Any]] = config[DOMAIN][CONF_TRACKERS]
tracker_ids: set[str] = set()
for conf in tracker_configs:
obj_id: str = conf[CONF_ID]
tracker_ids.add(obj_id)

if obj_id in cfg_entries:
hass.config_entries.async_update_entry(
cfg_entries[obj_id], **split_conf(conf) # type: ignore[arg-type]
async def process_config(config: ConfigType | None) -> None:
"""Process Composite config."""
tracker_configs = cast(
list[dict[str, Any]], (config or {}).get(DOMAIN, {}).get(CONF_TRACKERS, [])
)
tracker_ids = [conf[CONF_ID] for conf in tracker_configs]
tasks: list[Coroutine[Any, Any, Any]] = []

for entry in hass.config_entries.async_entries(DOMAIN):
if (
entry.source != SOURCE_IMPORT
or (obj_id := entry.data[CONF_ID]) in tracker_ids
):
continue
_LOGGER.debug(
"Removing %s (%s) because it is no longer in YAML configuration",
entry.data[CONF_NAME],
f"{DT_DOMAIN}.{obj_id}",
)
else:
hass.async_create_task(
tasks.append(hass.config_entries.async_remove(entry.entry_id))

for conf in tracker_configs:
tasks.append(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
for obj_id, entry in cfg_entries.items():
if entry.source == SOURCE_IMPORT and obj_id not in tracker_ids:
_LOGGER.warning(
"Removing %s (%s) because it is no longer in YAML configuration",
entry.data[CONF_NAME],
f"{DT_DOMAIN}.{entry.data[CONF_ID]}",
)
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))

if not tasks:
return

await asyncio.gather(*tasks)

async def reload_config(_: ServiceCall) -> None:
"""Reload configuration."""
await process_config(await async_integration_yaml_config(hass, DOMAIN))

await process_config(config)
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, reload_config)

return True

Expand Down
7 changes: 5 additions & 2 deletions custom_components/composite/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ class CompositeConfigFlow(ConfigFlow, domain=DOMAIN):

async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
"""Import config entry from configuration."""
await self.async_set_unique_id(data[CONF_ID])
self._abort_if_unique_id_configured()
if existing_entry := await self.async_set_unique_id(data[CONF_ID]):
self.hass.config_entries.async_update_entry(
existing_entry, **split_conf(data) # type: ignore[arg-type]
)
return self.async_abort(reason="already_configured")

return self.async_create_entry(
title=f"{data[CONF_NAME]} (from configuration)",
Expand Down
44 changes: 15 additions & 29 deletions custom_components/composite/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,8 @@
ATTR_LAST_SEEN = "last_seen"
ATTR_LAST_TIMESTAMP = "last_timestamp"
ATTR_LAST_ENTITY_ID = "last_entity_id"
ATTR_TIME_ZONE = "time_zone"

_RESTORE_EXTRA_ATTRS = (
ATTR_TIME_ZONE,
ATTR_ENTITY_ID,
ATTR_LAST_ENTITY_ID,
ATTR_LAST_SEEN,
Expand Down Expand Up @@ -137,7 +135,6 @@ class EntityData:
seen: datetime | None = None
source_type: str | None = None
data: Location | str | None = None
picture: str | None = None

def set_params(self, use_all_states: bool, use_picture: bool) -> None:
"""Set parameters."""
Expand All @@ -149,14 +146,12 @@ def good(
seen: datetime,
source_type: str,
data: Location | str,
picture: str | None = None,
) -> None:
"""Mark entity as good."""
self.status = EntityStatus.ACTIVE
self.seen = seen
self.source_type = source_type
self.data = data
self.picture = picture

def bad(self, message: str) -> None:
"""Mark entity as bad."""
Expand Down Expand Up @@ -253,13 +248,13 @@ async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()

await self._process_config_options()
self.async_on_remove(
cast(ConfigEntry, self.platform.config_entry).add_update_listener(
self._config_entry_updated
)
)
await self._restore_state()
await self.async_request_call(self._process_config_options())
await self.async_request_call(self._restore_state())

async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
Expand All @@ -279,6 +274,7 @@ async def _process_config_options(self) -> None:

cur_entity_ids = set(self._entities)
cfg_entity_ids = set(entity_cfgs)

del_entity_ids = cur_entity_ids - cfg_entity_ids
new_entity_ids = cfg_entity_ids - cur_entity_ids
cur_entity_ids &= cfg_entity_ids
Expand All @@ -287,14 +283,12 @@ async def _process_config_options(self) -> None:
self.extra_state_attributes
and self.extra_state_attributes[ATTR_LAST_ENTITY_ID]
)
state_cleared = False
for entity_id in del_entity_ids:
entity = self._entities.pop(entity_id)
if entity_id == last_entity_id:
self._clear_state()
if entity.use_picture:
self._attr_entity_picture = None
state_cleared = True

for entity_id in cur_entity_ids:
entity_cfg = entity_cfgs[entity_id]
Expand All @@ -308,15 +302,8 @@ async def _process_config_options(self) -> None:
entity_id, entity_cfg[CONF_ALL_STATES], entity_cfg[CONF_USE_PICTURE]
)

if state_cleared:
for entity_id in cfg_entity_ids:
await self._entity_updated(entity_id, self.hass.states.get(entity_id))
else:
for entity_id in cfg_entity_ids:
entity = self._entities[entity_id]
if entity.use_picture:
self._attr_entity_picture = entity.picture
break
for entity_id in cfg_entity_ids:
await self._entity_updated(entity_id, self.hass.states.get(entity_id))

async def state_listener(event: Event) -> None:
"""Process input entity state update."""
Expand All @@ -325,12 +312,11 @@ async def state_listener(event: Event) -> None:
)
self.async_write_ha_state()

if del_entity_ids or new_entity_ids:
if self._remove_track_states:
self._remove_track_states()
self._remove_track_states = async_track_state_change_event(
self.hass, cfg_entity_ids, state_listener
)
if self._remove_track_states:
self._remove_track_states()
self._remove_track_states = async_track_state_change_event(
self.hass, cfg_entity_ids, state_listener
)

async def _config_entry_updated(self, _: HomeAssistant, entry: ConfigEntry) -> None:
"""Run when the config entry has been updated."""
Expand Down Expand Up @@ -432,8 +418,10 @@ async def _entity_updated(self, entity_id: str, new_state: State | None) -> None
else:
source_type = new_attrs.get(ATTR_SOURCE_TYPE)

if entity.use_picture:
self._attr_entity_picture = new_attrs.get(ATTR_ENTITY_PICTURE)

state = new_state.state
picture = new_attrs.get(ATTR_ENTITY_PICTURE)

if source_type == SourceType.GPS:
# GPS coordinates and accuracy are required.
Expand All @@ -448,7 +436,7 @@ async def _entity_updated(self, entity_id: str, new_state: State | None) -> None
old_data = cast(Optional[Location], entity.data)
if last_seen == old_last_seen and new_data == old_data:
return
entity.good(last_seen, source_type, new_data, picture)
entity.good(last_seen, source_type, new_data)

if self._req_movement and old_data:
dist = distance(gps[0], gps[1], old_data.gps[0], old_data.gps[1])
Expand All @@ -469,7 +457,7 @@ async def _entity_updated(self, entity_id: str, new_state: State | None) -> None
else:
state = STATE_NOT_HOME

entity.good(last_seen, source_type, state, picture)
entity.good(last_seen, source_type, state)

if not self._use_non_gps_data(entity_id, state):
return
Expand Down Expand Up @@ -558,8 +546,6 @@ async def _entity_updated(self, entity_id: str, new_state: State | None) -> None
if charging is not None:
attrs[ATTR_BATTERY_CHARGING] = charging

if entity.use_picture:
self._attr_entity_picture = picture
self._set_state(location_name, gps, gps_accuracy, battery, attrs, source_type)

self._prev_seen = last_seen
Expand Down
1 change: 1 addition & 0 deletions custom_components/composite/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
reload: {}
9 changes: 9 additions & 0 deletions custom_components/composite/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "Composite",
"services": {
"reload": {
"name": "Reload",
"description": "Reloads Composite from the YAML-configuration."
}
}
}

0 comments on commit 8daf7d8

Please sign in to comment.