Add periodically resetting meter option to utility meter (#88446)
* Use last valid state if meter is not periodically resetting * Fix unload of entry, used during options flow submit * Adjustments based on code review * Move DecimalException handling to validation method * Add test for invalid new state in calculate_adjustment method
This commit is contained in:
parent
e45eab600f
commit
478a1d5e9a
8 changed files with 510 additions and 49 deletions
|
@ -21,6 +21,7 @@ from .const import (
|
|||
CONF_METER_DELTA_VALUES,
|
||||
CONF_METER_NET_CONSUMPTION,
|
||||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFF,
|
||||
|
@ -83,6 +84,7 @@ METER_CONFIG_SCHEMA = vol.Schema(
|
|||
),
|
||||
vol.Optional(CONF_METER_DELTA_VALUES, default=False): cv.boolean,
|
||||
vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean,
|
||||
vol.Optional(CONF_METER_PERIODICALLY_RESETTING, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TARIFFS, default=[]): vol.All(
|
||||
cv.ensure_list, vol.Unique(), [cv.string]
|
||||
),
|
||||
|
@ -221,13 +223,29 @@ async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry)
|
|||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
platforms_to_unload = [Platform.SENSOR]
|
||||
if entry.options.get(CONF_TARIFFS):
|
||||
platforms_to_unload.append(Platform.SELECT)
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
entry,
|
||||
(
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
),
|
||||
platforms_to_unload,
|
||||
):
|
||||
hass.data[DATA_UTILITY].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
if config_entry.version == 1:
|
||||
new = {**config_entry.options}
|
||||
new[CONF_METER_PERIODICALLY_RESETTING] = True
|
||||
config_entry.version = 2
|
||||
hass.config_entries.async_update_entry(config_entry, options=new)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
|
|
@ -21,6 +21,7 @@ from .const import (
|
|||
CONF_METER_DELTA_VALUES,
|
||||
CONF_METER_NET_CONSUMPTION,
|
||||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFFS,
|
||||
|
@ -64,6 +65,9 @@ OPTIONS_SCHEMA = vol.Schema(
|
|||
vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SENSOR_DOMAIN),
|
||||
),
|
||||
vol.Required(
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -95,6 +99,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
vol.Required(
|
||||
CONF_METER_DELTA_VALUES, default=False
|
||||
): selector.BooleanSelector(),
|
||||
vol.Required(
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
default=True,
|
||||
): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -110,6 +118,8 @@ OPTIONS_FLOW = {
|
|||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Utility Meter."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ CONF_METER_TYPE = "cycle"
|
|||
CONF_METER_OFFSET = "offset"
|
||||
CONF_METER_DELTA_VALUES = "delta_values"
|
||||
CONF_METER_NET_CONSUMPTION = "net_consumption"
|
||||
CONF_METER_PERIODICALLY_RESETTING = "periodically_resetting"
|
||||
CONF_PAUSED = "paused"
|
||||
CONF_TARIFFS = "tariffs"
|
||||
CONF_TARIFF = "tariff"
|
||||
|
|
|
@ -27,7 +27,7 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN,
|
||||
UnitOfEnergy,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
@ -50,6 +50,7 @@ from .const import (
|
|||
CONF_METER_DELTA_VALUES,
|
||||
CONF_METER_NET_CONSUMPTION,
|
||||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFF,
|
||||
|
@ -85,6 +86,7 @@ ATTR_SOURCE_ID = "source"
|
|||
ATTR_STATUS = "status"
|
||||
ATTR_PERIOD = "meter_period"
|
||||
ATTR_LAST_PERIOD = "last_period"
|
||||
ATTR_LAST_VALID_STATE = "last_valid_state"
|
||||
ATTR_TARIFF = "tariff"
|
||||
|
||||
DEVICE_CLASS_MAP = {
|
||||
|
@ -127,6 +129,7 @@ async def async_setup_entry(
|
|||
meter_type = None
|
||||
name = config_entry.title
|
||||
net_consumption = config_entry.options[CONF_METER_NET_CONSUMPTION]
|
||||
periodically_resetting = config_entry.options[CONF_METER_PERIODICALLY_RESETTING]
|
||||
tariff_entity = hass.data[DATA_UTILITY][entry_id][CONF_TARIFF_ENTITY]
|
||||
|
||||
meters = []
|
||||
|
@ -142,6 +145,7 @@ async def async_setup_entry(
|
|||
name=name,
|
||||
net_consumption=net_consumption,
|
||||
parent_meter=entry_id,
|
||||
periodically_resetting=periodically_resetting,
|
||||
source_entity=source_entity_id,
|
||||
tariff_entity=tariff_entity,
|
||||
tariff=None,
|
||||
|
@ -160,6 +164,7 @@ async def async_setup_entry(
|
|||
name=f"{name} {tariff}",
|
||||
net_consumption=net_consumption,
|
||||
parent_meter=entry_id,
|
||||
periodically_resetting=periodically_resetting,
|
||||
source_entity=source_entity_id,
|
||||
tariff_entity=tariff_entity,
|
||||
tariff=tariff,
|
||||
|
@ -223,6 +228,9 @@ async def async_setup_platform(
|
|||
conf_meter_net_consumption = hass.data[DATA_UTILITY][meter][
|
||||
CONF_METER_NET_CONSUMPTION
|
||||
]
|
||||
conf_meter_periodically_resetting = hass.data[DATA_UTILITY][meter][
|
||||
CONF_METER_PERIODICALLY_RESETTING
|
||||
]
|
||||
conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get(
|
||||
CONF_TARIFF_ENTITY
|
||||
)
|
||||
|
@ -235,6 +243,7 @@ async def async_setup_platform(
|
|||
name=conf_sensor_name,
|
||||
net_consumption=conf_meter_net_consumption,
|
||||
parent_meter=meter,
|
||||
periodically_resetting=conf_meter_periodically_resetting,
|
||||
source_entity=conf_meter_source,
|
||||
tariff_entity=conf_meter_tariff_entity,
|
||||
tariff=conf_sensor_tariff,
|
||||
|
@ -262,6 +271,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData):
|
|||
|
||||
last_period: Decimal
|
||||
last_reset: datetime | None
|
||||
last_valid_state: Decimal | None
|
||||
status: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
|
@ -270,6 +280,9 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData):
|
|||
data["last_period"] = str(self.last_period)
|
||||
if isinstance(self.last_reset, (datetime)):
|
||||
data["last_reset"] = self.last_reset.isoformat()
|
||||
data["last_valid_state"] = (
|
||||
str(self.last_valid_state) if self.last_valid_state else None
|
||||
)
|
||||
data["status"] = self.status
|
||||
|
||||
return data
|
||||
|
@ -284,6 +297,11 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData):
|
|||
try:
|
||||
last_period: Decimal = Decimal(restored["last_period"])
|
||||
last_reset: datetime | None = dt_util.parse_datetime(restored["last_reset"])
|
||||
last_valid_state: Decimal | None = (
|
||||
Decimal(restored["last_valid_state"])
|
||||
if restored.get("last_valid_state")
|
||||
else None
|
||||
)
|
||||
status: str = restored["status"]
|
||||
except KeyError:
|
||||
# restored is a dict, but does not have all values
|
||||
|
@ -297,6 +315,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData):
|
|||
extra.native_unit_of_measurement,
|
||||
last_period,
|
||||
last_reset,
|
||||
last_valid_state,
|
||||
status,
|
||||
)
|
||||
|
||||
|
@ -316,6 +335,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
name,
|
||||
net_consumption,
|
||||
parent_meter,
|
||||
periodically_resetting,
|
||||
source_entity,
|
||||
tariff_entity,
|
||||
tariff,
|
||||
|
@ -330,6 +350,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self._state = None
|
||||
self._last_period = Decimal(0)
|
||||
self._last_reset = dt_util.utcnow()
|
||||
self._last_valid_state = None
|
||||
self._collecting = None
|
||||
self._name = name
|
||||
self._unit_of_measurement = None
|
||||
|
@ -346,6 +367,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self._cron_pattern = cron_pattern
|
||||
self._sensor_delta_values = delta_values
|
||||
self._sensor_net_consumption = net_consumption
|
||||
self._sensor_periodically_resetting = periodically_resetting
|
||||
self._tariff = tariff
|
||||
self._tariff_entity = tariff_entity
|
||||
|
||||
|
@ -355,53 +377,70 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self._state = 0
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_reading(self, event):
|
||||
"""Handle the sensor state changes."""
|
||||
old_state = event.data.get("old_state")
|
||||
new_state = event.data.get("new_state")
|
||||
@staticmethod
|
||||
def _validate_state(state: State | None) -> Decimal | None:
|
||||
"""Parse the state as a Decimal if available. Throws DecimalException if the state is not a number."""
|
||||
try:
|
||||
return (
|
||||
None
|
||||
if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]
|
||||
else Decimal(state.state)
|
||||
)
|
||||
except DecimalException:
|
||||
return None
|
||||
|
||||
if self._state is None and new_state.state:
|
||||
def calculate_adjustment(
|
||||
self, old_state: State | None, new_state: State
|
||||
) -> Decimal | None:
|
||||
"""Calculate the adjustment based on the old and new state."""
|
||||
|
||||
# First check if the new_state is valid (see discussion in PR #88446)
|
||||
if (new_state_val := self._validate_state(new_state)) is None:
|
||||
_LOGGER.warning("Invalid state %s", new_state.state)
|
||||
return None
|
||||
|
||||
if self._sensor_delta_values:
|
||||
return new_state_val
|
||||
|
||||
if (
|
||||
not self._sensor_periodically_resetting
|
||||
and self._last_valid_state is not None
|
||||
): # Fallback to old_state if sensor is periodically resetting but last_valid_state is None
|
||||
return new_state_val - self._last_valid_state
|
||||
|
||||
if (old_state_val := self._validate_state(old_state)) is not None:
|
||||
return new_state_val - old_state_val
|
||||
_LOGGER.warning(
|
||||
"Invalid state (%s > %s)",
|
||||
old_state.state if old_state else None,
|
||||
new_state_val,
|
||||
)
|
||||
return None
|
||||
|
||||
@callback
|
||||
def async_reading(self, event: Event):
|
||||
"""Handle the sensor state changes."""
|
||||
old_state: State | None = event.data.get("old_state")
|
||||
new_state: State = event.data.get("new_state") # type: ignore[assignment] # a state change event always has a new state
|
||||
|
||||
if (new_state_val := self._validate_state(new_state)) is None:
|
||||
_LOGGER.warning("Invalid state %s", new_state.state)
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
# First state update initializes the utility_meter sensors
|
||||
source_state = self.hass.states.get(self._sensor_source_id)
|
||||
for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][
|
||||
DATA_TARIFF_SENSORS
|
||||
]:
|
||||
sensor.start(source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
|
||||
sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
|
||||
|
||||
if (
|
||||
new_state is None
|
||||
or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]
|
||||
or (
|
||||
not self._sensor_delta_values
|
||||
and (
|
||||
old_state is None
|
||||
or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]
|
||||
)
|
||||
)
|
||||
):
|
||||
return
|
||||
adjustment := self.calculate_adjustment(old_state, new_state)
|
||||
) is not None and (self._sensor_net_consumption or adjustment >= 0):
|
||||
# If net_consumption is off, the adjustment must be non-negative
|
||||
self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
|
||||
|
||||
self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
try:
|
||||
if self._sensor_delta_values:
|
||||
adjustment = Decimal(new_state.state)
|
||||
else:
|
||||
adjustment = Decimal(new_state.state) - Decimal(old_state.state)
|
||||
|
||||
if (not self._sensor_net_consumption) and adjustment < 0:
|
||||
# Source sensor just rolled over for unknown reasons,
|
||||
return
|
||||
self._state += adjustment
|
||||
|
||||
except DecimalException as err:
|
||||
if self._sensor_delta_values:
|
||||
_LOGGER.warning("Invalid adjustment of %s: %s", new_state.state, err)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Invalid state (%s > %s): %s", old_state.state, new_state.state, err
|
||||
)
|
||||
self._last_valid_state = new_state_val
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
|
@ -422,6 +461,11 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self._collecting()
|
||||
self._collecting = None
|
||||
|
||||
# Reset the last_valid_state during state change because if the last state before the tariff change was invalid,
|
||||
# there is no way to know how much "adjustment" counts for which tariff. Therefore, we set the last_valid_state
|
||||
# to None and let the fallback mechanism handle the case that the old state was valid
|
||||
self._last_valid_state = None
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - %s - source <%s>",
|
||||
self._name,
|
||||
|
@ -484,6 +528,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self._unit_of_measurement = last_sensor_data.native_unit_of_measurement
|
||||
self._last_period = last_sensor_data.last_period
|
||||
self._last_reset = last_sensor_data.last_reset
|
||||
self._last_valid_state = last_sensor_data.last_valid_state
|
||||
if last_sensor_data.status == COLLECTING:
|
||||
# Null lambda to allow cancelling the collection on tariff change
|
||||
self._collecting = lambda: None
|
||||
|
@ -508,6 +553,12 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
and is_number(state.attributes[ATTR_LAST_PERIOD])
|
||||
else Decimal(0)
|
||||
)
|
||||
self._last_valid_state = (
|
||||
Decimal(state.attributes[ATTR_LAST_VALID_STATE])
|
||||
if state.attributes.get(ATTR_LAST_VALID_STATE)
|
||||
and is_number(state.attributes[ATTR_LAST_VALID_STATE])
|
||||
else None
|
||||
)
|
||||
self._last_reset = dt_util.as_utc(
|
||||
dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET))
|
||||
)
|
||||
|
@ -590,6 +641,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
ATTR_SOURCE_ID: self._sensor_source_id,
|
||||
ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING,
|
||||
ATTR_LAST_PERIOD: str(self._last_period),
|
||||
ATTR_LAST_VALID_STATE: str(self._last_valid_state),
|
||||
}
|
||||
if self._period is not None:
|
||||
state_attr[ATTR_PERIOD] = self._period
|
||||
|
@ -620,6 +672,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
self.native_unit_of_measurement,
|
||||
self._last_period,
|
||||
self._last_reset,
|
||||
self._last_valid_state,
|
||||
PAUSED if self._collecting is None else COLLECTING,
|
||||
)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"cycle": "Meter reset cycle",
|
||||
"delta_values": "Delta values",
|
||||
"name": "Name",
|
||||
"periodically_resetting": "Periodically resetting",
|
||||
"net_consumption": "Net consumption",
|
||||
"offset": "Meter reset offset",
|
||||
"source": "Input sensor",
|
||||
|
@ -17,6 +18,7 @@
|
|||
"data_description": {
|
||||
"delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.",
|
||||
"net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.",
|
||||
"periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.",
|
||||
"offset": "Offset the day of a monthly meter reset.",
|
||||
"tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed."
|
||||
}
|
||||
|
@ -27,7 +29,11 @@
|
|||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"source": "[%key:component::utility_meter::config::step::user::data::source%]"
|
||||
"source": "[%key:component::utility_meter::config::step::user::data::source%]",
|
||||
"periodically_resetting": "[%key:component::utility_meter::config::step::user::data::periodically_resetting%]"
|
||||
},
|
||||
"data_description": {
|
||||
"periodically_resetting": "[%key:component::utility_meter::config::step::user::data_description::periodically_resetting%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
@ -96,6 +98,7 @@ async def test_tariffs(hass: HomeAssistant) -> None:
|
|||
"delta_values": False,
|
||||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"periodically_resetting": True,
|
||||
"offset": 0,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": ["cat", "dog", "horse", "cow"],
|
||||
|
@ -109,6 +112,7 @@ async def test_tariffs(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": ["cat", "dog", "horse", "cow"],
|
||||
}
|
||||
|
@ -136,6 +140,57 @@ async def test_tariffs(hass: HomeAssistant) -> None:
|
|||
assert result["errors"]["base"] == "tariffs_not_unique"
|
||||
|
||||
|
||||
async def test_non_periodically_resetting(hass: HomeAssistant) -> None:
|
||||
"""Test periodically resetting."""
|
||||
input_sensor_entity_id = "sensor.input"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"cycle": "monthly",
|
||||
"name": "Electricity meter",
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Electricity meter"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"cycle": "monthly",
|
||||
"delta_values": False,
|
||||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"periodically_resetting": False,
|
||||
"offset": 0,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"cycle": "monthly",
|
||||
"delta_values": False,
|
||||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
||||
|
||||
def get_suggested(schema, key):
|
||||
"""Get suggested value for key in voluptuous schema."""
|
||||
for k in schema:
|
||||
|
@ -162,6 +217,7 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": input_sensor1_entity_id,
|
||||
"tariffs": "",
|
||||
},
|
||||
|
@ -176,10 +232,11 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
assert result["step_id"] == "init"
|
||||
schema = result["data_schema"].schema
|
||||
assert get_suggested(schema, "source") == input_sensor1_entity_id
|
||||
assert get_suggested(schema, "periodically_resetting") is True
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"source": input_sensor2_entity_id},
|
||||
user_input={"source": input_sensor2_entity_id, "periodically_resetting": False},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
|
@ -188,6 +245,7 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": input_sensor2_entity_id,
|
||||
"tariffs": "",
|
||||
}
|
||||
|
@ -198,6 +256,7 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": input_sensor2_entity_id,
|
||||
"tariffs": "",
|
||||
}
|
||||
|
|
|
@ -186,6 +186,7 @@ async def test_services_config_entry(hass: HomeAssistant) -> None:
|
|||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["peak", "offpeak"],
|
||||
},
|
||||
|
@ -202,6 +203,7 @@ async def test_services_config_entry(hass: HomeAssistant) -> None:
|
|||
"name": "Energy bill2",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["peak", "offpeak"],
|
||||
},
|
||||
|
@ -413,6 +415,7 @@ async def test_setup_and_remove_config_entry(
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": tariffs,
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
|||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.utility_meter import DEFAULT_OFFSET
|
||||
from homeassistant.components.utility_meter.const import (
|
||||
ATTR_VALUE,
|
||||
DAILY,
|
||||
|
@ -24,9 +25,11 @@ from homeassistant.components.utility_meter.const import (
|
|||
)
|
||||
from homeassistant.components.utility_meter.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_LAST_VALID_STATE,
|
||||
ATTR_STATUS,
|
||||
COLLECTING,
|
||||
PAUSED,
|
||||
UtilityMeterSensor,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
|
@ -50,7 +53,7 @@ from tests.common import (
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_utc(hass):
|
||||
def set_utc(hass: HomeAssistant):
|
||||
"""Set timezone to UTC."""
|
||||
hass.config.set_time_zone("UTC")
|
||||
|
||||
|
@ -77,6 +80,7 @@ def set_utc(hass):
|
|||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["onpeak", "midpeak", "offpeak"],
|
||||
},
|
||||
|
@ -272,6 +276,7 @@ async def test_not_unique_tariffs(hass: HomeAssistant, yaml_config) -> None:
|
|||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["onpeak", "midpeak", "offpeak"],
|
||||
},
|
||||
|
@ -430,6 +435,7 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) ->
|
|||
"name": "Energy meter",
|
||||
"net_consumption": True,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": [],
|
||||
},
|
||||
|
@ -439,6 +445,7 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) ->
|
|||
"name": "Gas meter",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.gas",
|
||||
"tariffs": [],
|
||||
},
|
||||
|
@ -516,6 +523,7 @@ async def test_device_class(
|
|||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"],
|
||||
},
|
||||
|
@ -552,6 +560,7 @@ async def test_restore_state(
|
|||
"native_unit_of_measurement": "kWh",
|
||||
"last_reset": last_reset,
|
||||
"last_period": "7",
|
||||
"last_valid_state": "None",
|
||||
"status": "paused",
|
||||
},
|
||||
),
|
||||
|
@ -562,6 +571,7 @@ async def test_restore_state(
|
|||
attributes={
|
||||
ATTR_STATUS: PAUSED,
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_LAST_VALID_STATE: None,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
},
|
||||
),
|
||||
|
@ -571,6 +581,7 @@ async def test_restore_state(
|
|||
"decimal_str": "3",
|
||||
},
|
||||
"native_unit_of_measurement": "kWh",
|
||||
"last_valid_state": "None",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -580,6 +591,7 @@ async def test_restore_state(
|
|||
attributes={
|
||||
ATTR_STATUS: COLLECTING,
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_LAST_VALID_STATE: None,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
},
|
||||
),
|
||||
|
@ -589,6 +601,7 @@ async def test_restore_state(
|
|||
"decimal_str": "3f",
|
||||
},
|
||||
"native_unit_of_measurement": "kWh",
|
||||
"last_valid_state": "None",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -598,6 +611,7 @@ async def test_restore_state(
|
|||
attributes={
|
||||
ATTR_STATUS: COLLECTING,
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_LAST_VALID_STATE: None,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
},
|
||||
),
|
||||
|
@ -625,15 +639,18 @@ async def test_restore_state(
|
|||
assert state.state == "3"
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
assert state.attributes.get("last_reset") == last_reset
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_midpeak")
|
||||
assert state.state == "5"
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_offpeak")
|
||||
assert state.state == "6"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
assert state.attributes.get("last_reset") == last_reset
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_superpeak")
|
||||
|
@ -675,6 +692,7 @@ async def test_restore_state(
|
|||
"name": "Energy bill",
|
||||
"net_consumption": True,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": [],
|
||||
},
|
||||
|
@ -829,6 +847,7 @@ async def test_non_net_consumption(
|
|||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": [],
|
||||
},
|
||||
|
@ -884,7 +903,7 @@ async def test_delta_values(
|
|||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Invalid adjustment of None" in caplog.text
|
||||
assert "Invalid state None" in caplog.text
|
||||
|
||||
now += timedelta(seconds=30)
|
||||
with freeze_time(now):
|
||||
|
@ -918,6 +937,272 @@ async def test_delta_values(
|
|||
assert state.state == "9"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_config", "config_entry_config"),
|
||||
(
|
||||
(
|
||||
{
|
||||
"utility_meter": {
|
||||
"energy_bill": {
|
||||
"source": "sensor.energy",
|
||||
"periodically_resetting": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"cycle": "none",
|
||||
"delta_values": False,
|
||||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": [],
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_non_periodically_resetting(
|
||||
hass: HomeAssistant, yaml_config, config_entry_config
|
||||
) -> None:
|
||||
"""Test utility meter "non periodically resetting" mode."""
|
||||
# Home assistant is not runnit yet
|
||||
hass.state = CoreState.not_running
|
||||
|
||||
now = dt_util.utcnow()
|
||||
with freeze_time(now):
|
||||
if yaml_config:
|
||||
assert await async_setup_component(hass, DOMAIN, yaml_config)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
|
||||
else:
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options=config_entry_config,
|
||||
title=config_entry_config["name"],
|
||||
version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = config_entry_config["source"]
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
|
||||
async_fire_time_changed(hass, now)
|
||||
hass.states.async_set(
|
||||
entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
|
||||
now += timedelta(seconds=30)
|
||||
with freeze_time(now):
|
||||
async_fire_time_changed(hass, now)
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
3,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state.state == "2"
|
||||
assert state.attributes.get("last_valid_state") == "3"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
now += timedelta(seconds=30)
|
||||
with freeze_time(now):
|
||||
async_fire_time_changed(hass, now)
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_UNKNOWN,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state.state == "2"
|
||||
assert state.attributes.get("last_valid_state") == "3"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
now += timedelta(seconds=30)
|
||||
with freeze_time(now):
|
||||
async_fire_time_changed(hass, now)
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
6,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state.state == "5"
|
||||
assert state.attributes.get("last_valid_state") == "6"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
now += timedelta(seconds=30)
|
||||
with freeze_time(now):
|
||||
async_fire_time_changed(hass, now)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
9,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state.state == "8"
|
||||
assert state.attributes.get("last_valid_state") == "9"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_config", "config_entry_config"),
|
||||
(
|
||||
(
|
||||
{
|
||||
"utility_meter": {
|
||||
"energy_bill": {
|
||||
"source": "sensor.energy",
|
||||
"periodically_resetting": False,
|
||||
"tariffs": ["low", "high"],
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"cycle": "none",
|
||||
"delta_values": False,
|
||||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["low", "high"],
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_non_periodically_resetting_meter_with_tariffs(
|
||||
hass: HomeAssistant, yaml_config, config_entry_config
|
||||
) -> None:
|
||||
"""Test test_non_periodically_resetting_meter_with_tariffs."""
|
||||
if yaml_config:
|
||||
assert await async_setup_component(hass, DOMAIN, yaml_config)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
|
||||
else:
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options=config_entry_config,
|
||||
title=config_entry_config["name"],
|
||||
version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = config_entry_config["source"]
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_low")
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
assert state.attributes.get("last_valid_state") == "2"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_high")
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
now = dt_util.utcnow() + timedelta(seconds=10)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
3,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_low")
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
assert state.attributes.get("last_valid_state") == "3"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_high")
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.energy_bill", "option": "high"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_low")
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_high")
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
now = dt_util.utcnow() + timedelta(seconds=20)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
6,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
force_update=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_low")
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
assert state.attributes.get("last_valid_state") == "None"
|
||||
assert state.attributes.get("status") == PAUSED
|
||||
|
||||
state = hass.states.get("sensor.energy_bill_high")
|
||||
assert state is not None
|
||||
assert state.state == "3"
|
||||
assert state.attributes.get("last_valid_state") == "6"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
|
||||
def gen_config(cycle, offset=None):
|
||||
"""Generate configuration."""
|
||||
config = {
|
||||
|
@ -932,7 +1217,9 @@ def gen_config(cycle, offset=None):
|
|||
return config
|
||||
|
||||
|
||||
async def _test_self_reset(hass, config, start_time, expect_reset=True):
|
||||
async def _test_self_reset(
|
||||
hass: HomeAssistant, config, start_time, expect_reset=True
|
||||
) -> None:
|
||||
"""Test energy sensor self reset."""
|
||||
now = dt_util.parse_datetime(start_time)
|
||||
with freeze_time(now):
|
||||
|
@ -1142,3 +1429,27 @@ async def test_bad_offset(hass: HomeAssistant) -> None:
|
|||
assert not await async_setup_component(
|
||||
hass, DOMAIN, gen_config("monthly", timedelta(days=31))
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_adjustment_invalid_new_state(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that calculate_adjustment method returns None if the new state is invalid."""
|
||||
mock_sensor = UtilityMeterSensor(
|
||||
cron_pattern=None,
|
||||
delta_values=False,
|
||||
meter_offset=DEFAULT_OFFSET,
|
||||
meter_type=DAILY,
|
||||
name="Test utility meter",
|
||||
net_consumption=False,
|
||||
parent_meter="sensor.test",
|
||||
periodically_resetting=True,
|
||||
unique_id="test_utility_meter",
|
||||
source_entity="sensor.test",
|
||||
tariff=None,
|
||||
tariff_entity=None,
|
||||
)
|
||||
|
||||
new_state: State = State(entity_id="sensor.test", state="unknown")
|
||||
assert mock_sensor.calculate_adjustment(None, new_state) is None
|
||||
assert "Invalid state unknown" in caplog.text
|
||||
|
|
Loading…
Add table
Reference in a new issue