diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 599721ced3a..a5ebf727071 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -8,6 +8,7 @@ from .const import DOMAIN from .coordinator import LaMarzoccoUpdateCoordinator PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py new file mode 100644 index 00000000000..a0f4033710c --- /dev/null +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -0,0 +1,90 @@ +"""Binary Sensor platform for La Marzocco espresso machines.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from lmcloud import LMCloud as LaMarzoccoClient + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class LaMarzoccoBinarySensorEntityDescription( + LaMarzoccoEntityDescription, + BinarySensorEntityDescription, +): + """Description of a La Marzocco binary sensor.""" + + is_on_fn: Callable[[LaMarzoccoClient], bool] + icon_on: str + icon_off: str + + +ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( + LaMarzoccoBinarySensorEntityDescription( + key="water_tank", + translation_key="water_tank", + device_class=BinarySensorDeviceClass.PROBLEM, + icon_on="mdi:water-remove", + icon_off="mdi:water-check", + is_on_fn=lambda lm: not lm.current_status.get("water_reservoir_contact"), + entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=lambda coordinator: coordinator.local_connection_configured, + ), + LaMarzoccoBinarySensorEntityDescription( + key="brew_active", + translation_key="brew_active", + device_class=BinarySensorDeviceClass.RUNNING, + icon_off="mdi:cup-off", + icon_on="mdi:cup-water", + is_on_fn=lambda lm: bool(lm.current_status.get("brew_active")), + available_fn=lambda lm: lm.websocket_connected, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensor entities.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + LaMarzoccoBinarySensorEntity(coordinator, description) + for description in ENTITIES + if description.supported_fn(coordinator) + ) + + +class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity): + """Binary Sensor representing espresso machine water reservoir status.""" + + entity_description: LaMarzoccoBinarySensorEntityDescription + + @property + def icon(self) -> str | None: + """Return the icon.""" + return ( + self.entity_description.icon_on + if self.is_on + else self.entity_description.icon_off + ) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.entity_description.is_on_fn(self.coordinator.lm) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 759a9e327dc..db4f443b18b 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -43,6 +43,14 @@ } }, "entity": { + "binary_sensor": { + "brew_active": { + "name": "Brewing active" + }, + "water_tank": { + "name": "Water tank empty" + } + }, "sensor": { "current_temp_coffee": { "name": "Current coffee temperature" diff --git a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..4fb8c3cb828 --- /dev/null +++ b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr @@ -0,0 +1,91 @@ +# serializer version: 1 +# name: test_binary_sensors[GS01234_brewing_active-binary_sensor] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'GS01234 Brewing active', + 'icon': 'mdi:cup-off', + }), + 'context': , + 'entity_id': 'binary_sensor.gs01234_brewing_active', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[GS01234_brewing_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.gs01234_brewing_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:cup-off', + 'original_name': 'Brewing active', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'brew_active', + 'unique_id': 'GS01234_brew_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[GS01234_water_tank_empty-binary_sensor] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'GS01234 Water tank empty', + 'icon': 'mdi:water-check', + }), + 'context': , + 'entity_id': 'binary_sensor.gs01234_water_tank_empty', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[GS01234_water_tank_empty-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.gs01234_water_tank_empty', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:water-check', + 'original_name': 'Water tank empty', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_tank', + 'unique_id': 'GS01234_water_tank', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py new file mode 100644 index 00000000000..e475e663768 --- /dev/null +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -0,0 +1,71 @@ +"""Tests for La Marzocco binary sensors.""" +from unittest.mock import MagicMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_init_integration + +from tests.common import MockConfigEntry + +BINARY_SENSORS = ( + "brewing_active", + "water_tank_empty", +) + + +async def test_binary_sensors( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco binary sensors.""" + + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + + for binary_sensor in BINARY_SENSORS: + state = hass.states.get(f"binary_sensor.{serial_number}_{binary_sensor}") + assert state + assert state == snapshot(name=f"{serial_number}_{binary_sensor}-binary_sensor") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot(name=f"{serial_number}_{binary_sensor}-entry") + + +@pytest.mark.usefixtures("remove_local_connection") +async def test_brew_active_does_not_exists( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the La Marzocco currently_making_coffee doesn't exist if host not set.""" + + await async_init_integration(hass, mock_config_entry) + state = hass.states.get(f"sensor.{mock_lamarzocco.serial_number}_brewing_active") + assert state is None + + +async def test_brew_active_unavailable( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the La Marzocco currently_making_coffee becomes unavailable.""" + + mock_lamarzocco.websocket_connected = False + await async_init_integration(hass, mock_config_entry) + state = hass.states.get( + f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active" + ) + assert state + assert state.state == STATE_UNAVAILABLE