diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index a309e4eb603..3e21765dbed 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -39,11 +39,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" - supported_types = { - device_type - for device_types in DOMAINS_AND_TYPES - for device_type in device_types[1] - } + supported_types = set.union(*DOMAINS_AND_TYPES.values()) if device.type not in supported_types: _LOGGER.error( "Unsupported device: %s. If it worked before, please open " diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index b10f7e74ba7..fd060d23b35 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -5,11 +5,26 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "broadlink" -DOMAINS_AND_TYPES = ( - (REMOTE_DOMAIN, ("RM2", "RM4")), - (SENSOR_DOMAIN, ("A1", "RM2", "RM4")), - (SWITCH_DOMAIN, ("BG1", "MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")), -) +DOMAINS_AND_TYPES = { + REMOTE_DOMAIN: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, + SENSOR_DOMAIN: {"A1", "RM4MINI", "RM4PRO", "RMPRO"}, + SWITCH_DOMAIN: { + "BG1", + "MP1", + "RM4MINI", + "RM4PRO", + "RMMINI", + "RMMINIB", + "RMPRO", + "SP1", + "SP2", + "SP2S", + "SP3", + "SP3S", + "SP4", + "SP4B", + }, +} DEFAULT_PORT = 80 DEFAULT_TIMEOUT = 5 diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index be9c7626ac1..c460040c12b 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -22,9 +22,9 @@ from .updater import get_update_manager _LOGGER = logging.getLogger(__name__) -def get_domains(device_type): +def get_domains(dev_type): """Return the domains available for a device type.""" - return {domain for domain, types in DOMAINS_AND_TYPES if device_type in types} + return {d for d, t in DOMAINS_AND_TYPES.items() if dev_type in t} class BroadlinkDevice: diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 0562bc306a5..8d3b16b4582 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.16.0"], + "requirements": ["broadlink==0.17.0"], "codeowners": ["@danielhiversen", "@felipediel"], "config_flow": true } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 116c97aeb31..30043f487b1 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -26,6 +26,8 @@ from homeassistant.components.remote import ( DOMAIN as RM_DOMAIN, PLATFORM_SCHEMA, SERVICE_DELETE_COMMAND, + SERVICE_LEARN_COMMAND, + SERVICE_SEND_COMMAND, SUPPORT_DELETE_COMMAND, SUPPORT_LEARN_COMMAND, RemoteEntity, @@ -129,6 +131,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): self._codes = {} self._flags = defaultdict(int) self._state = True + self._lock = asyncio.Lock() @property def name(self): @@ -171,39 +174,44 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): "sw_version": self._device.fw_version, } - def get_code(self, command, device): - """Return a code and a boolean indicating a toggle command. + def _extract_codes(self, commands, device=None): + """Extract a list of codes. If the command starts with `b64:`, extract the code from it. - Otherwise, extract the code from the dictionary, using the device - and command as keys. + Otherwise, extract the code from storage, using the command and + device as keys. - You need to change the flag whenever a toggle command is sent - successfully. Use `self._flags[device] ^= 1`. + The codes are returned in sublists. For toggle commands, the + sublist contains two codes that must be sent alternately with + each call. """ - if command.startswith("b64:"): - code, is_toggle_cmd = command[4:], False + code_list = [] + for cmd in commands: + if cmd.startswith("b64:"): + codes = [cmd[4:]] - else: - if device is None: - raise KeyError("You need to specify a device") - - try: - code = self._codes[device][command] - except KeyError as err: - raise KeyError("Command not found") from err - - # For toggle commands, alternate between codes in a list. - if isinstance(code, list): - code = code[self._flags[device]] - is_toggle_cmd = True else: - is_toggle_cmd = False + if device is None: + raise ValueError("You need to specify a device") - try: - return data_packet(code), is_toggle_cmd - except ValueError as err: - raise ValueError("Invalid code") from err + try: + codes = self._codes[device][cmd] + except KeyError as err: + raise ValueError(f"Command not found: {repr(cmd)}") from err + + if isinstance(codes, list): + codes = codes[:] + else: + codes = [codes] + + for idx, code in enumerate(codes): + try: + codes[idx] = data_packet(code) + except ValueError as err: + raise ValueError(f"Invalid code: {repr(code)}") from err + + code_list.append(codes) + return code_list @callback def get_codes(self): @@ -261,44 +269,50 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): device = kwargs.get(ATTR_DEVICE) repeat = kwargs[ATTR_NUM_REPEATS] delay = kwargs[ATTR_DELAY_SECS] + service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}" if not self._state: _LOGGER.warning( - "remote.send_command canceled: %s entity is turned off", self.entity_id + "%s canceled: %s entity is turned off", service, self.entity_id ) return - should_delay = False + try: + code_list = self._extract_codes(commands, device) + except ValueError as err: + _LOGGER.error("Failed to call %s: %s", service, err) + raise - for _, cmd in product(range(repeat), commands): - if should_delay: + rf_flags = {0xB2, 0xD7} + if not hasattr(self._device.api, "sweep_frequency") and any( + c[0] in rf_flags for codes in code_list for c in codes + ): + err_msg = f"{self.entity_id} doesn't support sending RF commands" + _LOGGER.error("Failed to call %s: %s", service, err_msg) + raise ValueError(err_msg) + + at_least_one_sent = False + for _, codes in product(range(repeat), code_list): + if at_least_one_sent: await asyncio.sleep(delay) - try: - code, is_toggle_cmd = self.get_code(cmd, device) - - except (KeyError, ValueError) as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) - should_delay = False - continue + if len(codes) > 1: + code = codes[self._flags[device]] + else: + code = codes[0] try: await self._device.async_request(self._device.api.send_data, code) - - except (AuthorizationError, NetworkTimeoutError, OSError) as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) + except (BroadlinkException, OSError) as err: + _LOGGER.error("Error during %s: %s", service, err) break - except BroadlinkException as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) - should_delay = False - continue - - should_delay = True - if is_toggle_cmd: + if len(codes) > 1: self._flags[device] ^= 1 + at_least_one_sent = True - self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) + if at_least_one_sent: + self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) async def async_learn_command(self, **kwargs): """Learn a list of commands from a remote.""" @@ -307,39 +321,47 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): command_type = kwargs[ATTR_COMMAND_TYPE] device = kwargs[ATTR_DEVICE] toggle = kwargs[ATTR_ALTERNATIVE] + service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}" if not self._state: _LOGGER.warning( - "remote.learn_command canceled: %s entity is turned off", self.entity_id + "%s canceled: %s entity is turned off", service, self.entity_id ) return - if command_type == COMMAND_TYPE_IR: - learn_command = self._async_learn_ir_command - else: - learn_command = self._async_learn_rf_command + async with self._lock: + if command_type == COMMAND_TYPE_IR: + learn_command = self._async_learn_ir_command - should_store = False + elif hasattr(self._device.api, "sweep_frequency"): + learn_command = self._async_learn_rf_command - for command in commands: - try: - code = await learn_command(command) - if toggle: - code = [code, await learn_command(command)] + else: + err_msg = f"{self.entity_id} doesn't support learning RF commands" + _LOGGER.error("Failed to call %s: %s", service, err_msg) + raise ValueError(err_msg) - except (AuthorizationError, NetworkTimeoutError, OSError) as err: - _LOGGER.error("Failed to learn '%s': %s", command, err) - break + should_store = False - except BroadlinkException as err: - _LOGGER.error("Failed to learn '%s': %s", command, err) - continue + for command in commands: + try: + code = await learn_command(command) + if toggle: + code = [code, await learn_command(command)] - self._codes.setdefault(device, {}).update({command: code}) - should_store = True + except (AuthorizationError, NetworkTimeoutError, OSError) as err: + _LOGGER.error("Failed to learn '%s': %s", command, err) + break - if should_store: - await self._code_storage.async_save(self._codes) + except BroadlinkException as err: + _LOGGER.error("Failed to learn '%s': %s", command, err) + continue + + self._codes.setdefault(device, {}).update({command: code}) + should_store = True + + if should_store: + await self._code_storage.async_save(self._codes) async def _async_learn_ir_command(self, command): """Learn an infrared command.""" diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index b4cd43ac493..0a98530c806 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -109,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink switch.""" device = hass.data[DOMAIN].devices[config_entry.entry_id] - if device.api.type in {"RM2", "RM4"}: + if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {}) user_defined_switches = platform_data.get(device.api.mac, {}) switches = [ @@ -119,12 +119,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif device.api.type == "SP1": switches = [BroadlinkSP1Switch(device)] - elif device.api.type == "SP2": + elif device.api.type in {"SP2", "SP2S", "SP3", "SP3S", "SP4", "SP4B"}: switches = [BroadlinkSP2Switch(device)] - elif device.api.type in {"SP4", "SP4B"}: - switches = [BroadlinkSP4Switch(device)] - elif device.api.type == "BG1": switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)] @@ -143,7 +140,6 @@ class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): self._command_on = command_on self._command_off = command_off self._coordinator = device.update_manager.coordinator - self._device_class = None self._state = None @property @@ -174,7 +170,7 @@ class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): @property def device_class(self): """Return device class.""" - return self._device_class + return DEVICE_CLASS_SWITCH @property def device_info(self): @@ -254,7 +250,6 @@ class BroadlinkSP1Switch(BroadlinkSwitch): def __init__(self, device): """Initialize the switch.""" super().__init__(device, 1, 0) - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -277,10 +272,8 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): def __init__(self, device, *args, **kwargs): """Initialize the switch.""" super().__init__(device, *args, **kwargs) - self._state = self._coordinator.data["state"] - self._load_power = self._coordinator.data["load_power"] - if device.api.model == "SC1": - self._device_class = DEVICE_CLASS_SWITCH + self._state = self._coordinator.data["pwr"] + self._load_power = self._coordinator.data.get("power") @property def assumed_state(self): @@ -292,33 +285,12 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): """Return the current power usage in Watt.""" return self._load_power - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._state = self._coordinator.data["state"] - self._load_power = self._coordinator.data["load_power"] - self.async_write_ha_state() - - -class BroadlinkSP4Switch(BroadlinkSP1Switch): - """Representation of a Broadlink SP4 switch.""" - - def __init__(self, device, *args, **kwargs): - """Initialize the switch.""" - super().__init__(device, *args, **kwargs) - self._state = self._coordinator.data["pwr"] - - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return False - @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._state = self._coordinator.data["pwr"] + self._load_power = self._coordinator.data.get("power") self.async_write_ha_state() @@ -330,7 +302,6 @@ class BroadlinkMP1Slot(BroadlinkSwitch): super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"s{slot}"] - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -374,7 +345,6 @@ class BroadlinkBG1Slot(BroadlinkSwitch): super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"pwr{slot}"] - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -391,6 +361,11 @@ class BroadlinkBG1Slot(BroadlinkSwitch): """Return True if unable to access real state of the switch.""" return False + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_OUTLET + @callback def update_data(self): """Update data.""" diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index c9b273218b5..8401dba8c0d 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -1,17 +1,9 @@ """Support for fetching data from Broadlink devices.""" from abc import ABC, abstractmethod from datetime import timedelta -from functools import partial import logging -import broadlink as blk -from broadlink.exceptions import ( - AuthorizationError, - BroadlinkException, - CommandNotSupportedError, - NetworkTimeoutError, - StorageError, -) +from broadlink.exceptions import AuthorizationError, BroadlinkException from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -21,17 +13,20 @@ _LOGGER = logging.getLogger(__name__) def get_update_manager(device): """Return an update manager for a given Broadlink device.""" - if device.api.model.startswith("RM mini"): - return BroadlinkRMMini3UpdateManager(device) - update_managers = { "A1": BroadlinkA1UpdateManager, "BG1": BroadlinkBG1UpdateManager, "MP1": BroadlinkMP1UpdateManager, - "RM2": BroadlinkRMUpdateManager, - "RM4": BroadlinkRMUpdateManager, + "RM4MINI": BroadlinkRMUpdateManager, + "RM4PRO": BroadlinkRMUpdateManager, + "RMMINI": BroadlinkRMUpdateManager, + "RMMINIB": BroadlinkRMUpdateManager, + "RMPRO": BroadlinkRMUpdateManager, "SP1": BroadlinkSP1UpdateManager, "SP2": BroadlinkSP2UpdateManager, + "SP2S": BroadlinkSP2UpdateManager, + "SP3": BroadlinkSP2UpdateManager, + "SP3S": BroadlinkSP2UpdateManager, "SP4": BroadlinkSP4UpdateManager, "SP4B": BroadlinkSP4UpdateManager, } @@ -114,28 +109,18 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) -class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager): - """Manages updates for Broadlink RM mini 3 devices.""" - - async def async_fetch_data(self): - """Fetch data from the device.""" - hello = partial( - blk.discover, - discover_ip_address=self.device.api.host[0], - timeout=self.device.api.timeout, - ) - devices = await self.device.hass.async_add_executor_job(hello) - if not devices: - raise NetworkTimeoutError("The device is offline") - return {} - - class BroadlinkRMUpdateManager(BroadlinkUpdateManager): - """Manages updates for Broadlink RM2 and RM4 devices.""" + """Manages updates for Broadlink remotes.""" async def async_fetch_data(self): """Fetch data from the device.""" - return await self.device.async_request(self.device.api.check_sensors) + device = self.device + + if hasattr(device.api, "check_sensors"): + return await device.async_request(device.api.check_sensors) + + await device.async_request(device.api.update) + return {} class BroadlinkSP1UpdateManager(BroadlinkUpdateManager): @@ -151,14 +136,14 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager): async def async_fetch_data(self): """Fetch data from the device.""" + device = self.device + data = {} - data["state"] = await self.device.async_request(self.device.api.check_power) - try: - data["load_power"] = await self.device.async_request( - self.device.api.get_energy - ) - except (CommandNotSupportedError, StorageError): - data["load_power"] = None + data["pwr"] = await device.async_request(device.api.check_power) + + if hasattr(device.api, "get_energy"): + data["power"] = await device.async_request(device.api.get_energy) + return data diff --git a/requirements_all.txt b/requirements_all.txt index 140273de629..b0fae5c32ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -381,7 +381,7 @@ boto3==1.9.252 bravia-tv==1.0.8 # homeassistant.components.broadlink -broadlink==0.16.0 +broadlink==0.17.0 # homeassistant.components.brother brother==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2aaf83b113..96f6d6bb4ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ bond-api==0.1.11 bravia-tv==1.0.8 # homeassistant.components.broadlink -broadlink==0.16.0 +broadlink==0.17.0 # homeassistant.components.brother brother==0.2.1 diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 7185c605f5c..780887551f2 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -12,7 +12,7 @@ BROADLINK_DEVICES = { "34ea34befc25", "RM mini 3", "Broadlink", - "RM2", + "RMMINI", 0x2737, 57, 8, @@ -22,7 +22,7 @@ BROADLINK_DEVICES = { "34ea34b43b5a", "RM mini 3", "Broadlink", - "RM4", + "RMMINIB", 0x5F36, 44017, 10, @@ -32,7 +32,7 @@ BROADLINK_DEVICES = { "34ea34b43d22", "RM pro", "Broadlink", - "RM2", + "RMPRO", 0x2787, 20025, 7, @@ -42,7 +42,7 @@ BROADLINK_DEVICES = { "34ea34c43f31", "RM4 pro", "Broadlink", - "RM4", + "RM4PRO", 0x6026, 52, 4, @@ -62,7 +62,7 @@ BROADLINK_DEVICES = { "34ea34b61d2c", "LB1", "Broadlink", - "SmartBulb", + "LB1", 0x504E, 57, 5, @@ -96,9 +96,6 @@ class BroadlinkDevice: with patch( "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api, - ), patch( - "homeassistant.components.broadlink.updater.blk.discover", - return_value=[mock_api], ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done()