From 7713cf377d0dae74bacec69b166bca0641432205 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:46:33 -0300 Subject: [PATCH] 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 * Update homeassistant/components/utility_meter/sensor.py Co-authored-by: Robert Resch * 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 * Update homeassistant/components/utility_meter/strings.json Co-authored-by: Robert Resch * Changes tests * Add test_always_available * Use freezegun * Update homeassistant/components/utility_meter/strings.json --------- Co-authored-by: Robert Resch Co-authored-by: Erik Montnemery --- .../components/utility_meter/__init__.py | 2 + .../components/utility_meter/config_flow.py | 9 ++ .../components/utility_meter/const.py | 1 + .../components/utility_meter/sensor.py | 17 ++- .../components/utility_meter/strings.json | 4 + .../utility_meter/test_config_flow.py | 68 +++++++++++- tests/components/utility_meter/test_sensor.py | 101 ++++++++++++++++++ 7 files changed, 199 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index ffe6d7f5433..4b99611684a 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -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, ) diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index eb5c19941dc..0ca9ee12f58 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -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(), } ) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index f8a4c2d4b75..6e1cabac509 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -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" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 794a65db03a..ee0d5f85b3b 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -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 diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index f38989b536e..fc1c727fb0a 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -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%]" } } diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 262dbf36306..75ea6d3a4d2 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -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": "", } diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 37127363614..fa1e3aa8785 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -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,