1 """Data update coordinator for the Proximity integration."""
3 from collections
import defaultdict
4 from dataclasses
import dataclass
6 from typing
import cast
14 CONF_UNIT_OF_MEASUREMENT,
19 EventStateChangedData,
37 CONF_TRACKED_ENTITIES,
38 DEFAULT_DIR_OF_TRAVEL,
44 _LOGGER = logging.getLogger(__name__)
46 type ProximityConfigEntry = ConfigEntry[ProximityDataUpdateCoordinator]
51 """StateChangedData class."""
54 old_state: State |
None
55 new_state: State |
None
60 """ProximityCoordinatorData class."""
62 proximity: dict[str, str | int |
None]
63 entities: dict[str, dict[str, str | int |
None]]
66 DEFAULT_PROXIMITY_DATA: dict[str, str | int |
None] = {
67 ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
68 ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
69 ATTR_NEAREST: DEFAULT_NEAREST,
74 """Proximity data update coordinator."""
76 config_entry: ProximityConfigEntry
79 self, hass: HomeAssistant, friendly_name: str, config: ConfigType
81 """Initialize the Proximity coordinator."""
82 self.ignored_zone_ids: list[str] = config[CONF_IGNORED_ZONES]
83 self.tracked_entities: list[str] = config[CONF_TRACKED_ENTITIES]
84 self.tolerance: int = config[CONF_TOLERANCE]
85 self.proximity_zone_id: str = config[CONF_ZONE]
86 self.proximity_zone_name: str = self.proximity_zone_id.split(
".")[-1]
87 self.unit_of_measurement: str = config.get(
88 CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
90 self.entity_mapping: dict[str, list[str]] = defaultdict(list)
105 """Add an tracked entity to proximity entity mapping."""
106 self.entity_mapping[tracked_entity_id].append(entity_id)
110 event: Event[EventStateChangedData],
112 """Fetch and process state change event."""
115 data[
"entity_id"], data[
"old_state"], data[
"new_state"]
120 self, event: Event[er.EventEntityRegistryUpdatedData]
122 """Fetch and process tracked entity change event."""
124 if data[
"action"] ==
"remove":
127 if data[
"action"] ==
"update" and "entity_id" in data[
"changes"]:
128 old_tracked_entity_id = data[
"old_entity_id"]
129 new_tracked_entity_id = data[
"entity_id"]
131 self.
hasshass.config_entries.async_update_entry(
135 CONF_TRACKED_ENTITIES: [
137 for tracked_entity
in (
138 *self.tracked_entities,
139 new_tracked_entity_id,
141 if tracked_entity != old_tracked_entity_id
150 latitude: float |
None,
151 longitude: float |
None,
153 if device.state.lower() == self.proximity_zone_name.lower():
155 "%s: %s in zone -> distance=0",
161 if latitude
is None or longitude
is None:
163 "%s: %s has no coordinates -> distance=None",
170 zone.attributes[ATTR_LATITUDE],
171 zone.attributes[ATTR_LONGITUDE],
177 assert distance_to_zone
is not None
178 return round(distance_to_zone)
184 old_latitude: float |
None,
185 old_longitude: float |
None,
186 new_latitude: float |
None,
187 new_longitude: float |
None,
189 if device.state.lower() == self.proximity_zone_name.lower():
191 "%s: %s in zone -> direction_of_travel=arrived",
199 or old_longitude
is None
200 or new_latitude
is None
201 or new_longitude
is None
206 zone.attributes[ATTR_LATITUDE],
207 zone.attributes[ATTR_LONGITUDE],
212 zone.attributes[ATTR_LATITUDE],
213 zone.attributes[ATTR_LONGITUDE],
219 assert old_distance
is not None
220 assert new_distance
is not None
221 distance_travelled = round(new_distance - old_distance, 1)
223 if distance_travelled < self.tolerance * -1:
226 if distance_travelled > self.tolerance:
232 """Calculate Proximity data."""
233 if (zone_state := self.
hasshass.states.get(self.proximity_zone_id))
is None:
235 "%s: zone %s does not exist -> reset",
237 self.proximity_zone_id,
241 entities_data = self.
datadatadata.entities
244 for entity_id
in self.tracked_entities:
245 if (tracked_entity_state := self.
hasshass.states.get(entity_id))
is None:
246 if entities_data.pop(entity_id,
None)
is not None:
248 "%s: %s does not exist -> remove", self.
namename, entity_id
252 if entity_id
not in entities_data:
253 _LOGGER.debug(
"%s: %s is new -> add", self.
namename, entity_id)
254 entities_data[entity_id] = {
256 ATTR_DIR_OF_TRAVEL:
None,
257 ATTR_NAME: tracked_entity_state.name,
258 ATTR_IN_IGNORED_ZONE:
False,
260 entities_data[entity_id][ATTR_IN_IGNORED_ZONE] = (
261 f
"{ZONE_DOMAIN}.{tracked_entity_state.state.lower()}"
262 in self.ignored_zone_ids
266 tracked_entity_state,
267 tracked_entity_state.attributes.get(ATTR_LATITUDE),
268 tracked_entity_state.attributes.get(ATTR_LONGITUDE),
270 if entities_data[entity_id][ATTR_DIST_TO]
is None:
272 "%s: %s has unknown distance got -> direction_of_travel=None",
276 entities_data[entity_id][ATTR_DIR_OF_TRAVEL] =
None
279 if (state_change_data := self.
state_change_datastate_change_data)
is not None and (
280 new_state := state_change_data.new_state
283 "%s: calculate direction of travel for %s",
285 state_change_data.entity_id,
288 if (old_state := state_change_data.old_state)
is not None:
289 old_lat = old_state.attributes.get(ATTR_LATITUDE)
290 old_lon = old_state.attributes.get(ATTR_LONGITUDE)
295 entities_data[state_change_data.entity_id][ATTR_DIR_OF_TRAVEL] = (
301 new_state.attributes.get(ATTR_LATITUDE),
302 new_state.attributes.get(ATTR_LONGITUDE),
307 proximity_data: dict[str, str | int |
None] = {
308 ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
309 ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
310 ATTR_NEAREST: DEFAULT_NEAREST,
312 for entity_data
in entities_data.values():
313 if (distance_to := entity_data[ATTR_DIST_TO])
is None or entity_data[
318 if isinstance((nearest_distance_to := proximity_data[ATTR_DIST_TO]), str):
319 _LOGGER.debug(
"set first entity_data: %s", entity_data)
321 ATTR_DIST_TO: distance_to,
322 ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL],
323 ATTR_NEAREST:
str(entity_data[ATTR_NAME]),
327 if cast(int, nearest_distance_to) >
int(distance_to):
328 _LOGGER.debug(
"set closer entity_data: %s", entity_data)
330 ATTR_DIST_TO: distance_to,
331 ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL],
332 ATTR_NEAREST:
str(entity_data[ATTR_NAME]),
336 if cast(int, nearest_distance_to) ==
int(distance_to):
337 _LOGGER.debug(
"set equally close entity_data: %s", entity_data)
338 proximity_data[ATTR_NEAREST] = (
339 f
"{proximity_data[ATTR_NEAREST]}, {entity_data[ATTR_NAME]!s}"
345 """Create a repair issue for a removed tracked entity."""
349 f
"tracked_entity_removed_{entity_id}",
352 severity=IssueSeverity.WARNING,
353 translation_key=
"tracked_entity_removed",
354 translation_placeholders={
"entity_id": entity_id,
"name": self.
namename},
None async_check_tracked_entity_change(self, Event[er.EventEntityRegistryUpdatedData] event)
None async_check_proximity_state_change(self, Event[EventStateChangedData] event)
None __init__(self, HomeAssistant hass, str friendly_name, ConfigType config)
None async_add_entity_mapping(self, str tracked_entity_id, str entity_id)
str|None _calc_direction_of_travel(self, State zone, State device, float|None old_latitude, float|None old_longitude, float|None new_latitude, float|None new_longitude)
None _create_removed_tracked_entity_issue(self, str entity_id)
ProximityData _async_update_data(self)
int|None _calc_distance_to_zone(self, State zone, State device, float|None latitude, float|None longitude)
None async_create_issue(HomeAssistant hass, str entry_id)
def distance(hass, *args)