From ccdf182d31f1237668d029f306c0fb2a9d9b150d Mon Sep 17 00:00:00 2001 From: Niels AD Date: Sun, 6 Feb 2022 18:39:57 +0000 Subject: [PATCH] rfxtrx: Add command_on/command_off support for pt2262 switch entities (#65798) --- homeassistant/components/rfxtrx/switch.py | 65 ++++++++++++-- tests/components/rfxtrx/test_binary_sensor.py | 2 +- tests/components/rfxtrx/test_switch.py | 85 +++++++++++++++++++ 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 988beaa8fb6..8bc1fa42874 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -7,7 +7,7 @@ import RFXtrx as rfxtrxmod from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON +from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,8 +17,15 @@ from . import ( DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry, + get_pt2262_cmd, +) +from .const import ( + COMMAND_OFF_LIST, + COMMAND_ON_LIST, + CONF_DATA_BITS, + CONF_SIGNAL_REPETITIONS, + DEVICE_PACKET_TYPE_LIGHTING4, ) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS DATA_SWITCH = f"{DOMAIN}_switch" @@ -53,6 +60,9 @@ async def async_setup_entry( event.device, device_id, entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), + entity_info.get(CONF_DATA_BITS), + entity_info.get(CONF_COMMAND_ON), + entity_info.get(CONF_COMMAND_OFF), event=event if auto else None, ) ] @@ -65,6 +75,22 @@ async def async_setup_entry( class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): """Representation of a RFXtrx switch.""" + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + signal_repetitions: int = 1, + data_bits: int | None = None, + cmd_on: int | None = None, + cmd_off: int | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: + """Initialize the RFXtrx switch.""" + super().__init__(device, device_id, signal_repetitions, event=event) + self._data_bits = data_bits + self._cmd_on = cmd_on + self._cmd_off = cmd_off + async def async_added_to_hass(self): """Restore device state.""" await super().async_added_to_hass() @@ -74,15 +100,34 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if old_state is not None: self._state = old_state.state == STATE_ON - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: - """Apply command from rfxtrx.""" + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + """Apply event for a lighting 4 device.""" + if self._data_bits is not None: + cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) + assert cmdstr + cmd = int(cmdstr, 16) + if cmd == self._cmd_on: + self._state = True + elif cmd == self._cmd_off: + self._state = False + else: + self._state = True + + def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, rfxtrxmod.ControlEvent) - super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True elif event.values["Command"] in COMMAND_OFF_LIST: self._state = False + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + self._apply_event_lighting4(event) + else: + self._apply_event_standard(event) + @callback def _handle_event( self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple @@ -100,12 +145,18 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the device on.""" - await self._async_send(self._device.send_on) + if self._cmd_on is not None: + await self._async_send(self._device.send_command, self._cmd_on) + else: + await self._async_send(self._device.send_on) self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" - await self._async_send(self._device.send_off) + if self._cmd_off is not None: + await self._async_send(self._device.send_command, self._cmd_off) + else: + await self._async_send(self._device.send_off) self._state = False self.async_write_ha_state() diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 96368d166d7..175b455da6b 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -38,7 +38,7 @@ async def test_one(hass, rfxtrx): async def test_one_pt2262(hass, rfxtrx): - """Test with 1 sensor.""" + """Test with 1 PT2262 sensor.""" entry_data = create_rfx_test_cfg( devices={ "0913000022670e013970": { diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 6c22ee02920..a4560934ee1 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -52,6 +52,50 @@ async def test_one_switch(hass, rfxtrx): ] +async def test_one_pt2262_switch(hass, rfxtrx): + """Test with 1 PT2262 switch.""" + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "signal_repetitions": 1, + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.pt2262_22670e") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.pt2262_22670e"}, blocking=True + ) + + state = hass.states.get("switch.pt2262_22670e") + assert state.state == "on" + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.pt2262_22670e"}, blocking=True + ) + + state = hass.states.get("switch.pt2262_22670e") + assert state.state == "off" + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x09\x13\x00\x00\x22\x67\x0e\x01\x39\x00")), + call(bytearray(b"\x09\x13\x00\x00\x22\x67\x0f\x01\x39\x00")), + ] + + @pytest.mark.parametrize("state", ["on", "off"]) async def test_state_restore(hass, rfxtrx, state): """State restoration.""" @@ -182,6 +226,47 @@ async def test_switch_events(hass, rfxtrx): assert hass.states.get("switch.ac_213c7f2_16").state == "off" +async def test_pt2262_switch_events(hass, rfxtrx): + """Test with 1 PT2262 switch.""" + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "signal_repetitions": 1, + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.pt2262_22670e") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + # "Command: 0xE" + await rfxtrx.signal("0913000022670e013970") + assert hass.states.get("switch.pt2262_22670e").state == "on" + + # "Command: 0x0" + await rfxtrx.signal("09130000226700013970") + assert hass.states.get("switch.pt2262_22670e").state == "on" + + # "Command: 0x7" + await rfxtrx.signal("09130000226707013d70") + assert hass.states.get("switch.pt2262_22670e").state == "off" + + # "Command: 0x1" + await rfxtrx.signal("09130000226701013d70") + assert hass.states.get("switch.pt2262_22670e").state == "off" + + async def test_discover_switch(hass, rfxtrx_automatic): """Test with discovery of switches.""" rfxtrx = rfxtrx_automatic