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>
This commit is contained in:
Simon Hörrle 2024-08-05 11:08:27 +02:00 committed by GitHub
parent a7fbac5185
commit d246d02ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 553 additions and 7 deletions

View File

@ -13,7 +13,12 @@ from homeassistant.core import HomeAssistant
from .const import DEFAULT_PORT from .const import DEFAULT_PORT
from .coordinator import ApSystemsDataCoordinator 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 @dataclass

View File

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

View File

@ -2,9 +2,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from APsystemsEZ1 import APsystemsEZ1M, ReturnOutputData from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -12,7 +13,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import LOGGER 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.""" """Coordinator used for all sensors."""
def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None: def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None:
@ -25,5 +34,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]):
) )
self.api = api self.api = api
async def _async_update_data(self) -> ReturnOutputData: async def _async_update_data(self) -> ApSystemsSensorData:
return await self.api.get_output_data() 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)

View File

@ -148,4 +148,4 @@ class ApSystemsSensorWithDescription(
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.output_data)

View File

@ -19,6 +19,20 @@
} }
}, },
"entity": { "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": { "sensor": {
"total_power": { "total_power": {
"name": "Total power" "name": "Total power"

View File

@ -3,7 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData, Status from APsystemsEZ1 import ReturnAlarmInfo, ReturnDeviceInfo, ReturnOutputData, Status
import pytest import pytest
from homeassistant.components.apsystems.const import DOMAIN from homeassistant.components.apsystems.const import DOMAIN
@ -52,6 +52,12 @@ def mock_apsystems() -> Generator[MagicMock]:
e2=6.0, e2=6.0,
te2=7.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 mock_api.get_device_power_status.return_value = Status.normal
yield mock_api yield mock_api

View File

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

View File

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