Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ composite:

- **default_options** (*Optional*): Defines default values for corresponding options under **trackers**.
- **require_movement** (*Optional*): Default is `false`.
- **max_speed_age** (*Optional*)
- **driving_speed** (*Optional*)
- **end_driving_delay** (*Optoinal*)

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

Expand All @@ -84,7 +86,9 @@ composite:
- **name**: Friendly name of composite device.
- **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`.)
- **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.
- **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.)
- **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`.
- **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.
- **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.

#### Entity Dictionary
Expand Down Expand Up @@ -151,7 +155,10 @@ direction | Compass heading of movement direction (if moving.)
composite:
default_options:
require_movement: true
max_speed_age:
minutes: 5
driving_speed: 15
end_driving_delay: "00:02:00"
trackers:
- name: Me
driving_speed: 20
Expand All @@ -165,6 +172,7 @@ composite:
- name: Better Half
id: wife
require_movement: false
end_driving_delay: 30
entity_picture: /local/wife.jpg
entity_id: device_tracker.platform_wife
```
33 changes: 33 additions & 0 deletions custom_components/composite/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from contextlib import suppress
from datetime import timedelta
import logging
from pathlib import Path
from typing import Any, cast
Expand All @@ -18,8 +19,10 @@
CONF_ALL_STATES,
CONF_DEFAULT_OPTIONS,
CONF_DRIVING_SPEED,
CONF_END_DRIVING_DELAY,
CONF_ENTITY,
CONF_ENTITY_PICTURE,
CONF_MAX_SPEED_AGE,
CONF_REQ_MOVEMENT,
CONF_TIME_AS,
CONF_TRACKERS,
Expand Down Expand Up @@ -61,6 +64,17 @@ def _entities(entities: list[str | dict]) -> list[dict]:
return result


def _time_period_to_dict(delay: timedelta) -> dict[str, float]:
"""Return timedelta as a dict."""
result: dict[str, float] = {}
if delay.days:
result["days"] = delay.days
result["hours"] = delay.seconds // 3600
result["minutes"] = (delay.seconds // 60) % 60
result["seconds"] = delay.seconds % 60
return result


def _entity_picture(entity_picture: str) -> str:
"""Validate entity picture.

Expand Down Expand Up @@ -128,13 +142,22 @@ def _defaults(config: dict) -> dict:
unsupported_cfgs.add(CONF_TIME_AS)

def_req_mv = config[CONF_DEFAULT_OPTIONS][CONF_REQ_MOVEMENT]
def_max_sa = config[CONF_DEFAULT_OPTIONS].get(CONF_MAX_SPEED_AGE)
def_drv_sp = config[CONF_DEFAULT_OPTIONS].get(CONF_DRIVING_SPEED)
def_end_dd = config[CONF_DEFAULT_OPTIONS].get(CONF_END_DRIVING_DELAY)
end_dd_but_no_drv_sp = False
for tracker in config[CONF_TRACKERS]:
if tracker.pop(CONF_TIME_AS, None):
unsupported_cfgs.add(CONF_TIME_AS)
tracker[CONF_REQ_MOVEMENT] = tracker.get(CONF_REQ_MOVEMENT, def_req_mv)
if CONF_MAX_SPEED_AGE not in tracker and def_max_sa is not None:
tracker[CONF_MAX_SPEED_AGE] = def_max_sa
if CONF_DRIVING_SPEED not in tracker and def_drv_sp is not None:
tracker[CONF_DRIVING_SPEED] = def_drv_sp
if CONF_END_DRIVING_DELAY not in tracker and def_end_dd is not None:
tracker[CONF_END_DRIVING_DELAY] = def_end_dd
if CONF_END_DRIVING_DELAY in tracker and CONF_DRIVING_SPEED not in tracker:
end_dd_but_no_drv_sp = True

if unsupported_cfgs:
_LOGGER.warning(
Expand All @@ -143,6 +166,11 @@ def _defaults(config: dict) -> dict:
DOMAIN,
", ".join(sorted(unsupported_cfgs)),
)
if end_dd_but_no_drv_sp:
raise vol.Invalid(
f"using {CONF_END_DRIVING_DELAY}; "
f"{CONF_DRIVING_SPEED} must also be specified"
)

del config[CONF_DEFAULT_OPTIONS]
return config
Expand All @@ -165,13 +193,16 @@ def _defaults(config: dict) -> dict:
vol.Length(1),
_entities,
)
_POS_TIME_PERIOD = vol.All(cv.positive_time_period, _time_period_to_dict)
_TRACKER = {
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ID): cv.slugify,
vol.Required(CONF_ENTITY_ID): _ENTITIES,
vol.Optional(CONF_TIME_AS): cv.string,
vol.Optional(CONF_REQ_MOVEMENT): cv.boolean,
vol.Optional(CONF_MAX_SPEED_AGE): _POS_TIME_PERIOD,
vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float),
vol.Optional(CONF_END_DRIVING_DELAY): _POS_TIME_PERIOD,
vol.Optional(CONF_ENTITY_PICTURE): vol.All(cv.string, _entity_picture),
}
_CONFIG_SCHEMA = vol.Schema(
Expand All @@ -187,7 +218,9 @@ def _defaults(config: dict) -> dict:
vol.Optional(
CONF_REQ_MOVEMENT, default=DEF_REQ_MOVEMENT
): cv.boolean,
vol.Optional(CONF_MAX_SPEED_AGE): _POS_TIME_PERIOD,
vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float),
vol.Optional(CONF_END_DRIVING_DELAY): _POS_TIME_PERIOD,
}
),
vol.Required(CONF_TRACKERS, default=list): vol.All(
Expand Down
57 changes: 55 additions & 2 deletions custom_components/composite/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
from homeassistant.core import State, callback
from homeassistant.helpers.selector import (
BooleanSelector,
DurationSelector,
DurationSelectorConfig,
EntitySelector,
EntitySelectorConfig,
FileSelector,
Expand All @@ -55,8 +57,10 @@
ATTR_LON,
CONF_ALL_STATES,
CONF_DRIVING_SPEED,
CONF_END_DRIVING_DELAY,
CONF_ENTITY,
CONF_ENTITY_PICTURE,
CONF_MAX_SPEED_AGE,
CONF_REQ_MOVEMENT,
CONF_USE_PICTURE,
DOMAIN,
Expand All @@ -78,7 +82,9 @@ def split_conf(conf: dict[str, Any]) -> dict[str, dict[str, Any]]:
(
CONF_ENTITY_ID,
CONF_REQ_MOVEMENT,
CONF_MAX_SPEED_AGE,
CONF_DRIVING_SPEED,
CONF_END_DRIVING_DELAY,
CONF_ENTITY_PICTURE,
),
),
Expand Down Expand Up @@ -197,14 +203,21 @@ async def async_step_options(

if user_input is not None:
self.options[CONF_REQ_MOVEMENT] = user_input[CONF_REQ_MOVEMENT]
if CONF_MAX_SPEED_AGE in user_input:
self.options[CONF_MAX_SPEED_AGE] = user_input[CONF_MAX_SPEED_AGE]
elif CONF_MAX_SPEED_AGE in self.options:
del self.options[CONF_MAX_SPEED_AGE]
if CONF_DRIVING_SPEED in user_input:
self.options[CONF_DRIVING_SPEED] = SpeedConverter.convert(
user_input[CONF_DRIVING_SPEED],
self._speed_uom,
UnitOfSpeed.METERS_PER_SECOND,
)
elif CONF_DRIVING_SPEED in self.options:
del self.options[CONF_DRIVING_SPEED]
else:
if CONF_DRIVING_SPEED in self.options:
del self.options[CONF_DRIVING_SPEED]
if CONF_END_DRIVING_DELAY in self.options:
del self.options[CONF_END_DRIVING_DELAY]
prv_cfgs = {
cfg[CONF_ENTITY]: cfg for cfg in self.options.get(CONF_ENTITY_ID, [])
}
Expand All @@ -221,6 +234,8 @@ async def async_step_options(
]
self.options[CONF_ENTITY_ID] = new_cfgs
if new_cfgs:
if CONF_DRIVING_SPEED in self.options:
return await self.async_step_end_driving_delay()
return await self.async_step_ep_menu()
errors[CONF_ENTITY_ID] = "at_least_one_entity"

Expand Down Expand Up @@ -248,6 +263,11 @@ def entity_filter(state: State) -> bool:
)
),
vol.Required(CONF_REQ_MOVEMENT): BooleanSelector(),
vol.Optional(CONF_MAX_SPEED_AGE): DurationSelector(
DurationSelectorConfig(
enable_day=False, enable_millisecond=False, allow_negative=False
)
),
vol.Optional(CONF_DRIVING_SPEED): NumberSelector(
NumberSelectorConfig(
unit_of_measurement=self._speed_uom,
Expand All @@ -261,6 +281,8 @@ def entity_filter(state: State) -> bool:
CONF_ENTITY_ID: self._entity_ids,
CONF_REQ_MOVEMENT: self.options[CONF_REQ_MOVEMENT],
}
if CONF_MAX_SPEED_AGE in self.options:
suggested_values[CONF_MAX_SPEED_AGE] = self.options[CONF_MAX_SPEED_AGE]
if CONF_DRIVING_SPEED in self.options:
suggested_values[CONF_DRIVING_SPEED] = SpeedConverter.convert(
self.options[CONF_DRIVING_SPEED],
Expand All @@ -274,6 +296,37 @@ def entity_filter(state: State) -> bool:
step_id="options", data_schema=data_schema, errors=errors, last_step=False
)

async def async_step_end_driving_delay(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Get end driving delay."""
if user_input is not None:
if CONF_END_DRIVING_DELAY in user_input:
self.options[CONF_END_DRIVING_DELAY] = user_input[CONF_END_DRIVING_DELAY]
elif CONF_END_DRIVING_DELAY in self.options:
del self.options[CONF_END_DRIVING_DELAY]
return await self.async_step_ep_menu()

data_schema = vol.Schema(
{
vol.Optional(CONF_END_DRIVING_DELAY): DurationSelector(
DurationSelectorConfig(
enable_day=False, enable_millisecond=False, allow_negative=False
)
),
}
)
if CONF_END_DRIVING_DELAY in self.options:
suggested_values = {
CONF_END_DRIVING_DELAY: self.options[CONF_END_DRIVING_DELAY]
}
data_schema = self.add_suggested_values_to_schema(
data_schema, suggested_values
)
return self.async_show_form(
step_id="end_driving_delay", data_schema=data_schema, last_step=False
)

async def async_step_ep_menu(
self, _: dict[str, Any] | None = None
) -> ConfigFlowResult:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/composite/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
CONF_ALL_STATES = "all_states"
CONF_DEFAULT_OPTIONS = "default_options"
CONF_DRIVING_SPEED = "driving_speed"
CONF_END_DRIVING_DELAY = "end_driving_delay"
CONF_MAX_SPEED_AGE = "max_speed_age"
CONF_ENTITY = "entity"
CONF_ENTITY_PICTURE = "entity_picture"
CONF_REQ_MOVEMENT = "require_movement"
Expand Down
Loading