From 7af5bcc88a1d840f75d5a80819318dae73b28764 Mon Sep 17 00:00:00 2001 From: ajplotkin <61085222+ajplotkin@users.noreply.github.com> Date: Sat, 19 Sep 2026 02:39:24 -0700 Subject: [PATCH] Suppress the placeholder water temperature on Flo valves without the sensor (#182004) --- homeassistant/components/flo/const.py | 5 +++ homeassistant/components/flo/coordinator.py | 11 ++++-- tests/components/flo/conftest.py | 17 +++++++-- tests/components/flo/test_sensor.py | 41 ++++++++++++++++++++- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py index b2abe1150c51..4eceb8f4f5a3 100644 --- a/homeassistant/components/flo/const.py +++ b/homeassistant/components/flo/const.py @@ -10,3 +10,8 @@ FLO_HOME = "home" FLO_AWAY = "away" FLO_SLEEP = "sleep" FLO_MODES = [FLO_HOME, FLO_AWAY, FLO_SLEEP] + +# Valves without a water-temperature sensor report a fixed placeholder instead of +# omitting tempF. No domestic supply reaches boiling, so a reading at or above +# this is a sentinel rather than a measurement. +IMPLAUSIBLE_WATER_TEMP_F = 212.0 diff --git a/homeassistant/components/flo/coordinator.py b/homeassistant/components/flo/coordinator.py index 93d894b4d97c..0280bdbf26ac 100644 --- a/homeassistant/components/flo/coordinator.py +++ b/homeassistant/components/flo/coordinator.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import DOMAIN, LOGGER +from .const import DOMAIN, IMPLAUSIBLE_WATER_TEMP_F, LOGGER type FloConfigEntry = ConfigEntry[FloRuntimeData] @@ -144,9 +144,12 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): return self._device_information["telemetry"]["current"]["psi"] @property - def temperature(self) -> float: - """Return the current temperature in degrees F.""" - return self._device_information["telemetry"]["current"]["tempF"] + def temperature(self) -> float | None: + """Return the current temperature in degrees F, or None if not measured.""" + temperature = self._device_information["telemetry"]["current"]["tempF"] + if temperature is None or temperature >= IMPLAUSIBLE_WATER_TEMP_F: + return None + return temperature @property def humidity(self) -> float: diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 5b303d5c4b45..0cc0a5e40083 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONTENT_TYPE_JSON from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -26,7 +26,18 @@ def config_entry() -> MockConfigEntry: @pytest.fixture -def aioclient_mock_fixture(aioclient_mock: AiohttpClientMocker) -> None: +def device_info_response(request: pytest.FixtureRequest) -> str: + """Shutoff valve device info, with tempF overridden when parametrized.""" + device_info = load_json_object_fixture("flo/device_info_response.json") + if hasattr(request, "param"): + device_info["telemetry"]["current"]["tempF"] = request.param + return json.dumps(device_info) + + +@pytest.fixture +def aioclient_mock_fixture( + aioclient_mock: AiohttpClientMocker, device_info_response: str +) -> None: """Fixture to provide a aioclient mocker.""" now = round(time.time()) # Mocks the login response for flo. @@ -56,7 +67,7 @@ def aioclient_mock_fixture(aioclient_mock: AiohttpClientMocker) -> None: # Mocks the devices for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/devices/98765", - text=load_fixture("flo/device_info_response.json"), + text=device_info_response, status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 8c12da7c679a..118e27a259bc 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -1,5 +1,7 @@ """Test Flo by Moen sensor entities.""" +import json + import pytest from homeassistant.components.homeassistant import ( @@ -7,7 +9,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -105,3 +107,40 @@ async def test_manual_update_entity( blocking=True, ) assert aioclient_mock.call_count == call_count + 3 + + +@pytest.mark.parametrize("device_info_response", [225, 212, 220], indirect=True) +@pytest.mark.usefixtures("aioclient_mock_fixture") +async def test_water_temperature_placeholder_is_not_published( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """A valve with no temperature sensor reports a placeholder, not a reading.""" + hass.config.units = US_CUSTOMARY_SYSTEM + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.smart_water_shutoff_water_temperature").state + == STATE_UNKNOWN + ) + + # The detector measures ambient air and is unaffected. + assert hass.states.get("sensor.kitchen_sink_temperature").state == "61" + + +@pytest.mark.parametrize("device_info_response", [211.9, 70], indirect=True) +@pytest.mark.usefixtures("aioclient_mock_fixture") +async def test_water_temperature_below_threshold_is_published( + hass: HomeAssistant, config_entry: MockConfigEntry, device_info_response: str +) -> None: + """Anything below boiling is a real reading and is published.""" + hass.config.units = US_CUSTOMARY_SYSTEM + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + expected = json.loads(device_info_response)["telemetry"]["current"]["tempF"] + assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == str( + round(expected, 1) + )