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>
This commit is contained in:
dougiteixeira 2024-01-25 16:46:33 -03:00 committed by GitHub
parent faad9a7584
commit 7713cf377d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 199 additions and 3 deletions

View file

@ -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,
)

View file

@ -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(),
}
)

View file

@ -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"

View file

@ -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

View file

@ -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%]"
}
}

View file

@ -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": "",
}

View file

@ -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,