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.
This commit is contained in:
parent
8233278ccc
commit
069e70ff03
8 changed files with 111 additions and 5 deletions
|
@ -40,6 +40,7 @@ from .const import (
|
||||||
COMMAND_GROUP_LIST,
|
COMMAND_GROUP_LIST,
|
||||||
CONF_AUTOMATIC_ADD,
|
CONF_AUTOMATIC_ADD,
|
||||||
CONF_DATA_BITS,
|
CONF_DATA_BITS,
|
||||||
|
CONF_PROTOCOLS,
|
||||||
DATA_RFXOBJECT,
|
DATA_RFXOBJECT,
|
||||||
DEVICE_PACKET_TYPE_LIGHTING4,
|
DEVICE_PACKET_TYPE_LIGHTING4,
|
||||||
EVENT_RFXTRX_EVENT,
|
EVENT_RFXTRX_EVENT,
|
||||||
|
@ -123,15 +124,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|
||||||
def _create_rfx(config):
|
def _create_rfx(config):
|
||||||
"""Construct a rfx object based on 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 config[CONF_PORT] is not None:
|
||||||
# If port is set then we create a TCP connection
|
# If port is set then we create a TCP connection
|
||||||
rfx = rfxtrxmod.Connect(
|
rfx = rfxtrxmod.Connect(
|
||||||
(config[CONF_HOST], config[CONF_PORT]),
|
(config[CONF_HOST], config[CONF_PORT]),
|
||||||
None,
|
None,
|
||||||
transport_protocol=rfxtrxmod.PyNetworkTransport,
|
transport_protocol=rfxtrxmod.PyNetworkTransport,
|
||||||
|
modes=modes,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None)
|
rfx = rfxtrxmod.Connect(
|
||||||
|
config[CONF_DEVICE],
|
||||||
|
None,
|
||||||
|
modes=modes,
|
||||||
|
)
|
||||||
|
|
||||||
return rfx
|
return rfx
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import itertools
|
||||||
import os
|
import os
|
||||||
from typing import TypedDict, cast
|
from typing import TypedDict, cast
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ from homeassistant.const import (
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
DeviceEntry,
|
DeviceEntry,
|
||||||
DeviceRegistry,
|
DeviceRegistry,
|
||||||
|
@ -40,6 +42,7 @@ from .const import (
|
||||||
CONF_AUTOMATIC_ADD,
|
CONF_AUTOMATIC_ADD,
|
||||||
CONF_DATA_BITS,
|
CONF_DATA_BITS,
|
||||||
CONF_OFF_DELAY,
|
CONF_OFF_DELAY,
|
||||||
|
CONF_PROTOCOLS,
|
||||||
CONF_REPLACE_DEVICE,
|
CONF_REPLACE_DEVICE,
|
||||||
CONF_SIGNAL_REPETITIONS,
|
CONF_SIGNAL_REPETITIONS,
|
||||||
CONF_VENETIAN_BLIND_MODE,
|
CONF_VENETIAN_BLIND_MODE,
|
||||||
|
@ -55,6 +58,8 @@ from .switch import supported as switch_supported
|
||||||
CONF_EVENT_CODE = "event_code"
|
CONF_EVENT_CODE = "event_code"
|
||||||
CONF_MANUAL_PATH = "Enter Manually"
|
CONF_MANUAL_PATH = "Enter Manually"
|
||||||
|
|
||||||
|
RECV_MODES = sorted(itertools.chain(*rfxtrxmod.lowlevel.Status.RECMODES))
|
||||||
|
|
||||||
|
|
||||||
class DeviceData(TypedDict):
|
class DeviceData(TypedDict):
|
||||||
"""Dict data representing a device entry."""
|
"""Dict data representing a device entry."""
|
||||||
|
@ -96,6 +101,7 @@ class OptionsFlow(config_entries.OptionsFlow):
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._global_options = {
|
self._global_options = {
|
||||||
CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD],
|
CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD],
|
||||||
|
CONF_PROTOCOLS: user_input[CONF_PROTOCOLS] or None,
|
||||||
}
|
}
|
||||||
if CONF_DEVICE in user_input:
|
if CONF_DEVICE in user_input:
|
||||||
entry_id = user_input[CONF_DEVICE]
|
entry_id = user_input[CONF_DEVICE]
|
||||||
|
@ -145,6 +151,10 @@ class OptionsFlow(config_entries.OptionsFlow):
|
||||||
CONF_AUTOMATIC_ADD,
|
CONF_AUTOMATIC_ADD,
|
||||||
default=self._config_entry.data[CONF_AUTOMATIC_ADD],
|
default=self._config_entry.data[CONF_AUTOMATIC_ADD],
|
||||||
): bool,
|
): 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_EVENT_CODE): str,
|
||||||
vol.Optional(CONF_DEVICE): vol.In(configure_devices),
|
vol.Optional(CONF_DEVICE): vol.In(configure_devices),
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ CONF_AUTOMATIC_ADD = "automatic_add"
|
||||||
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
|
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
|
||||||
CONF_OFF_DELAY = "off_delay"
|
CONF_OFF_DELAY = "off_delay"
|
||||||
CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode"
|
CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode"
|
||||||
|
CONF_PROTOCOLS = "protocols"
|
||||||
|
|
||||||
CONF_REPLACE_DEVICE = "replace_device"
|
CONF_REPLACE_DEVICE = "replace_device"
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"data": {
|
"data": {
|
||||||
"debug": "Enable debugging",
|
"debug": "Enable debugging",
|
||||||
"automatic_add": "Enable automatic add",
|
"automatic_add": "Enable automatic add",
|
||||||
|
"protocols": "Protocols",
|
||||||
"event_code": "Enter event code to add",
|
"event_code": "Enter event code to add",
|
||||||
"device": "Select device to configure"
|
"device": "Select device to configure"
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"debug": "Enable debugging",
|
"debug": "Enable debugging",
|
||||||
"device": "Select device to configure",
|
"device": "Select device to configure",
|
||||||
"event_code": "Enter event code to add",
|
"event_code": "Enter event code to add",
|
||||||
|
"protocols": "Protocols",
|
||||||
"remove_device": "Select device to delete"
|
"remove_device": "Select device to delete"
|
||||||
},
|
},
|
||||||
"title": "Rfxtrx Options"
|
"title": "Rfxtrx Options"
|
||||||
|
|
|
@ -14,13 +14,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
from tests.components.light.conftest import mock_light_profiles # noqa: F401
|
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."""
|
"""Create rfxtrx config entry data."""
|
||||||
return {
|
return {
|
||||||
"device": device,
|
"device": device,
|
||||||
"host": None,
|
"host": None,
|
||||||
"port": None,
|
"port": None,
|
||||||
"automatic_add": automatic_add,
|
"automatic_add": automatic_add,
|
||||||
|
"protocols": protocols,
|
||||||
"debug": False,
|
"debug": False,
|
||||||
"devices": devices,
|
"devices": devices,
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
SOME_PROTOCOLS = ["ac", "arc"]
|
||||||
|
|
||||||
|
|
||||||
def serial_connect(self):
|
def serial_connect(self):
|
||||||
"""Mock a serial connection."""
|
"""Mock a serial connection."""
|
||||||
|
@ -286,6 +288,7 @@ async def test_options_global(hass):
|
||||||
"port": None,
|
"port": None,
|
||||||
"device": "/dev/tty123",
|
"device": "/dev/tty123",
|
||||||
"automatic_add": False,
|
"automatic_add": False,
|
||||||
|
"protocols": None,
|
||||||
"devices": {},
|
"devices": {},
|
||||||
},
|
},
|
||||||
unique_id=DOMAIN,
|
unique_id=DOMAIN,
|
||||||
|
@ -298,7 +301,8 @@ async def test_options_global(hass):
|
||||||
assert result["step_id"] == "prompt_options"
|
assert result["step_id"] == "prompt_options"
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_configure(
|
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
|
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 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):
|
async def test_options_add_device(hass):
|
||||||
"""Test we can add a device."""
|
"""Test we can add a device."""
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
"""The tests for the Rfxtrx component."""
|
"""The tests for the Rfxtrx component."""
|
||||||
from __future__ import annotations
|
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.components.rfxtrx.const import EVENT_RFXTRX_EVENT
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.setup import async_setup_component
|
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):
|
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
|
# Verify that the config entry has removed the device
|
||||||
assert mock_entry.data["devices"] == {}
|
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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue