Add unit conversion for energy costs (#81379)
Co-authored-by: Franck Nijhof <git@frenck.dev>pull/81423/head
parent
a5f209b219
commit
3aca376374
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
@ -22,6 +23,7 @@ from homeassistant.const import (
|
|||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
|
@ -34,29 +36,35 @@ from homeassistant.helpers import entity_registry as er
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import unit_conversion
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import EnergyManager, async_get_manager
|
||||
|
||||
SUPPORTED_STATE_CLASSES = [
|
||||
SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.MEASUREMENT,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
]
|
||||
VALID_ENERGY_UNITS = [
|
||||
}
|
||||
VALID_ENERGY_UNITS: set[str] = {
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
]
|
||||
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
|
||||
VALID_VOLUME_UNITS_WATER = [
|
||||
}
|
||||
VALID_ENERGY_UNITS_GAS = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
*VALID_ENERGY_UNITS,
|
||||
}
|
||||
VALID_VOLUME_UNITS_WATER = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
]
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -252,8 +260,24 @@ class EnergyCostSensor(SensorEntity):
|
|||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_cost(self) -> None: # noqa: C901
|
||||
def _update_cost(self) -> None:
|
||||
"""Update incurred costs."""
|
||||
if self._adapter.source_type == "grid":
|
||||
valid_units = VALID_ENERGY_UNITS
|
||||
default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
valid_units = VALID_ENERGY_UNITS_GAS
|
||||
# No conversion for gas.
|
||||
default_price_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
valid_units = VALID_VOLUME_UNITS_WATER
|
||||
if self.hass.config.units is METRIC_SYSTEM:
|
||||
default_price_unit = UnitOfVolume.CUBIC_METERS
|
||||
else:
|
||||
default_price_unit = UnitOfVolume.GALLONS
|
||||
|
||||
energy_state = self.hass.states.get(
|
||||
cast(str, self._config[self._adapter.stat_energy_key])
|
||||
)
|
||||
|
@ -298,52 +322,27 @@ class EnergyCostSensor(SensorEntity):
|
|||
except ValueError:
|
||||
return
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.WATT_HOUR}"
|
||||
):
|
||||
energy_price *= 1000.0
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.MEGA_WATT_HOUR}"
|
||||
):
|
||||
energy_price /= 1000.0
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.GIGA_JOULE}"
|
||||
):
|
||||
energy_price /= 1000 / 3.6
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
else:
|
||||
energy_price_state = None
|
||||
energy_price = cast(float, self._config["number_energy_price"])
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if self._adapter.source_type == "grid":
|
||||
if energy_unit not in VALID_ENERGY_UNITS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
if energy_unit not in VALID_ENERGY_UNITS_GAS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
if energy_unit not in VALID_VOLUME_UNITS_WATER:
|
||||
energy_unit = None
|
||||
|
||||
if energy_unit == UnitOfEnergy.WATT_HOUR:
|
||||
energy_price /= 1000
|
||||
elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR:
|
||||
energy_price *= 1000
|
||||
elif energy_unit == UnitOfEnergy.GIGA_JOULE:
|
||||
energy_price *= 1000 / 3.6
|
||||
|
||||
if energy_unit is None:
|
||||
if energy_unit is None or energy_unit not in valid_units:
|
||||
if not self._wrong_unit_reported:
|
||||
self._wrong_unit_reported = True
|
||||
_LOGGER.warning(
|
||||
|
@ -373,10 +372,30 @@ class EnergyCostSensor(SensorEntity):
|
|||
energy_state_copy = copy.copy(energy_state)
|
||||
energy_state_copy.state = "0.0"
|
||||
self._reset(energy_state_copy)
|
||||
|
||||
# Update with newly incurred cost
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
cur_value = cast(float, self._attr_native_value)
|
||||
self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price
|
||||
|
||||
if energy_price_unit is None:
|
||||
converted_energy_price = energy_price
|
||||
else:
|
||||
if self._adapter.source_type == "grid":
|
||||
converter: Callable[
|
||||
[float, str, str], float
|
||||
] = unit_conversion.EnergyConverter.convert
|
||||
elif self._adapter.source_type in ("gas", "water"):
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
converted_energy_price = converter(
|
||||
energy_price,
|
||||
energy_unit,
|
||||
energy_price_unit,
|
||||
)
|
||||
|
||||
self._attr_native_value = (
|
||||
cur_value + (energy - old_energy_value) * converted_energy_price
|
||||
)
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN,
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_GALLONS,
|
||||
UnitOfEnergy,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
|
@ -832,7 +834,10 @@ async def test_cost_sensor_handle_price_units(
|
|||
assert state.state == "20.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
|
||||
@pytest.mark.parametrize(
|
||||
"unit",
|
||||
(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
|
||||
)
|
||||
async def test_cost_sensor_handle_gas(
|
||||
setup_integration, hass, hass_storage, unit
|
||||
) -> None:
|
||||
|
@ -933,13 +938,22 @@ async def test_cost_sensor_handle_gas_kwh(
|
|||
assert state.state == "50.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
|
||||
@pytest.mark.parametrize(
|
||||
"unit_system,usage_unit,growth",
|
||||
(
|
||||
# 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3:
|
||||
(US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974),
|
||||
(US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0),
|
||||
(METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0),
|
||||
),
|
||||
)
|
||||
async def test_cost_sensor_handle_water(
|
||||
setup_integration, hass, hass_storage, unit
|
||||
setup_integration, hass, hass_storage, unit_system, usage_unit, growth
|
||||
) -> None:
|
||||
"""Test water cost price from sensor entity."""
|
||||
hass.config.units = unit_system
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: unit,
|
||||
ATTR_UNIT_OF_MEASUREMENT: usage_unit,
|
||||
ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
|
@ -981,7 +995,7 @@ async def test_cost_sensor_handle_water(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.water_consumption_cost")
|
||||
assert state.state == "50.0"
|
||||
assert float(state.state) == pytest.approx(growth)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state_class", [None])
|
||||
|
|
Loading…
Reference in New Issue