From d246d02ab8d028439ab56681657a6127c1589da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= <7945681+CM000n@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:08:27 +0200 Subject: [PATCH] Add apsystems diagnostic binary sensors (#123045) * add diagnostic sensors * select output_data from data * split sensor and binary_sensor configurations * adjust module description * convert values to bool * add strings * add tests * add tests * update translations * remove already available _attr_has_entity_name * use dataclass instead of TypedDict * Update tests/components/apsystems/test_binary_sensor.py --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> --- .../components/apsystems/__init__.py | 7 +- .../components/apsystems/binary_sensor.py | 102 +++++ .../components/apsystems/coordinator.py | 19 +- homeassistant/components/apsystems/sensor.py | 2 +- .../components/apsystems/strings.json | 14 + tests/components/apsystems/conftest.py | 8 +- .../snapshots/test_binary_sensor.ambr | 377 ++++++++++++++++++ .../apsystems/test_binary_sensor.py | 31 ++ 8 files changed, 553 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/apsystems/binary_sensor.py create mode 100644 tests/components/apsystems/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/apsystems/test_binary_sensor.py diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 91650201a87..372ce52e049 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -13,7 +13,12 @@ from homeassistant.core import HomeAssistant from .const import DEFAULT_PORT from .coordinator import ApSystemsDataCoordinator -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] @dataclass diff --git a/homeassistant/components/apsystems/binary_sensor.py b/homeassistant/components/apsystems/binary_sensor.py new file mode 100644 index 00000000000..528203dc2d9 --- /dev/null +++ b/homeassistant/components/apsystems/binary_sensor.py @@ -0,0 +1,102 @@ +"""The read-only binary sensors for APsystems local API integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from APsystemsEZ1 import ReturnAlarmInfo + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import ApSystemsConfigEntry, ApSystemsData +from .coordinator import ApSystemsDataCoordinator +from .entity import ApSystemsEntity + + +@dataclass(frozen=True, kw_only=True) +class ApsystemsLocalApiBinarySensorDescription(BinarySensorEntityDescription): + """Describes Apsystens Inverter binary sensor entity.""" + + is_on: Callable[[ReturnAlarmInfo], bool | None] + + +BINARY_SENSORS: tuple[ApsystemsLocalApiBinarySensorDescription, ...] = ( + ApsystemsLocalApiBinarySensorDescription( + key="off_grid_status", + translation_key="off_grid_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.og), + ), + ApsystemsLocalApiBinarySensorDescription( + key="dc_1_short_circuit_error_status", + translation_key="dc_1_short_circuit_error_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.isce1), + ), + ApsystemsLocalApiBinarySensorDescription( + key="dc_2_short_circuit_error_status", + translation_key="dc_2_short_circuit_error_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.isce2), + ), + ApsystemsLocalApiBinarySensorDescription( + key="output_fault_status", + translation_key="output_fault_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.oe), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ApSystemsConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + config = config_entry.runtime_data + + add_entities( + ApSystemsBinarySensorWithDescription( + data=config, + entity_description=desc, + ) + for desc in BINARY_SENSORS + ) + + +class ApSystemsBinarySensorWithDescription( + CoordinatorEntity[ApSystemsDataCoordinator], ApSystemsEntity, BinarySensorEntity +): + """Base binary sensor to be used with description.""" + + entity_description: ApsystemsLocalApiBinarySensorDescription + + def __init__( + self, + data: ApSystemsData, + entity_description: ApsystemsLocalApiBinarySensorDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(data.coordinator) + ApSystemsEntity.__init__(self, data) + self.entity_description = entity_description + self._attr_unique_id = f"{data.device_id}_{entity_description.key}" + + @property + def is_on(self) -> bool | None: + """Return value of sensor.""" + return self.entity_description.is_on(self.coordinator.data.alarm_info) diff --git a/homeassistant/components/apsystems/coordinator.py b/homeassistant/components/apsystems/coordinator.py index f2d076ce3fd..96956bafc3e 100644 --- a/homeassistant/components/apsystems/coordinator.py +++ b/homeassistant/components/apsystems/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from APsystemsEZ1 import APsystemsEZ1M, ReturnOutputData +from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -12,7 +13,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import LOGGER -class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]): +@dataclass +class ApSystemsSensorData: + """Representing different Apsystems sensor data.""" + + output_data: ReturnOutputData + alarm_info: ReturnAlarmInfo + + +class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]): """Coordinator used for all sensors.""" def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None: @@ -25,5 +34,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]): ) self.api = api - async def _async_update_data(self) -> ReturnOutputData: - return await self.api.get_output_data() + async def _async_update_data(self) -> ApSystemsSensorData: + output_data = await self.api.get_output_data() + alarm_info = await self.api.get_alarm_info() + return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info) diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py index 637def4e418..afeb9d071ab 100644 --- a/homeassistant/components/apsystems/sensor.py +++ b/homeassistant/components/apsystems/sensor.py @@ -148,4 +148,4 @@ class ApSystemsSensorWithDescription( @property def native_value(self) -> StateType: """Return value of sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + return self.entity_description.value_fn(self.coordinator.data.output_data) diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index 18200f7b49d..e02f86c2730 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -19,6 +19,20 @@ } }, "entity": { + "binary_sensor": { + "off_grid_status": { + "name": "Off grid status" + }, + "dc_1_short_circuit_error_status": { + "name": "DC 1 short circuit error status" + }, + "dc_2_short_circuit_error_status": { + "name": "DC 2 short circuit error status" + }, + "output_fault_status": { + "name": "Output fault status" + } + }, "sensor": { "total_power": { "name": "Total power" diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index c191c7ca2dc..7e6140e8279 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData, Status +from APsystemsEZ1 import ReturnAlarmInfo, ReturnDeviceInfo, ReturnOutputData, Status import pytest from homeassistant.components.apsystems.const import DOMAIN @@ -52,6 +52,12 @@ def mock_apsystems() -> Generator[MagicMock]: e2=6.0, te2=7.0, ) + mock_api.get_alarm_info.return_value = ReturnAlarmInfo( + og=Status.normal, + isce1=Status.alarm, + isce2=Status.normal, + oe=Status.alarm, + ) mock_api.get_device_power_status.return_value = Status.normal yield mock_api diff --git a/tests/components/apsystems/snapshots/test_binary_sensor.ambr b/tests/components/apsystems/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..bb06b019f31 --- /dev/null +++ b/tests/components/apsystems/snapshots/test_binary_sensor.ambr @@ -0,0 +1,377 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.mock_title_dc_1_short_circuit_error_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_dc_1_short_circuit_error_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'DC 1 short circuit error status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_1_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_1_short_circuit_error_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title DC 1 short circuit error status', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_dc_1_short_circuit_error_status', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_2_short_circuit_error_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_dc_2_short_circuit_error_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'DC 2 short circuit error status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_2_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_2_short_circuit_error_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title DC 2 short circuit error status', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_dc_2_short_circuit_error_status', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_off_grid_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_off_grid_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Off grid status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'off_grid_status', + 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_off_grid_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Off grid status', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_off_grid_status', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_output_fault_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_output_fault_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Output fault status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'output_fault_status', + 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_output_fault_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Output fault status', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_output_fault_status', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'off_grid_status', + 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_problem', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_problem_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_1_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_problem_2', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_problem_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_2_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_problem_3', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'binary_sensor.mock_title_problem_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'output_fault_status', + 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': <ANY>, + 'entity_id': 'binary_sensor.mock_title_problem_4', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'on', + }) +# --- diff --git a/tests/components/apsystems/test_binary_sensor.py b/tests/components/apsystems/test_binary_sensor.py new file mode 100644 index 00000000000..0c6fbffc93c --- /dev/null +++ b/tests/components/apsystems/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Test the APSystem binary sensor module.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.apsystems.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) -- GitLab