Skip to content

Commit ca53ad3

Browse files
authored
Merge pull request #4286 from bramstroker/fix/refactor-cleanup
Several cleanups removing duplication
2 parents 95e38c8 + c8d882f commit ca53ad3

27 files changed

Lines changed: 187 additions & 169 deletions

File tree

custom_components/powercalc/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
SensorType,
106106
UnitPrefix,
107107
)
108-
from .discovery import DiscoveryManager, DiscoveryStatus
108+
from .discovery import DiscoveryManager, DiscoveryStatus, get_discovery_manager
109109
from .migrate import async_fix_legacy_profile_config_entry, async_migrate_config_entry
110110
from .power_profile.power_profile import DeviceType
111111
from .sensor import SENSOR_CONFIG
@@ -307,7 +307,7 @@ async def _handle_change_gui_service(call: ServiceCall) -> None:
307307

308308
async def _handle_update_library_service(_: ServiceCall) -> None:
309309
_LOGGER.info("Updating library and rediscovering devices")
310-
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
310+
discovery_manager = get_discovery_manager(hass)
311311
await discovery_manager.update_library_and_rediscover()
312312

313313
hass.services.async_register(
@@ -461,7 +461,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
461461

462462
await async_fix_legacy_profile_config_entry(hass, entry)
463463
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR, Platform.SELECT])
464-
# await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
465464

466465
entry.async_on_unload(entry.add_update_listener(async_update_entry))
467466

@@ -474,7 +473,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
474473
await apply_global_gui_configuration_changes(hass)
475474

476475
discovery_enabled = bool(entry.data.get(CONF_DISCOVERY, {}).get(CONF_ENABLED, False))
477-
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
476+
discovery_manager = get_discovery_manager(hass)
478477
if discovery_enabled and discovery_manager.status == DiscoveryStatus.DISABLED:
479478
_LOGGER.debug("Enabling discovery manager based on global configuration")
480479
discovery_manager.enable()
@@ -530,7 +529,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
530529

531530
async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
532531
"""Called after a config entry is removed."""
533-
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
532+
discovery_manager = get_discovery_manager(hass)
534533
discovery_manager.remove_initialized_flow(config_entry)
535534

536535
updated_entries: list[ConfigEntry] = []

custom_components/powercalc/config_flow.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,11 @@
103103
Step.WLED,
104104
]
105105

106+
# Order matters: async_step() delegates to the first handler that defines the requested step.
106107
FLOW_HANDLERS: dict[FlowType, dict] = {
107-
FlowType.GROUP: {
108-
"config": GroupConfigFlow,
109-
"options": GroupOptionsFlow,
110-
},
111-
FlowType.DAILY_ENERGY: {
112-
"config": DailyEnergyConfigFlow,
113-
"options": DailyEnergyOptionsFlow,
108+
FlowType.GLOBAL_CONFIGURATION: {
109+
"config": GlobalConfigurationConfigFlow,
110+
"options": GlobalConfigurationOptionsFlow,
114111
},
115112
FlowType.LIBRARY: {
116113
"config": LibraryConfigFlow,
@@ -120,9 +117,13 @@
120117
"config": VirtualPowerConfigFlow,
121118
"options": VirtualPowerOptionsFlow,
122119
},
123-
FlowType.GLOBAL_CONFIGURATION: {
124-
"config": GlobalConfigurationConfigFlow,
125-
"options": GlobalConfigurationOptionsFlow,
120+
FlowType.GROUP: {
121+
"config": GroupConfigFlow,
122+
"options": GroupOptionsFlow,
123+
},
124+
FlowType.DAILY_ENERGY: {
125+
"config": DailyEnergyConfigFlow,
126+
"options": DailyEnergyOptionsFlow,
126127
},
127128
FlowType.REAL_POWER: {
128129
"config": RealPowerConfigFlow,
@@ -153,17 +154,9 @@ def __init__(self) -> None:
153154
self.name: str | None = None
154155
self.handled_steps: list[Step] = []
155156

156-
# Initialize flow handlers
157+
# Initialize flow handlers. Iteration order follows FLOW_HANDLERS, which async_step() relies on.
157158
flow_key = "options" if self.is_options_flow else "config"
158-
self.flow_handlers = {
159-
FlowType.GLOBAL_CONFIGURATION: FLOW_HANDLERS[FlowType.GLOBAL_CONFIGURATION][flow_key](self),
160-
FlowType.LIBRARY: FLOW_HANDLERS[FlowType.LIBRARY][flow_key](self),
161-
FlowType.VIRTUAL_POWER: FLOW_HANDLERS[FlowType.VIRTUAL_POWER][flow_key](self),
162-
FlowType.GROUP: FLOW_HANDLERS[FlowType.GROUP][flow_key](self),
163-
FlowType.DAILY_ENERGY: FLOW_HANDLERS[FlowType.DAILY_ENERGY][flow_key](self),
164-
FlowType.REAL_POWER: FLOW_HANDLERS[FlowType.REAL_POWER][flow_key](self),
165-
FlowType.COST: FLOW_HANDLERS[FlowType.COST][flow_key](self),
166-
}
159+
self.flow_handlers = {flow_type: handlers[flow_key](self) for flow_type, handlers in FLOW_HANDLERS.items()}
167160

168161
for step in Step:
169162
step_method = f"async_step_{step}"

custom_components/powercalc/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
STATE_OPEN,
1515
STATE_STANDBY,
1616
STATE_UNAVAILABLE,
17+
STATE_UNKNOWN,
1718
EntityCategory,
1819
)
1920

@@ -272,6 +273,7 @@ class UnitPrefix(StrEnum):
272273
SIGNAL_POWER_SENSOR_STATE_CHANGE = "powercalc_power_sensor_state_change"
273274

274275
OFF_STATES = {STATE_OFF, STATE_STANDBY, STATE_UNAVAILABLE}
276+
UNAVAILABLE_STATES = frozenset({STATE_UNAVAILABLE, STATE_UNKNOWN})
275277
OFF_STATES_BY_DOMAIN: dict[str, set[str]] = {
276278
cover.DOMAIN: {STATE_CLOSED, STATE_OPEN},
277279
device_tracker.DOMAIN: {STATE_NOT_HOME},

custom_components/powercalc/discovery.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,38 @@
5151
_DiscoverySourceT = TypeVar("_DiscoverySourceT", er.RegistryEntry, dr.DeviceEntry)
5252

5353

54-
async def get_power_profile_by_source_entity(hass: HomeAssistant, source_entity: SourceEntity) -> PowerProfile | None:
55-
"""Given a certain entity, lookup the manufacturer and model and return the power profile."""
54+
def get_discovery_manager(hass: HomeAssistant) -> DiscoveryManager:
55+
"""Return the shared discovery manager, creating a throwaway one when not yet set up."""
5656
try:
57-
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
57+
return hass.data[DOMAIN][DATA_DISCOVERY_MANAGER] # type: ignore[no-any-return]
5858
except KeyError:
59-
discovery_manager = DiscoveryManager(hass, {})
59+
return DiscoveryManager(hass, {})
60+
61+
62+
async def _get_power_profile_by_source(
63+
hass: HomeAssistant,
64+
source_entity: SourceEntity,
65+
discovery_by: DiscoveryBy,
66+
) -> PowerProfile | None:
67+
"""Look up a power profile for a source entity, discovered either by entity or by device."""
68+
discovery_manager = get_discovery_manager(hass)
6069
model_info = await discovery_manager.extract_model_info_from_device_info(source_entity.entity_entry)
6170
if not model_info:
6271
return None
63-
profiles = await discovery_manager.find_power_profiles(model_info, source_entity, DiscoveryBy.ENTITY)
72+
profiles = await discovery_manager.find_power_profiles(model_info, source_entity, discovery_by)
6473
return profiles[0] if profiles else None
6574

6675

76+
async def get_power_profile_by_source_entity(hass: HomeAssistant, source_entity: SourceEntity) -> PowerProfile | None:
77+
"""Given a certain entity, lookup the manufacturer and model and return the power profile."""
78+
return await _get_power_profile_by_source(hass, source_entity, DiscoveryBy.ENTITY)
79+
80+
6781
async def get_power_profile_by_source_device(hass: HomeAssistant, source_entity: SourceEntity) -> PowerProfile | None:
6882
"""Look up a device-discovered power profile for a source entity's device."""
6983
if not source_entity.device_entry or not source_entity.entity_entry:
7084
return None
71-
72-
try:
73-
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
74-
except KeyError:
75-
discovery_manager = DiscoveryManager(hass, {})
76-
77-
model_info = await discovery_manager.extract_model_info_from_device_info(source_entity.entity_entry)
78-
if not model_info:
79-
return None
80-
81-
profiles = await discovery_manager.find_power_profiles(model_info, source_entity, DiscoveryBy.DEVICE)
82-
return profiles[0] if profiles else None
85+
return await _get_power_profile_by_source(hass, source_entity, DiscoveryBy.DEVICE)
8386

8487

8588
class DiscoveryStatus(StrEnum):

custom_components/powercalc/flow_helper/dynamic_field_builder.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ def build_dynamic_field_schema(
2323
else:
2424
key = vol.Required(field.key, description=field_description)
2525

26+
field_selector = field.selector
2627
if "entity" in field.selector and source_entity and source_entity.device_entry:
2728
entity_reg = er.async_get(hass)
28-
field.selector["entity"]["include_entities"] = [
29-
entity.entity_id
30-
for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id)
31-
]
29+
# Build a new selector dict instead of mutating field.selector, which is a reference
30+
# into the (potentially cached) profile json_data.
31+
field_selector = {
32+
**field.selector,
33+
"entity": {
34+
**field.selector["entity"],
35+
"include_entities": [
36+
entity.entity_id
37+
for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id)
38+
],
39+
},
40+
}
3241

33-
schema[key] = selector(field.selector)
42+
schema[key] = selector(field_selector)
3443
return vol.Schema(schema)

custom_components/powercalc/group_include/filter.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from homeassistant.core import HomeAssistant, split_entity_id
1212
from homeassistant.helpers import area_registry, device_registry, entity_registry, floor_registry, label_registry
1313
import homeassistant.helpers.config_validation as cv
14+
from homeassistant.helpers.entity import Entity
1415
from homeassistant.helpers.entity_component import EntityComponent
1516
from homeassistant.helpers.entity_registry import RegistryEntry
1617
from homeassistant.helpers.template import Template
@@ -148,8 +149,6 @@ def is_valid(self, entity: RegistryEntry) -> bool:
148149

149150
class StandardGroupFilter(EntityFilter):
150151
def __init__(self, hass: HomeAssistant, group_id: str) -> None:
151-
entity_reg = entity_registry.async_get(hass)
152-
entity_reg.async_get(group_id)
153152
group_state = hass.states.get(group_id)
154153
if group_state is None:
155154
raise SensorConfigurationError(f"Group state {group_id} not found")
@@ -161,14 +160,7 @@ def is_valid(self, entity: RegistryEntry) -> bool:
161160

162161
class LightGroupFilter(EntityFilter):
163162
def __init__(self, hass: HomeAssistant, group_id: str) -> None:
164-
light_component = cast(EntityComponent, hass.data.get(LIGHT_DOMAIN))
165-
light_group = next(
166-
filter(
167-
lambda entity: entity.entity_id == group_id,
168-
light_component.entities,
169-
),
170-
None,
171-
)
163+
light_group = self._find_light_group(hass, group_id)
172164
if light_group is None or light_group.platform.platform_name != GROUP_DOMAIN:
173165
raise SensorConfigurationError(f"Light group {group_id} not found")
174166

@@ -177,22 +169,26 @@ def __init__(self, hass: HomeAssistant, group_id: str) -> None:
177169
def is_valid(self, entity: RegistryEntry) -> bool:
178170
return entity.entity_id in self.entity_ids
179171

180-
def find_all_entity_ids_recursively(
181-
self,
182-
hass: HomeAssistant,
183-
group_entity_id: str,
184-
all_entity_ids: list[str],
185-
) -> list[str]:
186-
entity_reg = entity_registry.async_get(hass)
172+
@staticmethod
173+
def _find_light_group(hass: HomeAssistant, group_entity_id: str) -> Entity | None:
187174
light_component = cast(EntityComponent, hass.data.get(LIGHT_DOMAIN))
188-
light_group = next(
175+
return next(
189176
filter(
190177
lambda entity: entity.entity_id == group_entity_id,
191178
light_component.entities,
192179
),
193180
None,
194181
)
195182

183+
def find_all_entity_ids_recursively(
184+
self,
185+
hass: HomeAssistant,
186+
group_entity_id: str,
187+
all_entity_ids: list[str],
188+
) -> list[str]:
189+
entity_reg = entity_registry.async_get(hass)
190+
light_group = self._find_light_group(hass, group_entity_id)
191+
196192
entity_ids: list[str] = light_group.extra_state_attributes.get(ATTR_ENTITY_ID) # type: ignore
197193
for entity_id in entity_ids:
198194
registry_entry = entity_reg.async_get(entity_id)
@@ -373,7 +369,7 @@ def __init__(
373369
self.operator = operator
374370

375371
def is_valid(self, entity: RegistryEntry) -> bool:
376-
evaluations = [entity_filter.is_valid(entity) for entity_filter in self.filters]
372+
evaluations = (entity_filter.is_valid(entity) for entity_filter in self.filters)
377373
if self.operator == FilterOperator.OR:
378374
return any(evaluations)
379375

custom_components/powercalc/helpers.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,6 @@ def _resolve_related_entity_by_device_class(
200200
return get_related_entity_by_device_class(hass, source_entity, device_class)
201201

202202

203-
def _resolve_related_entity_by_translation_key(
204-
hass: HomeAssistant,
205-
source_entity: SourceEntity,
206-
translation_key: str,
207-
) -> str | None:
208-
return get_related_entity_by_translation_key(hass, source_entity, translation_key)
209-
210-
211203
RELATED_ENTITY_PLACEHOLDER_DEFINITIONS = (
212204
RelatedEntityPlaceholderDefinition(
213205
PLACEHOLDER_ENTITY_BY_DEVICE_CLASS,
@@ -217,7 +209,11 @@ def _resolve_related_entity_by_translation_key(
217209
RelatedEntityPlaceholderDefinition(
218210
PLACEHOLDER_ENTITY_BY_TRANSLATION_KEY,
219211
"translation key",
220-
_resolve_related_entity_by_translation_key,
212+
lambda hass, source_entity, translation_key: get_related_entity_by_translation_key(
213+
hass,
214+
source_entity,
215+
translation_key,
216+
),
221217
),
222218
)
223219

custom_components/powercalc/migrate.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async def async_migrate_config_entry(hass: HomeAssistant, config_entry: ConfigEn
5151
if version <= 3:
5252
_migrate_playbook_trigger(data)
5353

54-
if version <= 4 and config_entry.entry_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID:
54+
if version <= 4 and config_entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID:
5555
_migrate_global_discovery_config(data)
5656

5757
if version <= 5:
@@ -96,16 +96,22 @@ def _migrate_playbook_trigger(data: dict) -> None:
9696

9797

9898
def _migrate_global_discovery_config(data: dict) -> None:
99+
deprecated_keys = [
100+
CONF_ENABLE_AUTODISCOVERY_DEPRECATED,
101+
CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED,
102+
CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED,
103+
]
104+
# Nothing to convert when there are no legacy keys and the new format is already present;
105+
# avoid clobbering an existing discovery config.
106+
if not any(key in data for key in deprecated_keys) and CONF_DISCOVERY in data:
107+
return
108+
99109
data[CONF_DISCOVERY] = {
100110
CONF_ENABLED: data.get(CONF_ENABLE_AUTODISCOVERY_DEPRECATED, True),
101111
CONF_EXCLUDE_DEVICE_TYPES: data.get(CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED, []),
102112
CONF_EXCLUDE_SELF_USAGE: data.get(CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED, False),
103113
}
104-
for key in [
105-
CONF_ENABLE_AUTODISCOVERY_DEPRECATED,
106-
CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED,
107-
CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED,
108-
]:
114+
for key in deprecated_keys:
109115
data.pop(key, None)
110116

111117

custom_components/powercalc/power_profile/loader/local.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33
import logging
44
import os
5-
import re
65
from typing import Any, cast
76

87
from homeassistant.core import HomeAssistant
@@ -154,8 +153,7 @@ def _load_custom_library(self) -> None:
154153

155154
manufacturer = manufacturer_dir.lower()
156155
for model_dir in next(os.walk(manufacturer_path))[1]:
157-
pattern = re.compile(r"^\..*")
158-
if pattern.match(model_dir):
156+
if model_dir.startswith("."):
159157
continue
160158

161159
model_path = os.path.join(manufacturer_path, model_dir)

custom_components/powercalc/power_profile/loader/remote.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def _get_library_model(self, manufacturer: str, model: str) -> LibraryModel:
279279
"""Retrieve model info, or raise an error if not found."""
280280
model_info = self.model_infos.get(f"{manufacturer}/{model}")
281281
if not model_info:
282-
raise LibraryLoadingError("Model not found in library: %s/%s", manufacturer, model)
282+
raise LibraryLoadingError(f"Model not found in library: {manufacturer}/{model}")
283283
return model_info
284284

285285
async def _needs_update(
@@ -424,10 +424,14 @@ def _save_file(data: bytes, directory: str) -> None:
424424
except (TimeoutError, aiohttp.ClientError) as e:
425425
raise ProfileDownloadError(f"Failed to download profile: {manufacturer}/{model}") from e
426426

427+
def _get_profile_hashes_path(self) -> str:
428+
"""Retrieve the local storage path for the profile hashes file."""
429+
return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, ".profile_hashes"))
430+
427431
def _load_profile_hashes(self) -> dict[str, str]:
428432
"""Load profile hashes from local storage"""
429433

430-
path = self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, ".profile_hashes")
434+
path = self._get_profile_hashes_path()
431435
if not os.path.exists(path):
432436
return {}
433437

@@ -437,6 +441,6 @@ def _load_profile_hashes(self) -> dict[str, str]:
437441
def _write_profile_hashes(self, hashes: dict[str, str]) -> None:
438442
"""Write profile hashes to local storage"""
439443

440-
path = self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, ".profile_hashes")
444+
path = self._get_profile_hashes_path()
441445
with open(path, "w") as json_file:
442446
json.dump(hashes, json_file, indent=4)

0 commit comments

Comments
 (0)