Coverage for custom_components/supernotify/people.py: 91%
78 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-21 23:31 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-21 23:31 +0000
1import logging
2from typing import TYPE_CHECKING, Any
4from homeassistant.const import ATTR_STATE, CONF_DEVICE_ID, STATE_HOME, STATE_NOT_HOME
5from homeassistant.helpers import device_registry, entity_registry
6from homeassistant.util import slugify
8from . import (
9 ATTR_EMAIL,
10 ATTR_MOBILE_APP_ID,
11 ATTR_PHONE,
12 ATTR_USER_ID,
13 CONF_ALIAS,
14 CONF_DATA,
15 CONF_DELIVERY,
16 CONF_DEVICE_NAME,
17 CONF_DEVICE_TRACKER,
18 CONF_EMAIL,
19 CONF_MANUFACTURER,
20 CONF_MOBILE_APP_ID,
21 CONF_MOBILE_DEVICES,
22 CONF_MOBILE_DISCOVERY,
23 CONF_MODEL,
24 CONF_PERSON,
25 CONF_PHONE_NUMBER,
26 CONF_TARGET,
27)
28from .common import ensure_list
29from .hass_api import HomeAssistantAPI
30from .model import Target
32if TYPE_CHECKING:
33 from homeassistant.core import State
34 from homeassistant.helpers.device_registry import DeviceRegistry
35 from homeassistant.helpers.entity_registry import EntityRegistry
37_LOGGER = logging.getLogger(__name__)
40class PeopleRegistry:
41 def __init__(self, recipients: list[dict[str, Any]], hass_api: HomeAssistantAPI) -> None:
42 self.hass_api = hass_api
43 self.people: dict[str, dict[str, Any]] = {}
44 self._recipients: list[dict[str, Any]] = ensure_list(recipients)
45 self.entity_registry = entity_registry
46 self.device_registry = device_registry
48 def initialize(self) -> None:
49 for r in self._recipients:
50 if CONF_PERSON not in r or not r[CONF_PERSON]:
51 _LOGGER.warning("SUPERNOTIFY Skipping invalid recipient with no 'person' key:%s", r)
52 continue
54 if r.get(CONF_TARGET):
55 r[CONF_TARGET] = Target(r[CONF_TARGET], target_data=r.get(CONF_DATA))
56 else:
57 r[CONF_TARGET] = Target()
58 if r.get(CONF_EMAIL):
59 r[CONF_TARGET].extend(ATTR_EMAIL, r[CONF_EMAIL])
60 if r.get(CONF_PHONE_NUMBER):
61 r[CONF_TARGET].extend(ATTR_PHONE, r[CONF_PHONE_NUMBER])
63 if r.get(CONF_MOBILE_DISCOVERY):
64 r[CONF_MOBILE_DEVICES].extend(self.mobile_devices_for_person(r[CONF_PERSON]))
65 if r.get(CONF_MOBILE_DEVICES):
66 _LOGGER.info("SUPERNOTIFY Auto configured %s for mobile devices %s", r[CONF_PERSON], r[CONF_MOBILE_DEVICES])
67 else:
68 _LOGGER.warning("SUPERNOTIFY Unable to find mobile devices for %s", r[CONF_PERSON])
69 if r.get(CONF_MOBILE_DEVICES):
70 r[CONF_TARGET].extend(ATTR_MOBILE_APP_ID, [d[CONF_MOBILE_APP_ID] for d in r[CONF_MOBILE_DEVICES]])
72 state: State | None = self.hass_api.get_state(r[CONF_PERSON])
73 if state is not None:
74 r[ATTR_USER_ID] = state.attributes.get(ATTR_USER_ID)
76 # TODO: replace dicts with typed classes and remove inconsistency of Target for target and {} for delivery
77 self.people[r[CONF_PERSON]] = {
78 k: v
79 for k, v in r.items()
80 if k
81 in (
82 CONF_PERSON,
83 CONF_ALIAS,
84 CONF_TARGET,
85 CONF_EMAIL,
86 CONF_TARGET,
87 CONF_PHONE_NUMBER,
88 ATTR_USER_ID,
89 CONF_MOBILE_DISCOVERY,
90 CONF_MOBILE_DEVICES,
91 CONF_DELIVERY,
92 )
93 }
95 def refresh_tracker_state(self) -> None:
96 for person, person_config in self.people.items():
97 # TODO: possibly rate limit this
98 try:
99 tracker: State | None = self.hass_api.get_state(person)
100 if tracker is None:
101 person_config[ATTR_STATE] = None
102 else:
103 person_config[ATTR_STATE] = tracker.state
104 except Exception as e:
105 _LOGGER.warning("SUPERNOTIFY Unable to determine occupied status for %s: %s", person, e)
107 def determine_occupancy(self) -> dict[str, list[dict[str, Any]]]:
108 results: dict[str, list[dict[str, Any]]] = {STATE_HOME: [], STATE_NOT_HOME: []}
109 self.refresh_tracker_state()
110 for person_config in self.people.values():
111 if person_config.get(ATTR_STATE) in (None, STATE_HOME):
112 # default to at home if unknown tracker
113 results[STATE_HOME].append(person_config)
114 else:
115 results[STATE_NOT_HOME].append(person_config)
116 return results
118 def mobile_devices_for_person(self, person_entity_id: str, validate_targets: bool = False) -> list[dict[str, Any]]:
119 """Auto detect mobile_app targets for a person.
121 Targets not currently validated as async registration may not be complete at this stage
123 Args:
124 ----
125 person_entity_id (str): _description_
126 validate_targets (bool, optional): _description_. Defaults to False.
128 Returns:
129 -------
130 list: mobile target actions for this person
132 """
133 mobile_devices = []
134 person_state = self.hass_api.get_state(person_entity_id)
135 if not person_state:
136 _LOGGER.warning("SUPERNOTIFY Unable to resolve %s", person_entity_id)
137 else:
138 ent_reg: EntityRegistry | None = self.hass_api.entity_registry()
139 dev_reg: DeviceRegistry | None = self.hass_api.device_registry()
140 if not ent_reg or not dev_reg:
141 _LOGGER.warning("SUPERNOTIFY Unable to access entity or device registries for %s", person_entity_id)
142 else:
143 for d_t in person_state.attributes.get("device_trackers", ()):
144 entity = ent_reg.async_get(d_t)
145 if entity and entity.platform == "mobile_app" and entity.device_id:
146 device = dev_reg.async_get(entity.device_id)
147 if not device:
148 _LOGGER.warning("SUPERNOTIFY Unable to find device %s", entity.device_id)
149 else:
150 mobile_app_id = f"mobile_app_{slugify(device.name)}"
151 if validate_targets and not self.hass_api.has_service("notify", mobile_app_id):
152 _LOGGER.warning("SUPERNOTIFY Unable to find notify action <%s>", mobile_app_id)
153 else:
154 mobile_devices.append({
155 CONF_MANUFACTURER: device.manufacturer,
156 CONF_MODEL: device.model,
157 CONF_MOBILE_APP_ID: mobile_app_id,
158 CONF_DEVICE_TRACKER: d_t,
159 CONF_DEVICE_ID: device.id,
160 CONF_DEVICE_NAME: device.name,
161 # CONF_DEVICE_LABELS: device.labels,
162 })
163 else:
164 _LOGGER.debug("SUPERNOTIFY Ignoring device tracker %s", d_t)
166 return mobile_devices