mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Add support for state class measurement to energy cost sensor (#55962)
This commit is contained in:
parent
ee7202d10a
commit
bb6c2093a2
@ -1,13 +1,16 @@
|
|||||||
"""Helper sensor for calculating utility costs."""
|
"""Helper sensor for calculating utility costs."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, Literal, TypeVar, cast
|
from typing import Any, Final, Literal, TypeVar, cast
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
ATTR_LAST_RESET,
|
||||||
ATTR_STATE_CLASS,
|
ATTR_STATE_CLASS,
|
||||||
DEVICE_CLASS_MONETARY,
|
DEVICE_CLASS_MONETARY,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
)
|
)
|
||||||
@ -18,14 +21,19 @@ from homeassistant.const import (
|
|||||||
ENERGY_WATT_HOUR,
|
ENERGY_WATT_HOUR,
|
||||||
VOLUME_CUBIC_METERS,
|
VOLUME_CUBIC_METERS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_state_change_event
|
from homeassistant.helpers.event import async_track_state_change_event
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .data import EnergyManager, async_get_manager
|
from .data import EnergyManager, async_get_manager
|
||||||
|
|
||||||
|
SUPPORTED_STATE_CLASSES = [
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -206,15 +214,16 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}"
|
f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}"
|
||||||
)
|
)
|
||||||
self._attr_device_class = DEVICE_CLASS_MONETARY
|
self._attr_device_class = DEVICE_CLASS_MONETARY
|
||||||
self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
|
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||||
self._config = config
|
self._config = config
|
||||||
self._last_energy_sensor_state: StateType | None = None
|
self._last_energy_sensor_state: State | None = None
|
||||||
self._cur_value = 0.0
|
self._cur_value = 0.0
|
||||||
|
|
||||||
def _reset(self, energy_state: StateType) -> None:
|
def _reset(self, energy_state: State) -> None:
|
||||||
"""Reset the cost sensor."""
|
"""Reset the cost sensor."""
|
||||||
self._attr_native_value = 0.0
|
self._attr_native_value = 0.0
|
||||||
self._cur_value = 0.0
|
self._cur_value = 0.0
|
||||||
|
self._attr_last_reset = dt_util.utcnow()
|
||||||
self._last_energy_sensor_state = energy_state
|
self._last_energy_sensor_state = energy_state
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@ -228,9 +237,8 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
if energy_state is None:
|
if energy_state is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if (
|
state_class = energy_state.attributes.get(ATTR_STATE_CLASS)
|
||||||
state_class := energy_state.attributes.get(ATTR_STATE_CLASS)
|
if state_class not in SUPPORTED_STATE_CLASSES:
|
||||||
) != STATE_CLASS_TOTAL_INCREASING:
|
|
||||||
if not self._wrong_state_class_reported:
|
if not self._wrong_state_class_reported:
|
||||||
self._wrong_state_class_reported = True
|
self._wrong_state_class_reported = True
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@ -240,6 +248,13 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# last_reset must be set if the sensor is STATE_CLASS_MEASUREMENT
|
||||||
|
if (
|
||||||
|
state_class == STATE_CLASS_MEASUREMENT
|
||||||
|
and ATTR_LAST_RESET not in energy_state.attributes
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
energy = float(energy_state.state)
|
energy = float(energy_state.state)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -273,7 +288,7 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
|
|
||||||
if self._last_energy_sensor_state is None:
|
if self._last_energy_sensor_state is None:
|
||||||
# Initialize as it's the first time all required entities are in place.
|
# Initialize as it's the first time all required entities are in place.
|
||||||
self._reset(energy_state.state)
|
self._reset(energy_state)
|
||||||
return
|
return
|
||||||
|
|
||||||
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
@ -298,20 +313,29 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if reset_detected(
|
if state_class != STATE_CLASS_TOTAL_INCREASING and energy_state.attributes.get(
|
||||||
|
ATTR_LAST_RESET
|
||||||
|
) != self._last_energy_sensor_state.attributes.get(ATTR_LAST_RESET):
|
||||||
|
# Energy meter was reset, reset cost sensor too
|
||||||
|
energy_state_copy = copy.copy(energy_state)
|
||||||
|
energy_state_copy.state = "0.0"
|
||||||
|
self._reset(energy_state_copy)
|
||||||
|
elif state_class == STATE_CLASS_TOTAL_INCREASING and reset_detected(
|
||||||
self.hass,
|
self.hass,
|
||||||
cast(str, self._config[self._adapter.entity_energy_key]),
|
cast(str, self._config[self._adapter.entity_energy_key]),
|
||||||
energy,
|
energy,
|
||||||
float(self._last_energy_sensor_state),
|
float(self._last_energy_sensor_state.state),
|
||||||
):
|
):
|
||||||
# Energy meter was reset, reset cost sensor too
|
# Energy meter was reset, reset cost sensor too
|
||||||
self._reset(0)
|
energy_state_copy = copy.copy(energy_state)
|
||||||
|
energy_state_copy.state = "0.0"
|
||||||
|
self._reset(energy_state_copy)
|
||||||
# Update with newly incurred cost
|
# Update with newly incurred cost
|
||||||
old_energy_value = float(self._last_energy_sensor_state)
|
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||||
self._cur_value += (energy - old_energy_value) * energy_price
|
self._cur_value += (energy - old_energy_value) * energy_price
|
||||||
self._attr_native_value = round(self._cur_value, 2)
|
self._attr_native_value = round(self._cur_value, 2)
|
||||||
|
|
||||||
self._last_energy_sensor_state = energy_state.state
|
self._last_energy_sensor_state = energy_state
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
|
@ -113,7 +113,11 @@ def _async_validate_usage_stat(
|
|||||||
|
|
||||||
state_class = state.attributes.get("state_class")
|
state_class = state.attributes.get("state_class")
|
||||||
|
|
||||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
supported_state_classes = [
|
||||||
|
sensor.STATE_CLASS_MEASUREMENT,
|
||||||
|
sensor.STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
]
|
||||||
|
if state_class not in supported_state_classes:
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"entity_unexpected_state_class_total_increasing",
|
"entity_unexpected_state_class_total_increasing",
|
||||||
@ -140,16 +144,13 @@ def _async_validate_price_entity(
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value: float | None = float(state.state)
|
float(state.state)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
|
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if value is not None and value < 0:
|
|
||||||
result.append(ValidationIssue("entity_negative_state", entity_id, value))
|
|
||||||
|
|
||||||
unit = state.attributes.get("unit_of_measurement")
|
unit = state.attributes.get("unit_of_measurement")
|
||||||
|
|
||||||
if unit is None or not unit.endswith(
|
if unit is None or not unit.endswith(
|
||||||
@ -203,7 +204,11 @@ def _async_validate_cost_entity(
|
|||||||
|
|
||||||
state_class = state.attributes.get("state_class")
|
state_class = state.attributes.get("state_class")
|
||||||
|
|
||||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
supported_state_classes = [
|
||||||
|
sensor.STATE_CLASS_MEASUREMENT,
|
||||||
|
sensor.STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
]
|
||||||
|
if state_class not in supported_state_classes:
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"entity_unexpected_state_class_total_increasing", entity_id, state_class
|
"entity_unexpected_state_class_total_increasing", entity_id, state_class
|
||||||
|
@ -78,7 +78,7 @@ async def test_cost_sensor_no_states(hass, hass_storage) -> None:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_cost_sensor_price_entity(
|
async def test_cost_sensor_price_entity_total_increasing(
|
||||||
hass,
|
hass,
|
||||||
hass_storage,
|
hass_storage,
|
||||||
hass_ws_client,
|
hass_ws_client,
|
||||||
@ -90,7 +90,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
cost_sensor_entity_id,
|
cost_sensor_entity_id,
|
||||||
flow_type,
|
flow_type,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy cost price from sensor entity."""
|
"""Test energy cost price from total_increasing type sensor entity."""
|
||||||
|
|
||||||
def _compile_statistics(_):
|
def _compile_statistics(_):
|
||||||
return compile_statistics(hass, now, now + timedelta(seconds=1))
|
return compile_statistics(hass, now, now + timedelta(seconds=1))
|
||||||
@ -137,6 +137,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
last_reset_cost_sensor = now.isoformat()
|
||||||
|
|
||||||
# Optionally initialize dependent entities
|
# Optionally initialize dependent entities
|
||||||
if initial_energy is not None:
|
if initial_energy is not None:
|
||||||
@ -153,7 +154,9 @@ async def test_cost_sensor_price_entity(
|
|||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == initial_cost
|
assert state.state == initial_cost
|
||||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
if initial_cost != "unknown":
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||||
|
|
||||||
# Optional late setup of dependent entities
|
# Optional late setup of dependent entities
|
||||||
@ -169,7 +172,8 @@ async def test_cost_sensor_price_entity(
|
|||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "0.0"
|
assert state.state == "0.0"
|
||||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||||
|
|
||||||
# # Unique ID temp disabled
|
# # Unique ID temp disabled
|
||||||
@ -186,6 +190,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR
|
assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
# Nothing happens when price changes
|
# Nothing happens when price changes
|
||||||
if price_entity is not None:
|
if price_entity is not None:
|
||||||
@ -200,6 +205,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR
|
assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
# Additional consumption is using the new price
|
# Additional consumption is using the new price
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -210,6 +216,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR
|
assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
# Check generated statistics
|
# Check generated statistics
|
||||||
await async_wait_recording_done_without_instance(hass)
|
await async_wait_recording_done_without_instance(hass)
|
||||||
@ -226,6 +233,7 @@ async def test_cost_sensor_price_entity(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -236,6 +244,8 @@ async def test_cost_sensor_price_entity(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR
|
assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR
|
||||||
|
assert state.attributes["last_reset"] != last_reset_cost_sensor
|
||||||
|
last_reset_cost_sensor = state.attributes["last_reset"]
|
||||||
|
|
||||||
# Energy use bumped to 10 kWh
|
# Energy use bumped to 10 kWh
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -246,6 +256,213 @@ async def test_cost_sensor_price_entity(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(cost_sensor_entity_id)
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR
|
assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
|
# Check generated statistics
|
||||||
|
await async_wait_recording_done_without_instance(hass)
|
||||||
|
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
||||||
|
assert cost_sensor_entity_id in statistics
|
||||||
|
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 38.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)]
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"usage_sensor_entity_id,cost_sensor_entity_id,flow_type",
|
||||||
|
[
|
||||||
|
("sensor.energy_consumption", "sensor.energy_consumption_cost", "flow_from"),
|
||||||
|
(
|
||||||
|
"sensor.energy_production",
|
||||||
|
"sensor.energy_production_compensation",
|
||||||
|
"flow_to",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("energy_state_class", ["measurement"])
|
||||||
|
async def test_cost_sensor_price_entity_total(
|
||||||
|
hass,
|
||||||
|
hass_storage,
|
||||||
|
hass_ws_client,
|
||||||
|
initial_energy,
|
||||||
|
initial_cost,
|
||||||
|
price_entity,
|
||||||
|
fixed_price,
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
cost_sensor_entity_id,
|
||||||
|
flow_type,
|
||||||
|
energy_state_class,
|
||||||
|
) -> None:
|
||||||
|
"""Test energy cost price from total type sensor entity."""
|
||||||
|
|
||||||
|
def _compile_statistics(_):
|
||||||
|
return compile_statistics(hass, now, now + timedelta(seconds=1))
|
||||||
|
|
||||||
|
energy_attributes = {
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||||
|
ATTR_STATE_CLASS: energy_state_class,
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_init_recorder_component(hass)
|
||||||
|
energy_data = data.EnergyManager.default_preferences()
|
||||||
|
energy_data["energy_sources"].append(
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"flow_from": [
|
||||||
|
{
|
||||||
|
"stat_energy_from": "sensor.energy_consumption",
|
||||||
|
"entity_energy_from": "sensor.energy_consumption",
|
||||||
|
"stat_cost": None,
|
||||||
|
"entity_energy_price": price_entity,
|
||||||
|
"number_energy_price": fixed_price,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if flow_type == "flow_from"
|
||||||
|
else [],
|
||||||
|
"flow_to": [
|
||||||
|
{
|
||||||
|
"stat_energy_to": "sensor.energy_production",
|
||||||
|
"entity_energy_to": "sensor.energy_production",
|
||||||
|
"stat_compensation": None,
|
||||||
|
"entity_energy_price": price_entity,
|
||||||
|
"number_energy_price": fixed_price,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if flow_type == "flow_to"
|
||||||
|
else [],
|
||||||
|
"cost_adjustment_day": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
hass_storage[data.STORAGE_KEY] = {
|
||||||
|
"version": 1,
|
||||||
|
"data": energy_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
last_reset = dt_util.utc_from_timestamp(0).isoformat()
|
||||||
|
last_reset_cost_sensor = now.isoformat()
|
||||||
|
|
||||||
|
# Optionally initialize dependent entities
|
||||||
|
if initial_energy is not None:
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
initial_energy,
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
hass.states.async_set("sensor.energy_price", "1")
|
||||||
|
|
||||||
|
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||||
|
await setup_integration(hass)
|
||||||
|
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == initial_cost
|
||||||
|
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||||
|
if initial_cost != "unknown":
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||||
|
|
||||||
|
# Optional late setup of dependent entities
|
||||||
|
if initial_energy is None:
|
||||||
|
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"0",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "0.0"
|
||||||
|
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||||
|
|
||||||
|
# # Unique ID temp disabled
|
||||||
|
# # entity_registry = er.async_get(hass)
|
||||||
|
# # entry = entity_registry.async_get(cost_sensor_entity_id)
|
||||||
|
# # assert entry.unique_id == "energy_energy_consumption cost"
|
||||||
|
|
||||||
|
# Energy use bumped to 10 kWh
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"10",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
|
# Nothing happens when price changes
|
||||||
|
if price_entity is not None:
|
||||||
|
hass.states.async_set(price_entity, "2")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
else:
|
||||||
|
energy_data = copy.deepcopy(energy_data)
|
||||||
|
energy_data["energy_sources"][0][flow_type][0]["number_energy_price"] = 2
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json({"id": 5, "type": "energy/save_prefs", **energy_data})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
|
# Additional consumption is using the new price
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"14.5",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
|
# Check generated statistics
|
||||||
|
await async_wait_recording_done_without_instance(hass)
|
||||||
|
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
||||||
|
assert cost_sensor_entity_id in statistics
|
||||||
|
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0
|
||||||
|
|
||||||
|
# Energy sensor has a small dip
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"14",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
|
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
||||||
|
last_reset = (now + timedelta(seconds=1)).isoformat()
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"4",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR
|
||||||
|
assert state.attributes["last_reset"] != last_reset_cost_sensor
|
||||||
|
last_reset_cost_sensor = state.attributes["last_reset"]
|
||||||
|
|
||||||
|
# Energy use bumped to 10 kWh
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"10",
|
||||||
|
{**energy_attributes, **{"last_reset": last_reset}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR
|
||||||
|
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||||
|
|
||||||
# Check generated statistics
|
# Check generated statistics
|
||||||
await async_wait_recording_done_without_instance(hass)
|
await async_wait_recording_done_without_instance(hass)
|
||||||
@ -285,6 +502,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
|||||||
|
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
# Initial state: 10kWh
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.energy_consumption",
|
"sensor.energy_consumption",
|
||||||
10000,
|
10000,
|
||||||
@ -297,7 +515,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
|||||||
state = hass.states.get("sensor.energy_consumption_cost")
|
state = hass.states.get("sensor.energy_consumption_cost")
|
||||||
assert state.state == "0.0"
|
assert state.state == "0.0"
|
||||||
|
|
||||||
# Energy use bumped to 10 kWh
|
# Energy use bumped by 10 kWh
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.energy_consumption",
|
"sensor.energy_consumption",
|
||||||
20000,
|
20000,
|
||||||
@ -362,7 +580,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None:
|
|||||||
async def test_cost_sensor_wrong_state_class(
|
async def test_cost_sensor_wrong_state_class(
|
||||||
hass, hass_storage, caplog, state_class
|
hass, hass_storage, caplog, state_class
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy sensor rejects wrong state_class."""
|
"""Test energy sensor rejects state_class with wrong state_class."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||||
ATTR_STATE_CLASS: state_class,
|
ATTR_STATE_CLASS: state_class,
|
||||||
@ -418,3 +636,61 @@ async def test_cost_sensor_wrong_state_class(
|
|||||||
|
|
||||||
state = hass.states.get("sensor.energy_consumption_cost")
|
state = hass.states.get("sensor.energy_consumption_cost")
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||||
|
async def test_cost_sensor_state_class_measurement_no_reset(
|
||||||
|
hass, hass_storage, caplog, state_class
|
||||||
|
) -> None:
|
||||||
|
"""Test energy sensor rejects state_class with no last_reset."""
|
||||||
|
energy_attributes = {
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||||
|
ATTR_STATE_CLASS: state_class,
|
||||||
|
}
|
||||||
|
energy_data = data.EnergyManager.default_preferences()
|
||||||
|
energy_data["energy_sources"].append(
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"flow_from": [
|
||||||
|
{
|
||||||
|
"stat_energy_from": "sensor.energy_consumption",
|
||||||
|
"entity_energy_from": "sensor.energy_consumption",
|
||||||
|
"stat_cost": None,
|
||||||
|
"entity_energy_price": None,
|
||||||
|
"number_energy_price": 0.5,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flow_to": [],
|
||||||
|
"cost_adjustment_day": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
hass_storage[data.STORAGE_KEY] = {
|
||||||
|
"version": 1,
|
||||||
|
"data": energy_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.energy_consumption",
|
||||||
|
10000,
|
||||||
|
energy_attributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||||
|
await setup_integration(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.energy_consumption_cost")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
# Energy use bumped to 10 kWh
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.energy_consumption",
|
||||||
|
20000,
|
||||||
|
energy_attributes,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.energy_consumption_cost")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
@ -382,15 +382,6 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
|||||||
"value": "123,123.12",
|
"value": "123,123.12",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"-100",
|
|
||||||
"$/kWh",
|
|
||||||
{
|
|
||||||
"type": "entity_negative_state",
|
|
||||||
"identifier": "sensor.grid_price_1",
|
|
||||||
"value": -100.0,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"123",
|
"123",
|
||||||
"$/Ws",
|
"$/Ws",
|
||||||
@ -414,7 +405,7 @@ async def test_validation_grid_price_errors(
|
|||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.grid_price_1",
|
"sensor.grid_price_1",
|
||||||
state,
|
state,
|
||||||
{"unit_of_measurement": unit, "state_class": "total_increasing"},
|
{"unit_of_measurement": unit, "state_class": "measurement"},
|
||||||
)
|
)
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user