From 57f792d88fc682475d3aa8c04687ec45ff2a392e Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Thu, 5 Jan 2023 18:45:06 -0500 Subject: [PATCH] Add support for `WetDry`, `WindHeading`, and `Flex` fields in LaCrosse View (#79062) * Add support for WetDry and WindHeading fields in LaCrosse View * Improve test coverage * Verify data type before conversion * Improve test coverage * Convert to more concise type conversion * Add Flex field as per #79529 * Improve code quality * Add check if expected field is missing --- .../components/lacrosse_view/sensor.py | 36 +++++++++-- tests/components/lacrosse_view/__init__.py | 55 +++++++++++++++++ tests/components/lacrosse_view/test_sensor.py | 61 +++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 7ef154015cb..a136ac86e66 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, UnitOfSpeed, @@ -33,7 +34,7 @@ from .const import DOMAIN, LOGGER class LaCrosseSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[Sensor, str], float] + value_fn: Callable[[Sensor, str], float | int | str | None] @dataclass @@ -43,9 +44,20 @@ class LaCrosseSensorEntityDescription( """Description for LaCrosse View sensor.""" -def get_value(sensor: Sensor, field: str) -> float: +def get_value(sensor: Sensor, field: str) -> float | int | str | None: """Get the value of a sensor field.""" - return float(sensor.data[field]["values"][-1]["s"]) + field_data = sensor.data.get(field) + if field_data is None: + LOGGER.warning( + "No field %s in response for %s (%s)", field, sensor.name, sensor.model + ) + return None + value = field_data["values"][-1]["s"] + try: + value = float(value) + except ValueError: + return str(value) # handle non-numericals + return int(value) if value.is_integer() else value PARALLEL_UPDATES = 0 @@ -90,6 +102,22 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, ), + "WindHeading": LaCrosseSensorEntityDescription( + key="WindHeading", + name="Wind heading", + value_fn=get_value, + native_unit_of_measurement=DEGREE, + ), + "WetDry": LaCrosseSensorEntityDescription( + key="WetDry", + name="Wet/Dry", + value_fn=get_value, + ), + "Flex": LaCrosseSensorEntityDescription( + key="Flex", + name="Flex", + value_fn=get_value, + ), } @@ -163,7 +191,7 @@ class LaCrosseViewSensor( self.index = index @property - def native_value(self) -> float | str: + def native_value(self) -> int | float | str | None: """Return the sensor value.""" return self.entity_description.value_fn( self.coordinator.data[self.index], self.entity_description.key diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index bd4ccb17b17..d242ea4d60a 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -41,3 +41,58 @@ TEST_UNSUPPORTED_SENSOR = Sensor( permissions={"read": True}, model="Test", ) +TEST_FLOAT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2.3"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_STRING_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WetDry"], + location=Location(id="1", name="Test"), + data={"WetDry": {"values": [{"s": "dry"}], "unit": "wet_dry"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_FLOAT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["HeatIndex"], + location=Location(id="1", name="Test"), + data={"HeatIndex": {"values": [{"s": 2.3}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_INT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WindSpeed"], + location=Location(id="1", name="Test"), + data={"WindSpeed": {"values": [{"s": 2}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_FIELD_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 0e102c2f3ef..9c93a7f0111 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -1,14 +1,23 @@ """Test the LaCrosse View sensors.""" +from typing import Any from unittest.mock import patch +from lacrosse_view import Sensor +import pytest + from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import ( MOCK_ENTRY_DATA, + TEST_ALREADY_FLOAT_SENSOR, + TEST_ALREADY_INT_SENSOR, + TEST_FLOAT_SENSOR, + TEST_NO_FIELD_SENSOR, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR, + TEST_STRING_SENSOR, TEST_UNSUPPORTED_SENSOR, ) @@ -71,3 +80,55 @@ async def test_field_not_supported(hass: HomeAssistant, caplog) -> None: assert entries[0].state == ConfigEntryState.LOADED assert hass.states.get("sensor.test_some_unsupported_field") is None assert "Unsupported sensor field" in caplog.text + + +@pytest.mark.parametrize( + "test_input,expected,entity_id", + [ + (TEST_FLOAT_SENSOR, "2.3", "temperature"), + (TEST_STRING_SENSOR, "dry", "wet_dry"), + (TEST_ALREADY_FLOAT_SENSOR, "-16.5", "heat_index"), + (TEST_ALREADY_INT_SENSOR, "2", "wind_speed"), + ], +) +async def test_field_types( + hass: HomeAssistant, test_input: Sensor, expected: Any, entity_id: str +) -> None: + """Test the different data types for fields.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[test_input], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get(f"sensor.test_{entity_id}").state == expected + + +async def test_no_field(hass: HomeAssistant, caplog: Any) -> None: + """Test behavior when the expected field is not present.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_NO_FIELD_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_temperature").state == "unknown" + assert "No field Temperature in response for Test (Test)" in caplog.text