Skip to content

Commit 050dffb

Browse files
committed
New config creates entity-based trackers
Deprecate legacy config.
1 parent 9e1892f commit 050dffb

File tree

4 files changed

+402
-45
lines changed

4 files changed

+402
-45
lines changed

custom_components/composite/__init__.py

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,62 @@
44

55
import voluptuous as vol
66

7+
from homeassistant.config import load_yaml_config_file
8+
from homeassistant.config_entries import SOURCE_IMPORT
79
from homeassistant.requirements import async_process_requirements, RequirementsNotFound
810
from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN
9-
from homeassistant.const import CONF_PLATFORM
11+
from homeassistant.components.device_tracker.legacy import YAML_DEVICES
12+
from homeassistant.components.persistent_notification import (
13+
async_create as pn_async_create,
14+
)
15+
from homeassistant.const import CONF_ID, CONF_NAME, CONF_PLATFORM
16+
17+
# Platform class did not exist before 2021.12
18+
try:
19+
from homeassistant.const import Platform
20+
21+
PLATFORMS = [Platform.DEVICE_TRACKER]
22+
except ImportError:
23+
PLATFORMS = [DT_DOMAIN]
24+
25+
from homeassistant.exceptions import HomeAssistantError
1026
import homeassistant.helpers.config_validation as cv
27+
from homeassistant.util import slugify
1128

12-
from .const import CONF_TIME_AS, DOMAIN, TZ_DEVICE_LOCAL, TZ_DEVICE_UTC
29+
from .const import (
30+
CONF_OPTS,
31+
CONF_TIME_AS,
32+
CONF_TRACKERS,
33+
DOMAIN,
34+
TZ_DEVICE_LOCAL,
35+
TZ_DEVICE_UTC,
36+
)
37+
from .device_tracker import COMPOSITE_TRACKER
1338

1439
CONF_TZ_FINDER = "tz_finder"
1540
DEFAULT_TZ_FINDER = "timezonefinderL==4.0.2"
1641
CONF_TZ_FINDER_CLASS = "tz_finder_class"
1742
TZ_FINDER_CLASS_OPTS = ["TimezoneFinder", "TimezoneFinderL"]
43+
TRACKER = COMPOSITE_TRACKER.copy()
44+
TRACKER.update({vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ID): cv.slugify})
45+
46+
47+
def _tracker_ids(value):
48+
"""Determine tracker ID."""
49+
ids = []
50+
for conf in value:
51+
if CONF_ID not in conf:
52+
name = conf[CONF_NAME]
53+
if name == slugify(name):
54+
conf[CONF_ID] = name
55+
conf[CONF_NAME] = name.replace("_", " ").title()
56+
else:
57+
conf[CONF_ID] = cv.slugify(conf[CONF_NAME])
58+
ids.append(conf[CONF_ID])
59+
if len(ids) != len(set(ids)):
60+
raise vol.Invalid("id's must be unique")
61+
return value
62+
1863

1964
CONFIG_SCHEMA = vol.Schema(
2065
{
@@ -24,29 +69,104 @@
2469
vol.Optional(
2570
CONF_TZ_FINDER_CLASS, default=TZ_FINDER_CLASS_OPTS[0]
2671
): vol.In(TZ_FINDER_CLASS_OPTS),
72+
vol.Optional(CONF_TRACKERS, default=list): vol.All(
73+
cv.ensure_list, [TRACKER], _tracker_ids
74+
),
2775
}
28-
),
76+
)
2977
},
3078
extra=vol.ALLOW_EXTRA,
3179
)
3280

3381
_LOGGER = logging.getLogger(__name__)
3482

3583

36-
def setup(hass, config):
84+
async def async_setup(hass, config):
85+
# Get a list of all the object IDs in known_devices.yaml to see if any were created
86+
# when this integration was a legacy device tracker, or would otherwise conflict
87+
# with IDs in our config.
88+
try:
89+
legacy_devices = await hass.async_add_executor_job(
90+
load_yaml_config_file, hass.config.path(YAML_DEVICES)
91+
)
92+
except (HomeAssistantError, FileNotFoundError):
93+
legacy_devices = {}
94+
try:
95+
legacy_ids = [
96+
cv.slugify(id)
97+
for id, dev in legacy_devices.items()
98+
if cv.boolean(dev.get("track", False))
99+
]
100+
except vol.Invalid:
101+
legacy_ids = []
102+
103+
# Get all existing composite config entries.
104+
cfg_entries = {
105+
entry.data[CONF_ID]: entry
106+
for entry in hass.config_entries.async_entries(DOMAIN)
107+
}
108+
109+
# For each tracker config, see if it conflicts with a known_devices.yaml entry.
110+
# If not, update the config entry if one already exists for it in case the config
111+
# has changed, or create a new config entry if one did not already exist.
112+
tracker_configs = config[DOMAIN][CONF_TRACKERS]
113+
conflict_ids = []
114+
for conf in tracker_configs:
115+
# These go in the "static" data field.
116+
id = conf[CONF_ID]
117+
name = conf[CONF_NAME]
118+
# These go in the options field, which can be updated via the UI (once support
119+
# for that is added.)
120+
options = {k: v for k, v in conf.items() if k in CONF_OPTS}
121+
122+
if id in legacy_ids:
123+
conflict_ids.append(id)
124+
elif id in cfg_entries:
125+
hass.config_entries.async_update_entry(
126+
cfg_entries[id], data={CONF_NAME: name, CONF_ID: id}, options=options
127+
)
128+
else:
129+
130+
async def create_config(conf):
131+
"""Create new config entry."""
132+
result = await hass.config_entries.flow.async_init(
133+
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
134+
)
135+
# Versions prior to 2021.6 did not support creating with options, so
136+
# update the created config entry with the options.
137+
hass.config_entries.async_update_entry(
138+
result["result"], options=options
139+
)
140+
141+
hass.async_create_task(create_config(conf))
142+
143+
if conflict_ids:
144+
_LOGGER.warning("%s in %s: skipping", ", ".join(conflict_ids), YAML_DEVICES)
145+
if len(conflict_ids) == 1:
146+
msg1 = "ID was"
147+
msg2 = "conflicts"
148+
else:
149+
msg1 = "IDs were"
150+
msg2 = "conflict"
151+
pn_async_create(
152+
hass,
153+
title="Conflicting IDs",
154+
message=f"The following {msg1} found in {YAML_DEVICES}"
155+
f" which {msg2} with the configuration of the {DOMAIN} integration."
156+
" Please remove from one or the other."
157+
f"\n\n{', '.join(conflict_ids)}",
158+
)
159+
160+
legacy_configs = [
161+
conf for conf in (config.get(DT_DOMAIN) or []) if conf[CONF_PLATFORM] == DOMAIN
162+
]
37163
if any(
38164
conf[CONF_TIME_AS] in (TZ_DEVICE_UTC, TZ_DEVICE_LOCAL)
39-
for conf in (config.get(DT_DOMAIN) or [])
40-
if conf[CONF_PLATFORM] == DOMAIN
165+
for conf in tracker_configs + legacy_configs
41166
):
42167
pkg = config[DOMAIN][CONF_TZ_FINDER]
43168
try:
44-
asyncio.run_coroutine_threadsafe(
45-
async_process_requirements(
46-
hass, "{}.{}".format(DOMAIN, DT_DOMAIN), [pkg]
47-
),
48-
hass.loop,
49-
).result()
169+
await async_process_requirements(hass, f"{DOMAIN}.{DT_DOMAIN}", [pkg])
50170
except RequirementsNotFound:
51171
_LOGGER.debug("Process requirements failed: %s", pkg)
52172
return False
@@ -68,3 +188,35 @@ def setup(hass, config):
68188
hass.data[DOMAIN] = tf
69189

70190
return True
191+
192+
193+
async def async_setup_entry(hass, entry):
194+
"""Set up config entry."""
195+
# async_forward_entry_setups was new in 2022.8
196+
if hasattr(hass.config_entries, "async_forward_entry_setups"):
197+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
198+
# async_setup_platforms was new in 2021.5
199+
elif hasattr(hass.config_entries, "async_setup_platforms"):
200+
await hass.config_entries.async_setup_platforms(entry, PLATFORMS)
201+
else:
202+
for platform in PLATFORMS:
203+
hass.async_create_task(
204+
hass.config_entries.async_forward_entry_setup(entry, platform)
205+
)
206+
return True
207+
208+
209+
async def async_unload_entry(hass, entry):
210+
"""Unload a config entry."""
211+
# async_unload_platforms was new in 2021.5
212+
if hasattr(hass.config_entries, "async_unload_platforms"):
213+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
214+
else:
215+
return all(
216+
await asyncio.gather(
217+
*(
218+
hass.config_entries.async_forward_entry_unload(entry, platform)
219+
for platform in PLATFORMS
220+
)
221+
)
222+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Config flow for Composite integration."""
2+
from homeassistant.config_entries import ConfigFlow
3+
from homeassistant.const import CONF_ID, CONF_NAME
4+
5+
from .const import DOMAIN
6+
7+
8+
class CompositeConfigFlow(ConfigFlow, domain=DOMAIN):
9+
"""Composite config flow."""
10+
11+
VERSION = 1
12+
13+
async def async_step_import(self, data):
14+
"""Import config entry from configuration."""
15+
name = data[CONF_NAME]
16+
id = data[CONF_ID]
17+
18+
await self.async_set_unique_id(id)
19+
self._abort_if_unique_id_configured()
20+
21+
# Versions prior to 2021.6 did not support creating with options, so only save
22+
# main (name/ID) data here. async_setup in __init__.py will update the entry
23+
# with the options.
24+
return self.async_create_entry(
25+
title=f"{name} (from configuration)", data={CONF_NAME: name, CONF_ID: id}
26+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"""Constants for Composite Integration."""
2+
from homeassistant.const import CONF_ENTITY_ID
3+
24
DOMAIN = "composite"
35

46
CONF_ALL_STATES = "all_states"
57
CONF_ENTITY = "entity"
68
CONF_REQ_MOVEMENT = "require_movement"
79
CONF_TIME_AS = "time_as"
10+
CONF_TRACKERS = "trackers"
811

912
TZ_UTC = "utc"
1013
TZ_LOCAL = "local"
1114
TZ_DEVICE_UTC = "device_or_utc"
1215
TZ_DEVICE_LOCAL = "device_or_local"
1316
# First item in list is default.
1417
TIME_AS_OPTS = [TZ_UTC, TZ_LOCAL, TZ_DEVICE_UTC, TZ_DEVICE_LOCAL]
18+
19+
CONF_OPTS = (CONF_ENTITY_ID, CONF_REQ_MOVEMENT, CONF_TIME_AS)

0 commit comments

Comments
 (0)