Files
HomeAssistantVS/custom_components/powercalc/sensors/utility_meter.py
T
2026-07-14 23:57:03 -04:00

294 lines
9.0 KiB
Python

from __future__ import annotations
from decimal import Decimal
import inspect
import logging
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.utility_meter import DEFAULT_OFFSET
from homeassistant.components.utility_meter.const import (
DATA_TARIFF_SENSORS,
DATA_UTILITY,
)
from homeassistant.components.utility_meter.select import TariffSelect
from homeassistant.components.utility_meter.sensor import UtilityMeterSensor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import async_generate_entity_id
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util import slugify
from custom_components.powercalc.const import (
CONF_CREATE_UTILITY_METERS,
CONF_ENERGY_SENSOR_PRECISION,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_UTILITY_METER_NET_CONSUMPTION,
CONF_UTILITY_METER_OFFSET,
CONF_UTILITY_METER_TARIFFS,
CONF_UTILITY_METER_TYPES,
DEFAULT_ENERGY_SENSOR_PRECISION,
DOMAIN,
)
from custom_components.powercalc.select import DATA_PENDING_SELECT_ENTITIES, SIGNAL_CREATE_SELECT_ENTITIES
from .abstract import BaseEntity
from .energy import EnergySensor, RealEnergySensor
_LOGGER = logging.getLogger(__name__)
GENERAL_TARIFF = "general"
def create_utility_meters(
hass: HomeAssistant,
energy_sensor: EnergySensor,
sensor_config: dict,
config_entry: ConfigEntry | None = None,
) -> list[VirtualUtilityMeter]:
"""Create the utility meters."""
if not sensor_config.get(CONF_CREATE_UTILITY_METERS):
return []
if DATA_UTILITY not in hass.data: # pragma: no cover
hass.data[DATA_UTILITY] = {}
tariffs = list(sensor_config.get(CONF_UTILITY_METER_TARIFFS, []))
meter_types = list(sensor_config.get(CONF_UTILITY_METER_TYPES, []))
utility_meters = []
for meter_type in meter_types:
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(
create_meters_for_type(
hass,
energy_sensor,
sensor_config,
config_entry,
unique_id,
meter_type,
tariffs,
),
)
return utility_meters
def should_create_utility_meter(
hass: HomeAssistant,
unique_id: str | None,
energy_sensor: EnergySensor,
) -> bool:
"""
Check if a utility meter should be created.
Prevent duplicate creation of utility meter. See #1322
"""
if not isinstance(energy_sensor, RealEnergySensor) or not unique_id:
return True
entity_registry = er.async_get(hass)
existing_entity_id = entity_registry.async_get_entity_id(
domain=SENSOR_DOMAIN,
platform=DOMAIN,
unique_id=unique_id,
)
return not (existing_entity_id and hass.states.get(existing_entity_id)) # pragma: no cover
def create_meters_for_type(
hass: HomeAssistant,
energy_sensor: EnergySensor,
sensor_config: dict,
config_entry: ConfigEntry | None,
unique_id: str | None,
meter_type: str,
tariffs: list[str],
) -> list[VirtualUtilityMeter]:
"""Create meters for a specific meter type."""
name = f"{energy_sensor.name} {meter_type}"
entity_id = f"{energy_sensor.entity_id}_{slugify(meter_type)}"
tariff_sensors = []
utility_meters = []
# Create generic utility meter
if not tariffs or GENERAL_TARIFF in tariffs:
utility_meter = create_utility_meter(
hass,
energy_sensor.entity_id,
entity_id,
name,
sensor_config,
meter_type,
unique_id,
)
tariff_sensors.append(utility_meter)
utility_meters.append(utility_meter)
# Create tariff-specific utility meters
if tariffs:
new_tariff_sensors = create_tariff_meters(
hass,
energy_sensor,
entity_id,
name,
sensor_config,
config_entry,
meter_type,
unique_id,
tariffs,
)
tariff_sensors.extend(new_tariff_sensors)
utility_meters.extend(new_tariff_sensors)
hass.data[DATA_UTILITY][entity_id] = {DATA_TARIFF_SENSORS: tariff_sensors}
return utility_meters
def create_tariff_meters(
hass: HomeAssistant,
energy_sensor: EnergySensor,
entity_id: str,
name: str,
sensor_config: dict,
config_entry: ConfigEntry | None,
meter_type: str,
unique_id: str | None,
tariffs: list[str],
) -> list[VirtualUtilityMeter]:
"""Create utility meters for specific tariffs."""
filtered_tariffs = [t for t in tariffs if t != GENERAL_TARIFF]
tariff_select = create_tariff_select(config_entry, filtered_tariffs, hass, name, unique_id)
tariff_sensors = []
for tariff in filtered_tariffs:
utility_meter = create_utility_meter(
hass,
energy_sensor.entity_id,
entity_id,
name,
sensor_config,
meter_type,
unique_id,
tariff,
tariff_select.entity_id,
)
tariff_sensors.append(utility_meter)
return tariff_sensors
def create_tariff_select(
config_entry: ConfigEntry | None,
tariffs: list,
hass: HomeAssistant,
name: str,
unique_id: str | None,
) -> TariffSelect:
"""Create tariff selection entity."""
_LOGGER.debug("Creating utility_meter tariff select: %s", name)
select_unique_id = None
if unique_id:
select_unique_id = f"{unique_id}_select"
tariff_select = TariffSelect(
name,
tariffs,
unique_id=select_unique_id,
)
tariff_select.entity_id = async_generate_entity_id("select.{}", name, hass=hass)
key = config_entry.entry_id if config_entry else ""
pending = hass.data[DOMAIN].setdefault(DATA_PENDING_SELECT_ENTITIES, {}).setdefault(key, [])
pending.append(tariff_select)
async_dispatcher_send(
hass,
SIGNAL_CREATE_SELECT_ENTITIES.format(key),
[tariff_select],
)
return tariff_select
def create_utility_meter(
hass: HomeAssistant,
source_entity: str,
entity_id: str,
name: str,
sensor_config: dict,
meter_type: str,
unique_id: str | None = None,
tariff: str | None = None,
tariff_entity: str | None = None,
) -> VirtualUtilityMeter:
"""Create a utility meter entity, one per tariff."""
parent_meter = entity_id
if tariff:
name = f"{name} {tariff}"
entity_id = f"{entity_id}_{slugify(tariff)}"
if unique_id:
unique_id = f"{unique_id}_{tariff}"
_LOGGER.debug("Creating utility_meter sensor: %s (entity_id=%s)", name, entity_id)
params = {
"hass": hass,
"source_entity": source_entity,
"name": name,
"meter_type": meter_type,
"meter_offset": sensor_config.get(CONF_UTILITY_METER_OFFSET, DEFAULT_OFFSET),
"net_consumption": bool(sensor_config.get(CONF_UTILITY_METER_NET_CONSUMPTION, False)),
"tariff": tariff,
"tariff_entity": tariff_entity,
"parent_meter": parent_meter,
"delta_values": False,
"cron_pattern": None,
"periodically_resetting": False,
"sensor_always_available": sensor_config.get(CONF_IGNORE_UNAVAILABLE_STATE) or False,
"unique_id": unique_id,
}
signature = inspect.signature(UtilityMeterSensor.__init__)
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._sensor_config = sensor_config # noqa: SLF001
utility_meter.entity_id = entity_id
return utility_meter
class VirtualUtilityMeter(BaseEntity, UtilityMeterSensor):
rounding_digits: int = DEFAULT_ENERGY_SENSOR_PRECISION
_sensor_config: ConfigType
@property
def unique_id(self) -> str | None:
"""Return the unique id."""
return self._attr_unique_id
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return self.rounding_digits
@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
if self.rounding_digits and value is not None:
return Decimal(round(value, self.rounding_digits)) # type: ignore[arg-type]
return value # type: ignore[return-value]