From 67038c6ba8d2d836f0d0f8d442cf14a06c256273 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 10 Jul 2020 14:52:07 +0200 Subject: [PATCH] Rewrite rfxtrx init logic to do away with global object (#37699) * Rewrite init logic to do away with global object * Put constant at end * Use a set instead of list for device_ids --- homeassistant/components/rfxtrx/__init__.py | 89 ++------ .../components/rfxtrx/binary_sensor.py | 121 +++++----- homeassistant/components/rfxtrx/const.py | 1 + homeassistant/components/rfxtrx/cover.py | 59 ++++- homeassistant/components/rfxtrx/light.py | 58 ++++- homeassistant/components/rfxtrx/sensor.py | 96 ++++---- homeassistant/components/rfxtrx/switch.py | 55 ++++- tests/components/rfxtrx/conftest.py | 2 - tests/components/rfxtrx/test_init.py | 6 +- tests/components/rfxtrx/test_light.py | 2 +- tests/components/rfxtrx/test_sensor.py | 210 +++++++++--------- 11 files changed, 365 insertions(+), 334 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c20d50fee3f..a689a297e76 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -8,10 +8,8 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_NAME, ATTR_STATE, CONF_DEVICE, - CONF_DEVICES, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START, @@ -25,6 +23,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify +from .const import DEVICE_PACKET_TYPE_LIGHTING4 + DOMAIN = "rfxtrx" DEFAULT_SIGNAL_REPETITIONS = 1 @@ -80,7 +80,6 @@ DATA_TYPES = OrderedDict( ] ) -RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) DATA_RFXOBJECT = "rfxobject" @@ -207,31 +206,15 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -def get_pt2262_device(device_id): +def find_possible_pt2262_device(device_ids, device_id): """Look for the device which id matches the given device_id parameter.""" - for device in RFX_DEVICES.values(): - if ( - hasattr(device, "is_lighting4") - and device.masked_id is not None - and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits) - ): - _LOGGER.debug( - "rfxtrx: found matching device %s for %s", device_id, device.masked_id, - ) - return device - return None - - -def find_possible_pt2262_device(device_id): - """Look for the device which id matches the given device_id parameter.""" - for dev_id, device in RFX_DEVICES.items(): - if hasattr(device, "is_lighting4") and len(dev_id) == len(device_id): + for dev_id in device_ids: + if len(dev_id) == len(device_id): size = None for i, (char1, char2) in enumerate(zip(dev_id, device_id)): if char1 != char2: break size = i - if size is not None: size = len(dev_id) - size - 1 _LOGGER.info( @@ -246,60 +229,19 @@ def find_possible_pt2262_device(device_id): dev_id[-size:], device_id[-size:], ) - return device - + return dev_id return None -def get_devices_from_config(config, device): - """Read rfxtrx configuration.""" - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] +def get_device_id(device, data_bits=None): + """Calculate a device id for device.""" + id_string = device.id_string + if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + masked_id = get_pt2262_deviceid(id_string, data_bits) + if masked_id: + id_string = str(masked_id) - devices = [] - for packet_id, entity_info in config[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - continue - _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) - - # Check if i must fire event - fire_event = entity_info[ATTR_FIRE_EVENT] - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} - - new_device = device( - entity_info[ATTR_NAME], event.device, datas, signal_repetitions - ) - RFX_DEVICES[device_id] = new_device - devices.append(new_device) - return devices - - -def get_new_device(event, config, device): - """Add entity if not exist and the automatic_add is True.""" - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - return - - if not config[ATTR_AUTOMATIC_ADD]: - return - - pkt_id = "".join(f"{x:02x}" for x in event.data) - _LOGGER.debug( - "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", - device_id, - event.device.__class__.__name__, - event.device.subtype, - pkt_id, - ) - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] - new_device = device(pkt_id, event.device, datas, signal_repetitions, event=event) - RFX_DEVICES[device_id] = new_device - return new_device + return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) def fire_command_event(hass, entity_id, command): @@ -330,7 +272,8 @@ class RfxtrxDevice(Entity): self._device = device self._state = datas[ATTR_STATE] self._should_fire_event = datas[ATTR_FIRE_EVENT] - self._unique_id = f"{device.packettype:x}_{device.subtype:x}_{device.id_string}" + self._device_id = get_device_id(device) + self._unique_id = "_".join(x for x in self._device_id) if event: self._apply_event(event) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index c37abdeb08e..529e20c956e 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -13,31 +13,29 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, + CONF_DEVICES, CONF_NAME, ) from homeassistant.helpers import config_validation as cv, event as evt -from homeassistant.util import slugify from . import ( - ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY, - RFX_DEVICES, SIGNAL_EVENT, find_possible_pt2262_device, fire_command_event, + get_device_id, get_pt2262_cmd, - get_pt2262_device, get_pt2262_deviceid, get_rfx_object, ) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, DEVICE_PACKET_TYPE_LIGHTING4 _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICES, default={}): { @@ -61,28 +59,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def _get_device_data_bits(device, device_bits): + """Deduce data bits for device based on a cache of device bits.""" + data_bits = None + if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + for id_masked, bits in device_bits.items(): + if get_pt2262_deviceid(device.id_string, bits) == id_masked: + data_bits = bits + break + return data_bits + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform to RFXtrx.""" sensors = [] + device_ids = set() + device_bits = {} + + pt2262_devices = [] + for packet_id, entity in config[CONF_DEVICES].items(): event = get_rfx_object(packet_id) - device_id = slugify(event.device.id_string.lower()) - - if device_id in RFX_DEVICES: + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) continue - if entity.get(CONF_DATA_BITS) is not None: - _LOGGER.debug( - "Masked device id: %s", - get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), - ) + device_id = get_device_id(event.device, data_bits=entity.get(CONF_DATA_BITS)) + if device_id in device_ids: + continue + device_ids.add(device_id) - _LOGGER.debug( - "Add %s rfxtrx.binary_sensor (class %s)", - entity[ATTR_NAME], - entity.get(CONF_DEVICE_CLASS), - ) + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + find_possible_pt2262_device(pt2262_devices, event.device.id_string) + pt2262_devices.append(event.device.id_string) device = RfxtrxBinarySensor( event.device, @@ -95,7 +105,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entity.get(CONF_COMMAND_OFF), ) sensors.append(device) - RFX_DEVICES[device_id] = device add_entities(sensors) @@ -104,35 +113,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not isinstance(event, rfxtrxmod.ControlEvent): return - device_id = slugify(event.device.id_string.lower()) + data_bits = _get_device_data_bits(event.device, device_bits) - sensor = RFX_DEVICES.get(device_id, get_pt2262_device(device_id)) + device_id = get_device_id(event.device, data_bits=data_bits) + if device_id in device_ids: + return + device_ids.add(device_id) - if sensor is None: - # Add the entity if not exists and automatic_add is True - if not config[CONF_AUTOMATIC_ADD]: - return - - if event.device.packettype == 0x13: - poss_dev = find_possible_pt2262_device(device_id) - if poss_dev is not None: - poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.debug("Found possible matching device ID: %s", poss_id) - - pkt_id = "".join(f"{x:02x}" for x in event.data) - sensor = RfxtrxBinarySensor(event.device, pkt_id, event=event) - RFX_DEVICES[device_id] = sensor - add_entities([sensor]) - _LOGGER.info( - "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", - pkt_id, - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype, - ) + _LOGGER.info( + "Added binary sensor (Device ID: %s Class: %s Sub: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + ) + pkt_id = "".join(f"{x:02x}" for x in event.data) + sensor = RfxtrxBinarySensor( + event.device, pkt_id, data_bits=data_bits, event=event + ) + add_entities([sensor]) # Subscribe to main RFXtrx events - hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, binary_sensor_update) + if config[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, binary_sensor_update) class RfxtrxBinarySensor(BinarySensorEntity): @@ -158,22 +160,13 @@ class RfxtrxBinarySensor(BinarySensorEntity): self._device_class = device_class self._off_delay = off_delay self._state = False - self.is_lighting4 = device.packettype == 0x13 self.delay_listener = None self._data_bits = data_bits self._cmd_on = cmd_on self._cmd_off = cmd_off - if data_bits is not None: - self._masked_id = get_pt2262_deviceid(device.id_string.lower(), data_bits) - self._unique_id = ( - f"{device.packettype:x}_{device.subtype:x}_{self._masked_id}" - ) - else: - self._masked_id = None - self._unique_id = ( - f"{device.packettype:x}_{device.subtype:x}_{device.id_string}" - ) + self._device_id = get_device_id(device, data_bits=data_bits) + self._unique_id = "_".join(x for x in self._device_id) if event: self._apply_event(event) @@ -193,11 +186,6 @@ class RfxtrxBinarySensor(BinarySensorEntity): """Return the device name.""" return self._name - @property - def masked_id(self): - """Return the masked device id (isolated address bits).""" - return self._masked_id - @property def data_bits(self): """Return the number of data bits.""" @@ -263,20 +251,15 @@ class RfxtrxBinarySensor(BinarySensorEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" - if self.is_lighting4: + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: self._apply_event_lighting4(event) else: self._apply_event_standard(event) def _handle_event(self, event): """Check if event applies to me and update.""" - if self._masked_id: - masked_id = get_pt2262_deviceid(event.device.id_string, self._data_bits) - if masked_id != self._masked_id: - return - else: - if event.device.id_string != self._device.id_string: - return + if get_device_id(event.device, data_bits=self._data_bits) != self._device_id: + return _LOGGER.debug( "Binary sensor update (Device ID: %s Class: %s Sub: %s)", diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 1eab56ac94b..40863a69eac 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -14,3 +14,4 @@ COMMAND_OFF_LIST = [ "Down", "Close (inline relay)", ] +DEVICE_PACKET_TYPE_LIGHTING4 = 0x13 diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index dde282423f2..cdb0a21e3b4 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,23 +1,25 @@ """Support for RFXtrx covers.""" +import logging + import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity -from homeassistant.const import CONF_NAME, STATE_OPEN +from homeassistant.const import ATTR_STATE, CONF_DEVICES, CONF_NAME, STATE_OPEN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from . import ( + ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, RfxtrxDevice, fire_command_event, - get_devices_from_config, - get_new_device, + get_device_id, + get_rfx_object, ) from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST @@ -38,11 +40,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - covers = get_devices_from_config(config, RfxtrxCover) - add_entities(covers) + device_ids = set() + + entities = [] + for packet_id, entity_info in config[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + datas = {ATTR_STATE: None, ATTR_FIRE_EVENT: entity_info[CONF_FIRE_EVENT]} + entity = RfxtrxCover( + entity_info[CONF_NAME], event.device, datas, config[CONF_SIGNAL_REPETITIONS] + ) + entities.append(entity) + + add_entities(entities) def cover_update(event): """Handle cover updates from the RFXtrx gateway.""" @@ -53,12 +76,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = get_new_device(event, config, RfxtrxCover) - if new_device: - add_entities([new_device]) + device_id = get_device_id(event.device) + if device_id in device_ids: + return + device_ids.add(device_id) + + _LOGGER.info( + "Added cover (Device ID: %s Class: %s Sub: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + ) + + pkt_id = "".join(f"{x:02x}" for x in event.data) + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} + entity = RfxtrxCover( + pkt_id, event.device, datas, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + add_entities([entity]) # Subscribe to main RFXtrx events - hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, cover_update) + if config[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, cover_update) class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index d6935677f2f..74915f2b91a 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -10,21 +10,21 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_NAME, STATE_ON +from homeassistant.const import ATTR_STATE, CONF_DEVICES, CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from . import ( + ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, RfxtrxDevice, fire_command_event, - get_devices_from_config, - get_new_device, + get_device_id, + get_rfx_object, ) from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST @@ -52,8 +52,29 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - lights = get_devices_from_config(config, RfxtrxLight) - add_entities(lights) + device_ids = set() + + # Add switch from config file + entities = [] + for packet_id, entity_info in config[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + datas = {ATTR_STATE: None, ATTR_FIRE_EVENT: entity_info[CONF_FIRE_EVENT]} + entity = RfxtrxLight( + entity_info[CONF_NAME], event.device, datas, config[CONF_SIGNAL_REPETITIONS] + ) + + entities.append(entity) + + add_entities(entities) def light_update(event): """Handle light updates from the RFXtrx gateway.""" @@ -63,12 +84,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = get_new_device(event, config, RfxtrxLight) - if new_device: - add_entities([new_device]) + device_id = get_device_id(event.device) + if device_id in device_ids: + return + device_ids.add(device_id) + + _LOGGER.debug( + "Added light (Device ID: %s Class: %s Sub: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + ) + + pkt_id = "".join(f"{x:02x}" for x in event.data) + datas = {ATTR_STATE: None, ATTR_FIRE_EVENT: False} + entity = RfxtrxLight( + pkt_id, event.device, datas, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + + add_entities([entity]) # Subscribe to main RFXtrx events - hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, light_update) + if config[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, light_update) class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index cc7a817a061..c718ab8412b 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -5,21 +5,17 @@ from RFXtrx import SensorEvent import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify from . import ( - ATTR_DATA_TYPE, - ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, - CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES, - RFX_DEVICES, SIGNAL_EVENT, + get_device_id, get_rfx_object, ) @@ -46,64 +42,63 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - sensors = [] + data_ids = set() + + entities = [] for packet_id, entity_info in config[CONF_DEVICES].items(): event = get_rfx_object(packet_id) - device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) - if device_id in RFX_DEVICES: + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) continue - _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) - sub_sensors = {} - data_types = entity_info[ATTR_DATA_TYPE] - if not data_types: - data_types = [""] - for data_type in DATA_TYPES: - if data_type in event.values: - data_types = [data_type] - break - for _data_type in data_types: - new_sensor = RfxtrxSensor( + if entity_info[CONF_DATA_TYPE]: + data_types = entity_info[CONF_DATA_TYPE] + else: + data_types = list(set(event.values) & set(DATA_TYPES)) + + device_id = get_device_id(event.device) + for data_type in data_types: + data_id = (*device_id, data_type) + if data_id in data_ids: + continue + data_ids.add(data_id) + + entity = RfxtrxSensor( event.device, - entity_info[ATTR_NAME], - _data_type, - entity_info[ATTR_FIRE_EVENT], + entity_info[CONF_NAME], + data_type, + entity_info[CONF_FIRE_EVENT], ) - sensors.append(new_sensor) - sub_sensors[_data_type] = new_sensor - RFX_DEVICES[device_id] = sub_sensors - add_entities(sensors) + entities.append(entity) + + add_entities(entities) def sensor_update(event): """Handle sensor updates from the RFXtrx gateway.""" if not isinstance(event, SensorEvent): return - device_id = f"sensor_{slugify(event.device.id_string.lower())}" - - if device_id in RFX_DEVICES: - return - - # Add entity if not exist and the automatic_add is True - if not config[CONF_AUTOMATIC_ADD]: - return - pkt_id = "".join(f"{x:02x}" for x in event.data) - _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) + device_id = get_device_id(event.device) + for data_type in set(event.values) & set(DATA_TYPES): + data_id = (*device_id, data_type) + if data_id in data_ids: + continue + data_ids.add(data_id) - data_type = "" - for _data_type in DATA_TYPES: - if _data_type in event.values: - data_type = _data_type - break - new_sensor = RfxtrxSensor(event.device, pkt_id, data_type, event=event) - sub_sensors = {} - sub_sensors[new_sensor.data_type] = new_sensor - RFX_DEVICES[device_id] = sub_sensors - add_entities([new_sensor]) + _LOGGER.debug( + "Added sensor (Device ID: %s Class: %s Sub: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + ) + + entity = RfxtrxSensor(event.device, pkt_id, data_type, event=event) + add_entities([entity]) # Subscribe to main RFXtrx events - hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, sensor_update) + if config[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, sensor_update) class RfxtrxSensor(Entity): @@ -117,9 +112,8 @@ class RfxtrxSensor(Entity): self.should_fire_event = should_fire_event self.data_type = data_type self._unit_of_measurement = DATA_TYPES.get(data_type, "") - self._unique_id = ( - f"{device.packettype:x}_{device.subtype:x}_{device.id_string}_{data_type}" - ) + self._device_id = get_device_id(device) + self._unique_id = "_".join(x for x in (*self._device_id, data_type)) if event: self._apply_event(event) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 110c5544d91..e66ce8be8a3 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -5,21 +5,21 @@ import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, STATE_ON +from homeassistant.const import ATTR_STATE, CONF_DEVICES, CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from . import ( + ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, RfxtrxDevice, fire_command_event, - get_devices_from_config, - get_new_device, + get_device_id, + get_rfx_object, ) from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST @@ -45,9 +45,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" + device_ids = set() + # Add switch from config file - switches = get_devices_from_config(config, RfxtrxSwitch) - add_entities_callback(switches) + entities = [] + for packet_id, entity_info in config[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + datas = {ATTR_STATE: None, ATTR_FIRE_EVENT: entity_info[CONF_FIRE_EVENT]} + entity = RfxtrxSwitch( + entity_info[CONF_NAME], event.device, datas, config[CONF_SIGNAL_REPETITIONS] + ) + entities.append(entity) + + add_entities_callback(entities) def switch_update(event): """Handle sensor updates from the RFXtrx gateway.""" @@ -58,12 +77,28 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ): return - new_device = get_new_device(event, config, RfxtrxSwitch) - if new_device: - add_entities_callback([new_device]) + device_id = get_device_id(event.device) + if device_id in device_ids: + return + device_ids.add(device_id) + + _LOGGER.info( + "Added switch (Device ID: %s Class: %s Sub: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + ) + + pkt_id = "".join(f"{x:02x}" for x in event.data) + datas = {ATTR_STATE: None, ATTR_FIRE_EVENT: False} + entity = RfxtrxSwitch( + pkt_id, event.device, datas, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + add_entities_callback([entity]) # Subscribe to main RFXtrx events - hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, switch_update) + if config[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.dispatcher_connect(SIGNAL_EVENT, switch_update) class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 4eaae48410e..18ab207677f 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -59,8 +59,6 @@ async def rfxtrx_cleanup(): ): yield - rfxtrx_core.RFX_DEVICES.clear() - @pytest.fixture(name="rfxtrx") async def rfxtrx_fixture(hass): diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 9366e32f3d2..f8dd2b17cf0 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -141,7 +141,7 @@ async def test_fire_event(hass): assert state assert state.state == "on" - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].data == {"entity_id": "switch.test", "state": "on"} @@ -187,5 +187,5 @@ async def test_fire_event_sensor(hass): hass.bus.async_listen("signal_received", record_event) await _signal_event(hass, "0a520802060101ff0f0269") - assert 1 == len(calls) - assert calls[0].data == {"entity_id": "sensor.test_temperature"} + assert len(calls) == 5 + assert any(call.data == {"entity_id": "sensor.test_temperature"} for call in calls) diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 121593e0ce5..a5e411a0c5f 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -57,7 +57,7 @@ async def test_default_config(hass, rfxtrx): ) await hass.async_block_till_done() - assert 0 == len(rfxtrx_core.RFX_DEVICES) + assert len(hass.states.async_all()) == 0 async def test_one_light(hass, rfxtrx): diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index b278bb49b44..ef0b53dc771 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -55,12 +55,39 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): ) await hass.async_block_till_done() - state = hass.states.get("sensor.test_temperature") + base_id = "sensor.test" + base_name = "Test" + + state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "Test Temperature" + assert state.attributes.get("friendly_name") == f"{base_name} Temperature" assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + state = hass.states.get(f"{base_id}_humidity") + assert state + assert state.state == "unknown" + assert state.attributes.get("friendly_name") == f"{base_name} Humidity" + assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + + state = hass.states.get(f"{base_id}_humidity_status") + assert state + assert state.state == "unknown" + assert state.attributes.get("friendly_name") == f"{base_name} Humidity status" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_rssi_numeric") + assert state + assert state.state == "unknown" + assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_battery_numeric") + assert state + assert state.state == "unknown" + assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" + assert state.attributes.get("unit_of_measurement") == "" + async def test_several_sensors(hass, rfxtrx): """Test with 3 sensors.""" @@ -113,61 +140,94 @@ async def test_discover_sensor(hass, rfxtrx): ) await hass.async_block_till_done() + # 1 await _signal_event(hass, "0a520801070100b81b0279") - state = hass.states.get("sensor.0a520801070100b81b0279_temperature") + base_id = "sensor.0a520801070100b81b0279" + + state = hass.states.get(f"{base_id}_humidity") + assert state + assert state.state == "27" + assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + + state = hass.states.get(f"{base_id}_humidity_status") + assert state + assert state.state == "normal" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_rssi_numeric") + assert state + assert state.state == "7" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "18.4" - assert ( - state.attributes.items() - >= { - "friendly_name": "0a520801070100b81b0279 Temperature", - "unit_of_measurement": TEMP_CELSIUS, - "Humidity status": "normal", - "Temperature": 18.4, - "Rssi numeric": 7, - "Humidity": 27, - "Battery numeric": 9, - "Humidity status numeric": 2, - }.items() - ) + assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + state = hass.states.get(f"{base_id}_battery_numeric") + assert state + assert state.state == "9" + assert state.attributes.get("unit_of_measurement") == "" + + # 2 await _signal_event(hass, "0a52080405020095240279") - state = hass.states.get("sensor.0a52080405020095240279_temperature") + base_id = "sensor.0a52080405020095240279" + state = hass.states.get(f"{base_id}_humidity") + + assert state + assert state.state == "36" + assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + + state = hass.states.get(f"{base_id}_humidity_status") + assert state + assert state.state == "normal" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_rssi_numeric") + assert state + assert state.state == "7" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "14.9" - assert ( - state.attributes.items() - >= { - "friendly_name": "0a52080405020095240279 Temperature", - "unit_of_measurement": TEMP_CELSIUS, - "Humidity status": "normal", - "Temperature": 14.9, - "Rssi numeric": 7, - "Humidity": 36, - "Battery numeric": 9, - "Humidity status numeric": 2, - }.items() - ) + assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + state = hass.states.get(f"{base_id}_battery_numeric") + assert state + assert state.state == "9" + assert state.attributes.get("unit_of_measurement") == "" + + # 1 Update await _signal_event(hass, "0a52085e070100b31b0279") - state = hass.states.get("sensor.0a520801070100b81b0279_temperature") + base_id = "sensor.0a520801070100b81b0279" + + state = hass.states.get(f"{base_id}_humidity") + assert state + assert state.state == "27" + assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + + state = hass.states.get(f"{base_id}_humidity_status") + assert state + assert state.state == "normal" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_rssi_numeric") + assert state + assert state.state == "7" + assert state.attributes.get("unit_of_measurement") == "" + + state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "17.9" - assert ( - state.attributes.items() - >= { - "friendly_name": "0a520801070100b81b0279 Temperature", - "unit_of_measurement": TEMP_CELSIUS, - "Humidity status": "normal", - "Temperature": 17.9, - "Rssi numeric": 7, - "Humidity": 27, - "Battery numeric": 9, - "Humidity status numeric": 2, - }.items() - ) + assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS - assert len(hass.states.async_all()) == 2 + state = hass.states.get(f"{base_id}_battery_numeric") + assert state + assert state.state == "9" + assert state.attributes.get("unit_of_measurement") == "" + + assert len(hass.states.async_all()) == 10 async def test_discover_sensor_noautoadd(hass, rfxtrx): @@ -215,35 +275,14 @@ async def test_update_of_sensors(hass, rfxtrx): state = hass.states.get("sensor.test_temperature") assert state assert state.state == "unknown" - assert ( - state.attributes.items() - >= { - "friendly_name": "Test Temperature", - "unit_of_measurement": TEMP_CELSIUS, - }.items() - ) state = hass.states.get("sensor.bath_temperature") assert state assert state.state == "unknown" - assert ( - state.attributes.items() - >= { - "friendly_name": "Bath Temperature", - "unit_of_measurement": TEMP_CELSIUS, - }.items() - ) state = hass.states.get("sensor.bath_humidity") assert state assert state.state == "unknown" - assert ( - state.attributes.items() - >= { - "friendly_name": "Bath Humidity", - "unit_of_measurement": UNIT_PERCENTAGE, - }.items() - ) assert len(hass.states.async_all()) == 3 @@ -253,52 +292,13 @@ async def test_update_of_sensors(hass, rfxtrx): state = hass.states.get("sensor.test_temperature") assert state assert state.state == "13.3" - assert ( - state.attributes.items() - >= { - "friendly_name": "Test Temperature", - "unit_of_measurement": TEMP_CELSIUS, - "Battery numeric": 9, - "Temperature": 13.3, - "Humidity": 34, - "Humidity status": "normal", - "Humidity status numeric": 2, - "Rssi numeric": 6, - }.items() - ) state = hass.states.get("sensor.bath_temperature") assert state assert state.state == "51.1" - assert ( - state.attributes.items() - >= { - "friendly_name": "Bath Temperature", - "unit_of_measurement": TEMP_CELSIUS, - "Battery numeric": 9, - "Temperature": 51.1, - "Humidity": 15, - "Humidity status": "normal", - "Humidity status numeric": 2, - "Rssi numeric": 6, - }.items() - ) state = hass.states.get("sensor.bath_humidity") assert state assert state.state == "15" - assert ( - state.attributes.items() - >= { - "friendly_name": "Bath Humidity", - "unit_of_measurement": UNIT_PERCENTAGE, - "Battery numeric": 9, - "Temperature": 51.1, - "Humidity": 15, - "Humidity status": "normal", - "Humidity status numeric": 2, - "Rssi numeric": 6, - }.items() - ) assert len(hass.states.async_all()) == 3