Files
HomeAssistantVS/custom_components/powercalc/sensors/daily_energy.py
T
2026-06-15 10:53:52 -04:00

311 lines
11 KiB
Python

from __future__ import annotations
from collections.abc import Callable
from datetime import datetime, time, timedelta
import decimal
from decimal import Decimal
import logging
from typing import Any
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
UnitOfEnergy,
UnitOfPower,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util
import voluptuous as vol
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import (
CONF_DAILY_FIXED_ENERGY,
CONF_ENERGY_SENSOR_CATEGORY,
CONF_ENERGY_SENSOR_PRECISION,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FIXED,
CONF_ON_TIME,
CONF_POWER,
CONF_START_TIME,
CONF_UPDATE_FREQUENCY,
CONF_VALUE,
DEFAULT_ENERGY_SENSOR_PRECISION,
UnitPrefix,
)
from .abstract import generate_energy_sensor_entity_id, generate_energy_sensor_name
from .energy import EnergySensor
from .power import VirtualPowerSensor, create_virtual_power_sensor
ENERGY_ICON = "mdi:lightning-bolt"
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
DEFAULT_DAILY_UPDATE_FREQUENCY = 1800
DAILY_FIXED_ENERGY_SCHEMA = vol.Schema(
{
vol.Required(CONF_VALUE): vol.Any(vol.Coerce(float), cv.template),
vol.Optional(
CONF_UNIT_OF_MEASUREMENT,
default=UnitOfEnergy.KILO_WATT_HOUR,
): vol.In(
[UnitOfEnergy.KILO_WATT_HOUR, UnitOfPower.WATT],
),
vol.Optional(CONF_ON_TIME, default=timedelta(days=1)): cv.time_period,
vol.Optional(CONF_START_TIME): cv.time,
vol.Optional(
CONF_UPDATE_FREQUENCY,
default=DEFAULT_DAILY_UPDATE_FREQUENCY,
): vol.Coerce(int),
},
)
_LOGGER = logging.getLogger(__name__)
def create_daily_fixed_energy_sensor(
hass: HomeAssistant,
sensor_config: ConfigType,
source_entity: SourceEntity | None = None,
) -> DailyEnergySensor:
mode_config: ConfigType = sensor_config.get(CONF_DAILY_FIXED_ENERGY) # type: ignore
name = generate_energy_sensor_name(
sensor_config,
sensor_config.get(CONF_NAME),
source_entity,
)
unique_id = sensor_config.get(CONF_UNIQUE_ID) or None
entity_id = generate_energy_sensor_entity_id(
hass,
sensor_config,
unique_id=unique_id,
source_entity=source_entity,
)
_LOGGER.debug(
"Creating daily_fixed_energy energy sensor (name=%s, entity_id=%s, unique_id=%s)",
name,
entity_id,
unique_id,
)
if CONF_ON_TIME in mode_config:
on_time = mode_config.get(CONF_ON_TIME)
if not isinstance(on_time, timedelta):
on_time = timedelta(seconds=on_time) # type: ignore
else:
on_time = timedelta(days=1)
return DailyEnergySensor(
hass,
name,
entity_id,
mode_config.get(CONF_VALUE), # type: ignore
str(mode_config.get(CONF_UNIT_OF_MEASUREMENT)),
int(mode_config.get(CONF_UPDATE_FREQUENCY, DEFAULT_DAILY_UPDATE_FREQUENCY)),
sensor_config,
on_time=on_time,
start_time=mode_config.get(CONF_START_TIME),
rounding_digits=int(sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION)),
)
async def create_daily_fixed_energy_power_sensor(
hass: HomeAssistant,
sensor_config: dict,
source_entity: SourceEntity,
) -> VirtualPowerSensor | None:
mode_config: dict = sensor_config.get(CONF_DAILY_FIXED_ENERGY) # type: ignore
if mode_config.get(CONF_ON_TIME) != timedelta(days=1):
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,
):
power_value = power_value * 1000 / 24
power_sensor_config = sensor_config.copy()
power_sensor_config[CONF_FIXED] = {CONF_POWER: power_value}
unique_id = sensor_config.get(CONF_UNIQUE_ID)
if unique_id:
power_sensor_config[CONF_UNIQUE_ID] = f"{unique_id}_power"
_LOGGER.debug(
"Creating daily_fixed_energy power sensor (base_name=%s unique_id=%s)",
sensor_config.get(CONF_NAME),
unique_id,
)
return await create_virtual_power_sensor(
hass,
power_sensor_config,
source_entity,
None,
)
class DailyEnergySensor(RestoreEntity, SensorEntity, EnergySensor):
_attr_device_class = SensorDeviceClass.ENERGY
_attr_state_class = SensorStateClass.TOTAL
_attr_should_poll = False
_attr_icon = ENERGY_ICON
def __init__(
self,
hass: HomeAssistant,
name: str,
entity_id: str,
value: float | Template,
user_unit_of_measurement: str,
update_frequency: int,
sensor_config: dict[str, Any],
on_time: timedelta | None = None,
start_time: time | None = None,
rounding_digits: int = 4,
) -> None:
self._hass = hass
self._attr_name = name
self._state: Decimal = Decimal(0)
self._attr_entity_category = sensor_config.get(CONF_ENERGY_SENSOR_CATEGORY)
self._value = value
self._user_unit_of_measurement = user_unit_of_measurement
self._update_frequency = update_frequency
self._sensor_config = sensor_config
self._on_time = on_time or timedelta(days=1)
self._start_time = start_time
self._rounding_digits = rounding_digits
self._attr_suggested_display_precision = self._rounding_digits
self._attr_unique_id = sensor_config.get(CONF_UNIQUE_ID)
self.entity_id = entity_id
self._last_updated: float = dt_util.utcnow().timestamp()
self._last_delta_calculate: float | None = None
self.set_native_unit_of_measurement()
self._update_timer_removal: Callable[[], None] | None = None
def set_native_unit_of_measurement(self) -> None:
"""Set the native unit of measurement."""
unit_prefix = self._sensor_config.get(CONF_ENERGY_SENSOR_UNIT_PREFIX) or UnitPrefix.KILO
if unit_prefix == UnitPrefix.KILO:
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
elif unit_prefix == UnitPrefix.NONE:
self._attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
elif unit_prefix == UnitPrefix.MEGA:
self._attr_native_unit_of_measurement = UnitOfEnergy.MEGA_WATT_HOUR
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
if state := await self.async_get_last_state():
try:
self._state = Decimal(state.state)
except decimal.DecimalException:
_LOGGER.warning(
"%s: Cannot restore state: %s",
self.entity_id,
state.state,
)
self._state = Decimal(0)
self._last_updated = state.last_changed.timestamp()
self._state += self.calculate_delta()
self.async_schedule_update_ha_state()
else:
self._state = Decimal(0)
_LOGGER.debug("%s: Restoring state: %s", self.entity_id, self._state)
@callback
def refresh(__: datetime) -> None:
"""Update the energy sensor state."""
delta = self.calculate_delta(self._update_frequency)
if delta > 0:
self._state = self._state + delta
_LOGGER.debug(
"%s: Updating daily_fixed_energy sensor: %.4f",
self.entity_id,
self._state,
)
self.async_schedule_update_ha_state()
self._last_updated = dt_util.now().timestamp()
self._update_timer_removal = async_track_time_interval(
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:
self._last_delta_calculate = self._last_updated
elapsed_seconds = (int(self._last_delta_calculate) - int(self._last_updated)) + elapsed_seconds
self._last_delta_calculate = dt_util.utcnow().timestamp()
value = self._value
if isinstance(value, Template):
value.hass = self.hass
try:
value = float(value.async_render())
except TemplateError as ex:
_LOGGER.error(
"%s: Could not render value template %s: %s",
self.entity_id,
value,
ex,
)
return Decimal(0)
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
if self._attr_native_unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR:
energy_per_day = wh_per_day / 1000
elif self._attr_native_unit_of_measurement == UnitOfEnergy.MEGA_WATT_HOUR:
energy_per_day = wh_per_day / 1000000
return Decimal((energy_per_day / 86400) * elapsed_seconds)
@property
def native_value(self) -> Decimal:
"""Return the state of the sensor."""
return Decimal(round(self._state, self._rounding_digits))
@callback
def async_reset(self) -> None:
_LOGGER.debug("%s: Reset energy sensor", self.entity_id)
self._state = Decimal(0)
self._attr_last_reset = dt_util.utcnow()
self.async_write_ha_state()
async def async_increase(self, value: str) -> None:
_LOGGER.debug("%s: Increasing energy sensor with %s", self.entity_id, value)
self._state += Decimal(value)
self.async_write_ha_state()
async def async_calibrate(self, value: str) -> None:
_LOGGER.debug("%s: Calibrate energy sensor with %s", self.entity_id, value)
self._state = Decimal(value)
self.async_write_ha_state()