New apps Added

This commit is contained in:
2026-07-08 10:43:39 -04:00
parent 3b1f4bbd75
commit fefc2c8b5c
1114 changed files with 406637 additions and 154 deletions
+5 -5
View File
@@ -36,7 +36,6 @@ from .common import validate_name_pattern
from .configuration.global_config import (
FLAG_HAS_GLOBAL_GUI_CONFIG,
get_global_configuration,
get_global_gui_configuration,
)
from .const import (
CONF_CREATE_DOMAIN_GROUPS,
@@ -458,7 +457,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID:
global_config = hass.data[DOMAIN][DOMAIN_CONFIG]
if global_config.get(FLAG_HAS_GLOBAL_GUI_CONFIG, False) is False:
await apply_global_gui_configuration_changes(hass, entry)
await apply_global_gui_configuration_changes(hass)
discovery_enabled = bool(entry.data.get(CONF_DISCOVERY, {}).get(CONF_ENABLED, False))
discovery_manager: DiscoveryManager = hass.data[DOMAIN][DATA_DISCOVERY_MANAGER]
@@ -477,7 +476,7 @@ async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update a given config entry."""
if entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID:
await apply_global_gui_configuration_changes(hass, entry)
await apply_global_gui_configuration_changes(hass)
await hass.config_entries.async_reload(entry.entry_id)
@@ -486,10 +485,11 @@ async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
await hass.config_entries.async_reload(related_entry.entry_id)
async def apply_global_gui_configuration_changes(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def apply_global_gui_configuration_changes(hass: HomeAssistant) -> None:
"""Apply global configuration changes to all entities."""
global_config = hass.data[DOMAIN][DOMAIN_CONFIG]
global_config.update(get_global_gui_configuration(entry))
global_config.clear()
global_config.update(get_global_configuration(hass, {}))
for entry in get_entries_excluding_global_config(hass):
if entry.state != ConfigEntryState.LOADED: # pragma: no cover
continue
+30 -12
View File
@@ -106,21 +106,39 @@ def get_wrapped_entity_name(
device_entry: dr.DeviceEntry | None,
) -> str:
"""Construct entity name based on the wrapped entity"""
if entity_entry:
if entity_entry.name:
return entity_entry.name
if entity_entry.has_entity_name and device_entry:
device_name = device_entry.name_by_user or device_entry.name
if device_name:
return f"{device_name} {entity_entry.original_name}" if entity_entry.original_name else device_name
if entity_entry is None:
return _get_state_name(hass, entity_id) or object_id
return entity_entry.original_name or object_id
if entity_entry.name:
return entity_entry.name
device_entity_name = _get_device_entity_name(entity_entry, device_entry)
if device_entity_name:
return device_entity_name
return entity_entry.original_name or object_id
def _get_device_entity_name(
entity_entry: er.RegistryEntry,
device_entry: dr.DeviceEntry | None,
) -> str | None:
if not entity_entry.has_entity_name or device_entry is None:
return None
device_name = device_entry.name_by_user or device_entry.name
if not device_name:
return None
if entity_entry.original_name:
return f"{device_name} {entity_entry.original_name}"
return device_name
def _get_state_name(hass: HomeAssistant, entity_id: str) -> str | None:
entity_state = hass.states.get(entity_id)
if entity_state:
return str(entity_state.name)
return object_id
return str(entity_state.name) if entity_state else None
def get_merged_sensor_configuration(*configs: dict, validate: bool = True) -> dict:
+46 -22
View File
@@ -246,32 +246,56 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow):
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle the current step."""
if user_input is not None:
if form_step.validate_user_input is not None:
try:
validated_input = form_step.validate_user_input(user_input)
user_input = await validated_input if isawaitable(validated_input) else validated_input
except SchemaFlowError as exc:
return await self._show_form(form_step, exc)
if user_input is None:
return await self._show_form(form_step)
if CONF_NAME in user_input:
self.name = user_input[CONF_NAME]
self.sensor_config.update(user_input)
try:
user_input = await self._validate_form_step_input(form_step, user_input)
except SchemaFlowError as exc:
return await self._show_form(form_step, exc)
self.handled_steps.append(form_step.step)
next_step = form_step.next_step
if callable(form_step.next_step):
resolved_next_step = form_step.next_step(user_input)
next_step = await resolved_next_step if isawaitable(resolved_next_step) else resolved_next_step
if not next_step:
return await self.handle_final_steps(
skip_advanced=not form_step.continue_advanced_step,
skip_utility_meter_options=not form_step.continue_utility_meter_options_step,
)
self._store_form_step_input(form_step, user_input)
next_step = await self._resolve_next_step(form_step, user_input)
if next_step is None:
return await self.handle_final_steps(
skip_advanced=not form_step.continue_advanced_step,
skip_utility_meter_options=not form_step.continue_utility_meter_options_step,
)
return await getattr(self, f"async_step_{next_step}")() # type: ignore[no-any-return]
return await getattr(self, f"async_step_{next_step}")() # type: ignore[no-any-return]
return await self._show_form(form_step)
@staticmethod
async def _validate_form_step_input(
form_step: PowercalcFormStep,
user_input: dict[str, Any],
) -> dict[str, Any]:
if form_step.validate_user_input is None:
return user_input
validated_input = form_step.validate_user_input(user_input)
return await validated_input if isawaitable(validated_input) else validated_input
def _store_form_step_input(
self,
form_step: PowercalcFormStep,
user_input: dict[str, Any],
) -> None:
if CONF_NAME in user_input:
self.name = user_input[CONF_NAME]
self.sensor_config.update(user_input)
self.handled_steps.append(form_step.step)
@staticmethod
async def _resolve_next_step(
form_step: PowercalcFormStep,
user_input: dict[str, Any],
) -> Step | None:
if not callable(form_step.next_step):
return form_step.next_step
next_step = form_step.next_step(user_input)
return await next_step if isawaitable(next_step) else next_step
async def handle_final_steps(
self,
+4
View File
@@ -210,10 +210,12 @@ ENTITY_CATEGORIES = [
DEFAULT_GROUP_POWER_UPDATE_INTERVAL = 2
DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL = 60
DEFAULT_POWER_NAME_PATTERN = "{} power"
DEFAULT_SELF_USAGE_POWER_NAME_PATTERN = "{} Device Power"
DEFAULT_POWER_SENSOR_PRECISION = 2
DEFAULT_ENERGY_UPDATE_INTERVAL = 600
DEFAULT_ENERGY_INTEGRATION_METHOD = ENERGY_INTEGRATION_METHOD_LEFT
DEFAULT_ENERGY_NAME_PATTERN = "{} energy"
DEFAULT_SELF_USAGE_ENERGY_NAME_PATTERN = "{} Device Energy"
DEFAULT_ENERGY_SENSOR_PRECISION = 4
DEFAULT_ENERGY_UNIT_PREFIX = UnitPrefix.KILO
DEFAULT_ENTITY_CATEGORY: str | None = None
@@ -260,6 +262,8 @@ OFF_STATES_BY_DOMAIN: dict[str, set[str]] = {
device_tracker.DOMAIN: {STATE_NOT_HOME},
}
DOCS_URI = "https://docs.powercalc.nl/configuration/global-configuration/"
class CalculationStrategy(StrEnum):
"""Possible virtual power calculation strategies."""
-1
View File
@@ -401,7 +401,6 @@ class DiscoveryManager:
if flow["context"]["source"] != SOURCE_INTEGRATION_DISCOVERY:
continue # pragma: no cover
self.hass.config_entries.flow.async_abort(flow["flow_id"])
return
async def extract_model_info_from_device_info(
self,
@@ -38,6 +38,7 @@ from custom_components.powercalc.const import (
DEFAULT_ENERGY_UPDATE_INTERVAL,
DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL,
DEFAULT_GROUP_POWER_UPDATE_INTERVAL,
DOCS_URI,
DOMAIN,
DOMAIN_CONFIG,
ENTITY_CATEGORIES,
@@ -132,6 +133,21 @@ SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR = vol.Schema(
)
def merge_global_config(global_config: ConfigType, user_input: dict[str, Any], schema: vol.Schema) -> None:
"""Merge submitted user input into the global config.
Keys present in the schema but absent from the user input were cleared in the form
and must be removed, otherwise a previously saved value would incorrectly persist.
"""
for key in schema.schema:
if isinstance(key, vol.Marker):
key = key.schema
if key in user_input:
global_config[key] = user_input[key]
elif key in global_config:
global_config.pop(key)
def get_global_powercalc_config(flow: PowercalcCommonFlow) -> ConfigType:
"""Get the global powercalc config."""
if flow.global_config:
@@ -182,9 +198,10 @@ class GlobalConfigurationFlow:
"""Handle the throttling related options."""
if user_input is not None:
self.flow.global_config.update(user_input)
if self.flow.is_options_flow:
merge_global_config(self.flow.global_config, user_input, SCHEMA_GLOBAL_CONFIGURATION_THROTTLING)
return self.flow.persist_config_entry()
self.flow.global_config.update(user_input)
return await self.flow.handle_form_step(
PowercalcFormStep(
@@ -207,9 +224,10 @@ class GlobalConfigurationFlow:
"""Handle the global configuration step."""
if user_input is not None:
self.flow.global_config.update(user_input)
if self.flow.is_options_flow:
merge_global_config(self.flow.global_config, user_input, SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR)
return self.flow.persist_config_entry()
self.flow.global_config.update(user_input)
if not bool(self.flow.global_config.get(CONF_CREATE_ENERGY_SENSORS)) or user_input is not None:
return await self.async_step_global_configuration_utility_meter()
@@ -220,7 +238,7 @@ class GlobalConfigurationFlow:
schema=SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR,
form_kwarg={
"description_placeholders": {
"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/",
"docs_uri": DOCS_URI,
},
},
),
@@ -233,9 +251,10 @@ class GlobalConfigurationFlow:
"""Handle the global configuration step."""
if user_input is not None:
self.flow.global_config.update(user_input)
if self.flow.is_options_flow:
merge_global_config(self.flow.global_config, user_input, SCHEMA_UTILITY_METER_OPTIONS)
return self.flow.persist_config_entry()
self.flow.global_config.update(user_input)
if not bool(self.flow.global_config.get(CONF_CREATE_UTILITY_METERS)) or user_input is not None:
return self.flow.async_create_entry(
@@ -249,7 +268,7 @@ class GlobalConfigurationFlow:
schema=SCHEMA_UTILITY_METER_OPTIONS,
form_kwarg={
"description_placeholders": {
"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/",
"docs_uri": DOCS_URI,
},
},
),
@@ -277,7 +296,7 @@ class GlobalConfigurationConfigFlow(GlobalConfigurationFlow):
schema=SCHEMA_GLOBAL_CONFIGURATION,
form_kwarg={
"description_placeholders": {
"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/",
"docs_uri": DOCS_URI,
},
},
),
@@ -306,7 +325,7 @@ class GlobalConfigurationOptionsFlow(GlobalConfigurationFlow):
"""Handle the global configuration step."""
if user_input is not None:
self.flow.global_config.update(user_input)
merge_global_config(self.flow.global_config, user_input, SCHEMA_GLOBAL_CONFIGURATION)
return self.flow.persist_config_entry()
return await self.flow.handle_form_step(
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [
"numpy>=1.21.1"
],
"version": "v1.21.0"
"version": "v1.21.2"
}
@@ -27,9 +27,13 @@ from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.const import (
BUILT_IN_LIBRARY_DIR,
CONF_ENERGY_SENSOR_NAMING,
CONF_MAX_POWER,
CONF_MIN_POWER,
CONF_POWER,
CONF_POWER_SENSOR_NAMING,
DEFAULT_SELF_USAGE_ENERGY_NAME_PATTERN,
DEFAULT_SELF_USAGE_POWER_NAME_PATTERN,
DOMAIN,
CalculationStrategy,
PowerProfileSource,
@@ -244,7 +248,12 @@ class PowerProfile:
@property
def sensor_config(self) -> ConfigType:
"""Additional sensor configuration."""
return self._json_data.get("sensor_config") or {}
sensor_config = dict(self._json_data.get("sensor_config") or {})
if self.only_self_usage and CONF_POWER_SENSOR_NAMING not in sensor_config:
sensor_config[CONF_POWER_SENSOR_NAMING] = DEFAULT_SELF_USAGE_POWER_NAME_PATTERN
if self.only_self_usage and CONF_ENERGY_SENSOR_NAMING not in sensor_config:
sensor_config[CONF_ENERGY_SENSOR_NAMING] = DEFAULT_SELF_USAGE_ENERGY_NAME_PATTERN
return sensor_config
def is_strategy_supported(self, mode: CalculationStrategy) -> bool:
"""Whether a certain calculation strategy is supported by this profile."""
+7 -6
View File
@@ -10,7 +10,7 @@ from functools import partial
import gzip
import logging
import os
from typing import Any, TextIO, TypeVar, cast
from typing import Any, TextIO, cast
from homeassistant.components import light
from homeassistant.components.light import (
@@ -213,7 +213,7 @@ class LutStrategy(PowerCalculationStrategyInterface):
brightness = attrs.get(ATTR_BRIGHTNESS)
if brightness is None:
_LOGGER.error(
_LOGGER.warning(
"%s: Could not calculate power. no brightness set",
entity_state.entity_id,
)
@@ -412,10 +412,11 @@ class LutStrategy(PowerCalculationStrategyInterface):
sat_values = self.get_nearest(hs_table, light_setting.hue or 0)
return self.get_nearest(sat_values, light_setting.saturation or 0)
# Generic nearest lookup for both float values and nested saturation dicts
_NearestT = TypeVar("_NearestT", float, dict[int, float])
def get_nearest(self, lookup_dict: dict[int, _NearestT], search_key: int) -> _NearestT:
def get_nearest[NearestT: (float, dict[int, float])](
self,
lookup_dict: dict[int, NearestT],
search_key: int,
) -> NearestT:
"""Return the value mapped at search_key or the nearest neighbour key."""
value = lookup_dict.get(search_key)
if value is not None:
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definujte fixní hodnotu příkonu pro vaši entitu. Případně můžete definovat hodnotu příkonu pro jednotlivé stavy. Například:\n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Definujte fixní hodnotu příkonu pro vaši entitu. Viz [documentation]({docs_uri}) pro více informací. Případně můžete definovat hodnotu příkonu pro jednotlivé stavy. Například:\n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Fixed config"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definieren Sie einen festen Leistungswert für Ihre Entität. Alternativ können Sie auch einen Leistungswert pro State definieren. Zum Beispiel:\n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Definieren Sie einen festen Leistungswert für Ihre Entität. Weitere Informationen finden Sie in der [Dokumentation]({docs_uri}). Alternativ können Sie auch einen Leistungswert pro State definieren. Zum Beispiel:\n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Fixed Konfiguration"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definir un valor de potencia fijo para tu entidad. Alternativamente, puedes definir un valor de potencia por estado. Por ejemplo: \n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Definir un valor de potencia fijo para tu entidad. Vea la [documentación]({docs_uri}) para más información. Alternativamente, puedes definir un valor de potencia por estado. Por ejemplo: \n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Configuración fija"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Définissez une valeur de puissance fixe pour votre entité. Vous pouvez également définir une valeur de puissance par état. Par exemple :\n\n`lecture : 8.3`\n`pause : 2.25`",
"description": "Définissez une valeur de puissance fixe pour votre entité. Voir [documentation]({docs_uri}) pour plus d'informations. Vous pouvez également définir une valeur de puissance par état. Par exemple :\n\n`lecture : 8.3`\n`pause : 2.25`",
"title": "Configuration fixe"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definisci un valore di potenza fisso per la tua entità. In alternativa puoi definire un valore di potenza per stato. Per esempio:\n\n`playing: 8.3`\n`pausa: 2.25`",
"description": "Definisci un valore di potenza fisso per la tua entità. Vedi [documentation]({docs_uri}) per maggiori informazioni. In alternativa puoi definire un valore di potenza per stato. Per esempio:\n\n`playing: 8.3`\n`pausa: 2.25`",
"title": "Configurazione fissa"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definieer een vast vermogen (in W) voor je entiteit. Eventueel kan je ook een vermogen per status instellen. Bijvoorbeeld:\n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Definieer een vast vermogen (in W) voor je entiteit. Zie [documentatie]({docs_uri}) voor meer informatie. Eventueel kan je ook een vermogen per status instellen. Bijvoorbeeld:\n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Gefixeerde configuratie"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Określ stałą wartość mocy dla swojej encji. Alternatywnie możesz zdefiniować wartość mocy dla każdego stanu. Na przykład:\n\n`odtwarzanie: 8.3`\n`pauza: 2.25`",
"description": "Określ stałą wartość mocy dla swojej encji. Zobacz [dokumentację]({docs_uri}) aby uzyskać więcej informacji. Alternatywnie możesz zdefiniować wartość mocy dla każdego stanu. Na przykład:\n\n`odtwarzanie: 8.3`\n`pauza: 2.25`",
"title": "Konfiguracja stała"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Defina um valor de potência fixo para sua entidade. Alternativamente, você pode definir um valor de potência por estado. Por exemplo:\n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Defina um valor de potência fixo para sua entidade. Veja [documentação]({docs_uri}) para mais informações. Alternativamente, você pode definir um valor de potência por estado. Por exemplo:\n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Configuração fixa"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Defina um valor de potência fixo para sua entidade. Alternativamente, você pode definir um valor de energia por estado. Por exemplo:\n\n`reproduzindo: 8.3`\n`pausado: 2.25`",
"description": "Defina um valor de potência fixo para sua entidade. Veja a [documentação]({docs_uri}) para mais informações. Alternativamente, você pode definir um valor de energia por estado. Por exemplo:\n\n`reproduzindo: 8.3`\n`pausado: 2.25`",
"title": "Configuração fixa"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definiți o valoare fixă a puterii pentru entitatea dvs. Alternativ, puteți defini o valoare de putere per stare. De exemplu:\n\n`redare: 8,3`\n`în pauză: 2,25`",
"description": "Definiți o valoare fixă a puterii pentru entitatea dvs. Consultați [documentația]({docs_uri}) pentru mai multe informații. Alternativ, puteți defini o valoare de putere per stare. De exemplu:\n\n`redare: 8,3`\n`în pauză: 2,25`",
"title": "Configurație fixă"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Определите фиксированное значение мощности для вашего объекта. Альтернативно можно указать мощность для каждого состояния. Например:\n\n`playing: 8.3`\n`paused: 2.25`",
"description": "Определите фиксированное значение мощности для вашего объекта. См. [документацию]({docs_uri}) для подробностей. Альтернативно можно указать мощность для каждого состояния. Например:\n\n`playing: 8.3`\n`paused: 2.25`",
"title": "Фиксированная конфигурация"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definujte pevnú hodnotu výkonu pre vašu entitu. Prípadne môžete definovať hodnotu výkonu pre každý stav. Napríklad:\n\n`prehrávanie: 8.3`\n`pozastavené: 2.25`",
"description": "Definujte pevnú hodnotu výkonu pre vašu entitu. Ďalšie informácie nájdete v [dokumentácii]({docs_uri}). Prípadne môžete definovať hodnotu výkonu pre každý stav. Napríklad:\n\n`prehrávanie: 8.3`\n`pozastavené: 2.25`",
"title": "Opravená konfigurácia"
},
"global_configuration": {
@@ -93,7 +93,7 @@
"data_description": {
"fixed_value": "Fixed value in Watts when the entity is ON"
},
"description": "Definiera ett fast värde för din entitet. Alternativt kan du definiera ett värde per tillstånd. Till exempel: \n\n`spelar: 8.3`\n`pausad: 2.25`",
"description": "Definiera ett fast värde för din entitet. Se [dokumentation]({docs_uri}) för mer information. Alternativt kan du definiera ett värde per tillstånd. Till exempel: \n\n`spelar: 8.3`\n`pausad: 2.25`",
"title": "Fast konfig"
},
"global_configuration": {