393 files
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.selector import selector
|
||||
from homeassistant.helpers.selector import EntitySelector, selector
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.common import SourceEntity
|
||||
from custom_components.powercalc.power_profile.power_profile import PowerProfile
|
||||
from custom_components.powercalc.power_profile.power_profile import EntityAutoSelectConfig, PowerProfile
|
||||
|
||||
|
||||
def build_dynamic_field_schema(
|
||||
@@ -12,20 +16,18 @@ def build_dynamic_field_schema(
|
||||
profile: PowerProfile,
|
||||
source_entity: SourceEntity | None,
|
||||
) -> vol.Schema:
|
||||
"""Build the profile's custom field schema, restricting entities to the source device when available.
|
||||
|
||||
Prefill entity fields with a unique auto-selection match unless an explicit default is set.
|
||||
"""
|
||||
schema = {}
|
||||
for field in profile.custom_fields:
|
||||
field_description = field.description
|
||||
if not field_description:
|
||||
field_description = field.label
|
||||
|
||||
if field.default is not None:
|
||||
key = vol.Required(field.key, description=field_description, default=field.default)
|
||||
else:
|
||||
key = vol.Required(field.key, description=field_description)
|
||||
|
||||
field_description = field.description or field.label
|
||||
field_selector = field.selector
|
||||
device_entities: list[er.RegistryEntry] = []
|
||||
if "entity" in field.selector and source_entity and source_entity.device_entry:
|
||||
entity_reg = er.async_get(hass)
|
||||
device_entities = list(entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id))
|
||||
# Build a new selector dict instead of mutating field.selector, which is a reference
|
||||
# into the (potentially cached) profile json_data.
|
||||
field_selector = {
|
||||
@@ -34,10 +36,74 @@ def build_dynamic_field_schema(
|
||||
**field.selector["entity"],
|
||||
"include_entities": [
|
||||
entity.entity_id
|
||||
for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id)
|
||||
for entity in device_entities
|
||||
if "include_entities" not in field.selector["entity"]
|
||||
or entity.entity_id in field.selector["entity"]["include_entities"]
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
schema[key] = selector(field_selector)
|
||||
entity_selector = selector(field_selector)
|
||||
default = field.default
|
||||
if default is None and field.auto_select and isinstance(entity_selector, EntitySelector):
|
||||
default = find_auto_selected_entity(device_entities, entity_selector, field.auto_select)
|
||||
|
||||
key = vol.Required(field.key, description=field_description)
|
||||
if default is not None:
|
||||
key = vol.Required(field.key, description=field_description, default=default)
|
||||
schema[key] = entity_selector
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
def find_auto_selected_entity(
|
||||
entities: list[er.RegistryEntry],
|
||||
entity_selector: EntitySelector,
|
||||
auto_select: EntityAutoSelectConfig,
|
||||
) -> str | None:
|
||||
"""Return a single enabled match without guessing when the selection is ambiguous."""
|
||||
matches = []
|
||||
for entity in entities:
|
||||
if entity.disabled or not matches_entity_filter(entity, auto_select):
|
||||
continue
|
||||
if not matches_entity_selector(entity, entity_selector):
|
||||
continue
|
||||
matches.append(entity.entity_id)
|
||||
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def matches_entity_selector(entity: er.RegistryEntry, entity_selector: EntitySelector) -> bool:
|
||||
"""Check the selector's allowed entities and metadata filters against the registry."""
|
||||
config = entity_selector.config
|
||||
included = config.get("include_entities")
|
||||
if included is not None and entity.entity_id not in included:
|
||||
return False
|
||||
if entity.entity_id in config.get("exclude_entities", []):
|
||||
return False
|
||||
|
||||
top_level_filters = {
|
||||
key: value for key, value in config.items() if key in ["domain", "device_class", "integration"]
|
||||
}
|
||||
if not matches_entity_filter(entity, top_level_filters):
|
||||
return False
|
||||
|
||||
# EntitySelector normalizes nested filters to a list of alternatives.
|
||||
filters = cast(list[Mapping[str, object]], config.get("filter", [{}]))
|
||||
return any(matches_entity_filter(entity, item) for item in filters)
|
||||
|
||||
|
||||
def matches_entity_filter(entity: er.RegistryEntry, filters: Mapping[str, object]) -> bool:
|
||||
"""Match registry metadata; leave filters requiring other data for manual selection."""
|
||||
values = {
|
||||
"domain": entity.domain,
|
||||
"integration": entity.platform,
|
||||
"device_class": entity.device_class or entity.original_device_class,
|
||||
"translation_key": entity.translation_key,
|
||||
}
|
||||
for key, expected in filters.items():
|
||||
if key == "unique_id_pattern":
|
||||
if not re.search(cast(str, expected), entity.unique_id):
|
||||
return False
|
||||
elif key not in values or values[key] not in (expected if isinstance(expected, list) else [expected]):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -29,6 +29,7 @@ from custom_components.powercalc.const import (
|
||||
CONF_ENERGY_UPDATE_INTERVAL,
|
||||
CONF_EXCLUDE_DEVICE_TYPES,
|
||||
CONF_EXCLUDE_SELF_USAGE,
|
||||
CONF_FOLLOW_DEVICE_NAME,
|
||||
CONF_GROUP_ENERGY_UPDATE_INTERVAL,
|
||||
CONF_GROUP_POWER_UPDATE_INTERVAL,
|
||||
CONF_IGNORE_UNAVAILABLE_STATE,
|
||||
@@ -72,6 +73,7 @@ SECTION_GLOBAL_ADVANCED = "advanced"
|
||||
|
||||
SCHEMA_GLOBAL_CONFIGURATION_POWER = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_FOLLOW_DEVICE_NAME, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_POWER_SENSOR_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_POWER_SENSOR_FRIENDLY_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_POWER_SENSOR_CATEGORY): selector.SelectSelector(
|
||||
|
||||
@@ -319,6 +319,7 @@ class LibraryFlow:
|
||||
next_step=Step.POST_LIBRARY,
|
||||
validate_user_input=_process_user_input,
|
||||
form_kwarg=form_kwarg,
|
||||
form_data=self.flow.sensor_config.get(CONF_VARIABLES, {}),
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user