393 files
This commit is contained in:
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user