1 """Support for Broadlink remotes."""
4 from base64
import b64encode
5 from collections
import defaultdict
6 from collections.abc
import Iterable
7 from datetime
import timedelta
8 from itertools
import product
10 from typing
import Any
12 from broadlink.exceptions
import (
19 import voluptuous
as vol
30 SERVICE_DELETE_COMMAND,
31 SERVICE_LEARN_COMMAND,
45 from .const
import DOMAIN
46 from .entity
import BroadlinkEntity
47 from .helpers
import data_packet
49 _LOGGER = logging.getLogger(__name__)
53 COMMAND_TYPE_IR =
"ir"
54 COMMAND_TYPE_RF =
"rf"
55 COMMAND_TYPES = [COMMAND_TYPE_IR, COMMAND_TYPE_RF]
57 CODE_STORAGE_VERSION = 1
58 FLAG_STORAGE_VERSION = 1
63 COMMAND_SCHEMA = vol.Schema(
65 vol.Required(ATTR_COMMAND): vol.All(
66 cv.ensure_list, [vol.All(cv.string, vol.Length(min=1))], vol.Length(min=1)
69 extra=vol.ALLOW_EXTRA,
72 SERVICE_SEND_SCHEMA = COMMAND_SCHEMA.extend(
74 vol.Optional(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1)),
75 vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float),
79 SERVICE_LEARN_SCHEMA = COMMAND_SCHEMA.extend(
81 vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1)),
82 vol.Optional(ATTR_COMMAND_TYPE, default=COMMAND_TYPE_IR): vol.In(COMMAND_TYPES),
83 vol.Optional(ATTR_ALTERNATIVE, default=
False): cv.boolean,
87 SERVICE_DELETE_SCHEMA = COMMAND_SCHEMA.extend(
88 {vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1))}
94 config_entry: ConfigEntry,
95 async_add_entities: AddEntitiesCallback,
97 """Set up a Broadlink remote."""
98 device = hass.data[DOMAIN].devices[config_entry.entry_id]
101 Store(hass, CODE_STORAGE_VERSION, f
"broadlink_remote_{device.unique_id}_codes"),
102 Store(hass, FLAG_STORAGE_VERSION, f
"broadlink_remote_{device.unique_id}_flags"),
108 """Representation of a Broadlink remote."""
110 _attr_has_entity_name =
True
114 """Initialize the remote."""
125 RemoteEntityFeature.LEARN_COMMAND | RemoteEntityFeature.DELETE_COMMAND
130 """Extract a list of codes.
132 If the command starts with `b64:`, extract the code from it.
133 Otherwise, extract the code from storage, using the command and
136 The codes are returned in sublists. For toggle commands, the
137 sublist contains two codes that must be sent alternately with
142 if cmd.startswith(
"b64:"):
147 raise ValueError(
"You need to specify a device")
150 codes = self.
_codes_codes[device][cmd]
151 except KeyError
as err:
152 raise ValueError(f
"Command not found: {cmd!r}")
from err
154 if isinstance(codes, list):
159 for idx, code
in enumerate(codes):
162 except ValueError
as err:
163 raise ValueError(f
"Invalid code: {code!r}")
from err
165 code_list.append(codes)
170 """Return a dictionary of codes."""
175 """Return a dictionary of toggle flags.
177 A toggle flag indicates whether the remote should send an
183 """Call when the remote is added to hass."""
185 self.
_attr_is_on_attr_is_on = state
is None or state.state != STATE_OFF
189 """Turn on the remote."""
194 """Turn off the remote."""
199 """Load code and flag storage from disk."""
207 """Send a list of commands to a device."""
208 kwargs[ATTR_COMMAND] = command
210 commands = kwargs[ATTR_COMMAND]
211 subdevice = kwargs.get(ATTR_DEVICE)
212 repeat = kwargs[ATTR_NUM_REPEATS]
213 delay = kwargs[ATTR_DELAY_SECS]
214 service = f
"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}"
219 "%s canceled: %s entity is turned off", service, self.
entity_identity_id
227 code_list = self.
_extract_codes_extract_codes(commands, subdevice)
228 except ValueError
as err:
229 _LOGGER.error(
"Failed to call %s: %s", service, err)
232 rf_flags = {0xB2, 0xD7}
233 if not hasattr(device.api,
"sweep_frequency")
and any(
234 c[0]
in rf_flags
for codes
in code_list
for c
in codes
236 err_msg = f
"{self.entity_id} doesn't support sending RF commands"
237 _LOGGER.error(
"Failed to call %s: %s", service, err_msg)
238 raise ValueError(err_msg)
240 at_least_one_sent =
False
241 for _, codes
in product(range(repeat), code_list):
242 if at_least_one_sent:
243 await asyncio.sleep(delay)
246 code = codes[self.
_flags_flags[subdevice]]
251 await device.async_request(device.api.send_data, code)
252 except (BroadlinkException, OSError)
as err:
253 _LOGGER.error(
"Error during %s: %s", service, err)
257 self.
_flags_flags[subdevice] ^= 1
258 at_least_one_sent =
True
260 if at_least_one_sent:
264 """Learn a list of commands from a remote."""
266 commands = kwargs[ATTR_COMMAND]
267 command_type = kwargs[ATTR_COMMAND_TYPE]
268 subdevice = kwargs[ATTR_DEVICE]
269 toggle = kwargs[ATTR_ALTERNATIVE]
270 service = f
"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}"
275 "%s canceled: %s entity is turned off", service, self.
entity_identity_id
282 async
with self.
_lock_lock:
283 if command_type == COMMAND_TYPE_IR:
286 elif hasattr(device.api,
"sweep_frequency"):
290 err_msg = f
"{self.entity_id} doesn't support learning RF commands"
291 _LOGGER.error(
"Failed to call %s: %s", service, err_msg)
292 raise ValueError(err_msg)
296 for command
in commands:
302 except (AuthorizationError, NetworkTimeoutError, OSError)
as err:
303 _LOGGER.error(
"Failed to learn '%s': %s", command, err)
306 except BroadlinkException
as err:
307 _LOGGER.error(
"Failed to learn '%s': %s", command, err)
310 self.
_codes_codes.setdefault(subdevice, {}).
update({command: code})
317 """Learn an infrared command."""
321 await device.async_request(device.api.enter_learning)
323 except (BroadlinkException, OSError)
as err:
324 _LOGGER.debug(
"Failed to enter learning mode: %s", err)
327 persistent_notification.async_create(
329 f
"Press the '{command}' button.",
330 title=
"Learn command",
331 notification_id=
"learn_command",
335 start_time = dt_util.utcnow()
336 while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
337 await asyncio.sleep(1)
339 code = await device.async_request(device.api.check_data)
340 except (ReadError, StorageError):
342 return b64encode(code).decode(
"utf8")
345 "No infrared code received within "
346 f
"{LEARNING_TIMEOUT.total_seconds()} seconds"
350 persistent_notification.async_dismiss(
351 self.
hasshass, notification_id=
"learn_command"
355 """Learn a radiofrequency command."""
359 await device.async_request(device.api.sweep_frequency)
361 except (BroadlinkException, OSError)
as err:
362 _LOGGER.debug(
"Failed to sweep frequency: %s", err)
365 persistent_notification.async_create(
367 f
"Press and hold the '{command}' button.",
368 title=
"Sweep frequency",
369 notification_id=
"sweep_frequency",
373 start_time = dt_util.utcnow()
374 while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
375 await asyncio.sleep(1)
376 is_found, frequency = await device.async_request(
377 device.api.check_frequency
380 _LOGGER.debug(
"Radiofrequency detected: %s MHz", frequency)
383 await device.async_request(device.api.cancel_sweep_frequency)
385 "No radiofrequency found within "
386 f
"{LEARNING_TIMEOUT.total_seconds()} seconds"
390 persistent_notification.async_dismiss(
391 self.
hasshass, notification_id=
"sweep_frequency"
394 await asyncio.sleep(1)
397 await device.async_request(device.api.find_rf_packet)
399 except (BroadlinkException, OSError)
as err:
400 _LOGGER.debug(
"Failed to enter learning mode: %s", err)
403 persistent_notification.async_create(
405 f
"Press the '{command}' button again.",
406 title=
"Learn command",
407 notification_id=
"learn_command",
411 start_time = dt_util.utcnow()
412 while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
413 await asyncio.sleep(1)
415 code = await device.async_request(device.api.check_data)
416 except (ReadError, StorageError):
418 return b64encode(code).decode(
"utf8")
421 "No radiofrequency code received within "
422 f
"{LEARNING_TIMEOUT.total_seconds()} seconds"
426 persistent_notification.async_dismiss(
427 self.
hasshass, notification_id=
"learn_command"
431 """Delete a list of commands from a remote."""
433 commands = kwargs[ATTR_COMMAND]
434 subdevice = kwargs[ATTR_DEVICE]
435 service = f
"{RM_DOMAIN}.{SERVICE_DELETE_COMMAND}"
439 "%s canceled: %s entity is turned off",
449 codes = self.
_codes_codes[subdevice]
450 except KeyError
as err:
451 err_msg = f
"Device not found: {subdevice!r}"
452 _LOGGER.error(
"Failed to call %s. %s", service, err_msg)
453 raise ValueError(err_msg)
from err
456 for command
in commands:
460 cmds_not_found.append(command)
463 if len(cmds_not_found) == 1:
464 err_msg = f
"Command not found: {cmds_not_found[0]!r}"
466 err_msg = f
"Commands not found: {cmds_not_found!r}"
468 if len(cmds_not_found) == len(commands):
469 _LOGGER.error(
"Failed to call %s. %s", service, err_msg)
470 raise ValueError(err_msg)
472 _LOGGER.error(
"Error during %s. %s", service, err_msg)
476 del self.
_codes_codes[subdevice]
477 if self.
_flags_flags.pop(subdevice,
None)
is not None:
None async_send_command(self, Iterable[str] command, **Any kwargs)
None async_turn_on(self, **Any kwargs)
def _async_learn_ir_command(self, command)
None async_learn_command(self, **Any kwargs)
None async_added_to_hass(self)
def _extract_codes(self, commands, device=None)
None async_turn_off(self, **Any kwargs)
def _async_load_storage(self)
None async_delete_command(self, **Any kwargs)
def _async_learn_rf_command(self, command)
def __init__(self, device, codes, flags)
None learn_command(self, **Any kwargs)
None async_write_ha_state(self)
State|None async_get_last_state(self)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
IssData update(pyiss.ISS iss)
None async_load(HomeAssistant hass)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
None async_save(self, _T data)