From a167766ed16fd9b475ac5678060b950cdbb9aa22 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 7 Sep 2024 21:28:04 +0200 Subject: [PATCH 01/10] add sensor platform --- homeassistant/components/bsblan/__init__.py | 2 +- .../components/bsblan/coordinator.py | 6 +- homeassistant/components/bsblan/sensor.py | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/bsblan/sensor.py diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 5ce90db5043693..79447c6cff5e21 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -18,7 +18,7 @@ from .const import CONF_PASSKEY, DOMAIN from .coordinator import BSBLanUpdateCoordinator -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] @dataclasses.dataclass diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py index 3320c0f75007b2..508f2c898c305f 100644 --- a/homeassistant/components/bsblan/coordinator.py +++ b/homeassistant/components/bsblan/coordinator.py @@ -4,7 +4,7 @@ from datetime import timedelta from random import randint -from bsblan import BSBLAN, BSBLANConnectionError, State +from bsblan import BSBLAN, BSBLANConnectionError, Sensor, State from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -19,6 +19,7 @@ class BSBLanCoordinatorData: """BSBLan data stored in the Home Assistant data object.""" state: State + sensor: Sensor class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]): @@ -54,6 +55,7 @@ async def _async_update_data(self) -> BSBLanCoordinatorData: """Get state and sensor data from BSB-Lan device.""" try: state = await self.client.state() + sensor = await self.client.sensor() except BSBLANConnectionError as err: host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown" raise UpdateFailed( @@ -61,4 +63,4 @@ async def _async_update_data(self) -> BSBLanCoordinatorData: ) from err self.update_interval = self._get_update_interval() - return BSBLanCoordinatorData(state=state) + return BSBLanCoordinatorData(state=state, sensor=sensor) diff --git a/homeassistant/components/bsblan/sensor.py b/homeassistant/components/bsblan/sensor.py new file mode 100644 index 00000000000000..2ed830546436fe --- /dev/null +++ b/homeassistant/components/bsblan/sensor.py @@ -0,0 +1,96 @@ +"""Support for BSB-Lan sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import BSBLanData +from .const import DOMAIN +from .coordinator import BSBLanCoordinatorData +from .entity import BSBLanEntity + + +@dataclass(frozen=True, kw_only=True) +class BSBLanSensorEntityDescription(SensorEntityDescription): + """Describes BSB-Lan sensor entity.""" + + value_fn: Callable[[BSBLanCoordinatorData], StateType] + + +SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = ( + BSBLanSensorEntityDescription( + key="current_temperature", + translation_key="current_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.sensor.current_temperature.value, + ), + BSBLanSensorEntityDescription( + key="outside_temperature", + translation_key="outside_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.sensor.outside_temperature.value, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BSB-Lan sensor based on a config entry.""" + data: BSBLanData = hass.data[DOMAIN][entry.entry_id] + async_add_entities(BSBLanSensor(data, description) for description in SENSOR_TYPES) + + +class BSBLanSensor(BSBLanEntity, SensorEntity): + """Defines a BSB-Lan sensor.""" + + entity_description: BSBLanSensorEntityDescription + + def __init__( + self, + data: BSBLanData, + description: BSBLanSensorEntityDescription, + ) -> None: + """Initialize BSB-Lan sensor.""" + super().__init__(data.coordinator, data) + self.entity_description = description + self._attr_unique_id = f"{data.device.MAC}-{description.key}" + self._attr_name = self._generate_name( + description.translation_key, description.key + ) + + @staticmethod + def _generate_name(translation_key: str | None, description_key: str) -> str: + """Generate a name from the translation key.""" + if translation_key is None: + # use key as name + return description_key.replace("_", " ").title() + return translation_key.replace("_", " ").title() + + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + try: + value = self.entity_description.value_fn(self.coordinator.data) + return float(value) if value is not None else None + except (ValueError, AttributeError): + return None From 33cb28ae32ba2f3b86f3d27c916841cf490b6b14 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 7 Sep 2024 21:28:20 +0200 Subject: [PATCH 02/10] refactor: Add sensor data to async_get_config_entry_diagnostics --- homeassistant/components/bsblan/diagnostics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index b4ff67f4fbfe97..88418f306c82be 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -22,6 +22,7 @@ async def async_get_config_entry_diagnostics( "device": data.device.to_dict(), "coordinator_data": { "state": data.coordinator.data.state.to_dict(), + "sensor": data.coordinator.data.sensor.to_dict(), }, "static": data.static.to_dict(), } From fb2fa0ac19f48cd743b13b706880074e40496ee3 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 7 Sep 2024 21:29:17 +0200 Subject: [PATCH 03/10] refactor: Add tests for sensor --- tests/components/bsblan/conftest.py | 5 +- tests/components/bsblan/fixtures/sensor.json | 20 ++ .../bsblan/snapshots/test_diagnostics.ambr | 16 + .../bsblan/snapshots/test_sensor.ambr | 307 ++++++++++++++++++ tests/components/bsblan/test_sensor.py | 175 ++++++++++ 5 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 tests/components/bsblan/fixtures/sensor.json create mode 100644 tests/components/bsblan/snapshots/test_sensor.ambr create mode 100644 tests/components/bsblan/test_sensor.py diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 96445a4bb2349a..68f716d836b3d2 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from bsblan import Device, Info, State, StaticState +from bsblan import Device, Info, Sensor, State, StaticState import pytest from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN @@ -55,6 +55,9 @@ def mock_bsblan() -> Generator[MagicMock, None, None]: bsblan.static_values.return_value = StaticState.from_json( load_fixture("static.json", DOMAIN) ) + bsblan.sensor.return_value = Sensor.from_json( + load_fixture("sensor.json", DOMAIN) + ) yield bsblan diff --git a/tests/components/bsblan/fixtures/sensor.json b/tests/components/bsblan/fixtures/sensor.json new file mode 100644 index 00000000000000..3448e7e98d89b5 --- /dev/null +++ b/tests/components/bsblan/fixtures/sensor.json @@ -0,0 +1,20 @@ +{ + "outside_temperature": { + "name": "Outside temp sensor local", + "error": 0, + "value": "6.1", + "desc": "", + "dataType": 0, + "readonly": 0, + "unit": "°C" + }, + "current_temperature": { + "name": "Room temp 1 actual value", + "error": 0, + "value": "18.6", + "desc": "", + "dataType": 0, + "readonly": 1, + "unit": "°C" + } +} diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index c9a82edf4e2f86..c1d152056ec428 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -2,6 +2,22 @@ # name: test_diagnostics dict({ 'coordinator_data': dict({ + 'sensor': dict({ + 'current_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp 1 actual value', + 'unit': '°C', + 'value': '18.6', + }), + 'outside_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Outside temp sensor local', + 'unit': '°C', + 'value': '6.1', + }), + }), 'state': dict({ 'current_temperature': dict({ 'data_type': 0, diff --git a/tests/components/bsblan/snapshots/test_sensor.ambr b/tests/components/bsblan/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..854bba6a6cce41 --- /dev/null +++ b/tests/components/bsblan/snapshots/test_sensor.ambr @@ -0,0 +1,307 @@ +# serializer version: 1 +# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_temperature', + 'unique_id': '00:80:41:19:69:90-current_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Current Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outside_temperature', + 'unique_id': '00:80:41:19:69:90-outside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Outside Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.1', + }) +# --- +# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_temperature', + 'unique_id': '00:80:41:19:69:90-current_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Current Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outside_temperature', + 'unique_id': '00:80:41:19:69:90-outside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Outside Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.1', + }) +# --- +# name: test_sensor_entity_properties[sensor.bsb_lan_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_temperature', + 'unique_id': '00:80:41:19:69:90-current_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_entity_properties[sensor.bsb_lan_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Current Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_sensor_entity_properties[sensor.bsb_lan_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside Temperature', + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outside_temperature', + 'unique_id': '00:80:41:19:69:90-outside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_entity_properties[sensor.bsb_lan_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'BSB-LAN Outside Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bsb_lan_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.1', + }) +# --- diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py new file mode 100644 index 00000000000000..9a1770dff1a086 --- /dev/null +++ b/tests/components/bsblan/test_sensor.py @@ -0,0 +1,175 @@ +"""Tests for the BSB-Lan sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.bsblan.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er + +from . import setup_with_selected_platforms + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + +ENTITY_CURRENT_TEMP = "sensor.bsb_lan_current_temperature" +ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature" + + +@pytest.mark.parametrize( + ("static_file"), + [ + ("static.json"), + ("static_F.json"), + ], +) +async def test_celsius_fahrenheit( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + static_file: str, +) -> None: + """Test Celsius and Fahrenheit temperature units.""" + # Load static data from fixture + static_data = load_fixture(static_file, DOMAIN) + + # Patch the static_values method to return our test data + with patch.object(mock_bsblan, "static_values", return_value=static_data): + # Set up the sensor platform + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + # Take a snapshot of the entity registry + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_sensor_entity_properties( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the sensor entity properties.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Test when current_temperature is "---" + mock_current_temp = MagicMock() + mock_current_temp.value = "---" + mock_bsblan.sensor.return_value.current_temperature = mock_current_temp + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_CURRENT_TEMP) + assert state.state == "unknown" + + # Test outside_temperature + mock_outside_temp = MagicMock() + mock_outside_temp.value = "6.1" + mock_bsblan.sensor.return_value.outside_temperature = mock_outside_temp + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_OUTSIDE_TEMP) + assert state.state == "6.1" + + +async def test_sensor_update( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor update.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + # Initial state + state = hass.states.get(ENTITY_CURRENT_TEMP) + assert state.state == "18.6" + + # Update the mock sensor value + mock_current_temp = MagicMock() + mock_current_temp.value = "20.0" + mock_bsblan.sensor.return_value.current_temperature = mock_current_temp + + # Trigger an update + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check if the state has been updated + state = hass.states.get(ENTITY_CURRENT_TEMP) + assert state.state == "20.0" + + +@pytest.mark.parametrize( + ("entity_id", "expected_name"), + [ + (ENTITY_CURRENT_TEMP, "BSB-LAN Current Temperature"), + (ENTITY_OUTSIDE_TEMP, "BSB-LAN Outside Temperature"), + ], +) +async def test_sensor_names( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + expected_name: str, +) -> None: + """Test sensor names.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + state = hass.states.get(entity_id) + assert state.name == expected_name + + +@pytest.mark.parametrize( + ("entity_id", "expected_name"), + [ + (ENTITY_CURRENT_TEMP, "BSB-LAN Current Temperature"), + ], +) +async def test_sensor_names_translation_key_None( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + expected_name: str, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor names.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + mock_sensor = MagicMock() + mock_sensor.description.key = "current_temperature" + mock_sensor.description.translation_key = None + mock_bsblan.sensor.return_value.current_temperature = mock_sensor + + # Trigger an update + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Get the updated state + state = hass.states.get(entity_id) + + # Assert the state name matches the expected fallback name + assert state.name == expected_name From 5f85062ddabe1bd078f7fc3efd8777c77dd667c5 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 8 Sep 2024 20:50:06 +0200 Subject: [PATCH 04/10] chore: remove duplicate test --- tests/components/bsblan/test_sensor.py | 40 ++------------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py index 9a1770dff1a086..196416839f2743 100644 --- a/tests/components/bsblan/test_sensor.py +++ b/tests/components/bsblan/test_sensor.py @@ -1,60 +1,24 @@ """Tests for the BSB-Lan sensor platform.""" from datetime import timedelta -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bsblan.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - load_fixture, - snapshot_platform, -) +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform ENTITY_CURRENT_TEMP = "sensor.bsb_lan_current_temperature" ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature" -@pytest.mark.parametrize( - ("static_file"), - [ - ("static.json"), - ("static_F.json"), - ], -) -async def test_celsius_fahrenheit( - hass: HomeAssistant, - mock_bsblan: AsyncMock, - mock_config_entry: MockConfigEntry, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, - static_file: str, -) -> None: - """Test Celsius and Fahrenheit temperature units.""" - # Load static data from fixture - static_data = load_fixture(static_file, DOMAIN) - - # Patch the static_values method to return our test data - with patch.object(mock_bsblan, "static_values", return_value=static_data): - # Set up the sensor platform - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - - # Take a snapshot of the entity registry - await snapshot_platform( - hass, entity_registry, snapshot, mock_config_entry.entry_id - ) - - async def test_sensor_entity_properties( hass: HomeAssistant, mock_bsblan: AsyncMock, From a5e8016718ff4c55f37d25b705af257a1c9bc1fd Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 11 Sep 2024 09:30:38 +0200 Subject: [PATCH 05/10] Update tests/components/bsblan/test_sensor.py Co-authored-by: Joost Lekkerkerker --- tests/components/bsblan/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py index 196416839f2743..ffc89ce7787707 100644 --- a/tests/components/bsblan/test_sensor.py +++ b/tests/components/bsblan/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensor_entity_properties( await hass.async_block_till_done() state = hass.states.get(ENTITY_CURRENT_TEMP) - assert state.state == "unknown" + assert state.state == STATE_UNKNOWN # Test outside_temperature mock_outside_temp = MagicMock() From ec97884da14e25555f8ebe25ef37bd2835c691f3 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Wed, 11 Sep 2024 22:16:20 +0200 Subject: [PATCH 06/10] refactor: let hass use translation_key fix raise --- homeassistant/components/bsblan/sensor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/bsblan/sensor.py b/homeassistant/components/bsblan/sensor.py index 2ed830546436fe..a362f0988ce80c 100644 --- a/homeassistant/components/bsblan/sensor.py +++ b/homeassistant/components/bsblan/sensor.py @@ -74,23 +74,19 @@ def __init__( super().__init__(data.coordinator, data) self.entity_description = description self._attr_unique_id = f"{data.device.MAC}-{description.key}" - self._attr_name = self._generate_name( - description.translation_key, description.key - ) - - @staticmethod - def _generate_name(translation_key: str | None, description_key: str) -> str: - """Generate a name from the translation key.""" - if translation_key is None: - # use key as name - return description_key.replace("_", " ").title() - return translation_key.replace("_", " ").title() @property def native_value(self) -> float | None: """Return the state of the sensor.""" try: value = self.entity_description.value_fn(self.coordinator.data) - return float(value) if value is not None else None - except (ValueError, AttributeError): + except AttributeError: + return None + + if value is None: + return None + + try: + return float(value) + except ValueError: return None From 6fa6371a4bfd08790eeb42a28470820110d12107 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Wed, 11 Sep 2024 22:16:40 +0200 Subject: [PATCH 07/10] refactor: Add new sensor entity names to strings.json --- homeassistant/components/bsblan/strings.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index 7a67d353803957..4fb374fee75eac 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -32,5 +32,15 @@ "set_data_error": { "message": "An error occurred while sending the data to the BSBLAN device" } + }, + "entity": { + "sensor": { + "current_temperature": { + "name": "Current Temperature" + }, + "outside_temperature": { + "name": "Outside Temperature" + } + } } } From e4acb7163fe87ea9f8fcd0fb1abfe147a4433d9c Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Wed, 11 Sep 2024 22:41:52 +0200 Subject: [PATCH 08/10] refactor: Add tests for current temperature sensor --- tests/components/bsblan/test_sensor.py | 58 ++++++++++---------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py index ffc89ce7787707..6fe52eaebeebc1 100644 --- a/tests/components/bsblan/test_sensor.py +++ b/tests/components/bsblan/test_sensor.py @@ -7,7 +7,7 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er @@ -31,6 +31,9 @@ async def test_sensor_entity_properties( await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + state = hass.states.get(ENTITY_CURRENT_TEMP) + assert state.state == "18.6" + # Test when current_temperature is "---" mock_current_temp = MagicMock() mock_current_temp.value = "---" @@ -85,55 +88,36 @@ async def test_sensor_update( @pytest.mark.parametrize( - ("entity_id", "expected_name"), + ("value", "expected_state"), [ - (ENTITY_CURRENT_TEMP, "BSB-LAN Current Temperature"), - (ENTITY_OUTSIDE_TEMP, "BSB-LAN Outside Temperature"), + (18.6, "18.6"), + (42, "42.0"), + (None, STATE_UNKNOWN), + ("---", STATE_UNKNOWN), + ("not a number", STATE_UNKNOWN), ], ) -async def test_sensor_names( +async def test_current_temperature_scenarios( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - entity_id: str, - expected_name: str, -) -> None: - """Test sensor names.""" - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - - state = hass.states.get(entity_id) - assert state.name == expected_name - - -@pytest.mark.parametrize( - ("entity_id", "expected_name"), - [ - (ENTITY_CURRENT_TEMP, "BSB-LAN Current Temperature"), - ], -) -async def test_sensor_names_translation_key_None( - hass: HomeAssistant, - mock_bsblan: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_id: str, - expected_name: str, freezer: FrozenDateTimeFactory, + value, + expected_state, ) -> None: - """Test sensor names.""" + """Test various scenarios for current temperature sensor.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - mock_sensor = MagicMock() - mock_sensor.description.key = "current_temperature" - mock_sensor.description.translation_key = None - mock_bsblan.sensor.return_value.current_temperature = mock_sensor + # Set up the mock value + mock_current_temp = MagicMock() + mock_current_temp.value = value + mock_bsblan.sensor.return_value.current_temperature = mock_current_temp # Trigger an update freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) await hass.async_block_till_done() - # Get the updated state - state = hass.states.get(entity_id) - - # Assert the state name matches the expected fallback name - assert state.name == expected_name + # Check the state + state = hass.states.get(ENTITY_CURRENT_TEMP) + assert state.state == expected_state From 0b1f0cf0df09b511ac0cfc2f7cd459a8079aa6ce Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Thu, 12 Sep 2024 09:23:49 +0200 Subject: [PATCH 09/10] refactor: Update native_value method in BSBLanSensor --- homeassistant/components/bsblan/sensor.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bsblan/sensor.py b/homeassistant/components/bsblan/sensor.py index a362f0988ce80c..346f972ea9ae58 100644 --- a/homeassistant/components/bsblan/sensor.py +++ b/homeassistant/components/bsblan/sensor.py @@ -76,17 +76,9 @@ def __init__( self._attr_unique_id = f"{data.device.MAC}-{description.key}" @property - def native_value(self) -> float | None: + def native_value(self) -> StateType: """Return the state of the sensor.""" - try: - value = self.entity_description.value_fn(self.coordinator.data) - except AttributeError: - return None - - if value is None: - return None - - try: - return float(value) - except ValueError: + value = self.entity_description.value_fn(self.coordinator.data) + if value == "---": return None + return value From 6ad204f7cda071334842f23f35bd8e23351eda84 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Thu, 12 Sep 2024 09:24:27 +0200 Subject: [PATCH 10/10] refactor: Update test --- .../bsblan/snapshots/test_sensor.ambr | 204 ------------------ tests/components/bsblan/test_sensor.py | 57 ----- 2 files changed, 261 deletions(-) diff --git a/tests/components/bsblan/snapshots/test_sensor.ambr b/tests/components/bsblan/snapshots/test_sensor.ambr index 854bba6a6cce41..0146dd23b3ddbc 100644 --- a/tests/components/bsblan/snapshots/test_sensor.ambr +++ b/tests/components/bsblan/snapshots/test_sensor.ambr @@ -1,208 +1,4 @@ # serializer version: 1 -# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_current_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.bsb_lan_current_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current Temperature', - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'current_temperature', - 'unique_id': '00:80:41:19:69:90-current_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_current_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'BSB-LAN Current Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bsb_lan_current_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '18.6', - }) -# --- -# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_outside_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.bsb_lan_outside_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Outside Temperature', - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'outside_temperature', - 'unique_id': '00:80:41:19:69:90-outside_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_celsius_fahrenheit[static.json][sensor.bsb_lan_outside_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'BSB-LAN Outside Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bsb_lan_outside_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6.1', - }) -# --- -# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_current_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.bsb_lan_current_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current Temperature', - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'current_temperature', - 'unique_id': '00:80:41:19:69:90-current_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_current_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'BSB-LAN Current Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bsb_lan_current_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '18.6', - }) -# --- -# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_outside_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.bsb_lan_outside_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Outside Temperature', - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'outside_temperature', - 'unique_id': '00:80:41:19:69:90-outside_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_celsius_fahrenheit[static_F.json][sensor.bsb_lan_outside_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'BSB-LAN Outside Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bsb_lan_outside_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6.1', - }) -# --- # name: test_sensor_entity_properties[sensor.bsb_lan_current_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py index 6fe52eaebeebc1..dc22574168d399 100644 --- a/tests/components/bsblan/test_sensor.py +++ b/tests/components/bsblan/test_sensor.py @@ -31,70 +31,13 @@ async def test_sensor_entity_properties( await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - state = hass.states.get(ENTITY_CURRENT_TEMP) - assert state.state == "18.6" - - # Test when current_temperature is "---" - mock_current_temp = MagicMock() - mock_current_temp.value = "---" - mock_bsblan.sensor.return_value.current_temperature = mock_current_temp - - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_CURRENT_TEMP) - assert state.state == STATE_UNKNOWN - - # Test outside_temperature - mock_outside_temp = MagicMock() - mock_outside_temp.value = "6.1" - mock_bsblan.sensor.return_value.outside_temperature = mock_outside_temp - - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_OUTSIDE_TEMP) - assert state.state == "6.1" - - -async def test_sensor_update( - hass: HomeAssistant, - mock_bsblan: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test sensor update.""" - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - - # Initial state - state = hass.states.get(ENTITY_CURRENT_TEMP) - assert state.state == "18.6" - - # Update the mock sensor value - mock_current_temp = MagicMock() - mock_current_temp.value = "20.0" - mock_bsblan.sensor.return_value.current_temperature = mock_current_temp - - # Trigger an update - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - # Check if the state has been updated - state = hass.states.get(ENTITY_CURRENT_TEMP) - assert state.state == "20.0" - @pytest.mark.parametrize( ("value", "expected_state"), [ (18.6, "18.6"), - (42, "42.0"), (None, STATE_UNKNOWN), ("---", STATE_UNKNOWN), - ("not a number", STATE_UNKNOWN), ], ) async def test_current_temperature_scenarios(