mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add Event platform/entity to Hue integration (#97256)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
d233438e1a
commit
2ae059d4fc
@ -29,6 +29,7 @@ HUB_BUSY_SLEEP = 0.5
|
||||
PLATFORMS_v1 = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR]
|
||||
PLATFORMS_v2 = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.EVENT,
|
||||
Platform.LIGHT,
|
||||
Platform.SCENE,
|
||||
Platform.SENSOR,
|
||||
|
@ -1,4 +1,9 @@
|
||||
"""Constants for the Hue component."""
|
||||
from aiohue.v2.models.button import ButtonEvent
|
||||
from aiohue.v2.models.relative_rotary import (
|
||||
RelativeRotaryAction,
|
||||
RelativeRotaryDirection,
|
||||
)
|
||||
|
||||
DOMAIN = "hue"
|
||||
|
||||
@ -33,3 +38,26 @@ DEFAULT_ALLOW_UNREACHABLE = False
|
||||
# How long to wait to actually do the refresh after requesting it.
|
||||
# We wait some time so if we control multiple lights, we batch requests.
|
||||
REQUEST_REFRESH_DELAY = 0.3
|
||||
|
||||
|
||||
# V2 API SPECIFIC CONSTANTS ##################
|
||||
|
||||
DEFAULT_BUTTON_EVENT_TYPES = (
|
||||
# I have never ever seen the `DOUBLE_SHORT_RELEASE`
|
||||
# or `DOUBLE_SHORT_RELEASE` events so leave them out here
|
||||
ButtonEvent.INITIAL_PRESS,
|
||||
ButtonEvent.REPEAT,
|
||||
ButtonEvent.SHORT_RELEASE,
|
||||
ButtonEvent.LONG_RELEASE,
|
||||
)
|
||||
|
||||
DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT)
|
||||
DEFAULT_ROTARY_EVENT_SUBTYPES = (
|
||||
RelativeRotaryDirection.CLOCK_WISE,
|
||||
RelativeRotaryDirection.COUNTER_CLOCK_WISE,
|
||||
)
|
||||
|
||||
DEVICE_SPECIFIC_EVENT_TYPES = {
|
||||
# device specific overrides of specific supported button events
|
||||
"Hue tap switch": (ButtonEvent.INITIAL_PRESS,),
|
||||
}
|
||||
|
133
homeassistant/components/hue/event.py
Normal file
133
homeassistant/components/hue/event.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""Hue event entities from Button resources."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.models.button import Button
|
||||
from aiohue.v2.models.relative_rotary import (
|
||||
RelativeRotary,
|
||||
RelativeRotaryDirection,
|
||||
)
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .bridge import HueBridge
|
||||
from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES, DOMAIN
|
||||
from .v2.entity import HueBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up event platform from Hue button resources."""
|
||||
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api: HueBridgeV2 = bridge.api
|
||||
|
||||
if bridge.api_version == 1:
|
||||
# should not happen, but just in case
|
||||
raise NotImplementedError("Event support is only available for V2 bridges")
|
||||
|
||||
# add entities for all button and relative rotary resources
|
||||
@callback
|
||||
def async_add_entity(
|
||||
event_type: EventType,
|
||||
resource: Button | RelativeRotary,
|
||||
) -> None:
|
||||
"""Add entity from Hue resource."""
|
||||
if isinstance(resource, RelativeRotary):
|
||||
async_add_entities(
|
||||
[HueRotaryEventEntity(bridge, api.sensors.relative_rotary, resource)]
|
||||
)
|
||||
else:
|
||||
async_add_entities(
|
||||
[HueButtonEventEntity(bridge, api.sensors.button, resource)]
|
||||
)
|
||||
|
||||
for controller in (api.sensors.button, api.sensors.relative_rotary):
|
||||
# add all current items in controller
|
||||
for item in controller:
|
||||
async_add_entity(EventType.RESOURCE_ADDED, item)
|
||||
|
||||
# register listener for new items only
|
||||
config_entry.async_on_unload(
|
||||
controller.subscribe(
|
||||
async_add_entity, event_filter=EventType.RESOURCE_ADDED
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HueButtonEventEntity(HueBaseEntity, EventEntity):
|
||||
"""Representation of a Hue Event entity from a button resource."""
|
||||
|
||||
entity_description = EventEntityDescription(
|
||||
key="button",
|
||||
device_class=EventDeviceClass.BUTTON,
|
||||
translation_key="button",
|
||||
)
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# fill the event types based on the features the switch supports
|
||||
hue_dev_id = self.controller.get_device(self.resource.id).id
|
||||
model_id = self.bridge.api.devices[hue_dev_id].product_data.product_name
|
||||
event_types: list[str] = []
|
||||
for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get(
|
||||
model_id, DEFAULT_BUTTON_EVENT_TYPES
|
||||
):
|
||||
event_types.append(event_type.value)
|
||||
self._attr_event_types = event_types
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name for the entity."""
|
||||
return f"{super().name} {self.resource.metadata.control_id}"
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_type: EventType, resource: Button) -> None:
|
||||
"""Handle status event for this resource (or it's parent)."""
|
||||
if event_type == EventType.RESOURCE_UPDATED and resource.id == self.resource.id:
|
||||
self._trigger_event(resource.button.last_event.value)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
super()._handle_event(event_type, resource)
|
||||
|
||||
|
||||
class HueRotaryEventEntity(HueBaseEntity, EventEntity):
|
||||
"""Representation of a Hue Event entity from a RelativeRotary resource."""
|
||||
|
||||
entity_description = EventEntityDescription(
|
||||
key="rotary",
|
||||
device_class=EventDeviceClass.BUTTON,
|
||||
translation_key="rotary",
|
||||
event_types=[
|
||||
RelativeRotaryDirection.CLOCK_WISE.value,
|
||||
RelativeRotaryDirection.COUNTER_CLOCK_WISE.value,
|
||||
],
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_type: EventType, resource: RelativeRotary) -> None:
|
||||
"""Handle status event for this resource (or it's parent)."""
|
||||
if event_type == EventType.RESOURCE_UPDATED and resource.id == self.resource.id:
|
||||
event_key = resource.relative_rotary.last_event.rotation.direction.value
|
||||
event_data = {
|
||||
"duration": resource.relative_rotary.last_event.rotation.duration,
|
||||
"steps": resource.relative_rotary.last_event.rotation.steps,
|
||||
"action": resource.relative_rotary.last_event.action.value,
|
||||
}
|
||||
self._trigger_event(event_key, event_data)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
super()._handle_event(event_type, resource)
|
@ -67,6 +67,34 @@
|
||||
"start": "[%key:component::hue::device_automation::trigger_type::initial_press%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"event": {
|
||||
"button": {
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"initial_press": "Initial press",
|
||||
"repeat": "Repeat",
|
||||
"short_release": "Short press",
|
||||
"long_release": "Long press",
|
||||
"double_short_release": "Double press"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rotary": {
|
||||
"name": "Rotary",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"clock_wise": "Clockwise",
|
||||
"counter_clock_wise": "Counter clockwise"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
@ -3,11 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohue.v2.models.button import ButtonEvent
|
||||
from aiohue.v2.models.relative_rotary import (
|
||||
RelativeRotaryAction,
|
||||
RelativeRotaryDirection,
|
||||
)
|
||||
from aiohue.v2.models.resource import ResourceTypes
|
||||
import voluptuous as vol
|
||||
|
||||
@ -24,7 +19,15 @@ from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN
|
||||
from ..const import (
|
||||
ATTR_HUE_EVENT,
|
||||
CONF_SUBTYPE,
|
||||
DEFAULT_BUTTON_EVENT_TYPES,
|
||||
DEFAULT_ROTARY_EVENT_SUBTYPES,
|
||||
DEFAULT_ROTARY_EVENT_TYPES,
|
||||
DEVICE_SPECIFIC_EVENT_TYPES,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
@ -41,26 +44,6 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
DEFAULT_BUTTON_EVENT_TYPES = (
|
||||
# all except `DOUBLE_SHORT_RELEASE`
|
||||
ButtonEvent.INITIAL_PRESS,
|
||||
ButtonEvent.REPEAT,
|
||||
ButtonEvent.SHORT_RELEASE,
|
||||
ButtonEvent.LONG_PRESS,
|
||||
ButtonEvent.LONG_RELEASE,
|
||||
)
|
||||
|
||||
DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT)
|
||||
DEFAULT_ROTARY_EVENT_SUBTYPES = (
|
||||
RelativeRotaryDirection.CLOCK_WISE,
|
||||
RelativeRotaryDirection.COUNTER_CLOCK_WISE,
|
||||
)
|
||||
|
||||
DEVICE_SPECIFIC_EVENT_TYPES = {
|
||||
# device specific overrides of specific supported button events
|
||||
"Hue tap switch": (ButtonEvent.INITIAL_PRESS,),
|
||||
}
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
bridge: HueBridge,
|
||||
|
@ -18,6 +18,7 @@ FAKE_DEVICE = {
|
||||
{"rid": "fake_zigbee_connectivity_id_1", "rtype": "zigbee_connectivity"},
|
||||
{"rid": "fake_temperature_sensor_id_1", "rtype": "temperature"},
|
||||
{"rid": "fake_motion_sensor_id_1", "rtype": "motion"},
|
||||
{"rid": "fake_relative_rotary", "rtype": "relative_rotary"},
|
||||
],
|
||||
"type": "device",
|
||||
}
|
||||
@ -95,3 +96,20 @@ FAKE_SCENE = {
|
||||
"auto_dynamic": False,
|
||||
"type": "scene",
|
||||
}
|
||||
|
||||
FAKE_ROTARY = {
|
||||
"id": "fake_relative_rotary",
|
||||
"id_v1": "/sensors/1",
|
||||
"owner": {"rid": "fake_device_id_1", "rtype": "device"},
|
||||
"relative_rotary": {
|
||||
"last_event": {
|
||||
"action": "start",
|
||||
"rotation": {
|
||||
"direction": "clock_wise",
|
||||
"steps": 0,
|
||||
"duration": 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
"type": "relative_rotary",
|
||||
}
|
||||
|
@ -475,6 +475,10 @@
|
||||
{
|
||||
"rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69",
|
||||
"rtype": "zigbee_connectivity"
|
||||
},
|
||||
{
|
||||
"rid": "2f029c7b-868b-49e9-aa01-a0bbc595990d",
|
||||
"rtype": "relative_rotary"
|
||||
}
|
||||
],
|
||||
"type": "device"
|
||||
|
@ -51,9 +51,16 @@ async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None:
|
||||
assert hue_bridge.api is mock_api_v2
|
||||
assert isinstance(hue_bridge.api, HueBridgeV2)
|
||||
assert hue_bridge.api_version == 2
|
||||
assert len(mock_forward.mock_calls) == 5
|
||||
assert len(mock_forward.mock_calls) == 6
|
||||
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
|
||||
assert forward_entries == {"light", "binary_sensor", "sensor", "switch", "scene"}
|
||||
assert forward_entries == {
|
||||
"light",
|
||||
"binary_sensor",
|
||||
"event",
|
||||
"sensor",
|
||||
"switch",
|
||||
"scene",
|
||||
}
|
||||
|
||||
|
||||
async def test_bridge_setup_invalid_api_key(hass: HomeAssistant) -> None:
|
||||
|
@ -92,7 +92,6 @@ async def test_get_triggers(
|
||||
}
|
||||
for event_type in (
|
||||
ButtonEvent.INITIAL_PRESS,
|
||||
ButtonEvent.LONG_PRESS,
|
||||
ButtonEvent.LONG_RELEASE,
|
||||
ButtonEvent.REPEAT,
|
||||
ButtonEvent.SHORT_RELEASE,
|
||||
|
100
tests/components/hue/test_event.py
Normal file
100
tests/components/hue/test_event.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""Philips Hue Event platform tests for V2 bridge/api."""
|
||||
from homeassistant.components.event import (
|
||||
ATTR_EVENT_TYPE,
|
||||
ATTR_EVENT_TYPES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import setup_platform
|
||||
from .const import FAKE_DEVICE, FAKE_ROTARY, FAKE_ZIGBEE_CONNECTIVITY
|
||||
|
||||
|
||||
async def test_event(
|
||||
hass: HomeAssistant, mock_bridge_v2, v2_resources_test_data
|
||||
) -> None:
|
||||
"""Test event entity for Hue integration."""
|
||||
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
|
||||
await setup_platform(hass, mock_bridge_v2, "event")
|
||||
# 7 entities should be created from test data
|
||||
assert len(hass.states.async_all()) == 7
|
||||
|
||||
# pick one of the remote buttons
|
||||
state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
assert state.name == "Hue Dimmer switch with 4 controls Button 1"
|
||||
# check event_types
|
||||
assert state.attributes[ATTR_EVENT_TYPES] == [
|
||||
"initial_press",
|
||||
"repeat",
|
||||
"short_release",
|
||||
"long_release",
|
||||
]
|
||||
# trigger firing 'initial_press' event from the device
|
||||
btn_event = {
|
||||
"button": {"last_event": "initial_press"},
|
||||
"id": "f92aa267-1387-4f02-9950-210fb7ca1f5a",
|
||||
"metadata": {"control_id": 1},
|
||||
"type": "button",
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", btn_event)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1")
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "initial_press"
|
||||
# trigger firing 'long_release' event from the device
|
||||
btn_event = {
|
||||
"button": {"last_event": "long_release"},
|
||||
"id": "f92aa267-1387-4f02-9950-210fb7ca1f5a",
|
||||
"metadata": {"control_id": 1},
|
||||
"type": "button",
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", btn_event)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1")
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "long_release"
|
||||
|
||||
|
||||
async def test_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None:
|
||||
"""Test Event entity for newly added Relative Rotary resource."""
|
||||
await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY])
|
||||
await setup_platform(hass, mock_bridge_v2, "event")
|
||||
|
||||
test_entity_id = "event.hue_mocked_device_relative_rotary"
|
||||
|
||||
# verify entity does not exist before we start
|
||||
assert hass.states.get(test_entity_id) is None
|
||||
|
||||
# Add new fake relative_rotary entity by emitting event
|
||||
mock_bridge_v2.api.emit_event("add", FAKE_ROTARY)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# the entity should now be available
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "unknown"
|
||||
assert state.name == "Hue mocked device Relative Rotary"
|
||||
# check event_types
|
||||
assert state.attributes[ATTR_EVENT_TYPES] == ["clock_wise", "counter_clock_wise"]
|
||||
|
||||
# test update of entity works on incoming event
|
||||
btn_event = {
|
||||
"id": "fake_relative_rotary",
|
||||
"relative_rotary": {
|
||||
"last_event": {
|
||||
"action": "repeat",
|
||||
"rotation": {
|
||||
"direction": "counter_clock_wise",
|
||||
"steps": 60,
|
||||
"duration": 400,
|
||||
},
|
||||
}
|
||||
},
|
||||
"type": "relative_rotary",
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", btn_event)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "counter_clock_wise"
|
||||
assert state.attributes["action"] == "repeat"
|
||||
assert state.attributes["steps"] == 60
|
||||
assert state.attributes["duration"] == 400
|
Loading…
x
Reference in New Issue
Block a user