Add EventEntity for Auto Shut Off events in Watergate integration (#135675)

* Add EventEntity for Auto Shut Off events in Watergate integration

* Split events into two: volume and duration

* Add icons to json. Extract some common translation keys. Simplify tests

* Apply suggestions from code review

* Fix

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
adam-the-hero 2025-03-25 10:53:36 +01:00 committed by GitHub
parent e7eb173e07
commit a1a808b843
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 333 additions and 6 deletions

View File

@ -18,8 +18,9 @@ from homeassistant.components.webhook import (
)
from homeassistant.const import CONF_IP_ADDRESS, CONF_WEBHOOK_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DOMAIN
from .const import AUTO_SHUT_OFF_EVENT_NAME, DOMAIN
from .coordinator import WatergateConfigEntry, WatergateDataCoordinator
_LOGGER = logging.getLogger(__name__)
@ -28,8 +29,10 @@ WEBHOOK_TELEMETRY_TYPE = "telemetry"
WEBHOOK_VALVE_TYPE = "valve"
WEBHOOK_WIFI_CHANGED_TYPE = "wifi-changed"
WEBHOOK_POWER_SUPPLY_CHANGED_TYPE = "power-supply-changed"
WEBHOOK_AUTO_SHUT_OFF = "auto-shut-off-report"
PLATFORMS: list[Platform] = [
Platform.EVENT,
Platform.SENSOR,
Platform.VALVE,
]
@ -120,6 +123,10 @@ def get_webhook_handler(
coordinator_data.networking.rssi = data.rssi
elif body_type == WEBHOOK_POWER_SUPPLY_CHANGED_TYPE:
coordinator_data.state.power_supply = data.supply
elif body_type == WEBHOOK_AUTO_SHUT_OFF:
async_dispatcher_send(
hass, AUTO_SHUT_OFF_EVENT_NAME.format(data.type.lower()), data
)
coordinator.async_set_updated_data(coordinator_data)

View File

@ -3,3 +3,5 @@
DOMAIN = "watergate"
MANUFACTURER = "Watergate"
AUTO_SHUT_OFF_EVENT_NAME = "watergate_{}"

View File

@ -0,0 +1,78 @@
"""Module contains the AutoShutOffEvent class for handling auto shut off events."""
from watergate_local_api.models.auto_shut_off_report import AutoShutOffReport
from homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import WatergateConfigEntry
from .const import AUTO_SHUT_OFF_EVENT_NAME
from .coordinator import WatergateDataCoordinator
from .entity import WatergateEntity
VOLUME_AUTO_SHUT_OFF = "volume_threshold"
DURATION_AUTO_SHUT_OFF = "duration_threshold"
DESCRIPTIONS: list[EventEntityDescription] = [
EventEntityDescription(
translation_key="auto_shut_off_volume",
key="auto_shut_off_volume",
event_types=[VOLUME_AUTO_SHUT_OFF],
),
EventEntityDescription(
translation_key="auto_shut_off_duration",
key="auto_shut_off_duration",
event_types=[DURATION_AUTO_SHUT_OFF],
),
]
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WatergateConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Event entities from config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
AutoShutOffEvent(coordinator, description) for description in DESCRIPTIONS
)
class AutoShutOffEvent(WatergateEntity, EventEntity):
"""Event for Auto Shut Off."""
def __init__(
self,
coordinator: WatergateDataCoordinator,
entity_description: EventEntityDescription,
) -> None:
"""Initialize Auto Shut Off Entity."""
super().__init__(coordinator, entity_description.key)
self.entity_description = entity_description
async def async_added_to_hass(self):
"""Register the callback for event handling when the entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
AUTO_SHUT_OFF_EVENT_NAME.format(self.event_types[0]),
self._async_handle_event,
)
)
@callback
def _async_handle_event(self, event: AutoShutOffReport) -> None:
self._trigger_event(
event.type.lower(),
{"volume": event.volume, "duration": event.duration},
)
self.async_write_ha_state()

View File

@ -0,0 +1,12 @@
{
"entity": {
"event": {
"auto_shut_off_volume": {
"default": "mdi:water"
},
"auto_shut_off_duration": {
"default": "mdi:timelapse"
}
}
}
}

View File

@ -17,10 +17,7 @@ rules:
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done

View File

@ -19,6 +19,42 @@
}
},
"entity": {
"event": {
"auto_shut_off_volume": {
"name": "Volume auto shut-off",
"state_attributes": {
"event_type": {
"state": {
"volume_threshold": "Volume",
"duration_threshold": "Duration"
}
},
"volume": {
"name": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::volume_threshold%]"
},
"duration": {
"name": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::duration_threshold%]"
}
}
},
"auto_shut_off_duration": {
"name": "Duration auto shut-off",
"state_attributes": {
"event_type": {
"state": {
"volume_threshold": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::volume_threshold%]",
"duration_threshold": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::duration_threshold%]"
}
},
"volume": {
"name": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::volume_threshold%]"
},
"duration": {
"name": "[%key:component::watergate::entity::event::auto_shut_off_volume::state_attributes::event_type::state::duration_threshold%]"
}
}
}
},
"sensor": {
"water_meter_volume": {
"name": "Water meter volume"

View File

@ -0,0 +1,111 @@
# serializer version: 1
# name: test_event[event.sonic_duration_auto_shut_off-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'duration_threshold',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.sonic_duration_auto_shut_off',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Duration auto shut-off',
'platform': 'watergate',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'auto_shut_off_duration',
'unique_id': 'a63182948ce2896a.auto_shut_off_duration',
'unit_of_measurement': None,
})
# ---
# name: test_event[event.sonic_duration_auto_shut_off-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'event_type': None,
'event_types': list([
'duration_threshold',
]),
'friendly_name': 'Sonic Duration auto shut-off',
}),
'context': <ANY>,
'entity_id': 'event.sonic_duration_auto_shut_off',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_event[event.sonic_volume_auto_shut_off-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'volume_threshold',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.sonic_volume_auto_shut_off',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Volume auto shut-off',
'platform': 'watergate',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'auto_shut_off_volume',
'unique_id': 'a63182948ce2896a.auto_shut_off_volume',
'unit_of_measurement': None,
})
# ---
# name: test_event[event.sonic_volume_auto_shut_off-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'event_type': None,
'event_types': list([
'volume_threshold',
]),
'friendly_name': 'Sonic Volume auto shut-off',
}),
'context': <ANY>,
'entity_id': 'event.sonic_volume_auto_shut_off',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@ -0,0 +1,84 @@
"""Tests for the Watergate event entity platform."""
from collections.abc import Generator
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import StateType
from . import init_integration
from .const import MOCK_WEBHOOK_ID
from tests.common import AsyncMock, MockConfigEntry, patch, snapshot_platform
from tests.typing import ClientSessionGenerator
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_event(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_entry: MockConfigEntry,
mock_watergate_client: Generator[AsyncMock],
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test states of the sensor."""
freezer.move_to("2021-01-09 12:00:00+00:00")
with patch("homeassistant.components.watergate.PLATFORMS", [Platform.EVENT]):
await init_integration(hass, mock_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "event_type"),
[
("sonic_volume_auto_shut_off", "volume_threshold"),
("sonic_duration_auto_shut_off", "duration_threshold"),
],
)
async def test_auto_shut_off_webhook(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
mock_entry: MockConfigEntry,
mock_watergate_client: Generator[AsyncMock],
entity_id: str,
event_type: str,
) -> None:
"""Test if water flow webhook is handled correctly."""
await init_integration(hass, mock_entry)
def assert_state(entity_id: str, expected_state: str):
state = hass.states.get(f"event.{entity_id}")
assert state.state == str(expected_state)
assert_state(entity_id, "unknown")
telemetry_change_data = {
"type": "auto-shut-off-report",
"data": {
"type": event_type,
"volume": 1500,
"duration": 30,
"timestamp": 1730148016,
},
}
client = await hass_client_no_auth()
await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=telemetry_change_data)
await hass.async_block_till_done()
def assert_extra_state(
entity_id: str, attribute: str, expected_attribute: StateType
):
attributes = hass.states.get(f"event.{entity_id}").attributes
assert attributes.get(attribute) == expected_attribute
assert_extra_state(entity_id, "event_type", event_type)
assert_extra_state(entity_id, "volume", 1500)
assert_extra_state(entity_id, "duration", 30)

View File

@ -1,4 +1,4 @@
"""Tests for the Watergate valve platform."""
"""Tests for the Watergate sensor platform."""
from collections.abc import Generator