393 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-27 17:16:54 +00:00
1 parent b05b7a897e
commit 39d97ac2db
404 files changed
+54006 -11034

No files matched your search

@@ -25,6 +25,7 @@ from custom_components.powercalc.const import (
DOMAIN,
)
from custom_components.powercalc.device_binding import bind_entity_to_registry_metadata
from custom_components.powercalc.device_naming import DeviceName
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
@@ -32,6 +33,17 @@ _LOGGER = logging.getLogger(__name__)
class BaseEntity(Entity):
device_name: DeviceName | None = None
def enable_device_naming(self) -> None:
"""Use a translated relative name after legacy names and IDs have been consumed."""
if self.device_name is None:
return
self._attr_has_entity_name = True
self._attr_translation_key = self.device_name.translation_key
self._attr_translation_placeholders = self.device_name.placeholders or {}
del self._attr_name
async def async_added_to_hass(self) -> None:
"""Bind configured registry metadata."""
await super().async_added_to_hass()
+7 -1
View File
@@ -30,6 +30,7 @@ from custom_components.powercalc.const import (
DOMAIN,
DOMAIN_CONFIG,
)
from custom_components.powercalc.device_naming import DeviceName
from custom_components.powercalc.unit import convert_to_decimal, parse_decimal
from .abstract import (
@@ -199,7 +200,7 @@ def create_cost_sensor(
price_config,
)
return CostSensor(
cost_sensor = CostSensor(
hass=hass,
source_energy_entity=energy_sensor.entity_id,
entity_id=entity_id,
@@ -209,6 +210,10 @@ def create_cost_sensor(
price_config=price_config,
reset_on_source_reset=reset_on_source_reset,
)
if isinstance(energy_sensor, VirtualUtilityMeter):
meter_name = energy_sensor.device_name
cost_sensor.device_name = DeviceName(f"{meter_name.translation_key}_cost", meter_name.placeholders)
return cost_sensor
def create_cost_sensor_for_energy_entity(hass: HomeAssistant, sensor_config: ConfigType) -> CostSensor | None:
@@ -234,6 +239,7 @@ def create_cost_sensor_for_energy_entity(hass: HomeAssistant, sensor_config: Con
class CostSensor(BaseEntity, RestoreEntity, SensorEntity):
"""Cost sensor, accumulating the cost of the energy consumed at price-at-consumption."""
device_name = DeviceName("cost")
_attr_device_class = SensorDeviceClass.MONETARY
_attr_state_class = SensorStateClass.TOTAL
_attr_should_poll = False
@@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
)
@@ -181,7 +182,9 @@ class DailyEnergySensor(EnergySensor, RestoreEntity, SensorEntity):
self._hass = hass
self._attr_name = name
self._state: Decimal = Decimal(0)
self._attr_entity_category = sensor_config.get(CONF_ENERGY_SENSOR_CATEGORY)
entity_category = sensor_config.get(CONF_ENERGY_SENSOR_CATEGORY)
if entity_category:
self._attr_entity_category = EntityCategory(entity_category)
self._value = value
self._user_unit_of_measurement = user_unit_of_measurement
self._update_frequency = update_frequency
+26 -1
View File
@@ -4,7 +4,7 @@ import inspect
import logging
from typing import Any
from homeassistant.components.integration.sensor import IntegrationSensor
from homeassistant.components.integration.sensor import IntegrationSensor, IntegrationSensorExtraStoredData
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
@@ -39,6 +39,7 @@ from custom_components.powercalc.const import (
UNAVAILABLE_STATES,
UnitPrefix,
)
from custom_components.powercalc.device_naming import DeviceName
from custom_components.powercalc.errors import SensorConfigurationError
from custom_components.powercalc.filter.outlier import OutlierFilter
@@ -282,9 +283,20 @@ class EnergySensor(BaseEntity):
"""Class which all energy sensors should extend from."""
class VirtualEnergyExtraStoredData(IntegrationSensorExtraStoredData):
"""Preserve zero totals which HA's integration serializer treats as missing."""
def as_dict(self) -> dict[str, Any]:
data = super().as_dict()
data["last_valid_state"] = str(self.last_valid_state) if self.last_valid_state is not None else None
return data
class VirtualEnergySensor(IntegrationSensor, EnergySensor):
"""Virtual energy sensor, totalling kWh."""
device_name = DeviceName("energy")
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_unrecorded_attributes = frozenset({ATTR_SOURCE_DOMAIN, ATTR_SOURCE_ENTITY})
@@ -438,6 +450,15 @@ class VirtualEnergySensor(IntegrationSensor, EnergySensor):
attrs.update(super_attrs)
return attrs
@property
def extra_restore_state_data(self) -> VirtualEnergyExtraStoredData:
return VirtualEnergyExtraStoredData(
self.native_value,
self.native_unit_of_measurement,
self._source_entity,
self._last_valid_state,
)
@property
def icon(self) -> str:
return ENERGY_ICON
@@ -446,17 +467,21 @@ class VirtualEnergySensor(IntegrationSensor, EnergySensor):
def async_reset(self) -> None:
_LOGGER.debug("%s: Reset energy sensor", self.entity_id)
self._state = Decimal(0)
self._last_valid_state = self._state
self.async_write_ha_state()
async def async_calibrate(self, value: str) -> None:
_LOGGER.debug("%s: Calibrate energy sensor to: %s", self.entity_id, value)
self._state = Decimal(value)
self._last_valid_state = self._state
self.async_write_ha_state()
class VirtualStandbyEnergySensor(VirtualEnergySensor):
"""Energy sensor integrating only the standby portion of a virtual power sensor."""
device_name = DeviceName("standby_energy")
def __init__(
self,
hass: HomeAssistant,
@@ -5,6 +5,7 @@ from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import CONF_CREATE_COST_SENSOR
from custom_components.powercalc.device_binding import get_device_entry
from .cost import create_cost_sensor
from .energy import EnergySensor
@@ -19,6 +20,7 @@ def create_energy_related_sensors(
config_entry: ConfigEntry | None = None,
utility_meter_config: ConfigType | None = None,
cost_name: str | None = None,
follow_device_name: bool = False,
) -> list[Entity]:
"""Create optional utility meters and cost sensor for an energy sensor.
@@ -27,7 +29,15 @@ def create_energy_related_sensors(
"""
entities: list[Entity] = []
meter_config = sensor_config if utility_meter_config is None else utility_meter_config
utility_meters = create_utility_meters(hass, energy_sensor, meter_config, config_entry)
device_entry = get_device_entry(hass, sensor_config, source_entity, config_entry)
utility_meters = create_utility_meters(
hass,
energy_sensor,
meter_config,
config_entry,
device_entry=device_entry,
follow_device_name=follow_device_name,
)
entities.extend(utility_meters)
cost_sensor = create_cost_sensor_if_needed(hass, sensor_config, energy_sensor, source_entity, cost_name)
@@ -24,6 +24,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
)
@@ -59,6 +60,7 @@ from custom_components.powercalc.const import (
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_GROUP,
CONF_DISABLE_EXTENDED_ATTRIBUTES,
CONF_ENERGY_SENSOR_CATEGORY,
CONF_ENERGY_SENSOR_PRECISION,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_EXCLUDE_ENTITIES,
@@ -75,6 +77,7 @@ from custom_components.powercalc.const import (
CONF_HIDE_MEMBERS,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_INCLUDE_NON_POWERCALC_SENSORS,
CONF_POWER_SENSOR_CATEGORY,
CONF_POWER_SENSOR_PRECISION,
CONF_SENSOR_TYPE,
CONF_SUB_GROUPS,
@@ -428,6 +431,7 @@ def create_grouped_energy_sensor(
name=name,
unique_id=energy_unique_id,
sensor_config=sensor_config,
entity_category=sensor_config.get(CONF_ENERGY_SENSOR_CATEGORY),
unit_prefix=sensor_config.get(CONF_ENERGY_SENSOR_UNIT_PREFIX, UnitPrefix.NONE),
)
@@ -484,6 +488,10 @@ class GroupedSensor(BaseEntity, SensorEntity):
sensor_config.get(CONF_GROUP_POWER_UPDATE_INTERVAL, DEFAULT_GROUP_POWER_UPDATE_INTERVAL),
)
self._attr_suggested_display_precision = self._rounding_digits
category_key = CONF_ENERGY_SENSOR_CATEGORY if self._is_energy_sensor else CONF_POWER_SENSOR_CATEGORY
entity_category = sensor_config.get(category_key)
if entity_category:
self._attr_entity_category = EntityCategory(entity_category)
if unique_id:
self._attr_unique_id = unique_id
self._native_value_exact = Decimal(0)
@@ -7,7 +7,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, UnitOfPower
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, EntityCategory, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -18,6 +18,7 @@ from custom_components.powercalc.const import (
ATTR_MEMBERS,
ATTR_STATE,
CONF_CREATE_ENERGY_SENSORS,
CONF_POWER_SENSOR_CATEGORY,
CONF_POWER_SENSOR_PRECISION,
DATA_STANDBY_POWER_SENSORS,
DEFAULT_POWER_SENSOR_PRECISION,
@@ -40,6 +41,7 @@ def create_general_standby_sensors(
power_sensor = StandbyPowerSensor(
hass,
rounding_digits=int(config.get(CONF_POWER_SENSOR_PRECISION, DEFAULT_POWER_SENSOR_PRECISION)),
entity_category=config.get(CONF_POWER_SENSOR_CATEGORY),
)
sensors.append(power_sensor)
if config.get(CONF_CREATE_ENERGY_SENSORS):
@@ -66,9 +68,16 @@ class StandbyPowerSensor(PowerSensor, SensorEntity):
_attr_unique_id = "powercalc_standby_group"
_attr_name = "All standby power"
def __init__(self, hass: HomeAssistant, rounding_digits: int = 2) -> None:
def __init__(
self,
hass: HomeAssistant,
rounding_digits: int = 2,
entity_category: str | None = None,
) -> None:
self.standby_sensors: dict[str, Decimal] = hass.data[DOMAIN][DATA_STANDBY_POWER_SENSORS]
self._rounding_digits = rounding_digits
if entity_category:
self._attr_entity_category = EntityCategory(entity_category)
async def async_added_to_hass(self) -> None:
"""Register state listeners."""
+7 -2
View File
@@ -88,6 +88,7 @@ from custom_components.powercalc.const import (
CalculationStrategy,
PowerProfileSource,
)
from custom_components.powercalc.device_naming import DeviceName
from custom_components.powercalc.discovery import get_discovery_manager
from custom_components.powercalc.errors import (
ModelNotSupportedError,
@@ -372,6 +373,8 @@ class PowerSensor(BaseEntity):
class VirtualPowerSensor(PowerSensor, SensorEntity):
"""Virtual power sensor."""
device_name = DeviceName("power")
_attr_device_class = SensorDeviceClass.POWER
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfPower.WATT
@@ -406,7 +409,9 @@ class VirtualPowerSensor(PowerSensor, SensorEntity):
self._attr_force_update = True
self._attr_unique_id = unique_id
multiply_factor = sensor_config.get(CONF_MULTIPLY_FACTOR)
self._multiply_factor: Decimal | None = Decimal(multiply_factor) if multiply_factor else None
self._multiply_factor: Decimal | None = Decimal(multiply_factor) if multiply_factor is not None else None
if multiply_factor is None and power_profile:
self._multiply_factor = power_profile.multiply_factor
self._multiply_factor_standby = bool(sensor_config.get(CONF_MULTIPLY_FACTOR_STANDBY, False))
self._ignore_unavailable_state = bool(sensor_config.get(CONF_IGNORE_UNAVAILABLE_STATE, False))
self._rounding_digits = int(sensor_config.get(CONF_POWER_SENSOR_PRECISION, DEFAULT_POWER_SENSOR_PRECISION))
@@ -722,7 +727,7 @@ class VirtualPowerSensor(PowerSensor, SensorEntity):
def _apply_multiply_factor(self, power: Decimal) -> Decimal:
"""Apply the configured multiply factor to a power value."""
return power * self._multiply_factor if self._multiply_factor else power
return power * self._multiply_factor if self._multiply_factor is not None else power
def _apply_standby_multiply_factor(self, power: Decimal) -> Decimal:
"""Apply the multiply factor to a standby power value, only when enabled for standby."""
@@ -18,6 +18,7 @@ import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util import slugify
from custom_components.powercalc.common import AnyDeviceEntry
from custom_components.powercalc.const import (
CONF_CREATE_UTILITY_METERS,
CONF_ENERGY_SENSOR_PRECISION,
@@ -29,6 +30,7 @@ from custom_components.powercalc.const import (
DEFAULT_ENERGY_SENSOR_PRECISION,
DOMAIN,
)
from custom_components.powercalc.device_naming import DeviceName
from custom_components.powercalc.select import DATA_PENDING_SELECT_ENTITIES, SIGNAL_CREATE_SELECT_ENTITIES
from .abstract import BaseEntity
@@ -44,6 +46,8 @@ def create_utility_meters(
energy_sensor: EnergySensor,
sensor_config: ConfigType,
config_entry: ConfigEntry | None = None,
follow_device_name: bool = False,
device_entry: AnyDeviceEntry | None = None,
) -> list[VirtualUtilityMeter]:
"""Create the utility meters."""
if not sensor_config.get(CONF_CREATE_UTILITY_METERS):
@@ -68,6 +72,8 @@ def create_utility_meters(
unique_id,
meter_type,
tariffs,
device_entry=device_entry,
follow_device_name=follow_device_name,
),
)
@@ -104,6 +110,8 @@ def create_meters_for_type(
unique_id: str | None,
meter_type: str,
tariffs: list[str],
follow_device_name: bool = False,
device_entry: AnyDeviceEntry | None = None,
) -> list[VirtualUtilityMeter]:
"""Create meters for a specific meter type."""
name = f"{energy_sensor.name} {meter_type}"
@@ -138,6 +146,8 @@ def create_meters_for_type(
meter_type,
unique_id,
tariffs,
device_entry=device_entry,
follow_device_name=follow_device_name,
)
tariff_sensors.extend(new_tariff_sensors)
utility_meters.extend(new_tariff_sensors)
@@ -156,10 +166,21 @@ def create_tariff_meters(
meter_type: str,
unique_id: str | None,
tariffs: list[str],
follow_device_name: bool = False,
device_entry: AnyDeviceEntry | None = None,
) -> 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_select = create_tariff_select(
config_entry,
filtered_tariffs,
hass,
name,
unique_id,
device_entry=device_entry,
follow_device_name=follow_device_name,
meter_type=meter_type,
)
tariff_sensors = []
for tariff in filtered_tariffs:
@@ -185,6 +206,9 @@ def create_tariff_select(
hass: HomeAssistant,
name: str,
unique_id: str | None,
follow_device_name: bool = False,
meter_type: str = "",
device_entry: AnyDeviceEntry | None = None,
) -> TariffSelect:
"""Create tariff selection entity."""
_LOGGER.debug("Creating utility_meter tariff select: %s", name)
@@ -193,12 +217,21 @@ def create_tariff_select(
if unique_id:
select_unique_id = f"{unique_id}_select"
tariff_select = TariffSelect(
tariff_select = VirtualTariffSelect(
name,
tariffs,
unique_id=select_unique_id,
)
tariff_select.entity_id = async_generate_entity_id("select.{}", name, hass=hass)
existing_entity_id = None
if select_unique_id:
existing_entity_id = er.async_get(hass).async_get_entity_id("select", DOMAIN, select_unique_id)
tariff_select.entity_id = existing_entity_id or async_generate_entity_id("select.{}", name, hass=hass)
# Platform entities (YAML and group selects) cannot attach a device directly.
if config_entry:
tariff_select.device_entry = device_entry
if follow_device_name:
tariff_select.device_name = DeviceName("utility_meter_cycle", {"period": meter_type})
tariff_select.enable_device_naming()
key = config_entry.entry_id if config_entry else ""
pending = hass.data[DOMAIN].setdefault(DATA_PENDING_SELECT_ENTITIES, {}).setdefault(key, [])
@@ -261,11 +294,19 @@ def create_utility_meter(
)
utility_meter._sensor_config = sensor_config # noqa: SLF001
utility_meter.entity_id = entity_id
utility_meter.device_name = DeviceName("utility_meter_cycle", {"period": meter_type})
if tariff:
utility_meter.device_name = DeviceName("utility_meter_tariff", {"period": meter_type, "tariff": tariff})
return utility_meter
class VirtualTariffSelect(BaseEntity, TariffSelect):
"""Tariff selector supporting opt-in device naming."""
class VirtualUtilityMeter(BaseEntity, UtilityMeterSensor):
device_name = DeviceName("utility_meter_cycle")
rounding_digits: int = DEFAULT_ENERGY_SENSOR_PRECISION
_sensor_config: ConfigType