Skip to content

Commit 6e188a6

Browse files
Merge pull request #39 from andrew-codechimp/no-device-entry
No device entry
2 parents 383154c + 5060d4e commit 6e188a6

File tree

7 files changed

+123
-41
lines changed

7 files changed

+123
-41
lines changed

custom_components/periodic_min_max/__init__.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66

77
from __future__ import annotations
88

9+
import voluptuous as vol
910
from awesomeversion.awesomeversion import AwesomeVersion
1011
from homeassistant.config_entries import ConfigEntry
1112
from homeassistant.const import __version__ as HA_VERSION # noqa: N812
12-
from homeassistant.core import HomeAssistant
13+
from homeassistant.core import HomeAssistant, callback
1314
from homeassistant.helpers import config_validation as cv
15+
from homeassistant.helpers import entity_registry as er
16+
from homeassistant.helpers.helper_integration import (
17+
async_handle_source_entity_changes,
18+
async_remove_helper_config_entry_from_source_device,
19+
)
1420
from homeassistant.helpers.typing import ConfigType
1521

1622
from .const import (
23+
CONF_ENTITY_ID,
1724
DOMAIN,
1825
LOGGER,
1926
MIN_HA_VERSION,
@@ -23,6 +30,19 @@
2330
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
2431

2532

33+
@callback
34+
def async_get_source_entity_device_id(
35+
hass: HomeAssistant, entity_id: str
36+
) -> str | None:
37+
"""Get the entity device id."""
38+
registry = er.async_get(hass)
39+
40+
if not (source_entity := registry.async_get(entity_id)):
41+
return None
42+
43+
return source_entity.device_id
44+
45+
2646
async def async_setup(
2747
hass: HomeAssistant, # pylint: disable=unused-argument
2848
config: ConfigType, # pylint: disable=unused-argument
@@ -43,10 +63,46 @@ async def async_setup(
4363

4464
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4565
"""Set up Min/Max from a config entry."""
46-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
4766

67+
entity_registry = er.async_get(hass)
68+
try:
69+
entity_id = er.async_validate_entity_id(
70+
entity_registry, entry.options[CONF_ENTITY_ID]
71+
)
72+
except vol.Invalid:
73+
# The entity is identified by an unknown entity registry ID
74+
LOGGER.error(
75+
"Failed to setup periodic_min_max for unknown entity %s",
76+
entry.options[CONF_ENTITY_ID],
77+
)
78+
return False
79+
80+
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
81+
hass.config_entries.async_update_entry(
82+
entry,
83+
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
84+
)
85+
86+
async def source_entity_removed() -> None:
87+
# The source entity has been removed, we remove the config entry because
88+
# periodic_min_max will not work without the source entity.
89+
await hass.config_entries.async_remove(entry.entry_id)
90+
91+
entry.async_on_unload(
92+
async_handle_source_entity_changes(
93+
hass,
94+
add_helper_config_entry_to_device=False,
95+
helper_config_entry_id=entry.entry_id,
96+
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
97+
source_device_id=async_get_source_entity_device_id(hass, entity_id),
98+
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
99+
source_entity_removed=source_entity_removed,
100+
)
101+
)
48102
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
49103

104+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
105+
50106
return True
51107

52108

@@ -58,3 +114,37 @@ async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry)
58114
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
59115
"""Unload a config entry."""
60116
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
117+
118+
119+
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
120+
"""Migrate old entry."""
121+
LOGGER.debug(
122+
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
123+
)
124+
125+
if config_entry.version > 1:
126+
# This means the user has downgraded from a future version
127+
return False
128+
if config_entry.version == 1:
129+
options = {**config_entry.options}
130+
if config_entry.minor_version < 2:
131+
# Remove the periodic min/max config entry from the source device
132+
if source_device_id := async_get_source_entity_device_id(
133+
hass, options[CONF_ENTITY_ID]
134+
):
135+
async_remove_helper_config_entry_from_source_device(
136+
hass,
137+
helper_config_entry_id=config_entry.entry_id,
138+
source_device_id=source_device_id,
139+
)
140+
hass.config_entries.async_update_entry(
141+
config_entry, options=options, minor_version=2
142+
)
143+
144+
LOGGER.debug(
145+
"Migration to version %s.%s successful",
146+
config_entry.version,
147+
config_entry.minor_version,
148+
)
149+
150+
return True

custom_components/periodic_min_max/config_flow.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
5858
config_flow = CONFIG_FLOW
5959
options_flow = OPTIONS_FLOW
6060

61+
VERSION = 1
62+
MINOR_VERSION = 2
63+
6164
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
6265
"""Return config entry title."""
6366
return cast(str, options["name"]) if "name" in options else ""

custom_components/periodic_min_max/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
LOGGER: Logger = getLogger(__package__)
1010

11-
MIN_HA_VERSION = "2025.3"
11+
MIN_HA_VERSION = "2025.8"
1212

1313
manifestfile = Path(__file__).parent / "manifest.json"
1414
with open(file=manifestfile, encoding="UTF-8") as json_file:

custom_components/periodic_min_max/sensor.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from homeassistant.helpers import device_registry as dr
2525
from homeassistant.helpers import entity_platform
2626
from homeassistant.helpers import entity_registry as er
27-
from homeassistant.helpers.device import async_device_info_to_link_from_entity
2827
from homeassistant.helpers.entity_platform import (
2928
AddConfigEntryEntitiesCallback,
3029
AddEntitiesCallback,
@@ -62,24 +61,16 @@
6261

6362

6463
@callback
65-
def async_add_to_device(
66-
hass: HomeAssistant, entry: ConfigEntry, entity_id: str
64+
def async_get_source_entity_device_id(
65+
hass: HomeAssistant, entity_id: str
6766
) -> str | None:
68-
"""Add our config entry to the tracked entity's device."""
67+
"""Get the entity device id."""
6968
registry = er.async_get(hass)
70-
device_registry = dr.async_get(hass)
71-
device_id = None
72-
73-
if (
74-
not (source_device := registry.async_get(entity_id))
75-
or not (device_id := source_device.device_id)
76-
or not (device_registry.async_get(device_id))
77-
):
78-
return device_id
7969

80-
device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id)
70+
if not (source_entity := registry.async_get(entity_id)):
71+
return None
8172

82-
return device_id
73+
return source_entity.device_id
8374

8475

8576
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
@@ -96,7 +87,7 @@ async def async_setup_entry(
9687
entity_registry = er.async_get(hass)
9788
device_registry = dr.async_get(hass)
9889
try:
99-
entity_id = er.async_validate_entity_id(
90+
source_entity_id = er.async_validate_entity_id(
10091
entity_registry, config_entry.options[CONF_ENTITY_ID]
10192
)
10293
except vol.Invalid:
@@ -107,6 +98,9 @@ async def async_setup_entry(
10798
)
10899
return False
109100

101+
source_entity = entity_registry.async_get(source_entity_id)
102+
device_id = source_entity.device_id if source_entity else None
103+
110104
sensor_type = config_entry.options[CONF_TYPE]
111105

112106
async def async_registry_updated(
@@ -141,20 +135,18 @@ async def async_registry_updated(
141135

142136
config_entry.async_on_unload(
143137
async_track_entity_registry_updated_event(
144-
hass, entity_id, async_registry_updated
138+
hass, source_entity_id, async_registry_updated
145139
)
146140
)
147141
config_entry.async_on_unload(
148142
config_entry.add_update_listener(config_entry_update_listener)
149143
)
150144

151-
device_id = async_add_to_device(hass, config_entry, entity_id)
152-
153145
async_add_entities(
154146
[
155147
PeriodicMinMaxSensor(
156148
hass,
157-
entity_id,
149+
source_entity_id,
158150
config_entry.title,
159151
sensor_type,
160152
config_entry.entry_id,
@@ -180,15 +172,15 @@ async def async_setup_platform(
180172
discovery_info: DiscoveryInfoType | None = None,
181173
) -> None:
182174
"""Set up the periodic min/max sensor."""
183-
entity_id: str = config[CONF_ENTITY_ID]
175+
source_entity_id: str = config[CONF_ENTITY_ID]
184176
name: str | None = config.get(CONF_NAME)
185177
sensor_type: str = config[CONF_TYPE]
186178
unique_id = config.get(CONF_UNIQUE_ID)
187179

188180
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
189181

190182
async_add_entities(
191-
[PeriodicMinMaxSensor(hass, entity_id, name, sensor_type, unique_id)]
183+
[PeriodicMinMaxSensor(hass, source_entity_id, name, sensor_type, unique_id)]
192184
)
193185

194186

@@ -220,17 +212,20 @@ def __init__(
220212
self._attr_name = f"{sensor_type} sensor".capitalize()
221213
self._sensor_attr = SENSOR_TYPE_TO_ATTR[self._sensor_type]
222214

223-
self._attr_device_info = async_device_info_to_link_from_entity(
224-
hass,
225-
source_entity_id,
226-
)
227-
228215
self._unit_of_measurement: str | None = None
229216
self._unit_of_measurement_mismatch = False
230217
self.min_value: float | None = None
231218
self.max_value: float | None = None
232219
self._state: Any = None
233220

221+
registry = er.async_get(hass)
222+
device_registry = dr.async_get(hass)
223+
source_entity = registry.async_get(source_entity_id)
224+
device_id = source_entity.device_id if source_entity else None
225+
226+
if device_id and (device := device_registry.async_get(device_id)):
227+
self.device_entry = device
228+
234229
async def async_added_to_hass(self) -> None:
235230
"""Handle added to Hass."""
236231

hacs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"name": "Periodic Min/Max",
33
"filename": "periodic_min_max.zip",
44
"hide_default_branch": true,
5-
"homeassistant": "2025.3.0",
5+
"homeassistant": "2025.8.0",
66
"zip_release": true
77
}

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
colorlog==6.9.0
2-
homeassistant==2025.3.0
2+
homeassistant==2025.8.0
33
pip>=21.0,<25.3
44
ruff==0.11.13

tests/test_init.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def test_setup(
5959
"entity_id": "sensor.test_source",
6060
"type": "max",
6161
},
62-
title=DEFAULT_NAME
62+
title=DEFAULT_NAME,
6363
)
6464
periodic_min_max_config_entry.add_to_hass(hass)
6565
assert await hass.config_entries.async_setup(periodic_min_max_config_entry.entry_id)
@@ -74,16 +74,10 @@ async def test_setup(
7474
assert periodic_min_max_entity is not None
7575
assert periodic_min_max_entity.device_id == source_entity.device_id
7676

77-
# After reloading the config entry, check linked device
78-
devices_after_reload = device_registry.devices.get_devices_for_config_entry_id(
77+
# Remove the config entry
78+
assert await hass.config_entries.async_remove(
7979
periodic_min_max_config_entry.entry_id
8080
)
81-
assert len(devices_after_reload) == 1
82-
83-
assert devices_after_reload[0].id == source_device_entry.id
84-
85-
# Remove the config entry
86-
assert await hass.config_entries.async_remove(periodic_min_max_config_entry.entry_id)
8781
await hass.async_block_till_done()
8882

8983
# Check the state and entity registry entry are removed

0 commit comments

Comments
 (0)