diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index 473c67c4ea8..2145b3a134e 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -17,7 +17,12 @@ from .coordinator import AutomowerDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.LAWN_MOWER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.LAWN_MOWER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/husqvarna_automower/binary_sensor.py b/homeassistant/components/husqvarna_automower/binary_sensor.py new file mode 100644 index 00000000000..e8e64e7ffc7 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/binary_sensor.py @@ -0,0 +1,82 @@ +"""Creates the binary sensor entities for the mower.""" + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from aioautomower.model import MowerActivities, MowerAttributes + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class AutomowerBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Automower binary sensor entity.""" + + value_fn: Callable[[MowerAttributes], bool] + + +BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = ( + AutomowerBinarySensorEntityDescription( + key="battery_charging", + value_fn=lambda data: data.mower.activity == MowerActivities.CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + ), + AutomowerBinarySensorEntityDescription( + key="leaving_dock", + translation_key="leaving_dock", + value_fn=lambda data: data.mower.activity == MowerActivities.LEAVING, + ), + AutomowerBinarySensorEntityDescription( + key="returning_to_dock", + translation_key="returning_to_dock", + value_fn=lambda data: data.mower.activity == MowerActivities.GOING_HOME, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up binary sensor platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerBinarySensorEntity(mower_id, coordinator, description) + for mower_id in coordinator.data + for description in BINARY_SENSOR_TYPES + ) + + +class AutomowerBinarySensorEntity(AutomowerBaseEntity, BinarySensorEntity): + """Defining the Automower Sensors with AutomowerBinarySensorEntityDescription.""" + + entity_description: AutomowerBinarySensorEntityDescription + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + description: AutomowerBinarySensorEntityDescription, + ) -> None: + """Set up AutomowerSensors.""" + super().__init__(mower_id, coordinator) + self.entity_description = description + self._attr_unique_id = f"{mower_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.mower_attributes) diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index a0dd316b535..a3e2bd6bb8b 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -1,5 +1,13 @@ { "entity": { + "binary_sensor": { + "leaving_dock": { + "default": "mdi:debug-step-out" + }, + "returning_to_dock": { + "default": "mdi:debug-step-into" + } + }, "sensor": { "number_of_charging_cycles": { "default": "mdi:battery-sync-outline" diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 2d42172506d..4280ea097e8 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -29,6 +29,14 @@ } }, "entity": { + "binary_sensor": { + "leaving_dock": { + "name": "Leaving dock" + }, + "returning_to_dock": { + "name": "Returning to dock" + } + }, "switch": { "enable_schedule": { "name": "Enable schedule" diff --git a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..f586cca1ba5 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr @@ -0,0 +1,273 @@ +# serializer version: 1 +# name: test_sensor[binary_sensor.test_mower_1_charging-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': None, + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Mower 1 Charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_leaving_dock-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': None, + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaving dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaving_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_leaving_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_leaving_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Leaving dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_returning_to_dock-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': None, + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Returning to dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'returning_to_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_returning_to_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_returning_to_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Returning to dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_charging-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': None, + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Mower 1 Charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_leaving_dock-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': None, + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaving dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaving_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_leaving_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_leaving_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Leaving dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_returning_to_dock-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': None, + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Returning to dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'returning_to_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_returning_to_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_returning_to_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Returning to dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py new file mode 100644 index 00000000000..425636ba915 --- /dev/null +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -0,0 +1,83 @@ +"""Tests for binary sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from aioautomower.model import MowerActivities +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, +) + + +async def test_binary_sensor_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor states.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + state = hass.states.get("binary_sensor.test_mower_1_charging") + assert state is not None + assert state.state == "off" + state = hass.states.get("binary_sensor.test_mower_1_leaving_dock") + assert state is not None + assert state.state == "off" + state = hass.states.get("binary_sensor.test_mower_1_returning_to_dock") + assert state is not None + assert state.state == "off" + + for activity, entity in [ + (MowerActivities.CHARGING, "test_mower_1_charging"), + (MowerActivities.LEAVING, "test_mower_1_leaving_dock"), + (MowerActivities.GOING_HOME, "test_mower_1_returning_to_dock"), + ]: + values[TEST_MOWER_ID].mower.activity = activity + mock_automower_client.get_status.return_value = values + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(f"binary_sensor.{entity}") + assert state.state == "on" + + +async def test_snapshot_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the binary sensors.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + await setup_integration(hass, mock_config_entry) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")