Add platform sensor to BSBLAN integration (#125474)

* add sensor platform

* refactor: Add sensor data to async_get_config_entry_diagnostics

* refactor: Add tests for sensor

* chore: remove duplicate test

* Update tests/components/bsblan/test_sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* refactor: let hass use translation_key

fix raise

* refactor: Add new sensor entity names to strings.json

* refactor: Add tests for current temperature sensor

* refactor: Update native_value method in BSBLanSensor

* refactor: Update test

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/125761/head
Willem-Jan van Rootselaar 2024-09-13 14:04:00 +02:00 committed by GitHub
parent 6aa07243cd
commit 1ae1391cb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 309 additions and 4 deletions

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PASSKEY, DOMAIN
from .coordinator import BSBLanUpdateCoordinator
PLATFORMS = [Platform.CLIMATE]
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
@dataclasses.dataclass

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
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 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[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 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
) from err
self.update_interval = self._get_update_interval()
return BSBLanCoordinatorData(state=state)
return BSBLanCoordinatorData(state=state, sensor=sensor)

View File

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

View File

@ -0,0 +1,84 @@
"""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}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
value = self.entity_description.value_fn(self.coordinator.data)
if value == "---":
return None
return value

View File

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

View File

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

View File

@ -0,0 +1,20 @@
{
"outside_temperature": {
"name": "Outside temp sensor local",
"error": 0,
"value": "6.1",
"desc": "",
"dataType": 0,
"readonly": 0,
"unit": "&deg;C"
},
"current_temperature": {
"name": "Room temp 1 actual value",
"error": 0,
"value": "18.6",
"desc": "",
"dataType": 0,
"readonly": 1,
"unit": "&deg;C"
}
}

View File

@ -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': '&deg;C',
'value': '18.6',
}),
'outside_temperature': dict({
'data_type': 0,
'desc': '',
'name': 'Outside temp sensor local',
'unit': '&deg;C',
'value': '6.1',
}),
}),
'state': dict({
'current_temperature': dict({
'data_type': 0,

View File

@ -0,0 +1,103 @@
# serializer version: 1
# name: test_sensor_entity_properties[sensor.bsb_lan_current_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'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': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# 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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bsb_lan_current_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '18.6',
})
# ---
# name: test_sensor_entity_properties[sensor.bsb_lan_outside_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'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': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# 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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bsb_lan_outside_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6.1',
})
# ---

View File

@ -0,0 +1,66 @@
"""Tests for the BSB-Lan sensor platform."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_UNKNOWN, 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, snapshot_platform
ENTITY_CURRENT_TEMP = "sensor.bsb_lan_current_temperature"
ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature"
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)
@pytest.mark.parametrize(
("value", "expected_state"),
[
(18.6, "18.6"),
(None, STATE_UNKNOWN),
("---", STATE_UNKNOWN),
],
)
async def test_current_temperature_scenarios(
hass: HomeAssistant,
mock_bsblan: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
value,
expected_state,
) -> None:
"""Test various scenarios for current temperature sensor."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.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()
# Check the state
state = hass.states.get(ENTITY_CURRENT_TEMP)
assert state.state == expected_state