From 13653be09b4bac1d7165ac1138563e4c37ad16c7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 3 Mar 2024 17:15:54 +0100 Subject: [PATCH] Add event platform to rfxtrx (#111526) --- homeassistant/components/rfxtrx/__init__.py | 1 + homeassistant/components/rfxtrx/event.py | 94 ++++++++++++++ homeassistant/components/rfxtrx/strings.json | 88 +++++++++++++ .../rfxtrx/snapshots/test_event.ambr | 121 ++++++++++++++++++ tests/components/rfxtrx/test_event.py | 102 +++++++++++++++ 5 files changed, 406 insertions(+) create mode 100644 homeassistant/components/rfxtrx/event.py create mode 100644 tests/components/rfxtrx/snapshots/test_event.ambr create mode 100644 tests/components/rfxtrx/test_event.py diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0f3988442c7..ce17316e6c7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -79,6 +79,7 @@ SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string}) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, + Platform.EVENT, Platform.LIGHT, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/rfxtrx/event.py b/homeassistant/components/rfxtrx/event.py new file mode 100644 index 00000000000..de4b32c5475 --- /dev/null +++ b/homeassistant/components/rfxtrx/event.py @@ -0,0 +1,94 @@ +"""Support for RFXtrx sensors.""" +from __future__ import annotations + +import logging +from typing import Any + +from RFXtrx import ControlEvent, RFXtrxDevice, RFXtrxEvent, SensorEvent + +from homeassistant.components.event import EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up config entry.""" + + def _supported(event: RFXtrxEvent) -> bool: + return isinstance(event, (ControlEvent, SensorEvent)) + + def _constructor( + event: RFXtrxEvent, + auto: RFXtrxEvent | None, + device_id: DeviceTuple, + entity_info: dict[str, Any], + ) -> list[Entity]: + entities: list[Entity] = [] + + if hasattr(event.device, "COMMANDS"): + entities.append( + RfxtrxEventEntity( + event.device, device_id, "COMMANDS", "Command", "command" + ) + ) + + if hasattr(event.device, "STATUS"): + entities.append( + RfxtrxEventEntity( + event.device, device_id, "STATUS", "Sensor Status", "status" + ) + ) + + return entities + + await async_setup_platform_entry( + hass, config_entry, async_add_entities, _supported, _constructor + ) + + +class RfxtrxEventEntity(RfxtrxEntity, EventEntity): + """Representation of a RFXtrx event.""" + + def __init__( + self, + device: RFXtrxDevice, + device_id: DeviceTuple, + device_attribute: str, + value_attribute: str, + translation_key: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(device, device_id) + commands: dict[int, str] = getattr(device, device_attribute) + self._attr_name = None + self._attr_unique_id = "_".join(x for x in device_id) + self._attr_event_types = [slugify(command) for command in commands.values()] + self._attr_translation_key = translation_key + self._value_attribute = value_attribute + + @callback + def _handle_event(self, event: RFXtrxEvent, device_id: DeviceTuple) -> None: + """Check if event applies to me and update.""" + if not self._event_applies(event, device_id): + return + + assert isinstance(event, (ControlEvent, SensorEvent)) + + event_type = slugify(event.values[self._value_attribute]) + if event_type not in self._attr_event_types: + _LOGGER.warning("Event type %s is not known", event_type) + return + + self._trigger_event(event_type, event.values) + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 9b99553d3f0..aeb4b2395d3 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -83,6 +83,94 @@ } }, "entity": { + "event": { + "command": { + "state_attributes": { + "event_type": { + "state": { + "sound_0": "Sound 0", + "sound_1": "Sound 1", + "sound_2": "Sound 2", + "sound_3": "Sound 3", + "sound_4": "Sound 4", + "sound_5": "Sound 5", + "sound_6": "Sound 6", + "sound_7": "Sound 7", + "sound_8": "Sound 8", + "sound_9": "Sound 9", + "sound_10": "Sound 10", + "sound_11": "Sound 11", + "sound_12": "Sound 12", + "sound_13": "Sound 13", + "sound_14": "Sound 14", + "sound_15": "Sound 15", + "down": "Down", + "up": "Up", + "all_off": "All Off", + "all_on": "All On", + "scene": "Scene", + "off": "Off", + "on": "On", + "dim": "Dim", + "bright": "Bright", + "all_group_off": "All/group Off", + "all_group_on": "All/group On", + "chime": "Chime", + "illegal_command": "Illegal command", + "set_level": "Set level", + "group_off": "Group off", + "group_on": "Group on", + "set_group_level": "Set group level", + "level_1": "Level 1", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9", + "program": "Program", + "stop": "Stop", + "0_5_seconds_up": "0.5 Seconds Up", + "0_5_seconds_down": "0.5 Seconds Down", + "2_seconds_up": "2 Seconds Up", + "2_seconds_down": "2 Seconds Down", + "enable_sun_automation": "Enable sun automation", + "disable_sun_automation": "Disable sun automation", + "normal": "Normal", + "normal_delayed": "Normal Delayed", + "alarm": "Alarm", + "alarm_delayed": "Alarm Delayed", + "motion": "Motion", + "no_motion": "No Motion", + "panic": "Panic", + "end_panic": "End Panic", + "ir": "IR", + "arm_away": "Arm Away", + "arm_away_delayed": "Arm Away Delayed", + "arm_home": "Arm Home", + "arm_home_delayed": "Arm Home Delayed", + "disarm": "Disarm", + "light_1_off": "Light 1 Off", + "light_1_on": "Light 1 On", + "light_2_off": "Light 2 Off", + "light_2_on": "Light 2 On", + "dark_detected": "Dark Detected", + "light_detected": "Light Detected", + "battery_low": "Battery low", + "pairing_kd101": "Pairing KD101", + "normal_tamper": "Normal Tamper", + "normal_delayed_tamper": "Normal Delayed Tamper", + "alarm_tamper": "Alarm Tamper", + "alarm_delayed_tamper": "Alarm Delayed Tamper", + "motion_tamper": "Motion Tamper", + "no_motion_tamper": "No Motion Tamper" + } + } + } + } + }, "sensor": { "current_ch_1": { "name": "Current Ch. 1" diff --git a/tests/components/rfxtrx/snapshots/test_event.ambr b/tests/components/rfxtrx/snapshots/test_event.ambr new file mode 100644 index 00000000000..99bb0195c65 --- /dev/null +++ b/tests/components/rfxtrx/snapshots/test_event.ambr @@ -0,0 +1,121 @@ +# serializer version: 1 +# name: test_control_event.2 + ... +# --- +# name: test_control_event.3 + ... + + 'Command': 'On', + + 'Rssi numeric': 5, + ... + - 'event_type': None, + + 'event_type': 'on', + ... + - 'state': 'unknown', + + 'state': '2021-01-09T12:00:00.000+00:00', + ... +# --- +# name: test_control_event[1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'off', + 'on', + 'dim', + 'bright', + 'all_group_off', + 'all_group_on', + 'chime', + 'illegal_command', + ]), + 'friendly_name': 'ARC C1', + }), + 'context': , + 'entity_id': 'event.arc_c1', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_control_event[2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'off', + 'on', + 'dim', + 'bright', + 'all_group_off', + 'all_group_on', + 'chime', + 'illegal_command', + ]), + 'friendly_name': 'ARC D1', + }), + 'context': , + 'entity_id': 'event.arc_d1', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_status_event.1 + ... + + 'Battery numeric': 9, + + 'Rssi numeric': 8, + + 'Sensor Status': 'Normal', + ... + - 'event_type': None, + + 'event_type': 'normal', + ... + - 'state': 'unknown', + + 'state': '2021-01-09T12:00:00.000+00:00', + ... +# --- +# name: test_status_event[1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'normal', + 'normal_delayed', + 'alarm', + 'alarm_delayed', + 'motion', + 'no_motion', + 'panic', + 'end_panic', + 'ir', + 'arm_away', + 'arm_away_delayed', + 'arm_home', + 'arm_home_delayed', + 'disarm', + 'light_1_off', + 'light_1_on', + 'light_2_off', + 'light_2_on', + 'dark_detected', + 'light_detected', + 'battery_low', + 'pairing_kd101', + 'normal_tamper', + 'normal_delayed_tamper', + 'alarm_tamper', + 'alarm_delayed_tamper', + 'motion_tamper', + 'no_motion_tamper', + ]), + 'friendly_name': 'X10 Security d3dc54:32', + }), + 'context': , + 'entity_id': 'event.x10_security_d3dc54_32', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/rfxtrx/test_event.py b/tests/components/rfxtrx/test_event.py new file mode 100644 index 00000000000..d0322c3ed82 --- /dev/null +++ b/tests/components/rfxtrx/test_event.py @@ -0,0 +1,102 @@ +"""The tests for the Rfxtrx sensor platform.""" +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from RFXtrx import ControlEvent +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.rfxtrx import get_rfx_object +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .conftest import setup_rfx_test_cfg + + +@pytest.fixture(autouse=True) +def required_platforms_only(): + """Only set up the required platform and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.rfxtrx.PLATFORMS", + (Platform.EVENT,), + ): + yield + + +async def test_control_event( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test event update updates correct event object.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + + await setup_rfx_test_cfg( + hass, + devices={ + "0710013d43010150": {}, + "0710013d44010150": {}, + }, + ) + + assert hass.states.get("event.arc_c1") == snapshot(name="1") + assert hass.states.get("event.arc_d1") == snapshot(name="2") + + # only signal one, to make sure we have no overhearing + await rfxtrx.signal("0710013d44010150") + + assert hass.states.get("event.arc_c1") == snapshot(diff="1") + assert hass.states.get("event.arc_d1") == snapshot(diff="2") + + +async def test_status_event( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test event update updates correct event object.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + + await setup_rfx_test_cfg( + hass, + devices={ + "0820004dd3dc540089": {}, + }, + ) + + assert hass.states.get("event.x10_security_d3dc54_32") == snapshot(name="1") + + await rfxtrx.signal("0820004dd3dc540089") + + assert hass.states.get("event.x10_security_d3dc54_32") == snapshot(diff="1") + + +async def test_invalid_event_type( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test with 1 sensor.""" + await setup_rfx_test_cfg( + hass, + devices={ + "0710013d43010150": {}, + }, + ) + + state = hass.states.get("event.arc_c1") + + # Invalid event type should not trigger change + event = get_rfx_object("0710013d43010150") + assert isinstance(event, ControlEvent) + event.values["Command"] = "invalid_command" + + rfxtrx.event_callback(event) + await hass.async_block_till_done() + + assert hass.states.get("event.arc_c1") == state