Skip to content

Commit 7bbdf37

Browse files
authored
Improve handling of speed and corresponding driving state (#99)
* Maintain driving state even when not updating speed sensor When updates come too close together, speed sensor is not updated. However, if speed was previously above set driving speed threshold, still need to keep device_tracker state as driving, which was previously not the case. This update fixes that bug. * Add time to hold driving state after speed falls below configured driving speed Can help avoid undesired state changes when, e.g., slowing to go around a corner or when stopping at an intersection. * Add max time between speed sensor updates If set, will clear speed sensor state when exceeded.
1 parent 7d30f7e commit 7bbdf37

File tree

7 files changed

+252
-31
lines changed

7 files changed

+252
-31
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ composite:
7474
7575
- **default_options** (*Optional*): Defines default values for corresponding options under **trackers**.
7676
- **require_movement** (*Optional*): Default is `false`.
77+
- **max_speed_age** (*Optional*)
7778
- **driving_speed** (*Optional*)
79+
- **end_driving_delay** (*Optoinal*)
7880

7981
- **trackers**: The list of composite trackers to create. For each entry see [Tracker entries](#tracker-entries).
8082

@@ -84,7 +86,9 @@ composite:
8486
- **name**: Friendly name of composite device.
8587
- **id** (*Optional*): Object ID (i.e., part of entity ID after the dot) of composite device. If not supplied, then object ID will be generated from the `name` variable. For example, `My Name` would result in a tracker entity ID of `device_tracker.my_name`. The speed sensor's object ID will be the same as for the device tracker, but with a suffix of "`_speed`" added (e.g., `sensor.my_name_speed`.)
8688
- **require_movement** (*Optional*): `true` or `false`. If `true`, will skip update from a GPS-based tracker if it has not moved. Specifically, if circle defined by new GPS coordinates and accuracy overlaps circle defined by previous GPS coordinates and accuracy then update will be ignored.
89+
- **max_speed_age** (*Optional*): If set, defines the maximum amount of time between speed sensor updates. When this time is exceeded, speed sensor's state will be cleared (i.e., state will become `unknown` and `angle` & `direction` attributes will become null.)
8790
- **driving_speed** (*Optional*): Defines a driving speed threshold (in MPH or KPH, depending on general unit system setting.) If set, and current speed is at or above this value, and tracker is not in a zone, then the state of the tracker will be set to `driving`.
91+
- **end_driving_delay** (*Optional*): If set, defines the amount of time to wait before changing state from `driving` (i.e., Driving) back to `not_home` (i.e., Away) after speed falls below set `driving_speed`. This can prevent state changing back and forth when, e.g., slowing for a turn or stopping at a traffic light. If not set, state will change back to `not_home` immediately after speed drops below threshold. May only be used if `driving_speed` is set.
8892
- **entity_picture** (*Optional*): Specifies image to use for entity. Can be an URL or a file in "/local". Note that /local is used by the frontend to access files in `<config_path>/www` (which is typically `/config/www`.) You can specify file names with or without the "/local" prefix. If this option is used, then `use_picture` cannot be used.
8993

9094
#### Entity Dictionary
@@ -151,7 +155,10 @@ direction | Compass heading of movement direction (if moving.)
151155
composite:
152156
default_options:
153157
require_movement: true
158+
max_speed_age:
159+
minutes: 5
154160
driving_speed: 15
161+
end_driving_delay: "00:02:00"
155162
trackers:
156163
- name: Me
157164
driving_speed: 20
@@ -165,6 +172,7 @@ composite:
165172
- name: Better Half
166173
id: wife
167174
require_movement: false
175+
end_driving_delay: 30
168176
entity_picture: /local/wife.jpg
169177
entity_id: device_tracker.platform_wife
170178
```

custom_components/composite/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
from contextlib import suppress
5+
from datetime import timedelta
56
import logging
67
from pathlib import Path
78
from typing import Any, cast
@@ -18,8 +19,10 @@
1819
CONF_ALL_STATES,
1920
CONF_DEFAULT_OPTIONS,
2021
CONF_DRIVING_SPEED,
22+
CONF_END_DRIVING_DELAY,
2123
CONF_ENTITY,
2224
CONF_ENTITY_PICTURE,
25+
CONF_MAX_SPEED_AGE,
2326
CONF_REQ_MOVEMENT,
2427
CONF_TIME_AS,
2528
CONF_TRACKERS,
@@ -61,6 +64,17 @@ def _entities(entities: list[str | dict]) -> list[dict]:
6164
return result
6265

6366

67+
def _time_period_to_dict(delay: timedelta) -> dict[str, float]:
68+
"""Return timedelta as a dict."""
69+
result: dict[str, float] = {}
70+
if delay.days:
71+
result["days"] = delay.days
72+
result["hours"] = delay.seconds // 3600
73+
result["minutes"] = (delay.seconds // 60) % 60
74+
result["seconds"] = delay.seconds % 60
75+
return result
76+
77+
6478
def _entity_picture(entity_picture: str) -> str:
6579
"""Validate entity picture.
6680
@@ -128,13 +142,22 @@ def _defaults(config: dict) -> dict:
128142
unsupported_cfgs.add(CONF_TIME_AS)
129143

130144
def_req_mv = config[CONF_DEFAULT_OPTIONS][CONF_REQ_MOVEMENT]
145+
def_max_sa = config[CONF_DEFAULT_OPTIONS].get(CONF_MAX_SPEED_AGE)
131146
def_drv_sp = config[CONF_DEFAULT_OPTIONS].get(CONF_DRIVING_SPEED)
147+
def_end_dd = config[CONF_DEFAULT_OPTIONS].get(CONF_END_DRIVING_DELAY)
148+
end_dd_but_no_drv_sp = False
132149
for tracker in config[CONF_TRACKERS]:
133150
if tracker.pop(CONF_TIME_AS, None):
134151
unsupported_cfgs.add(CONF_TIME_AS)
135152
tracker[CONF_REQ_MOVEMENT] = tracker.get(CONF_REQ_MOVEMENT, def_req_mv)
153+
if CONF_MAX_SPEED_AGE not in tracker and def_max_sa is not None:
154+
tracker[CONF_MAX_SPEED_AGE] = def_max_sa
136155
if CONF_DRIVING_SPEED not in tracker and def_drv_sp is not None:
137156
tracker[CONF_DRIVING_SPEED] = def_drv_sp
157+
if CONF_END_DRIVING_DELAY not in tracker and def_end_dd is not None:
158+
tracker[CONF_END_DRIVING_DELAY] = def_end_dd
159+
if CONF_END_DRIVING_DELAY in tracker and CONF_DRIVING_SPEED not in tracker:
160+
end_dd_but_no_drv_sp = True
138161

139162
if unsupported_cfgs:
140163
_LOGGER.warning(
@@ -143,6 +166,11 @@ def _defaults(config: dict) -> dict:
143166
DOMAIN,
144167
", ".join(sorted(unsupported_cfgs)),
145168
)
169+
if end_dd_but_no_drv_sp:
170+
raise vol.Invalid(
171+
f"using {CONF_END_DRIVING_DELAY}; "
172+
f"{CONF_DRIVING_SPEED} must also be specified"
173+
)
146174

147175
del config[CONF_DEFAULT_OPTIONS]
148176
return config
@@ -165,13 +193,16 @@ def _defaults(config: dict) -> dict:
165193
vol.Length(1),
166194
_entities,
167195
)
196+
_POS_TIME_PERIOD = vol.All(cv.positive_time_period, _time_period_to_dict)
168197
_TRACKER = {
169198
vol.Required(CONF_NAME): cv.string,
170199
vol.Optional(CONF_ID): cv.slugify,
171200
vol.Required(CONF_ENTITY_ID): _ENTITIES,
172201
vol.Optional(CONF_TIME_AS): cv.string,
173202
vol.Optional(CONF_REQ_MOVEMENT): cv.boolean,
203+
vol.Optional(CONF_MAX_SPEED_AGE): _POS_TIME_PERIOD,
174204
vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float),
205+
vol.Optional(CONF_END_DRIVING_DELAY): _POS_TIME_PERIOD,
175206
vol.Optional(CONF_ENTITY_PICTURE): vol.All(cv.string, _entity_picture),
176207
}
177208
_CONFIG_SCHEMA = vol.Schema(
@@ -187,7 +218,9 @@ def _defaults(config: dict) -> dict:
187218
vol.Optional(
188219
CONF_REQ_MOVEMENT, default=DEF_REQ_MOVEMENT
189220
): cv.boolean,
221+
vol.Optional(CONF_MAX_SPEED_AGE): _POS_TIME_PERIOD,
190222
vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float),
223+
vol.Optional(CONF_END_DRIVING_DELAY): _POS_TIME_PERIOD,
191224
}
192225
),
193226
vol.Required(CONF_TRACKERS, default=list): vol.All(

custom_components/composite/config_flow.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from homeassistant.core import State, callback
3535
from homeassistant.helpers.selector import (
3636
BooleanSelector,
37+
DurationSelector,
38+
DurationSelectorConfig,
3739
EntitySelector,
3840
EntitySelectorConfig,
3941
FileSelector,
@@ -55,8 +57,10 @@
5557
ATTR_LON,
5658
CONF_ALL_STATES,
5759
CONF_DRIVING_SPEED,
60+
CONF_END_DRIVING_DELAY,
5861
CONF_ENTITY,
5962
CONF_ENTITY_PICTURE,
63+
CONF_MAX_SPEED_AGE,
6064
CONF_REQ_MOVEMENT,
6165
CONF_USE_PICTURE,
6266
DOMAIN,
@@ -78,7 +82,9 @@ def split_conf(conf: dict[str, Any]) -> dict[str, dict[str, Any]]:
7882
(
7983
CONF_ENTITY_ID,
8084
CONF_REQ_MOVEMENT,
85+
CONF_MAX_SPEED_AGE,
8186
CONF_DRIVING_SPEED,
87+
CONF_END_DRIVING_DELAY,
8288
CONF_ENTITY_PICTURE,
8389
),
8490
),
@@ -197,14 +203,21 @@ async def async_step_options(
197203

198204
if user_input is not None:
199205
self.options[CONF_REQ_MOVEMENT] = user_input[CONF_REQ_MOVEMENT]
206+
if CONF_MAX_SPEED_AGE in user_input:
207+
self.options[CONF_MAX_SPEED_AGE] = user_input[CONF_MAX_SPEED_AGE]
208+
elif CONF_MAX_SPEED_AGE in self.options:
209+
del self.options[CONF_MAX_SPEED_AGE]
200210
if CONF_DRIVING_SPEED in user_input:
201211
self.options[CONF_DRIVING_SPEED] = SpeedConverter.convert(
202212
user_input[CONF_DRIVING_SPEED],
203213
self._speed_uom,
204214
UnitOfSpeed.METERS_PER_SECOND,
205215
)
206-
elif CONF_DRIVING_SPEED in self.options:
207-
del self.options[CONF_DRIVING_SPEED]
216+
else:
217+
if CONF_DRIVING_SPEED in self.options:
218+
del self.options[CONF_DRIVING_SPEED]
219+
if CONF_END_DRIVING_DELAY in self.options:
220+
del self.options[CONF_END_DRIVING_DELAY]
208221
prv_cfgs = {
209222
cfg[CONF_ENTITY]: cfg for cfg in self.options.get(CONF_ENTITY_ID, [])
210223
}
@@ -221,6 +234,8 @@ async def async_step_options(
221234
]
222235
self.options[CONF_ENTITY_ID] = new_cfgs
223236
if new_cfgs:
237+
if CONF_DRIVING_SPEED in self.options:
238+
return await self.async_step_end_driving_delay()
224239
return await self.async_step_ep_menu()
225240
errors[CONF_ENTITY_ID] = "at_least_one_entity"
226241

@@ -248,6 +263,11 @@ def entity_filter(state: State) -> bool:
248263
)
249264
),
250265
vol.Required(CONF_REQ_MOVEMENT): BooleanSelector(),
266+
vol.Optional(CONF_MAX_SPEED_AGE): DurationSelector(
267+
DurationSelectorConfig(
268+
enable_day=False, enable_millisecond=False, allow_negative=False
269+
)
270+
),
251271
vol.Optional(CONF_DRIVING_SPEED): NumberSelector(
252272
NumberSelectorConfig(
253273
unit_of_measurement=self._speed_uom,
@@ -261,6 +281,8 @@ def entity_filter(state: State) -> bool:
261281
CONF_ENTITY_ID: self._entity_ids,
262282
CONF_REQ_MOVEMENT: self.options[CONF_REQ_MOVEMENT],
263283
}
284+
if CONF_MAX_SPEED_AGE in self.options:
285+
suggested_values[CONF_MAX_SPEED_AGE] = self.options[CONF_MAX_SPEED_AGE]
264286
if CONF_DRIVING_SPEED in self.options:
265287
suggested_values[CONF_DRIVING_SPEED] = SpeedConverter.convert(
266288
self.options[CONF_DRIVING_SPEED],
@@ -274,6 +296,37 @@ def entity_filter(state: State) -> bool:
274296
step_id="options", data_schema=data_schema, errors=errors, last_step=False
275297
)
276298

299+
async def async_step_end_driving_delay(
300+
self, user_input: dict[str, Any] | None = None
301+
) -> ConfigFlowResult:
302+
"""Get end driving delay."""
303+
if user_input is not None:
304+
if CONF_END_DRIVING_DELAY in user_input:
305+
self.options[CONF_END_DRIVING_DELAY] = user_input[CONF_END_DRIVING_DELAY]
306+
elif CONF_END_DRIVING_DELAY in self.options:
307+
del self.options[CONF_END_DRIVING_DELAY]
308+
return await self.async_step_ep_menu()
309+
310+
data_schema = vol.Schema(
311+
{
312+
vol.Optional(CONF_END_DRIVING_DELAY): DurationSelector(
313+
DurationSelectorConfig(
314+
enable_day=False, enable_millisecond=False, allow_negative=False
315+
)
316+
),
317+
}
318+
)
319+
if CONF_END_DRIVING_DELAY in self.options:
320+
suggested_values = {
321+
CONF_END_DRIVING_DELAY: self.options[CONF_END_DRIVING_DELAY]
322+
}
323+
data_schema = self.add_suggested_values_to_schema(
324+
data_schema, suggested_values
325+
)
326+
return self.async_show_form(
327+
step_id="end_driving_delay", data_schema=data_schema, last_step=False
328+
)
329+
277330
async def async_step_ep_menu(
278331
self, _: dict[str, Any] | None = None
279332
) -> ConfigFlowResult:

custom_components/composite/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
CONF_ALL_STATES = "all_states"
1515
CONF_DEFAULT_OPTIONS = "default_options"
1616
CONF_DRIVING_SPEED = "driving_speed"
17+
CONF_END_DRIVING_DELAY = "end_driving_delay"
18+
CONF_MAX_SPEED_AGE = "max_speed_age"
1719
CONF_ENTITY = "entity"
1820
CONF_ENTITY_PICTURE = "entity_picture"
1921
CONF_REQ_MOVEMENT = "require_movement"

0 commit comments

Comments
 (0)