Add deconz_relative_rotary event for Hue Tap Dial (#82727)

This commit is contained in:
Robert Svensson 2022-11-27 21:01:58 +01:00 committed by GitHub
parent 4928c3d683
commit 3853182ccf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 6 deletions

View File

@ -57,3 +57,6 @@ POWER_PLUGS = [
CONF_ANGLE = "angle"
CONF_GESTURE = "gesture"
ATTR_DURATION = "duration"
ATTR_ROTATION = "rotation"

View File

@ -10,6 +10,7 @@ from pydeconz.models.sensor.ancillary_control import (
AncillaryControlAction,
)
from pydeconz.models.sensor.presence import Presence, PresenceStatePresenceEvent
from pydeconz.models.sensor.relative_rotary import RelativeRotary, RelativeRotaryEvent
from pydeconz.models.sensor.switch import Switch
from homeassistant.const import (
@ -23,13 +24,14 @@ from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.util import slugify
from .const import CONF_ANGLE, CONF_GESTURE, LOGGER
from .const import ATTR_DURATION, ATTR_ROTATION, CONF_ANGLE, CONF_GESTURE, LOGGER
from .deconz_device import DeconzBase
from .gateway import DeconzGateway
CONF_DECONZ_EVENT = "deconz_event"
CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event"
CONF_DECONZ_PRESENCE_EVENT = "deconz_presence_event"
CONF_DECONZ_RELATIVE_ROTARY_EVENT = "deconz_relative_rotary_event"
SUPPORTED_DECONZ_ALARM_EVENTS = {
AncillaryControlAction.EMERGENCY,
@ -47,6 +49,10 @@ SUPPORTED_DECONZ_PRESENCE_EVENTS = {
PresenceStatePresenceEvent.APPROACHING,
PresenceStatePresenceEvent.ABSENTING,
}
RELATIVE_ROTARY_DECONZ_TO_EVENT = {
RelativeRotaryEvent.NEW: "new",
RelativeRotaryEvent.REPEAT: "repeat",
}
async def async_setup_events(gateway: DeconzGateway) -> None:
@ -55,7 +61,7 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
@callback
def async_add_sensor(_: EventType, sensor_id: str) -> None:
"""Create DeconzEvent."""
new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent
new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent
sensor = gateway.api.sensors[sensor_id]
if isinstance(sensor, Switch):
@ -69,6 +75,9 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
return
new_event = DeconzPresenceEvent(sensor, gateway)
elif isinstance(sensor, RelativeRotary):
new_event = DeconzRelativeRotaryEvent(sensor, gateway)
gateway.hass.async_create_task(new_event.async_update_device_registry())
gateway.events.append(new_event)
@ -84,6 +93,10 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
async_add_sensor,
gateway.api.sensors.presence,
)
gateway.register_platform_add_device_callback(
async_add_sensor,
gateway.api.sensors.relative_rotary,
)
@callback
@ -104,7 +117,7 @@ class DeconzEventBase(DeconzBase):
def __init__(
self,
device: AncillaryControl | Presence | Switch,
device: AncillaryControl | Presence | RelativeRotary | Switch,
gateway: DeconzGateway,
) -> None:
"""Register callback that will be used for signals."""
@ -227,3 +240,29 @@ class DeconzPresenceEvent(DeconzEventBase):
}
self.gateway.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data)
class DeconzRelativeRotaryEvent(DeconzEventBase):
"""Relative rotary event."""
_device: RelativeRotary
@callback
def async_update_callback(self) -> None:
"""Fire the event if reason is new action is updated."""
if (
self.gateway.ignore_state_updates
or "rotaryevent" not in self._device.changed_keys
):
return
data = {
CONF_ID: self.event_id,
CONF_UNIQUE_ID: self.serial,
CONF_DEVICE_ID: self.device_id,
CONF_EVENT: RELATIVE_ROTARY_DECONZ_TO_EVENT[self._device.rotary_event],
ATTR_ROTATION: self._device.expected_rotation,
ATTR_DURATION: self._device.expected_event_duration,
}
self.gateway.hass.bus.async_fire(CONF_DECONZ_RELATIVE_ROTARY_EVENT, data)

View File

@ -28,6 +28,7 @@ from .deconz_event import (
DeconzAlarmEvent,
DeconzEvent,
DeconzPresenceEvent,
DeconzRelativeRotaryEvent,
)
from .gateway import DeconzGateway
@ -635,7 +636,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
def _get_deconz_event_from_device(
hass: HomeAssistant,
device: dr.DeviceEntry,
) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent:
) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent:
"""Resolve deconz event from device."""
gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {})
for gateway in gateways.values():

View File

@ -41,7 +41,12 @@ from .const import (
from .errors import AuthenticationRequired, CannotConnect
if TYPE_CHECKING:
from .deconz_event import DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent
from .deconz_event import (
DeconzAlarmEvent,
DeconzEvent,
DeconzPresenceEvent,
DeconzRelativeRotaryEvent,
)
SENSORS = (
sensors.SensorResourceManager,
@ -93,7 +98,12 @@ class DeconzGateway:
self.deconz_ids: dict[str, str] = {}
self.entities: dict[str, set[str]] = {}
self.events: list[DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent] = []
self.events: list[
DeconzAlarmEvent
| DeconzEvent
| DeconzPresenceEvent
| DeconzRelativeRotaryEvent
] = []
self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set()
self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set()

View File

@ -10,9 +10,13 @@ from pydeconz.models.sensor.presence import PresenceStatePresenceEvent
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.deconz_event import (
ATTR_DURATION,
ATTR_ROTATION,
CONF_DECONZ_ALARM_EVENT,
CONF_DECONZ_EVENT,
CONF_DECONZ_PRESENCE_EVENT,
CONF_DECONZ_RELATIVE_ROTARY_EVENT,
RELATIVE_ROTARY_DECONZ_TO_EVENT,
)
from homeassistant.const import (
CONF_DEVICE_ID,
@ -515,6 +519,105 @@ async def test_deconz_presence_events(hass, aioclient_mock, mock_deconz_websocke
assert len(hass.states.async_all()) == 0
async def test_deconz_relative_rotary_events(
hass, aioclient_mock, mock_deconz_websocket
):
"""Test successful creation of deconz relative rotary events."""
data = {
"sensors": {
"1": {
"config": {
"battery": 100,
"on": True,
"reachable": True,
},
"etag": "463728970bdb7d04048fc4373654f45a",
"lastannounced": "2022-07-03T13:57:59Z",
"lastseen": "2022-07-03T14:02Z",
"manufacturername": "Signify Netherlands B.V.",
"modelid": "RDM002",
"name": "RDM002 44",
"state": {
"expectedeventduration": 400,
"expectedrotation": 75,
"lastupdated": "2022-07-03T11:37:49.586",
"rotaryevent": 2,
},
"swversion": "2.59.19",
"type": "ZHARelativeRotary",
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-14-fc00",
}
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
device_registry = dr.async_get(hass)
assert len(hass.states.async_all()) == 1
assert (
len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id))
== 3
)
device = device_registry.async_get_device(
identifiers={(DECONZ_DOMAIN, "xx:xx:xx:xx:xx:xx:xx:xx")}
)
captured_events = async_capture_events(hass, CONF_DECONZ_RELATIVE_ROTARY_EVENT)
for rotary_event, duration, rotation in ((1, 100, 50), (2, 200, -50)):
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {
"rotaryevent": rotary_event,
"expectedeventduration": duration,
"expectedrotation": rotation,
},
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert len(captured_events) == 1
assert captured_events[0].data == {
CONF_ID: "rdm002_44",
CONF_UNIQUE_ID: "xx:xx:xx:xx:xx:xx:xx:xx",
CONF_DEVICE_ID: device.id,
CONF_EVENT: RELATIVE_ROTARY_DECONZ_TO_EVENT[rotary_event],
ATTR_DURATION: duration,
ATTR_ROTATION: rotation,
}
captured_events.clear()
# Unsupported relative rotary event
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"name": "123",
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert len(captured_events) == 0
await hass.config_entries.async_unload(config_entry.entry_id)
states = hass.states.async_all()
assert len(hass.states.async_all()) == 1
for state in states:
assert state.state == STATE_UNAVAILABLE
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
async def test_deconz_events_bad_unique_id(hass, aioclient_mock):
"""Verify no devices are created if unique id is bad or missing."""
data = {