Initil after Upgrade

This commit is contained in:
2026-06-15 10:53:52 -04:00
parent 2fe9bf0dd6
commit 887feaa50a
143 changed files with 2288 additions and 881 deletions
@@ -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
+21 -13
View File
@@ -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
@@ -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
+123 -54
View File
@@ -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]