diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 229a2ac8ac6..ca4579a31b2 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -28,6 +28,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.EVENT, Platform.IMAGE, Platform.LAWN_MOWER, Platform.NUMBER, diff --git a/homeassistant/components/ecovacs/event.py b/homeassistant/components/ecovacs/event.py new file mode 100644 index 00000000000..dbef806fa76 --- /dev/null +++ b/homeassistant/components/ecovacs/event.py @@ -0,0 +1,65 @@ +"""Event module.""" + + +from deebot_client.capabilities import Capabilities, CapabilityEvent +from deebot_client.device import Device +from deebot_client.events import CleanJobStatus, ReportStatsEvent + +from homeassistant.components.event import EventEntity, EventEntityDescription +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 .controller import EcovacsController +from .entity import EcovacsEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add entities for passed config_entry in HA.""" + controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + EcovacsLastJobEventEntity(device) for device in controller.devices(Capabilities) + ) + + +class EcovacsLastJobEventEntity( + EcovacsEntity[Capabilities, CapabilityEvent[ReportStatsEvent]], + EventEntity, +): + """Ecovacs last job event entity.""" + + entity_description = EventEntityDescription( + key="stats_report", + translation_key="last_job", + entity_category=EntityCategory.DIAGNOSTIC, + event_types=["finished", "finished_with_warnings", "manually_stopped"], + ) + + def __init__(self, device: Device[Capabilities]) -> None: + """Initialize entity.""" + super().__init__(device, device.capabilities.stats.report) + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + await super().async_added_to_hass() + + async def on_event(event: ReportStatsEvent) -> None: + """Handle event.""" + if event.status in (CleanJobStatus.NO_STATUS, CleanJobStatus.CLEANING): + # we trigger only on job done + return + + event_type = event.status.name.lower() + if event.status == CleanJobStatus.MANUAL_STOPPED: + event_type = "manually_stopped" + + self._trigger_event(event_type) + self.async_write_ha_state() + + self._subscribe(self._capability.event, on_event) diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index 7a57259ca5a..2e2d897c455 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -22,6 +22,11 @@ "default": "mdi:broom" } }, + "event": { + "last_job": { + "default": "mdi:history" + } + }, "number": { "clean_count": { "default": "mdi:counter" diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index 1f43b830778..a21f57a7a24 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -56,6 +56,20 @@ "name": "Reset side brushes lifespan" } }, + "event": { + "last_job": { + "name": "Last job", + "state_attributes": { + "event_type": { + "state": { + "finished": "Finished", + "finished_with_warnings": "Finished with warnings", + "manually_stopped": "Manually stopped" + } + } + } + } + }, "image": { "map": { "name": "Map" diff --git a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr index 564323e91b2..62b356e379d 100644 --- a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr @@ -33,35 +33,15 @@ }) # --- # name: test_mop_attached[binary_sensor.ozmo_950_mop_attached-state] - EntityRegistryEntrySnapshot({ - 'aliases': set({ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ozmo 950 Mop attached', }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , + 'context': , 'entity_id': 'binary_sensor.ozmo_950_mop_attached', - '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': 'Mop attached', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'water_mop_attached', - 'unique_id': 'E1234567890000000001_water_mop_attached', - 'unit_of_measurement': None, + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', }) # --- diff --git a/tests/components/ecovacs/snapshots/test_event.ambr b/tests/components/ecovacs/snapshots/test_event.ambr new file mode 100644 index 00000000000..8f433560cd1 --- /dev/null +++ b/tests/components/ecovacs/snapshots/test_event.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_last_job[event.ozmo_950_last_job-entity_entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'finished', + 'finished_with_warnings', + 'manually_stopped', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': , + 'entity_id': 'event.ozmo_950_last_job', + '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': 'Last job', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_job', + 'unique_id': 'E1234567890000000001_stats_report', + 'unit_of_measurement': None, + }) +# --- +# name: test_last_job[event.ozmo_950_last_job-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'finished', + 'event_types': list([ + 'finished', + 'finished_with_warnings', + 'manually_stopped', + ]), + 'friendly_name': 'Ozmo 950 Last job', + }), + 'context': , + 'entity_id': 'event.ozmo_950_last_job', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-20T00:00:00.000+00:00', + }) +# --- diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py index 2ca0100be31..697e57c6def 100644 --- a/tests/components/ecovacs/test_binary_sensor.py +++ b/tests/components/ecovacs/test_binary_sensor.py @@ -49,7 +49,7 @@ async def test_mop_attached( ) assert (state := hass.states.get(state.entity_id)) - assert entity_entry == snapshot(name=f"{entity_id}-state") + assert state == snapshot(name=f"{entity_id}-state") await notify_and_wait( hass, event_bus, WaterInfoEvent(WaterAmount.HIGH, mop_attached=False) diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py new file mode 100644 index 00000000000..0e7adaad954 --- /dev/null +++ b/tests/components/ecovacs/test_event.py @@ -0,0 +1,97 @@ +"""Tests for Ecovacs event entities.""" + +from datetime import timedelta + +from deebot_client.capabilities import Capabilities +from deebot_client.events import CleanJobStatus, ReportStatsEvent +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.ecovacs.const import DOMAIN +from homeassistant.components.ecovacs.controller import EcovacsController +from homeassistant.components.event.const import ATTR_EVENT_TYPE +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .util import notify_and_wait + +pytestmark = [pytest.mark.usefixtures("init_integration")] + + +@pytest.fixture +def platforms() -> Platform | list[Platform]: + """Platforms, which should be loaded during the test.""" + return Platform.EVENT + + +async def test_last_job( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, + controller: EcovacsController, +) -> None: + """Test last job event entity.""" + freezer.move_to("2024-03-20T00:00:00+00:00") + entity_id = "event.ozmo_950_last_job" + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNKNOWN + + assert (entity_entry := entity_registry.async_get(state.entity_id)) + assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") + assert entity_entry.device_id + + device = next(controller.devices(Capabilities)) + + assert (device_entry := device_registry.async_get(entity_entry.device_id)) + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} + + event_bus = device.events + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(10, 5, "spotArea", "1", CleanJobStatus.FINISHED, [1, 2]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state == snapshot(name=f"{entity_id}-state") + + freezer.tick(timedelta(minutes=5)) + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent( + 100, 50, "spotArea", "2", CleanJobStatus.FINISHED_WITH_WARNINGS, [2, 3] + ), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:05:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "finished_with_warnings" + + freezer.tick(timedelta(minutes=5)) + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUAL_STOPPED, [1]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:10:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "manually_stopped" + + freezer.tick(timedelta(minutes=5)) + for status in (CleanJobStatus.NO_STATUS, CleanJobStatus.CLEANING): + # we should not trigger on these statuses + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(12, 11, "spotArea", "4", status, [1, 2, 3]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:10:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "manually_stopped" diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index bfaf2005e6d..7780b86d714 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -121,8 +121,8 @@ async def test_devices_in_dr( @pytest.mark.parametrize( ("device_fixture", "entities"), [ - ("yna5x1", 25), - ("5xu9h3", 19), + ("yna5x1", 26), + ("5xu9h3", 20), ], ) async def test_all_entities_loaded(