mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 22:38:02 -04:00
Add a binary sensor platform to the Brother integration (#183084)
This commit is contained in:
@@ -20,7 +20,7 @@ from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Support for the Brother binary sensors."""
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_TYPE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import PRINTER_TYPE_INK, PRINTER_TYPE_LASER
|
||||
from .coordinator import BrotherConfigEntry
|
||||
from .entity import BrotherPrinterEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
BinarySensorEntityDescription(
|
||||
key="door_open",
|
||||
translation_key="door_open",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="input_tray_empty",
|
||||
translation_key="input_tray_empty",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="input_tray_missing",
|
||||
translation_key="input_tray_missing",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="jammed",
|
||||
translation_key="jammed",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="low_paper",
|
||||
translation_key="low_paper",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="no_paper",
|
||||
translation_key="no_paper",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="output_full",
|
||||
translation_key="output_full",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="output_near_full",
|
||||
translation_key="output_near_full",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="output_tray_missing",
|
||||
translation_key="output_tray_missing",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="overdue_prevent_maint",
|
||||
translation_key="overdue_prevent_maint",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="service_requested",
|
||||
translation_key="service_requested",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
SUPPLY_BINARY_SENSOR_TYPES: dict[str, tuple[BinarySensorEntityDescription, ...]] = {
|
||||
PRINTER_TYPE_LASER: (
|
||||
BinarySensorEntityDescription(
|
||||
key="low_toner",
|
||||
translation_key="low_toner",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="no_toner",
|
||||
translation_key="no_toner",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
PRINTER_TYPE_INK: (
|
||||
BinarySensorEntityDescription(
|
||||
key="low_toner",
|
||||
translation_key="low_ink",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="no_toner",
|
||||
translation_key="no_ink",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BrotherConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Brother entities from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# printer_errors is None only if the printer does not support the OID that returns
|
||||
# errors, in that case, we do not create binary sensors
|
||||
if coordinator.data.printer_errors is None:
|
||||
return
|
||||
|
||||
descriptions = (
|
||||
BINARY_SENSOR_TYPES + SUPPLY_BINARY_SENSOR_TYPES[entry.data[CONF_TYPE]]
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BrotherPrinterBinarySensor(coordinator, description)
|
||||
for description in descriptions
|
||||
)
|
||||
|
||||
|
||||
class BrotherPrinterBinarySensor(BrotherPrinterEntity, BinarySensorEntity):
|
||||
"""Define a Brother Printer binary sensor."""
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the printer reports this error."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.coordinator.data.printer_errors is not None
|
||||
|
||||
return self.entity_description.key in self.coordinator.data.printer_errors
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Define the Brother entity."""
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -15,9 +16,13 @@ class BrotherPrinterEntity(CoordinatorEntity[BrotherDataUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BrotherDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.brother.serial.lower()}_{description.key}"
|
||||
self.entity_description = description
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"http://{coordinator.brother.host}/",
|
||||
identifiers={(DOMAIN, coordinator.brother.serial)},
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator
|
||||
from .coordinator import BrotherConfigEntry
|
||||
from .entity import BrotherPrinterEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
@@ -354,17 +354,6 @@ class BrotherPrinterSensor(BrotherPrinterEntity, SensorEntity):
|
||||
|
||||
entity_description: BrotherSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BrotherDataUpdateCoordinator,
|
||||
description: BrotherSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.brother.serial.lower()}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType | datetime:
|
||||
|
||||
@@ -83,6 +83,53 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"door_open": {
|
||||
"name": "Door open"
|
||||
},
|
||||
"input_tray_empty": {
|
||||
"name": "Input tray empty"
|
||||
},
|
||||
"input_tray_missing": {
|
||||
"name": "Input tray missing"
|
||||
},
|
||||
"jammed": {
|
||||
"name": "Paper jam"
|
||||
},
|
||||
"low_ink": {
|
||||
"name": "Low ink"
|
||||
},
|
||||
"low_paper": {
|
||||
"name": "Low paper"
|
||||
},
|
||||
"low_toner": {
|
||||
"name": "Low toner"
|
||||
},
|
||||
"no_ink": {
|
||||
"name": "No ink"
|
||||
},
|
||||
"no_paper": {
|
||||
"name": "No paper"
|
||||
},
|
||||
"no_toner": {
|
||||
"name": "No toner"
|
||||
},
|
||||
"output_full": {
|
||||
"name": "Output tray full"
|
||||
},
|
||||
"output_near_full": {
|
||||
"name": "Output tray near full"
|
||||
},
|
||||
"output_tray_missing": {
|
||||
"name": "Output tray missing"
|
||||
},
|
||||
"overdue_prevent_maint": {
|
||||
"name": "Preventive maintenance overdue"
|
||||
},
|
||||
"service_requested": {
|
||||
"name": "Service requested"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"belt_unit_remaining_life": {
|
||||
"name": "Belt unit remaining lifetime"
|
||||
|
||||
@@ -68,8 +68,8 @@ BROTHER_DATA = BrotherSensors(
|
||||
pf_kit_1_remaining_pages=48741,
|
||||
pf_kit_mp_remaining_life=None,
|
||||
pf_kit_mp_remaining_pages=None,
|
||||
printer_errors=None,
|
||||
printer_status="idle",
|
||||
printer_errors=["door_open", "low_toner"],
|
||||
status="waiting",
|
||||
uptime=datetime(2024, 3, 3, 15, 4, 24, tzinfo=UTC),
|
||||
yellow_counter=None,
|
||||
@@ -117,6 +117,15 @@ def mock_brother_client(mock_brother: AsyncMock) -> AsyncMock:
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ink_brother_client(mock_brother_client: AsyncMock) -> AsyncMock:
|
||||
"""Mock Brother client for an inkjet printer."""
|
||||
mock_brother_client.serial = "9876543210"
|
||||
mock_brother_client.model = "DCP-J562DW"
|
||||
|
||||
return mock_brother_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
@@ -131,3 +140,19 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
},
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ink_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry for an inkjet printer."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="DCP-J562DW 9876543210",
|
||||
unique_id="9876543210",
|
||||
data={
|
||||
CONF_HOST: "localhost",
|
||||
CONF_TYPE: "ink",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,10 @@
|
||||
'pf_kit_1_remaining_pages': 48741,
|
||||
'pf_kit_mp_remaining_life': None,
|
||||
'pf_kit_mp_remaining_pages': None,
|
||||
'printer_errors': None,
|
||||
'printer_errors': list([
|
||||
'door_open',
|
||||
'low_toner',
|
||||
]),
|
||||
'printer_status': 'idle',
|
||||
'status': 'waiting',
|
||||
'uptime': '2024-03-03T15:04:24+00:00',
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Test binary sensor of Brother integration."""
|
||||
|
||||
from dataclasses import replace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.brother.const import UPDATE_INTERVAL
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
from .conftest import BROTHER_DATA
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_brother_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test states of the binary sensors."""
|
||||
with patch("homeassistant.components.brother.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_binary_sensors_ink(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_ink_brother_client: AsyncMock,
|
||||
mock_ink_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test states of the binary sensors for an inkjet printer."""
|
||||
with patch("homeassistant.components.brother.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
await init_integration(hass, mock_ink_config_entry)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_ink_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id",
|
||||
[
|
||||
"binary_sensor.hl_l2340dw_input_tray_empty",
|
||||
"binary_sensor.hl_l2340dw_input_tray_missing",
|
||||
"binary_sensor.hl_l2340dw_low_paper",
|
||||
"binary_sensor.hl_l2340dw_output_tray_full",
|
||||
"binary_sensor.hl_l2340dw_output_tray_missing",
|
||||
"binary_sensor.hl_l2340dw_output_tray_near_full",
|
||||
"binary_sensor.hl_l2340dw_preventive_maintenance_overdue",
|
||||
"binary_sensor.hl_l2340dw_service_requested",
|
||||
],
|
||||
)
|
||||
async def test_binary_sensors_disabled_by_default(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_brother_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test the less critical errors are disabled by default."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
async def test_no_binary_sensors_when_printer_errors_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_brother_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test no binary sensors are created when the printer does not report errors."""
|
||||
mock_brother_client.async_update.return_value = replace(
|
||||
BROTHER_DATA, printer_errors=None
|
||||
)
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_state"),
|
||||
[
|
||||
("binary_sensor.hl_l2340dw_door_open", STATE_ON),
|
||||
("binary_sensor.hl_l2340dw_low_toner", STATE_ON),
|
||||
("binary_sensor.hl_l2340dw_low_paper", STATE_OFF),
|
||||
("binary_sensor.hl_l2340dw_no_toner", STATE_OFF),
|
||||
],
|
||||
)
|
||||
async def test_binary_sensor_state_reflects_printer_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_brother_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test the binary sensor state matches the active printer errors."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_brother_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Ensure that we mark the entities unavailable correctly when device is offline."""
|
||||
entity_id = "binary_sensor.hl_l2340dw_door_open"
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == STATE_ON
|
||||
|
||||
mock_brother_client.async_update.side_effect = ConnectionError
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
mock_brother_client.async_update.side_effect = None
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == STATE_ON
|
||||
Reference in New Issue
Block a user