Initil after Upgrade
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
__version__ as HA_VERSION, # noqa
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
|
||||
@@ -75,7 +75,7 @@ DAILY_FIXED_ENERGY_SCHEMA = vol.Schema(
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_daily_fixed_energy_sensor(
|
||||
def create_daily_fixed_energy_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
source_entity: SourceEntity | None = None,
|
||||
@@ -134,7 +134,10 @@ async def create_daily_fixed_energy_power_sensor(
|
||||
return None
|
||||
|
||||
power_value: float = mode_config.get(CONF_VALUE) # type: ignore
|
||||
if mode_config.get(CONF_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR and not isinstance(power_value, Template):
|
||||
if mode_config.get(CONF_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR and not isinstance(
|
||||
power_value,
|
||||
Template,
|
||||
):
|
||||
power_value = power_value * 1000 / 24
|
||||
|
||||
power_sensor_config = sensor_config.copy()
|
||||
@@ -244,7 +247,9 @@ class DailyEnergySensor(RestoreEntity, SensorEntity, EnergySensor):
|
||||
self.hass,
|
||||
refresh,
|
||||
timedelta(seconds=self._update_frequency),
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
self.async_on_remove(self._update_timer_removal)
|
||||
|
||||
def calculate_delta(self, elapsed_seconds: int = 0) -> Decimal:
|
||||
if self._last_delta_calculate is None:
|
||||
@@ -267,7 +272,11 @@ class DailyEnergySensor(RestoreEntity, SensorEntity, EnergySensor):
|
||||
)
|
||||
return Decimal(0)
|
||||
|
||||
wh_per_day = value * (self._on_time.total_seconds() / 3600) if self._user_unit_of_measurement == UnitOfPower.WATT else value * 1000
|
||||
wh_per_day = (
|
||||
value * (self._on_time.total_seconds() / 3600)
|
||||
if self._user_unit_of_measurement == UnitOfPower.WATT
|
||||
else value * 1000
|
||||
)
|
||||
|
||||
# Convert Wh to the native measurement unit
|
||||
energy_per_day = wh_per_day
|
||||
|
||||
@@ -60,7 +60,7 @@ ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_energy_sensor(
|
||||
def create_energy_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
power_sensor: PowerSensor,
|
||||
@@ -69,20 +69,20 @@ async def create_energy_sensor(
|
||||
"""Create the energy sensor entity."""
|
||||
|
||||
# Check for existing energy sensor
|
||||
energy_sensor = await _get_existing_energy_sensor(hass, sensor_config)
|
||||
energy_sensor = _get_existing_energy_sensor(hass, sensor_config)
|
||||
if energy_sensor:
|
||||
return energy_sensor
|
||||
|
||||
# Check if we should find or create a related energy sensor
|
||||
energy_sensor = await _get_related_energy_sensor(hass, sensor_config, power_sensor)
|
||||
energy_sensor = _get_related_energy_sensor(hass, sensor_config, power_sensor)
|
||||
if energy_sensor:
|
||||
return energy_sensor
|
||||
|
||||
# Create a new virtual energy sensor based on the virtual power sensor
|
||||
return await _create_virtual_energy_sensor(hass, sensor_config, power_sensor, source_entity)
|
||||
return _create_virtual_energy_sensor(hass, sensor_config, power_sensor, source_entity)
|
||||
|
||||
|
||||
async def _get_existing_energy_sensor(
|
||||
def _get_existing_energy_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
) -> EnergySensor | None:
|
||||
@@ -95,7 +95,8 @@ async def _get_existing_energy_sensor(
|
||||
entity_entry = ent_reg.async_get(energy_sensor_id)
|
||||
if entity_entry is None:
|
||||
raise SensorConfigurationError(
|
||||
f"No energy sensor with id {energy_sensor_id} found in your HA instance. Double check `energy_sensor_id` setting",
|
||||
f"No energy sensor with id {energy_sensor_id} found in your HA instance. "
|
||||
"Double check `energy_sensor_id` setting",
|
||||
)
|
||||
return RealEnergySensor(
|
||||
entity_entry.entity_id,
|
||||
@@ -104,7 +105,7 @@ async def _get_existing_energy_sensor(
|
||||
)
|
||||
|
||||
|
||||
async def _get_related_energy_sensor(
|
||||
def _get_related_energy_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
power_sensor: PowerSensor,
|
||||
@@ -137,7 +138,7 @@ async def _get_related_energy_sensor(
|
||||
return None
|
||||
|
||||
|
||||
async def _create_virtual_energy_sensor(
|
||||
def _create_virtual_energy_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
power_sensor: PowerSensor,
|
||||
@@ -188,10 +189,15 @@ def get_unit_prefix(
|
||||
) -> str | None:
|
||||
unit_prefix = sensor_config.get(CONF_ENERGY_SENSOR_UNIT_PREFIX)
|
||||
|
||||
power_unit = UnitOfPower(power_sensor.unit_of_measurement) # type: ignore
|
||||
try:
|
||||
power_unit: UnitOfPower | str | None = (
|
||||
UnitOfPower(power_sensor.unit_of_measurement) if power_sensor.unit_of_measurement else None
|
||||
)
|
||||
except ValueError:
|
||||
power_unit = None
|
||||
power_state = hass.states.get(power_sensor.entity_id)
|
||||
if power_unit is None and power_state: # type: ignore
|
||||
power_unit = power_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # type: ignore # pragma: no cover
|
||||
if power_unit is None and power_state:
|
||||
power_unit = power_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # pragma: no cover
|
||||
|
||||
# When the power sensor is in kW, we don't want to add an extra k prefix.
|
||||
# As this would result in an energy sensor having kkWh unit, which is obviously invalid
|
||||
@@ -269,14 +275,16 @@ class VirtualEnergySensor(IntegrationSensor, EnergySensor):
|
||||
"integration_method": integration_method,
|
||||
"unique_id": unique_id,
|
||||
"device_info": device_info,
|
||||
"max_sub_interval": timedelta(seconds=sensor_config.get(CONF_ENERGY_UPDATE_INTERVAL, DEFAULT_ENERGY_UPDATE_INTERVAL)),
|
||||
"max_sub_interval": timedelta(
|
||||
seconds=sensor_config.get(CONF_ENERGY_UPDATE_INTERVAL, DEFAULT_ENERGY_UPDATE_INTERVAL),
|
||||
),
|
||||
}
|
||||
|
||||
signature = inspect.signature(IntegrationSensor.__init__)
|
||||
|
||||
params = {key: val for key, val in params.items() if key in signature.parameters}
|
||||
|
||||
super().__init__(**params) # type: ignore[arg-type]
|
||||
super().__init__(**params) # type: ignore[arg-type] # ty: ignore[invalid-argument-type]
|
||||
|
||||
self._powercalc_source_entity = powercalc_source_entity
|
||||
self._powercalc_source_domain = powercalc_source_domain
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -20,11 +20,11 @@ from custom_components.powercalc.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def remove_power_sensor_from_associated_groups(
|
||||
def remove_power_sensor_from_associated_groups(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
) -> list[ConfigEntry]:
|
||||
"""When the user remove a virtual power config entry we need to update all the groups which this sensor belongs to."""
|
||||
"""When the user removes a virtual power config entry, update all groups this sensor belongs to."""
|
||||
group_entries = get_groups_having_member(hass, config_entry)
|
||||
|
||||
for group_entry in group_entries:
|
||||
@@ -39,18 +39,18 @@ async def remove_power_sensor_from_associated_groups(
|
||||
return group_entries
|
||||
|
||||
|
||||
async def add_to_associated_groups(hass: HomeAssistant, config_entry: ConfigEntry) -> ConfigEntry | None: # type: ignore
|
||||
async def add_to_associated_groups(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""
|
||||
When the user has set a group on a virtual power config entry,
|
||||
we need to add this config entry to the group members sensors and update the group.
|
||||
"""
|
||||
sensor_type = config_entry.data.get(CONF_SENSOR_TYPE)
|
||||
if sensor_type not in [SensorType.VIRTUAL_POWER, SensorType.DAILY_ENERGY]:
|
||||
return None
|
||||
return
|
||||
|
||||
raw_groups = config_entry.data.get(CONF_GROUP)
|
||||
if not raw_groups:
|
||||
return None
|
||||
return
|
||||
|
||||
group_ids = raw_groups if isinstance(raw_groups, list) else [raw_groups]
|
||||
for group_entry_id in group_ids:
|
||||
@@ -130,7 +130,9 @@ async def add_to_associated_group(
|
||||
|
||||
def get_entries_having_subgroup(hass: HomeAssistant, subgroup_entry: ConfigEntry) -> list[ConfigEntry]:
|
||||
"""Get all virtual power entries which have the subgroup in their subgroups list."""
|
||||
return [entry for entry in get_group_entries(hass) if subgroup_entry.entry_id in (entry.data.get(CONF_SUB_GROUPS) or [])]
|
||||
return [
|
||||
entry for entry in get_group_entries(hass) if subgroup_entry.entry_id in (entry.data.get(CONF_SUB_GROUPS) or [])
|
||||
]
|
||||
|
||||
|
||||
def get_groups_having_member(hass: HomeAssistant, member_entry: ConfigEntry) -> list[ConfigEntry]:
|
||||
@@ -138,7 +140,8 @@ def get_groups_having_member(hass: HomeAssistant, member_entry: ConfigEntry) ->
|
||||
return [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.data.get(CONF_SENSOR_TYPE) == SensorType.GROUP and member_entry.entry_id in (entry.data.get(CONF_GROUP_MEMBER_SENSORS) or [])
|
||||
if entry.data.get(CONF_SENSOR_TYPE) == SensorType.GROUP
|
||||
and member_entry.entry_id in (entry.data.get(CONF_GROUP_MEMBER_SENSORS) or [])
|
||||
]
|
||||
|
||||
|
||||
@@ -154,4 +157,6 @@ def get_group_entries(hass: HomeAssistant, group_type: GroupType | None = None)
|
||||
|
||||
@callback
|
||||
def get_entries_excluding_global_config(hass: HomeAssistant) -> list[ConfigEntry]:
|
||||
return [entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.unique_id != ENTRY_GLOBAL_CONFIG_UNIQUE_ID]
|
||||
return [
|
||||
entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.unique_id != ENTRY_GLOBAL_CONFIG_UNIQUE_ID
|
||||
]
|
||||
|
||||
@@ -59,6 +59,8 @@ from custom_components.powercalc.analytics.analytics import collect_analytics
|
||||
from custom_components.powercalc.const import (
|
||||
ATTR_ENTITIES,
|
||||
ATTR_IS_GROUP,
|
||||
ATTR_MEMBERS,
|
||||
ATTR_STATE,
|
||||
CONF_ALL,
|
||||
CONF_AREA,
|
||||
CONF_CREATE_ENERGY_SENSOR,
|
||||
@@ -99,7 +101,14 @@ from custom_components.powercalc.const import (
|
||||
UnitPrefix,
|
||||
)
|
||||
from custom_components.powercalc.device_binding import get_device_info
|
||||
from custom_components.powercalc.group_include.filter import AreaFilter, CompositeFilter, DeviceFilter, EntityFilter, FilterOperator, FloorFilter
|
||||
from custom_components.powercalc.group_include.filter import (
|
||||
AreaFilter,
|
||||
CompositeFilter,
|
||||
DeviceFilter,
|
||||
EntityFilter,
|
||||
FilterOperator,
|
||||
FloorFilter,
|
||||
)
|
||||
from custom_components.powercalc.group_include.include import find_entities
|
||||
from custom_components.powercalc.helpers import async_cache
|
||||
from custom_components.powercalc.sensors.abstract import (
|
||||
@@ -133,7 +142,7 @@ UNIT_CONVERTERS: dict[str | None, type[BaseUnitConverter]] = {
|
||||
}
|
||||
|
||||
|
||||
async def create_group_sensors_yaml(
|
||||
def create_group_sensors_yaml(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: dict[str, Any],
|
||||
entities: list[Entity],
|
||||
@@ -152,7 +161,7 @@ async def create_group_sensors_yaml(
|
||||
)
|
||||
|
||||
group_name = str(sensor_config.get(CONF_CREATE_GROUP))
|
||||
return await create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids)
|
||||
return create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids)
|
||||
|
||||
|
||||
async def create_group_sensors_gui(
|
||||
@@ -171,10 +180,10 @@ async def create_group_sensors_gui(
|
||||
|
||||
energy_sensor_ids = await resolve_entity_ids_recursively(hass, entry, SensorDeviceClass.ENERGY)
|
||||
|
||||
return await create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids)
|
||||
return create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids)
|
||||
|
||||
|
||||
async def create_group_sensors_custom(
|
||||
def create_group_sensors_custom(
|
||||
hass: HomeAssistant,
|
||||
group_name: str,
|
||||
sensor_config: dict[str, Any],
|
||||
@@ -216,7 +225,7 @@ async def create_group_sensors_custom(
|
||||
|
||||
sensor_config[CONF_UTILITY_METER_NET_CONSUMPTION] = True
|
||||
group_sensors.extend(
|
||||
await create_utility_meters(
|
||||
create_utility_meters(
|
||||
hass,
|
||||
energy_sensor,
|
||||
sensor_config,
|
||||
@@ -273,64 +282,83 @@ async def resolve_entity_ids_recursively(
|
||||
if resolved_ids is None:
|
||||
resolved_ids = set()
|
||||
|
||||
def add_member_entry_ids() -> None:
|
||||
"""Add power/energy sensors from the group member entries."""
|
||||
member_entry_ids = entry.data.get(CONF_GROUP_MEMBER_SENSORS) or []
|
||||
for member_entry_id in member_entry_ids:
|
||||
member_entry = hass.config_entries.async_get_entry(member_entry_id)
|
||||
if member_entry is None:
|
||||
continue
|
||||
|
||||
key = resolve_key_based_on_device_class(member_entry)
|
||||
if key and key in member_entry.data:
|
||||
resolved_ids.add(str(member_entry.data.get(key)))
|
||||
|
||||
def resolve_key_based_on_device_class(member_entry: ConfigEntry) -> str | None:
|
||||
"""Resolve the correct key for power/energy sensor based on device class."""
|
||||
if member_entry.data.get(CONF_SENSOR_TYPE) == SensorType.REAL_POWER:
|
||||
return CONF_ENTITY_ID if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY
|
||||
return ENTRY_DATA_POWER_ENTITY if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY
|
||||
|
||||
def add_specified_sensors() -> None:
|
||||
"""Add additional power/energy sensors specified by the user."""
|
||||
conf_key = CONF_GROUP_POWER_ENTITIES if device_class == SensorDeviceClass.POWER else CONF_GROUP_ENERGY_ENTITIES
|
||||
resolved_ids.update(entry.data.get(conf_key) or [])
|
||||
|
||||
async def add_include_based_sensors() -> None:
|
||||
"""Add entities from the defined areas, devices and floors."""
|
||||
if all(k not in entry.data for k in (CONF_AREA, CONF_FLOOR, CONF_GROUP_MEMBER_DEVICES)):
|
||||
return
|
||||
|
||||
result = await find_entities(
|
||||
hass,
|
||||
await build_entity_include_filter(hass, entry),
|
||||
bool(entry.data.get(CONF_INCLUDE_NON_POWERCALC_SENSORS)),
|
||||
)
|
||||
resolved_ids.update(filter_entity_list_by_class(result.resolved, device_class))
|
||||
|
||||
async def add_subgroup_entities() -> None:
|
||||
"""Recursively add entities from subgroups."""
|
||||
subgroups = entry.data.get(CONF_SUB_GROUPS)
|
||||
if not subgroups:
|
||||
return
|
||||
|
||||
for subgroup_entry_id in subgroups:
|
||||
subgroup_entry = hass.config_entries.async_get_entry(subgroup_entry_id)
|
||||
if subgroup_entry is None:
|
||||
_LOGGER.error("Subgroup config entry not found: %s", subgroup_entry_id)
|
||||
continue
|
||||
|
||||
await resolve_entity_ids_recursively(hass, subgroup_entry, device_class, resolved_ids)
|
||||
|
||||
# Process the main logic
|
||||
add_member_entry_ids()
|
||||
add_specified_sensors()
|
||||
await add_include_based_sensors()
|
||||
await add_subgroup_entities()
|
||||
_add_member_entry_ids(hass, entry, device_class, resolved_ids)
|
||||
_add_specified_sensors(entry, device_class, resolved_ids)
|
||||
await _add_include_based_sensors(hass, entry, device_class, resolved_ids)
|
||||
await _add_subgroup_entities(hass, entry, device_class, resolved_ids)
|
||||
|
||||
return resolved_ids
|
||||
|
||||
|
||||
def _add_member_entry_ids(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
device_class: SensorDeviceClass,
|
||||
resolved_ids: set[str],
|
||||
) -> None:
|
||||
"""Add power/energy sensors from the group member entries."""
|
||||
member_entry_ids = entry.data.get(CONF_GROUP_MEMBER_SENSORS) or []
|
||||
for member_entry_id in member_entry_ids:
|
||||
member_entry = hass.config_entries.async_get_entry(member_entry_id)
|
||||
if member_entry is None:
|
||||
continue
|
||||
|
||||
key = _resolve_key_based_on_device_class(member_entry, device_class)
|
||||
if key and key in member_entry.data:
|
||||
resolved_ids.add(str(member_entry.data.get(key)))
|
||||
|
||||
|
||||
def _resolve_key_based_on_device_class(member_entry: ConfigEntry, device_class: SensorDeviceClass) -> str | None:
|
||||
"""Resolve the correct key for power/energy sensor based on device class."""
|
||||
if member_entry.data.get(CONF_SENSOR_TYPE) == SensorType.REAL_POWER:
|
||||
return CONF_ENTITY_ID if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY
|
||||
return ENTRY_DATA_POWER_ENTITY if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY
|
||||
|
||||
|
||||
def _add_specified_sensors(entry: ConfigEntry, device_class: SensorDeviceClass, resolved_ids: set[str]) -> None:
|
||||
"""Add additional power/energy sensors specified by the user."""
|
||||
conf_key = CONF_GROUP_POWER_ENTITIES if device_class == SensorDeviceClass.POWER else CONF_GROUP_ENERGY_ENTITIES
|
||||
resolved_ids.update(entry.data.get(conf_key) or [])
|
||||
|
||||
|
||||
async def _add_include_based_sensors(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
device_class: SensorDeviceClass,
|
||||
resolved_ids: set[str],
|
||||
) -> None:
|
||||
"""Add entities from the defined areas, devices and floors."""
|
||||
if all(k not in entry.data for k in (CONF_AREA, CONF_FLOOR, CONF_GROUP_MEMBER_DEVICES)):
|
||||
return
|
||||
|
||||
result = await find_entities(
|
||||
hass,
|
||||
await build_entity_include_filter(hass, entry),
|
||||
bool(entry.data.get(CONF_INCLUDE_NON_POWERCALC_SENSORS)),
|
||||
)
|
||||
resolved_ids.update(filter_entity_list_by_class(result.resolved, device_class))
|
||||
|
||||
|
||||
async def _add_subgroup_entities(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
device_class: SensorDeviceClass,
|
||||
resolved_ids: set[str],
|
||||
) -> None:
|
||||
"""Recursively add entities from subgroups."""
|
||||
subgroups = entry.data.get(CONF_SUB_GROUPS)
|
||||
if not subgroups:
|
||||
return
|
||||
|
||||
for subgroup_entry_id in subgroups:
|
||||
subgroup_entry = hass.config_entries.async_get_entry(subgroup_entry_id)
|
||||
if subgroup_entry is None:
|
||||
_LOGGER.error("Subgroup config entry not found: %s", subgroup_entry_id)
|
||||
continue
|
||||
|
||||
await resolve_entity_ids_recursively(hass, subgroup_entry, device_class, resolved_ids)
|
||||
|
||||
|
||||
@callback
|
||||
def create_grouped_power_sensor(
|
||||
hass: HomeAssistant,
|
||||
@@ -448,11 +476,17 @@ class GroupedSensor(BaseEntity, SensorEntity):
|
||||
self._entities = entities
|
||||
self._sensor_config = sensor_config
|
||||
if self._is_energy_sensor:
|
||||
self._rounding_digits = int(sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION))
|
||||
self._update_interval: int = int(sensor_config.get(CONF_GROUP_ENERGY_UPDATE_INTERVAL, DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL))
|
||||
self._rounding_digits = int(
|
||||
sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION),
|
||||
)
|
||||
self._update_interval: int = int(
|
||||
sensor_config.get(CONF_GROUP_ENERGY_UPDATE_INTERVAL, DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL),
|
||||
)
|
||||
else:
|
||||
self._rounding_digits = int(sensor_config.get(CONF_POWER_SENSOR_PRECISION, DEFAULT_POWER_SENSOR_PRECISION))
|
||||
self._update_interval = int(sensor_config.get(CONF_GROUP_POWER_UPDATE_INTERVAL, DEFAULT_GROUP_POWER_UPDATE_INTERVAL))
|
||||
self._update_interval = int(
|
||||
sensor_config.get(CONF_GROUP_POWER_UPDATE_INTERVAL, DEFAULT_GROUP_POWER_UPDATE_INTERVAL),
|
||||
)
|
||||
self._attr_suggested_display_precision = self._rounding_digits
|
||||
if unique_id:
|
||||
self._attr_unique_id = unique_id
|
||||
@@ -523,7 +557,11 @@ class GroupedSensor(BaseEntity, SensorEntity):
|
||||
domain = self._sensor_config.get(CONF_DOMAIN)
|
||||
if domain == CONF_ALL:
|
||||
entity_registry = er.async_get(self.hass)
|
||||
entities = {entity.entity_id for entity in entity_registry.entities.values() if entity.device_class == self.device_class}
|
||||
entities = {
|
||||
entity.entity_id
|
||||
for entity in entity_registry.entities.values()
|
||||
if entity.device_class == self.device_class
|
||||
}
|
||||
else:
|
||||
entities = self.hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES).get(domain, [])
|
||||
entities = filter_entity_list_by_class(
|
||||
@@ -533,7 +571,7 @@ class GroupedSensor(BaseEntity, SensorEntity):
|
||||
excluded_entities = self._sensor_config.get(CONF_EXCLUDE_ENTITIES) or []
|
||||
self._entities = set({entity for entity in entities if entity not in excluded_entities})
|
||||
|
||||
async def on_start(self, _: Any) -> None: # noqa
|
||||
async def on_start(self, _: HomeAssistant) -> None:
|
||||
"""Initialize group sensor when HA is starting."""
|
||||
await self.init_domain_group()
|
||||
|
||||
@@ -557,7 +595,9 @@ class GroupedSensor(BaseEntity, SensorEntity):
|
||||
"""Initial update for the group sensor state."""
|
||||
all_states = [self.hass.states.get(entity_id) for entity_id in self._entities]
|
||||
states: list[State] = list(filter(None, all_states))
|
||||
available_states = [state for state in states if state and state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE]]
|
||||
available_states = [
|
||||
state for state in states if state and state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
|
||||
]
|
||||
if not available_states and not self._ignore_unavailable_state:
|
||||
new_state: Decimal | str = STATE_UNAVAILABLE
|
||||
else:
|
||||
@@ -652,6 +692,37 @@ class GroupedSensor(BaseEntity, SensorEntity):
|
||||
def get_group_entities(self) -> dict[str, set[str]]:
|
||||
return {ATTR_ENTITIES: self._entities}
|
||||
|
||||
def debug_group(self) -> dict[str, Any]:
|
||||
members: dict[str, dict[str, str | None]] = {}
|
||||
for entity_id in sorted(self._entities):
|
||||
members[entity_id] = self._get_member_debug_info(entity_id)
|
||||
|
||||
return {
|
||||
ATTR_STATE: str(self.state),
|
||||
ATTR_UNIT_OF_MEASUREMENT: self.native_unit_of_measurement,
|
||||
ATTR_MEMBERS: members,
|
||||
}
|
||||
|
||||
def _get_member_debug_info(self, entity_id: str) -> dict[str, str | None]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is None:
|
||||
return {
|
||||
ATTR_STATE: None,
|
||||
ATTR_UNIT_OF_MEASUREMENT: None,
|
||||
}
|
||||
|
||||
if state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
|
||||
return {
|
||||
ATTR_STATE: str(state.state),
|
||||
ATTR_UNIT_OF_MEASUREMENT: state.attributes.get(ATTR_UNIT_OF_MEASUREMENT),
|
||||
}
|
||||
|
||||
converted_value = round(self._get_state_value_in_native_unit(state), self._rounding_digits)
|
||||
return {
|
||||
ATTR_STATE: str(converted_value),
|
||||
ATTR_UNIT_OF_MEASUREMENT: self.native_unit_of_measurement,
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
def calculate_initial_state(
|
||||
self,
|
||||
@@ -681,7 +752,9 @@ class GroupedPowerSensor(GroupedSensor, PowerSensor):
|
||||
member_available_states: list[State],
|
||||
member_states: list[State],
|
||||
) -> Decimal | str:
|
||||
self._member_states = {state.entity_id: self._get_state_value_in_native_unit(state) for state in member_available_states}
|
||||
self._member_states = {
|
||||
state.entity_id: self._get_state_value_in_native_unit(state) for state in member_available_states
|
||||
}
|
||||
return self.get_summed_state()
|
||||
|
||||
def calculate_new_state(self, state: State) -> Decimal | str:
|
||||
@@ -776,8 +849,9 @@ class GroupedEnergySensor(GroupedSensor, RestoreSensor, EnergySensor):
|
||||
member_available_states: list[State],
|
||||
member_states: list[State],
|
||||
) -> Decimal:
|
||||
"""Calculate the new group energy sensor state
|
||||
For each member sensor we calculate the delta by looking at the previous known state and compare it to the current.
|
||||
"""Calculate the new group energy sensor state.
|
||||
|
||||
For each member, calculate the delta between the previous known state and the current.
|
||||
"""
|
||||
group_sum = Decimal(self._native_value_exact) if self._native_value_exact else Decimal(0)
|
||||
_LOGGER.debug("%s: Recalculate, current value: %s", self.entity_id, group_sum)
|
||||
@@ -830,10 +904,7 @@ class GroupedEnergySensor(GroupedSensor, RestoreSensor, EnergySensor):
|
||||
)
|
||||
|
||||
start_at_zero = self._sensor_config.get(CONF_GROUP_ENERGY_START_AT_ZERO, True)
|
||||
if prev_state is None and start_at_zero: # noqa: SIM108
|
||||
delta = Decimal(0)
|
||||
else:
|
||||
delta = cur_value - prev_value
|
||||
delta = Decimal(0) if prev_state is None and start_at_zero else cur_value - prev_value
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG): # pragma: no cover
|
||||
_LOGGER.debug(
|
||||
@@ -890,7 +961,9 @@ class PreviousStateStore:
|
||||
_LOGGER.debug("Load previous energy sensor states from store")
|
||||
stored_states = await instance.store.async_load() or {}
|
||||
for group, entities in stored_states.items():
|
||||
instance.states[group] = {entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items()}
|
||||
instance.states[group] = {
|
||||
entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items()
|
||||
}
|
||||
except HomeAssistantError as exc: # pragma: no cover
|
||||
_LOGGER.error("Error loading previous energy sensor states", exc_info=exc)
|
||||
|
||||
@@ -943,7 +1016,7 @@ class PreviousStateStore:
|
||||
def async_setup_dump(self) -> None:
|
||||
"""Set up the listeners for persistence."""
|
||||
|
||||
async def _async_dump_states(*_: Any) -> None: # noqa: ANN401
|
||||
async def _async_dump_states(*_: object) -> None:
|
||||
await self.persist_states()
|
||||
|
||||
# Dump states periodically
|
||||
@@ -951,9 +1024,10 @@ class PreviousStateStore:
|
||||
self.hass,
|
||||
_async_dump_states,
|
||||
STATE_DUMP_INTERVAL,
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
|
||||
async def _async_dump_states_at_stop(*_: Any) -> None: # noqa: ANN401
|
||||
async def _async_dump_states_at_stop(*_: object) -> None:
|
||||
cancel_interval()
|
||||
await self.persist_states()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from custom_components.powercalc.const import CONF_GROUP_TYPE, GroupType
|
||||
from custom_components.powercalc.sensors.group.custom import create_group_sensors_custom
|
||||
|
||||
|
||||
async def create_domain_group_sensor(
|
||||
def create_domain_group_sensor(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
) -> list[Entity]:
|
||||
@@ -16,7 +16,7 @@ async def create_domain_group_sensor(
|
||||
if CONF_UNIQUE_ID not in config:
|
||||
config[CONF_UNIQUE_ID] = generate_unique_id(config)
|
||||
config[CONF_GROUP_TYPE] = GroupType.DOMAIN
|
||||
return await create_group_sensors_custom(
|
||||
return create_group_sensors_custom(
|
||||
hass,
|
||||
name,
|
||||
config,
|
||||
|
||||
@@ -24,12 +24,12 @@ async def create_group_sensors(
|
||||
collect_analytics(hass, config_entry).inc(DATA_GROUP_TYPES, group_type)
|
||||
|
||||
if group_type == GroupType.DOMAIN:
|
||||
return await domain_group.create_domain_group_sensor(
|
||||
return domain_group.create_domain_group_sensor(
|
||||
hass,
|
||||
sensor_config,
|
||||
)
|
||||
if group_type == GroupType.STANDBY:
|
||||
return await standby_group.create_general_standby_sensors(hass, sensor_config)
|
||||
return standby_group.create_general_standby_sensors(hass, sensor_config)
|
||||
|
||||
if group_type == GroupType.CUSTOM:
|
||||
if config_entry:
|
||||
@@ -38,14 +38,14 @@ async def create_group_sensors(
|
||||
entry=config_entry,
|
||||
sensor_config=sensor_config,
|
||||
)
|
||||
return await custom_group.create_group_sensors_yaml(
|
||||
return custom_group.create_group_sensors_yaml(
|
||||
hass=hass,
|
||||
sensor_config=sensor_config,
|
||||
entities=entities or [],
|
||||
)
|
||||
|
||||
if group_type == GroupType.SUBTRACT:
|
||||
return await subtract_group.create_subtract_group_sensors(
|
||||
return subtract_group.create_subtract_group_sensors(
|
||||
hass=hass,
|
||||
config=sensor_config,
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ from custom_components.powercalc.sensors.power import PowerSensor
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_general_standby_sensors(
|
||||
def create_general_standby_sensors(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
) -> list[Entity]:
|
||||
@@ -44,8 +44,8 @@ async def create_general_standby_sensors(
|
||||
power_sensor.entity_id = "sensor.all_standby_power"
|
||||
sensor_config = config.copy()
|
||||
sensor_config[CONF_NAME] = "All standby"
|
||||
source_entity = await create_source_entity(DUMMY_ENTITY_ID, hass)
|
||||
energy_sensor = await create_energy_sensor(
|
||||
source_entity = create_source_entity(DUMMY_ENTITY_ID, hass)
|
||||
energy_sensor = create_energy_sensor(
|
||||
hass,
|
||||
sensor_config,
|
||||
power_sensor,
|
||||
@@ -80,8 +80,8 @@ class StandbyPowerSensor(SensorEntity, PowerSensor):
|
||||
"""Calculate sum of all power sensors in standby, and update the state of the sensor."""
|
||||
if self.standby_sensors:
|
||||
self._attr_native_value = Decimal(
|
||||
round( # type: ignore
|
||||
sum(self.standby_sensors.values()),
|
||||
round(
|
||||
sum(self.standby_sensors.values(), Decimal(0)),
|
||||
self._rounding_digits,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ from custom_components.powercalc.sensors.utility_meter import create_utility_met
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_subtract_group_sensors(
|
||||
def create_subtract_group_sensors(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
) -> list[Entity]:
|
||||
@@ -58,7 +58,7 @@ async def create_subtract_group_sensors(
|
||||
)
|
||||
sensors.append(power_sensor)
|
||||
if config.get(CONF_CREATE_ENERGY_SENSORS):
|
||||
energy_sensor = await create_energy_sensor(
|
||||
energy_sensor = create_energy_sensor(
|
||||
hass,
|
||||
config,
|
||||
power_sensor,
|
||||
@@ -67,7 +67,7 @@ async def create_subtract_group_sensors(
|
||||
|
||||
config[CONF_UTILITY_METER_NET_CONSUMPTION] = True
|
||||
sensors.extend(
|
||||
await create_utility_meters(
|
||||
create_utility_meters(
|
||||
hass,
|
||||
energy_sensor,
|
||||
config,
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -53,7 +52,11 @@ async def find_auto_tracked_power_entities(hass: HomeAssistant, exclude_entities
|
||||
if exclude_entities:
|
||||
entity_filter = LambdaFilter(lambda entity: entity.entity_id not in exclude_entities)
|
||||
result = await find_entities(hass, entity_filter)
|
||||
return {entity.entity_id for entity in result.resolved if isinstance(entity, PowerSensor) and not isinstance(entity, GroupedSensor)}
|
||||
return {
|
||||
entity.entity_id
|
||||
for entity in result.resolved
|
||||
if isinstance(entity, PowerSensor) and not isinstance(entity, GroupedSensor)
|
||||
}
|
||||
|
||||
|
||||
class TrackedPowerSensorFactory:
|
||||
@@ -67,7 +70,9 @@ class TrackedPowerSensorFactory:
|
||||
"""Create tracked/untracked group sensors."""
|
||||
|
||||
unique_id = str(self.config.get(CONF_UNIQUE_ID))
|
||||
main_power_sensor = str(self.config.get(CONF_MAIN_POWER_SENSOR)) if self.config.get(CONF_MAIN_POWER_SENSOR) else None
|
||||
main_power_sensor = (
|
||||
str(self.config.get(CONF_MAIN_POWER_SENSOR)) if self.config.get(CONF_MAIN_POWER_SENSOR) else None
|
||||
)
|
||||
self.config[CONF_DISABLE_EXTENDED_ATTRIBUTES] = True # prevent adding all entities in the state attributes
|
||||
|
||||
self.tracked_entities = await self.get_tracked_power_entities()
|
||||
@@ -83,7 +88,7 @@ class TrackedPowerSensorFactory:
|
||||
energy_sensor = await self.create_energy_sensor(SensorType.TRACKED, tracked_sensor)
|
||||
entities.append(energy_sensor)
|
||||
entities.extend(
|
||||
await create_utility_meters(
|
||||
create_utility_meters(
|
||||
self.hass,
|
||||
energy_sensor,
|
||||
{CONF_UTILITY_METER_NET_CONSUMPTION: True, **self.config},
|
||||
@@ -102,7 +107,7 @@ class TrackedPowerSensorFactory:
|
||||
energy_sensor = await self.create_energy_sensor(SensorType.UNTRACKED, untracked_sensor)
|
||||
entities.append(energy_sensor)
|
||||
entities.extend(
|
||||
await create_utility_meters(
|
||||
create_utility_meters(
|
||||
self.hass,
|
||||
energy_sensor,
|
||||
{CONF_UTILITY_METER_NET_CONSUMPTION: True, **self.config},
|
||||
@@ -116,12 +121,15 @@ class TrackedPowerSensorFactory:
|
||||
Get all power entities which are part of the tracked sensor group
|
||||
"""
|
||||
if not bool(self.config.get(CONF_GROUP_TRACKED_AUTO, False)):
|
||||
return set(self.config.get(CONF_GROUP_TRACKED_POWER_ENTITIES)) # type: ignore
|
||||
tracked_entities: list[str] | None = self.config.get(CONF_GROUP_TRACKED_POWER_ENTITIES)
|
||||
if not isinstance(tracked_entities, list):
|
||||
return set()
|
||||
return set(tracked_entities)
|
||||
|
||||
# For auto mode, we also want to listen for any changes in the entity registry
|
||||
# Dynamically add/remove power sensors from the tracked group
|
||||
@callback
|
||||
def _start_entity_registry_listener(_: Any) -> None: # noqa ANN401
|
||||
def _start_entity_registry_listener(_: Event) -> None:
|
||||
self.hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated)
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _start_entity_registry_listener)
|
||||
@@ -137,8 +145,9 @@ class TrackedPowerSensorFactory:
|
||||
entity_id = event.data["entity_id"]
|
||||
action = event.data["action"]
|
||||
|
||||
if action == "update" and "old_entity_id" in event.data:
|
||||
if event.data["old_entity_id"] in self.tracked_entities: # type: ignore
|
||||
old_entity_id = event.data.get("old_entity_id")
|
||||
if action == "update" and old_entity_id is not None:
|
||||
if old_entity_id in self.tracked_entities:
|
||||
return await self.reload()
|
||||
return None # pragma: no cover
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from copy import copy
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -25,6 +26,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
@@ -37,6 +39,7 @@ import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.event import (
|
||||
EventStateChangedData,
|
||||
TrackTemplate,
|
||||
TrackTemplateResult,
|
||||
async_call_later,
|
||||
async_track_state_change_event,
|
||||
async_track_template_result,
|
||||
@@ -122,7 +125,7 @@ async def create_power_sensor(
|
||||
"""Create the power sensor based on powercalc sensor configuration."""
|
||||
if CONF_POWER_SENSOR_ID in sensor_config:
|
||||
# Use an existing power sensor, only create energy sensors / utility meters
|
||||
return await create_real_power_sensor(hass, sensor_config)
|
||||
return create_real_power_sensor(hass, sensor_config)
|
||||
|
||||
return await create_virtual_power_sensor(
|
||||
hass,
|
||||
@@ -147,7 +150,11 @@ async def create_virtual_power_sensor(
|
||||
if CONF_CALCULATION_ENABLED_CONDITION not in sensor_config and power_profile.calculation_enabled_condition:
|
||||
sensor_config[CONF_CALCULATION_ENABLED_CONDITION] = power_profile.calculation_enabled_condition
|
||||
|
||||
if config_entry and await power_profile.requires_manual_sub_profile_selection and "/" not in sensor_config.get(CONF_MODEL, ""):
|
||||
if (
|
||||
config_entry
|
||||
and await power_profile.requires_manual_sub_profile_selection
|
||||
and "/" not in sensor_config.get(CONF_MODEL, "")
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -181,10 +188,14 @@ async def create_virtual_power_sensor(
|
||||
a = collect_analytics(hass, config_entry)
|
||||
a.inc(DATA_STRATEGIES, strategy)
|
||||
a.add(DATA_POWER_PROFILES, power_profile)
|
||||
a.inc(DATA_POWER_PROFILE_SOURCES, power_profile.configuration_source if power_profile else PowerProfileSource.MANUAL)
|
||||
a.inc(
|
||||
DATA_POWER_PROFILE_SOURCES,
|
||||
power_profile.configuration_source if power_profile else PowerProfileSource.MANUAL,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Creating power sensor (entity_id=%s entity_category=%s, sensor_name=%s strategy=%s manufacturer=%s model=%s unique_id=%s)",
|
||||
"Creating power sensor (entity_id=%s entity_category=%s, sensor_name=%s strategy=%s "
|
||||
"manufacturer=%s model=%s unique_id=%s)",
|
||||
source_entity.entity_id,
|
||||
entity_category,
|
||||
name,
|
||||
@@ -313,7 +324,7 @@ def _get_standby_power(
|
||||
return standby_power, standby_power_on
|
||||
|
||||
|
||||
async def create_real_power_sensor(
|
||||
def create_real_power_sensor(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: dict,
|
||||
) -> RealPowerSensor:
|
||||
@@ -450,7 +461,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
new_state,
|
||||
)
|
||||
|
||||
async def template_change_listener(*_: Any) -> None: # noqa: ANN401
|
||||
async def template_change_listener(*_: object) -> None:
|
||||
"""Handle for state changes for referenced templates."""
|
||||
state = self.hass.states.get(self._source_entity.entity_id)
|
||||
await self._handle_source_entity_state_change(
|
||||
@@ -461,9 +472,9 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
async def initial_update(hass: HomeAssistant) -> None:
|
||||
"""Calculate initial value and push state"""
|
||||
|
||||
# When using reload service energy sensor became unavailable
|
||||
# This is caused because state change listener of energy sensor is registered before power sensor pushes initial update
|
||||
# Adding sleep 0 fixes this issue.
|
||||
# When using reload service the energy sensor became unavailable.
|
||||
# This is caused because the state change listener of the energy sensor is registered
|
||||
# before the power sensor pushes its initial update. Adding sleep 0 fixes this issue.
|
||||
await asyncio.sleep(0)
|
||||
if self._strategy_instance:
|
||||
await self._strategy_instance.on_start(hass)
|
||||
@@ -472,7 +483,9 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
if (not entities and self._source_entity.entity_id == DUMMY_ENTITY_ID) or not entities:
|
||||
entities.add(DUMMY_ENTITY_ID)
|
||||
for entity_id in entities:
|
||||
new_state = self.hass.states.get(entity_id) if entity_id != DUMMY_ENTITY_ID else State(entity_id, STATE_ON)
|
||||
new_state = (
|
||||
self.hass.states.get(entity_id) if entity_id != DUMMY_ENTITY_ID else State(entity_id, STATE_ON)
|
||||
)
|
||||
await self._handle_source_entity_state_change(
|
||||
entity_id,
|
||||
new_state,
|
||||
@@ -480,30 +493,56 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
|
||||
# Add listeners for all tracking entities and templates.
|
||||
entities_to_track = self._get_tracking_entities()
|
||||
|
||||
self._track_entities = {e for e in entities_to_track if isinstance(e, str)}
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(self.hass, self._track_entities, appliance_state_listener),
|
||||
)
|
||||
|
||||
track_templates: list[TrackTemplate] = [e for e in entities_to_track if isinstance(e, TrackTemplate)]
|
||||
if track_templates:
|
||||
async_track_template_result(self.hass, track_templates=track_templates, action=template_change_listener)
|
||||
self._register_tracking_listeners(entities_to_track, appliance_state_listener, template_change_listener)
|
||||
|
||||
# Trigger initial update
|
||||
self.async_on_remove(start.async_at_start(self.hass, initial_update))
|
||||
|
||||
if hasattr(self._strategy_instance, "set_update_callback"):
|
||||
self._strategy_instance.set_update_callback(self._update_power_sensor)
|
||||
self._strategy_instance.set_update_callback(self._update_power_sensor)
|
||||
|
||||
self._register_force_update_interval()
|
||||
|
||||
def _register_tracking_listeners(
|
||||
self,
|
||||
entities_to_track: list[str | TrackTemplate],
|
||||
appliance_state_listener: Callable[[Event[EventStateChangedData]], Awaitable[None]],
|
||||
template_change_listener: Callable[
|
||||
[Event[EventStateChangedData] | None, list[TrackTemplateResult]],
|
||||
Coroutine[Any, Any, None] | None,
|
||||
],
|
||||
) -> None:
|
||||
self._track_entities = {e for e in entities_to_track if isinstance(e, str)}
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(self.hass, self._track_entities, appliance_state_listener),
|
||||
)
|
||||
track_templates: list[TrackTemplate] = [e for e in entities_to_track if isinstance(e, TrackTemplate)]
|
||||
if track_templates:
|
||||
template_tracker = async_track_template_result(
|
||||
self.hass,
|
||||
track_templates=track_templates,
|
||||
action=template_change_listener,
|
||||
)
|
||||
self.async_on_remove(
|
||||
template_tracker.async_remove,
|
||||
)
|
||||
|
||||
def _register_force_update_interval(self) -> None:
|
||||
force_update_interval = self._sensor_config.get(CONF_POWER_UPDATE_INTERVAL, 0)
|
||||
if force_update_interval > 0:
|
||||
if force_update_interval <= 0:
|
||||
return
|
||||
|
||||
@callback
|
||||
def async_update(__: datetime | None = None) -> None:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
@callback
|
||||
def async_update(__: datetime | None = None) -> None:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_track_time_interval(self.hass, async_update, timedelta(seconds=force_update_interval))
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
async_update,
|
||||
timedelta(seconds=force_update_interval),
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
def _get_tracking_entities(self) -> list[str | TrackTemplate]:
|
||||
"""Return entities and templates that should be tracked."""
|
||||
@@ -616,16 +655,8 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
"""Calculate power consumption using configured strategy."""
|
||||
assert self._strategy_instance is not None
|
||||
|
||||
# Resolve the relevant entity state
|
||||
entity_state = state
|
||||
if self._source_entity.entity_id == DUMMY_ENTITY_ID and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH:
|
||||
if self._availability_entity and state.entity_id == self._availability_entity:
|
||||
entity_state = State(DUMMY_ENTITY_ID, STATE_ON)
|
||||
elif (
|
||||
self._calculation_strategy != CalculationStrategy.MULTI_SWITCH
|
||||
and state.entity_id != self._source_entity.entity_id
|
||||
and (entity_state := self.hass.states.get(self._source_entity.entity_id)) is None
|
||||
):
|
||||
entity_state = self._resolve_calculation_state(state)
|
||||
if entity_state is None:
|
||||
return None
|
||||
|
||||
# Handle unavailable power
|
||||
@@ -633,31 +664,56 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
if entity_state.state == STATE_UNAVAILABLE and unavailable_power is not None:
|
||||
return Decimal(unavailable_power)
|
||||
|
||||
# Handle standby power
|
||||
standby_power = None
|
||||
if entity_state.state in self._off_states or not await self.is_calculation_enabled(entity_state):
|
||||
if isinstance(self._strategy_instance, PlaybookStrategy):
|
||||
await self._strategy_instance.stop_playbook()
|
||||
standby_power = await self.calculate_standby_power(entity_state)
|
||||
self._standby_sensors[self.entity_id] = standby_power
|
||||
|
||||
if self._strategy_instance.can_calculate_standby() or self._calculation_strategy != CalculationStrategy.MULTI_SWITCH:
|
||||
return standby_power
|
||||
standby_power = await self._calculate_state_standby_power(entity_state)
|
||||
if standby_power is not None and (
|
||||
self._strategy_instance.can_calculate_standby()
|
||||
or self._calculation_strategy != CalculationStrategy.MULTI_SWITCH
|
||||
):
|
||||
return standby_power
|
||||
|
||||
# Calculate actual power using configured strategy
|
||||
power = await self._strategy_instance.calculate(entity_state)
|
||||
if power is None:
|
||||
return None
|
||||
|
||||
# Add standby power if available
|
||||
return Decimal(self._apply_power_adjustments(power, standby_power))
|
||||
|
||||
def _resolve_calculation_state(self, state: State) -> State | None:
|
||||
if (
|
||||
self._source_entity.entity_id == DUMMY_ENTITY_ID
|
||||
and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH
|
||||
):
|
||||
if self._availability_entity and state.entity_id == self._availability_entity:
|
||||
return State(DUMMY_ENTITY_ID, STATE_ON)
|
||||
return state
|
||||
|
||||
if (
|
||||
self._calculation_strategy == CalculationStrategy.MULTI_SWITCH
|
||||
or state.entity_id == self._source_entity.entity_id
|
||||
):
|
||||
return state
|
||||
|
||||
return cast(State | None, self.hass.states.get(self._source_entity.entity_id))
|
||||
|
||||
async def _calculate_state_standby_power(self, entity_state: State) -> Decimal | None:
|
||||
if entity_state.state not in self._off_states and await self.is_calculation_enabled(entity_state):
|
||||
return None
|
||||
|
||||
assert self._strategy_instance is not None
|
||||
if isinstance(self._strategy_instance, PlaybookStrategy):
|
||||
await self._strategy_instance.stop_playbook()
|
||||
|
||||
standby_power = await self.calculate_standby_power(entity_state)
|
||||
self._standby_sensors[self.entity_id] = standby_power
|
||||
return standby_power
|
||||
|
||||
def _apply_power_adjustments(self, power: Decimal, standby_power: Decimal | None) -> Decimal:
|
||||
if standby_power:
|
||||
power += standby_power
|
||||
|
||||
# Apply multiply factor to power
|
||||
if self._multiply_factor:
|
||||
power *= Decimal(self._multiply_factor)
|
||||
|
||||
# Add standby power-on adjustments if applicable
|
||||
if self._standby_power_on and not standby_power:
|
||||
additional_standby_power = self._standby_power_on
|
||||
self._standby_sensors[self.entity_id] = self._standby_power_on
|
||||
@@ -665,7 +721,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
additional_standby_power *= Decimal(self._multiply_factor)
|
||||
power += additional_standby_power
|
||||
|
||||
return Decimal(power)
|
||||
return power
|
||||
|
||||
async def _switch_sub_profile_dynamically(self, state: State) -> None:
|
||||
"""Dynamically select a different sub profile depending on the entity state or attributes
|
||||
@@ -698,7 +754,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
delay = sleep_power.get(CONF_DELAY) or 0
|
||||
|
||||
@callback
|
||||
def _update_sleep_power(*_: Any) -> None: # noqa: ANN401
|
||||
def _update_sleep_power(*_: object) -> None:
|
||||
power = Decimal(sleep_power.get(CONF_POWER) or 0)
|
||||
if self._multiply_factor_standby and self._multiply_factor:
|
||||
power *= Decimal(self._multiply_factor)
|
||||
@@ -707,14 +763,14 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
self._sleep_power_timer = async_call_later(
|
||||
self.hass,
|
||||
delay,
|
||||
_update_sleep_power,
|
||||
HassJob(_update_sleep_power, name=f"{self.entity_id} sleep power", cancel_on_shutdown=True),
|
||||
)
|
||||
|
||||
standby_power = self._standby_power
|
||||
if self._strategy_instance.can_calculate_standby():
|
||||
standby_power = await self._strategy_instance.calculate(state) or self._standby_power
|
||||
|
||||
evaluated = await evaluate_power(standby_power)
|
||||
evaluated = evaluate_power(standby_power)
|
||||
if evaluated is None:
|
||||
evaluated = Decimal(0)
|
||||
standby_power = evaluated
|
||||
@@ -775,9 +831,22 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
|
||||
raise HomeAssistantError("supported only playbook enabled sensors")
|
||||
return self._strategy_instance
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Cancel outstanding timers when the entity is removed."""
|
||||
if self._sleep_power_timer is not None:
|
||||
self._sleep_power_timer()
|
||||
self._sleep_power_timer = None
|
||||
if isinstance(self._strategy_instance, PlaybookStrategy):
|
||||
await self._strategy_instance.stop_playbook()
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
async def async_switch_sub_profile(self, profile: str) -> None:
|
||||
"""Switches to a new sub profile"""
|
||||
if not self._power_profile or not await self._power_profile.has_sub_profiles or self._power_profile.sub_profile_select:
|
||||
if (
|
||||
not self._power_profile
|
||||
or not await self._power_profile.has_sub_profiles
|
||||
or self._power_profile.sub_profile_select
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
"This is only supported for sensors having sub profiles, and no automatic profile selection",
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
GENERAL_TARIFF = "general"
|
||||
|
||||
|
||||
async def create_utility_meters(
|
||||
def create_utility_meters(
|
||||
hass: HomeAssistant,
|
||||
energy_sensor: EnergySensor,
|
||||
sensor_config: dict,
|
||||
@@ -62,7 +62,7 @@ async def create_utility_meters(
|
||||
unique_id = f"{energy_sensor.unique_id}_{meter_type}" if energy_sensor.unique_id else None
|
||||
if should_create_utility_meter(hass, unique_id, energy_sensor):
|
||||
utility_meters.extend(
|
||||
await create_meters_for_type(
|
||||
create_meters_for_type(
|
||||
hass,
|
||||
energy_sensor,
|
||||
sensor_config,
|
||||
@@ -98,7 +98,7 @@ def should_create_utility_meter(
|
||||
return not (existing_entity_id and hass.states.get(existing_entity_id)) # pragma: no cover
|
||||
|
||||
|
||||
async def create_meters_for_type(
|
||||
def create_meters_for_type(
|
||||
hass: HomeAssistant,
|
||||
energy_sensor: EnergySensor,
|
||||
sensor_config: dict,
|
||||
@@ -116,7 +116,7 @@ async def create_meters_for_type(
|
||||
|
||||
# Create generic utility meter
|
||||
if not tariffs or GENERAL_TARIFF in tariffs:
|
||||
utility_meter = await create_utility_meter(
|
||||
utility_meter = create_utility_meter(
|
||||
hass,
|
||||
energy_sensor.entity_id,
|
||||
entity_id,
|
||||
@@ -130,7 +130,7 @@ async def create_meters_for_type(
|
||||
|
||||
# Create tariff-specific utility meters
|
||||
if tariffs:
|
||||
new_tariff_sensors = await create_tariff_meters(
|
||||
new_tariff_sensors = create_tariff_meters(
|
||||
hass,
|
||||
energy_sensor,
|
||||
entity_id,
|
||||
@@ -148,7 +148,7 @@ async def create_meters_for_type(
|
||||
return utility_meters
|
||||
|
||||
|
||||
async def create_tariff_meters(
|
||||
def create_tariff_meters(
|
||||
hass: HomeAssistant,
|
||||
energy_sensor: EnergySensor,
|
||||
entity_id: str,
|
||||
@@ -161,11 +161,11 @@ async def create_tariff_meters(
|
||||
) -> list[VirtualUtilityMeter]:
|
||||
"""Create utility meters for specific tariffs."""
|
||||
filtered_tariffs = [t for t in tariffs if t != GENERAL_TARIFF]
|
||||
tariff_select = await create_tariff_select(config_entry, filtered_tariffs, hass, name, unique_id)
|
||||
tariff_select = create_tariff_select(config_entry, filtered_tariffs, hass, name, unique_id)
|
||||
|
||||
tariff_sensors = []
|
||||
for tariff in filtered_tariffs:
|
||||
utility_meter = await create_utility_meter(
|
||||
utility_meter = create_utility_meter(
|
||||
hass,
|
||||
energy_sensor.entity_id,
|
||||
entity_id,
|
||||
@@ -181,7 +181,7 @@ async def create_tariff_meters(
|
||||
return tariff_sensors
|
||||
|
||||
|
||||
async def create_tariff_select(
|
||||
def create_tariff_select(
|
||||
config_entry: ConfigEntry | None,
|
||||
tariffs: list,
|
||||
hass: HomeAssistant,
|
||||
@@ -215,7 +215,7 @@ async def create_tariff_select(
|
||||
return tariff_select
|
||||
|
||||
|
||||
async def create_utility_meter(
|
||||
def create_utility_meter(
|
||||
hass: HomeAssistant,
|
||||
source_entity: str,
|
||||
entity_id: str,
|
||||
@@ -258,7 +258,9 @@ async def create_utility_meter(
|
||||
params = {key: value for key, value in params.items() if key in signature.parameters}
|
||||
|
||||
utility_meter = VirtualUtilityMeter(**params) # type: ignore[no-untyped-call]
|
||||
utility_meter.rounding_digits = int(sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION))
|
||||
utility_meter.rounding_digits = int(
|
||||
sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION),
|
||||
)
|
||||
utility_meter.entity_id = entity_id
|
||||
|
||||
return utility_meter
|
||||
@@ -280,8 +282,10 @@ class VirtualUtilityMeter(UtilityMeterSensor, BaseEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType | Decimal:
|
||||
"""Return the state of the sensor."""
|
||||
value = self._state if hasattr(self, "_state") else self._attr_native_value # pre HA 2024.12 value was stored in _state
|
||||
value = (
|
||||
self._state if hasattr(self, "_state") else self._attr_native_value
|
||||
) # pre HA 2024.12 value was stored in _state
|
||||
if self.rounding_digits and value is not None:
|
||||
return Decimal(round(value, self.rounding_digits)) # type: ignore
|
||||
return Decimal(round(value, self.rounding_digits)) # type: ignore[arg-type]
|
||||
|
||||
return value # type: ignore
|
||||
return value # type: ignore[return-value]
|
||||
|
||||
Reference in New Issue
Block a user