Add utility meter option for the sensor to always be available (#103481)
* Adds option for the sensor to always be available * Remove logger debug * Add migration config entry version * Update homeassistant/components/utility_meter/config_flow.py Co-authored-by: Robert Resch <robert@resch.dev> * Update homeassistant/components/utility_meter/sensor.py Co-authored-by: Robert Resch <robert@resch.dev> * Remove migration config entry version * Change CONF_SENSOR_ALWAYS_AVAILABLE optional in CONFIG_SCHEMA * Remove CONF_SENSOR_ALWAYS_AVAILABLE in tests * Remove CONF_SENSOR_ALWAYS_AVAILABLE in tests * Remove CONF_SENSOR_ALWAYS_AVAILABLE in tests * Add option in yaml * Update homeassistant/components/utility_meter/strings.json Co-authored-by: Robert Resch <robert@resch.dev> * Update homeassistant/components/utility_meter/strings.json Co-authored-by: Robert Resch <robert@resch.dev> * Changes tests * Add test_always_available * Use freezegun * Update homeassistant/components/utility_meter/strings.json --------- Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Erik Montnemery <erik@montnemery.com>pull/105955/head
parent
faad9a7584
commit
7713cf377d
|
@ -27,6 +27,7 @@ from .const import (
|
|||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFF,
|
||||
CONF_TARIFF_ENTITY,
|
||||
|
@ -93,6 +94,7 @@ METER_CONFIG_SCHEMA = vol.Schema(
|
|||
cv.ensure_list, vol.Unique(), [cv.string]
|
||||
),
|
||||
vol.Optional(CONF_CRON_PATTERN): validate_cron_pattern,
|
||||
vol.Optional(CONF_SENSOR_ALWAYS_AVAILABLE, default=False): cv.boolean,
|
||||
},
|
||||
period_or_cron,
|
||||
)
|
||||
|
|
|
@ -23,6 +23,7 @@ from .const import (
|
|||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFFS,
|
||||
DAILY,
|
||||
|
@ -68,6 +69,10 @@ OPTIONS_SCHEMA = vol.Schema(
|
|||
vol.Required(
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
): selector.BooleanSelector(),
|
||||
vol.Optional(
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE,
|
||||
default=False,
|
||||
): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -103,6 +108,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
default=True,
|
||||
): selector.BooleanSelector(),
|
||||
vol.Optional(
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE,
|
||||
default=False,
|
||||
): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ CONF_TARIFFS = "tariffs"
|
|||
CONF_TARIFF = "tariff"
|
||||
CONF_TARIFF_ENTITY = "tariff_entity"
|
||||
CONF_CRON_PATTERN = "cron"
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE = "always_available"
|
||||
|
||||
ATTR_TARIFF = "tariff"
|
||||
ATTR_TARIFFS = "tariffs"
|
||||
|
|
|
@ -58,6 +58,7 @@ from .const import (
|
|||
CONF_METER_OFFSET,
|
||||
CONF_METER_PERIODICALLY_RESETTING,
|
||||
CONF_METER_TYPE,
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE,
|
||||
CONF_SOURCE_SENSOR,
|
||||
CONF_TARIFF,
|
||||
CONF_TARIFF_ENTITY,
|
||||
|
@ -158,6 +159,9 @@ async def async_setup_entry(
|
|||
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]
|
||||
sensor_always_available = config_entry.options.get(
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE, False
|
||||
)
|
||||
|
||||
meters = []
|
||||
tariffs = config_entry.options[CONF_TARIFFS]
|
||||
|
@ -178,6 +182,7 @@ async def async_setup_entry(
|
|||
tariff=None,
|
||||
unique_id=entry_id,
|
||||
device_info=device_info,
|
||||
sensor_always_available=sensor_always_available,
|
||||
)
|
||||
meters.append(meter_sensor)
|
||||
hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor)
|
||||
|
@ -198,6 +203,7 @@ async def async_setup_entry(
|
|||
tariff=tariff,
|
||||
unique_id=f"{entry_id}_{tariff}",
|
||||
device_info=device_info,
|
||||
sensor_always_available=sensor_always_available,
|
||||
)
|
||||
meters.append(meter_sensor)
|
||||
hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor)
|
||||
|
@ -264,6 +270,9 @@ async def async_setup_platform(
|
|||
CONF_TARIFF_ENTITY
|
||||
)
|
||||
conf_cron_pattern = hass.data[DATA_UTILITY][meter].get(CONF_CRON_PATTERN)
|
||||
conf_sensor_always_available = hass.data[DATA_UTILITY][meter][
|
||||
CONF_SENSOR_ALWAYS_AVAILABLE
|
||||
]
|
||||
meter_sensor = UtilityMeterSensor(
|
||||
cron_pattern=conf_cron_pattern,
|
||||
delta_values=conf_meter_delta_values,
|
||||
|
@ -278,6 +287,7 @@ async def async_setup_platform(
|
|||
tariff=conf_sensor_tariff,
|
||||
unique_id=conf_sensor_unique_id,
|
||||
suggested_entity_id=suggested_entity_id,
|
||||
sensor_always_available=conf_sensor_always_available,
|
||||
)
|
||||
meters.append(meter_sensor)
|
||||
|
||||
|
@ -370,6 +380,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
tariff_entity,
|
||||
tariff,
|
||||
unique_id,
|
||||
sensor_always_available,
|
||||
suggested_entity_id=None,
|
||||
device_info=None,
|
||||
):
|
||||
|
@ -397,6 +408,7 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
_LOGGER.debug("CRON pattern: %s", self._cron_pattern)
|
||||
else:
|
||||
self._cron_pattern = cron_pattern
|
||||
self._sensor_always_available = sensor_always_available
|
||||
self._sensor_delta_values = delta_values
|
||||
self._sensor_net_consumption = net_consumption
|
||||
self._sensor_periodically_resetting = periodically_resetting
|
||||
|
@ -458,8 +470,9 @@ class UtilityMeterSensor(RestoreSensor):
|
|||
if (
|
||||
source_state := self.hass.states.get(self._sensor_source_id)
|
||||
) is None or source_state.state == STATE_UNAVAILABLE:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
if not self._sensor_always_available:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"title": "Add Utility Meter",
|
||||
"description": "Create a sensor which tracks consumption of various utilities (e.g., energy, gas, water, heating) over a configured period of time, typically monthly. The utility meter sensor optionally supports splitting the consumption by tariffs, in that case one sensor for each tariff is created as well as a select entity to choose the current tariff.",
|
||||
"data": {
|
||||
"always_available": "Sensor always available",
|
||||
"cycle": "Meter reset cycle",
|
||||
"delta_values": "Delta values",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
|
@ -16,6 +17,7 @@
|
|||
"tariffs": "Supported tariffs"
|
||||
},
|
||||
"data_description": {
|
||||
"always_available": "If activated, the sensor will always be show the last known value, even if the source entity is unavailable or unknown.",
|
||||
"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.",
|
||||
|
@ -29,10 +31,12 @@
|
|||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"always_available": "[%key:component::utility_meter::config::step::user::data::always_available%]",
|
||||
"source": "[%key:component::utility_meter::config::step::user::data::source%]",
|
||||
"periodically_resetting": "[%key:component::utility_meter::config::step::user::data::periodically_resetting%]"
|
||||
},
|
||||
"data_description": {
|
||||
"always_available": "[%key:component::utility_meter::config::step::user::data_description::always_available%]",
|
||||
"periodically_resetting": "[%key:component::utility_meter::config::step::user::data_description::periodically_resetting%]"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"always_available": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
@ -63,6 +64,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"always_available": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
@ -100,6 +102,7 @@ async def test_tariffs(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"periodically_resetting": True,
|
||||
"always_available": False,
|
||||
"offset": 0,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": ["cat", "dog", "horse", "cow"],
|
||||
|
@ -114,6 +117,7 @@ async def test_tariffs(hass: HomeAssistant) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"always_available": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": ["cat", "dog", "horse", "cow"],
|
||||
}
|
||||
|
@ -173,6 +177,7 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None:
|
|||
"name": "Electricity meter",
|
||||
"net_consumption": False,
|
||||
"periodically_resetting": False,
|
||||
"always_available": False,
|
||||
"offset": 0,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
|
@ -187,6 +192,61 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"always_available": False,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_always_available(hass: HomeAssistant) -> None:
|
||||
"""Test sensor always available."""
|
||||
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": [],
|
||||
"always_available": True,
|
||||
},
|
||||
)
|
||||
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,
|
||||
"always_available": True,
|
||||
"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,
|
||||
"always_available": True,
|
||||
"source": input_sensor_entity_id,
|
||||
"tariffs": [],
|
||||
}
|
||||
|
@ -237,7 +297,11 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"source": input_sensor2_entity_id, "periodically_resetting": False},
|
||||
user_input={
|
||||
"source": input_sensor2_entity_id,
|
||||
"periodically_resetting": False,
|
||||
"always_available": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
|
@ -247,6 +311,7 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"always_available": True,
|
||||
"source": input_sensor2_entity_id,
|
||||
"tariffs": "",
|
||||
}
|
||||
|
@ -258,6 +323,7 @@ async def test_options(hass: HomeAssistant) -> None:
|
|||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": False,
|
||||
"always_available": True,
|
||||
"source": input_sensor2_entity_id,
|
||||
"tariffs": "",
|
||||
}
|
||||
|
|
|
@ -231,6 +231,106 @@ async def test_state(hass: HomeAssistant, yaml_config, config_entry_config) -> N
|
|||
assert state.state == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_config", "config_entry_config"),
|
||||
(
|
||||
(
|
||||
{
|
||||
"utility_meter": {
|
||||
"energy_bill": {
|
||||
"source": "sensor.energy",
|
||||
"always_available": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"cycle": "none",
|
||||
"delta_values": False,
|
||||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": [],
|
||||
"always_available": True,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_state_always_available(
|
||||
hass: HomeAssistant, yaml_config, config_entry_config
|
||||
) -> None:
|
||||
"""Test utility sensor state."""
|
||||
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"],
|
||||
)
|
||||
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_STARTED)
|
||||
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")
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
now = dt_util.utcnow() + timedelta(seconds=10)
|
||||
with freeze_time(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 is not None
|
||||
assert state.state == "1"
|
||||
assert state.attributes.get("status") == COLLECTING
|
||||
|
||||
# test unavailable state
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"unavailable",
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
|
||||
# test unknown state
|
||||
hass.states.async_set(
|
||||
entity_id, None, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"yaml_config",
|
||||
(
|
||||
|
@ -1460,6 +1560,7 @@ def test_calculate_adjustment_invalid_new_state(
|
|||
net_consumption=False,
|
||||
parent_meter="sensor.test",
|
||||
periodically_resetting=True,
|
||||
sensor_always_available=False,
|
||||
unique_id="test_utility_meter",
|
||||
source_entity="sensor.test",
|
||||
tariff=None,
|
||||
|
|
Loading…
Reference in New Issue