Add mode setting to RFXtrx device configuration (#67173)

This allows users to configure the modes on their RFXtrx device without
downloading additional software that only runs on Windows. It also
enables the use of modes that cannot be permanently enabled on the
device, such as for undecoded and raw messages.
pull/67315/head
James Hewitt 2022-02-25 19:43:29 +00:00 committed by GitHub
parent 8233278ccc
commit 069e70ff03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 111 additions and 5 deletions

View File

@ -40,6 +40,7 @@ from .const import (
COMMAND_GROUP_LIST,
CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
CONF_PROTOCOLS,
DATA_RFXOBJECT,
DEVICE_PACKET_TYPE_LIGHTING4,
EVENT_RFXTRX_EVENT,
@ -123,15 +124,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def _create_rfx(config):
"""Construct a rfx object based on config."""
modes = config.get(CONF_PROTOCOLS)
if modes:
_LOGGER.debug("Using modes: %s", ",".join(modes))
else:
_LOGGER.debug("No modes defined, using device configuration")
if config[CONF_PORT] is not None:
# If port is set then we create a TCP connection
rfx = rfxtrxmod.Connect(
(config[CONF_HOST], config[CONF_PORT]),
None,
transport_protocol=rfxtrxmod.PyNetworkTransport,
modes=modes,
)
else:
rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None)
rfx = rfxtrxmod.Connect(
config[CONF_DEVICE],
None,
modes=modes,
)
return rfx

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import copy
import itertools
import os
from typing import TypedDict, cast
@ -23,6 +24,7 @@ from homeassistant.const import (
CONF_TYPE,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import (
DeviceEntry,
DeviceRegistry,
@ -40,6 +42,7 @@ from .const import (
CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
CONF_OFF_DELAY,
CONF_PROTOCOLS,
CONF_REPLACE_DEVICE,
CONF_SIGNAL_REPETITIONS,
CONF_VENETIAN_BLIND_MODE,
@ -55,6 +58,8 @@ from .switch import supported as switch_supported
CONF_EVENT_CODE = "event_code"
CONF_MANUAL_PATH = "Enter Manually"
RECV_MODES = sorted(itertools.chain(*rfxtrxmod.lowlevel.Status.RECMODES))
class DeviceData(TypedDict):
"""Dict data representing a device entry."""
@ -96,6 +101,7 @@ class OptionsFlow(config_entries.OptionsFlow):
if user_input is not None:
self._global_options = {
CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD],
CONF_PROTOCOLS: user_input[CONF_PROTOCOLS] or None,
}
if CONF_DEVICE in user_input:
entry_id = user_input[CONF_DEVICE]
@ -145,6 +151,10 @@ class OptionsFlow(config_entries.OptionsFlow):
CONF_AUTOMATIC_ADD,
default=self._config_entry.data[CONF_AUTOMATIC_ADD],
): bool,
vol.Optional(
CONF_PROTOCOLS,
default=self._config_entry.data.get(CONF_PROTOCOLS) or [],
): cv.multi_select(RECV_MODES),
vol.Optional(CONF_EVENT_CODE): str,
vol.Optional(CONF_DEVICE): vol.In(configure_devices),
}

View File

@ -5,6 +5,7 @@ CONF_AUTOMATIC_ADD = "automatic_add"
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
CONF_OFF_DELAY = "off_delay"
CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode"
CONF_PROTOCOLS = "protocols"
CONF_REPLACE_DEVICE = "replace_device"

View File

@ -41,6 +41,7 @@
"data": {
"debug": "Enable debugging",
"automatic_add": "Enable automatic add",
"protocols": "Protocols",
"event_code": "Enter event code to add",
"device": "Select device to configure"
},

View File

@ -61,6 +61,7 @@
"debug": "Enable debugging",
"device": "Select device to configure",
"event_code": "Enter event code to add",
"protocols": "Protocols",
"remove_device": "Select device to delete"
},
"title": "Rfxtrx Options"

View File

@ -14,13 +14,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.light.conftest import mock_light_profiles # noqa: F401
def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None):
def create_rfx_test_cfg(
device="abcd", automatic_add=False, protocols=None, devices=None
):
"""Create rfxtrx config entry data."""
return {
"device": device,
"host": None,
"port": None,
"automatic_add": automatic_add,
"protocols": protocols,
"debug": False,
"devices": devices,
}

View File

@ -11,6 +11,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
SOME_PROTOCOLS = ["ac", "arc"]
def serial_connect(self):
"""Mock a serial connection."""
@ -286,6 +288,7 @@ async def test_options_global(hass):
"port": None,
"device": "/dev/tty123",
"automatic_add": False,
"protocols": None,
"devices": {},
},
unique_id=DOMAIN,
@ -298,7 +301,8 @@ async def test_options_global(hass):
assert result["step_id"] == "prompt_options"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={"automatic_add": True}
result["flow_id"],
user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@ -307,6 +311,44 @@ async def test_options_global(hass):
assert entry.data["automatic_add"]
assert not set(entry.data["protocols"]) ^ set(SOME_PROTOCOLS)
async def test_no_protocols(hass):
"""Test we set protocols to None if none are selected."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": None,
"port": None,
"device": "/dev/tty123",
"automatic_add": True,
"protocols": SOME_PROTOCOLS,
"devices": {},
},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "prompt_options"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"automatic_add": False, "protocols": []},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert not entry.data["automatic_add"]
assert entry.data["protocols"] is None
async def test_options_add_device(hass):
"""Test we can add a device."""

View File

@ -1,14 +1,20 @@
"""The tests for the Rfxtrx component."""
from __future__ import annotations
from unittest.mock import call
from unittest.mock import ANY, call, patch
import RFXtrx as rfxtrxmod
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from tests.components.rfxtrx.conftest import setup_rfx_test_cfg
from tests.common import MockConfigEntry
from tests.components.rfxtrx.conftest import create_rfx_test_cfg, setup_rfx_test_cfg
SOME_PROTOCOLS = ["ac", "arc"]
async def test_fire_event(hass, rfxtrx):
@ -118,3 +124,31 @@ async def test_ws_device_remove(hass, hass_ws_client):
# Verify that the config entry has removed the device
assert mock_entry.data["devices"] == {}
async def test_connect(hass):
"""Test that we attempt to connect to the device."""
entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake")
mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
with patch.object(rfxtrxmod, "Connect") as connect:
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
connect.assert_called_once_with("/dev/ttyUSBfake", ANY, modes=ANY)
async def test_connect_with_protocols(hass):
"""Test that we attempt to set protocols."""
entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake", protocols=SOME_PROTOCOLS)
mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
with patch.object(rfxtrxmod, "Connect") as connect:
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
connect.assert_called_once_with("/dev/ttyUSBfake", ANY, modes=SOME_PROTOCOLS)