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: ConfigType, 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: ConfigType, 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: ConfigType, 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[str], 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: ConfigType, 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]