diff --git a/.coveragerc b/.coveragerc index 3dc57050bdf..eedb567bf46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,7 @@ omit = homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py homeassistant/components/broadlink/const.py + homeassistant/components/broadlink/device.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index be6aa266491..040b22945fd 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -5,12 +5,11 @@ from binascii import unhexlify from datetime import timedelta import logging import re -import socket +from broadlink.exceptions import BroadlinkException, ReadError import voluptuous as vol from homeassistant.const import CONF_HOST -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -65,36 +64,35 @@ SERVICE_SEND_SCHEMA = vol.Schema( SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) -@callback -def async_setup_service(hass, host, device): +async def async_setup_service(hass, host, device): """Register a device for given host for use in services.""" hass.data.setdefault(DOMAIN, {})[host] = device if hass.services.has_service(DOMAIN, SERVICE_LEARN): return - async def _learn_command(call): + async def async_learn_command(call): """Learn a packet from remote.""" device = hass.data[DOMAIN][call.data[CONF_HOST]] - for retry in range(DEFAULT_RETRY): - try: - await hass.async_add_executor_job(device.enter_learning) - break - except (socket.timeout, ValueError): - try: - await hass.async_add_executor_job(device.auth) - except socket.timeout: - if retry == DEFAULT_RETRY - 1: - _LOGGER.error("Failed to enter learning mode") - return + try: + await device.async_request(device.api.enter_learning) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to enter learning mode: %s", err_msg) + return _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): - packet = await hass.async_add_executor_job(device.check_data) - if packet: + try: + packet = await device.async_request(device.api.check_data) + except ReadError: + await asyncio.sleep(1) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to learn: %s", err_msg) + return + else: data = b64encode(packet).decode("utf8") log_msg = f"Received packet is: {data}" _LOGGER.info(log_msg) @@ -102,32 +100,26 @@ def async_setup_service(hass, host, device): log_msg, title="Broadlink switch" ) return - await asyncio.sleep(1) - _LOGGER.error("No signal was received") + _LOGGER.error("Failed to learn: No signal received") hass.components.persistent_notification.async_create( "No signal was received", title="Broadlink switch" ) hass.services.async_register( - DOMAIN, SERVICE_LEARN, _learn_command, schema=SERVICE_LEARN_SCHEMA + DOMAIN, SERVICE_LEARN, async_learn_command, schema=SERVICE_LEARN_SCHEMA ) - async def _send_packet(call): + async def async_send_packet(call): """Send a packet.""" device = hass.data[DOMAIN][call.data[CONF_HOST]] packets = call.data[CONF_PACKET] for packet in packets: - for retry in range(DEFAULT_RETRY): - try: - await hass.async_add_executor_job(device.send_data, packet) - break - except (socket.timeout, ValueError): - try: - await hass.async_add_executor_job(device.auth) - except socket.timeout: - if retry == DEFAULT_RETRY - 1: - _LOGGER.error("Failed to send packet to device") + try: + await device.async_request(device.api.send_data, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return hass.services.async_register( - DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA + DOMAIN, SERVICE_SEND, async_send_packet, schema=SERVICE_SEND_SCHEMA ) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py new file mode 100644 index 00000000000..a036557e58a --- /dev/null +++ b/homeassistant/components/broadlink/device.py @@ -0,0 +1,57 @@ +"""Support for Broadlink devices.""" +from functools import partial +import logging + +from broadlink.exceptions import ( + AuthorizationError, + BroadlinkException, + ConnectionClosedError, + DeviceOfflineError, +) + +from .const import DEFAULT_RETRY + +_LOGGER = logging.getLogger(__name__) + + +class BroadlinkDevice: + """Manages a Broadlink device.""" + + def __init__(self, hass, api): + """Initialize the device.""" + self.hass = hass + self.api = api + self.available = None + + async def async_connect(self): + """Connect to the device.""" + try: + await self.hass.async_add_executor_job(self.api.auth) + except BroadlinkException as err_msg: + if self.available: + self.available = False + _LOGGER.warning( + "Disconnected from device at %s: %s", self.api.host[0], err_msg + ) + return False + else: + if not self.available: + if self.available is not None: + _LOGGER.warning("Connected to device at %s", self.api.host[0]) + self.available = True + return True + + async def async_request(self, function, *args, **kwargs): + """Send a request to the device.""" + partial_function = partial(function, *args, **kwargs) + for attempt in range(DEFAULT_RETRY): + try: + result = await self.hass.async_add_executor_job(partial_function) + except (AuthorizationError, ConnectionClosedError, DeviceOfflineError): + if attempt == DEFAULT_RETRY - 1 or not await self.async_connect(): + raise + else: + if not self.available: + self.available = True + _LOGGER.warning("Connected to device at %s", self.api.host[0]) + return result diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index f894fe46a53..76443ae7467 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,6 +2,6 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.13.2"], + "requirements": ["broadlink==0.14.0"], "codeowners": ["@danielhiversen", "@felipediel"] } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 364fa39cdb1..b03bf7a4a04 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -9,6 +9,12 @@ from itertools import product import logging import broadlink as blk +from broadlink.exceptions import ( + AuthorizationError, + BroadlinkException, + DeviceOfflineError, + ReadError, +) import voluptuous as vol from homeassistant.components.remote import ( @@ -36,11 +42,11 @@ from .const import ( DEFAULT_LEARNING_TIMEOUT, DEFAULT_NAME, DEFAULT_PORT, - DEFAULT_RETRY, DEFAULT_TIMEOUT, RM4_TYPES, RM_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -103,17 +109,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= else: api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) api.timeout = timeout + device = BroadlinkDevice(hass, api) + code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes") flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags") - remote = BroadlinkRemote(name, unique_id, api, code_storage, flag_storage) - connected, loaded = (False, False) - try: - connected, loaded = await asyncio.gather( - hass.async_add_executor_job(api.auth), remote.async_load_storage_files() - ) - except OSError: - pass + remote = BroadlinkRemote(name, unique_id, device, code_storage, flag_storage) + + connected, loaded = await asyncio.gather( + device.async_connect(), remote.async_load_storage_files() + ) if not connected: hass.data[DOMAIN][COMPONENT].remove(unique_id) raise PlatformNotReady @@ -127,11 +132,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BroadlinkRemote(RemoteEntity): """Representation of a Broadlink remote.""" - def __init__(self, name, unique_id, api, code_storage, flag_storage): + def __init__(self, name, unique_id, device, code_storage, flag_storage): """Initialize the remote.""" + self.device = device self._name = name self._unique_id = unique_id - self._api = api self._code_storage = code_storage self._flag_storage = flag_storage self._codes = {} @@ -157,7 +162,7 @@ class BroadlinkRemote(RemoteEntity): @property def available(self): """Return True if the remote is available.""" - return self._available + return self.device.available @property def supported_features(self): @@ -182,9 +187,9 @@ class BroadlinkRemote(RemoteEntity): self._state = False async def async_update(self): - """Update the availability of the remote.""" + """Update the availability of the device.""" if not self.available: - await self._async_connect() + await self.device.async_connect() async def async_load_storage_files(self): """Load codes and toggle flags from storage files.""" @@ -213,8 +218,10 @@ class BroadlinkRemote(RemoteEntity): should_delay = await self._async_send_code( cmd, device, delay if should_delay else 0 ) - except ConnectionError: + except (AuthorizationError, DeviceOfflineError): break + except BroadlinkException: + pass self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) @@ -227,7 +234,7 @@ class BroadlinkRemote(RemoteEntity): try: code = self._codes[device][command] except KeyError: - _LOGGER.error("Failed to send '%s/%s': command not found", command, device) + _LOGGER.error("Failed to send '%s/%s': Command not found", command, device) return False if isinstance(code, list): @@ -238,12 +245,14 @@ class BroadlinkRemote(RemoteEntity): await asyncio.sleep(delay) try: - await self._async_attempt(self._api.send_data, data_packet(code)) + await self.device.async_request( + self.device.api.send_data, data_packet(code) + ) except ValueError: - _LOGGER.error("Failed to send '%s/%s': invalid code", command, device) + _LOGGER.error("Failed to send '%s/%s': Invalid code", command, device) return False - except ConnectionError: - _LOGGER.error("Failed to send '%s/%s': remote is offline", command, device) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send '%s/%s': %s", command, device, err_msg) raise if should_alternate: @@ -268,8 +277,10 @@ class BroadlinkRemote(RemoteEntity): should_store |= await self._async_learn_code( command, device, toggle, timeout ) - except ConnectionError: + except (AuthorizationError, DeviceOfflineError): break + except BroadlinkException: + pass if should_store: await self._code_storage.async_save(self._codes) @@ -287,22 +298,19 @@ class BroadlinkRemote(RemoteEntity): await self._async_capture_code(command, timeout), await self._async_capture_code(command, timeout), ] - except (ValueError, TimeoutError): - _LOGGER.error( - "Failed to learn '%s/%s': no signal received", command, device - ) + except TimeoutError: + _LOGGER.error("Failed to learn '%s/%s': No code received", command, device) return False - except ConnectionError: - _LOGGER.error("Failed to learn '%s/%s': remote is offline", command, device) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to learn '%s/%s': %s", command, device, err_msg) raise self._codes.setdefault(device, {}).update({command: code}) - return True async def _async_capture_code(self, command, timeout): """Enter learning mode and capture a code from a remote.""" - await self._async_attempt(self._api.enter_learning) + await self.device.async_request(self.device.api.enter_learning) self.hass.components.persistent_notification.async_create( f"Press the '{command}' button.", @@ -313,44 +321,18 @@ class BroadlinkRemote(RemoteEntity): code = None start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): - code = await self.hass.async_add_executor_job(self._api.check_data) - if code: + try: + code = await self.device.async_request(self.device.api.check_data) + except ReadError: + await asyncio.sleep(1) + else: break - await asyncio.sleep(1) self.hass.components.persistent_notification.async_dismiss( notification_id="learn_command" ) - if not code: + if code is None: raise TimeoutError - if all(not value for value in code): - raise ValueError return b64encode(code).decode("utf8") - - async def _async_attempt(self, function, *args): - """Retry a socket-related function until it succeeds.""" - for retry in range(DEFAULT_RETRY): - if retry and not await self._async_connect(): - continue - try: - await self.hass.async_add_executor_job(function, *args) - except OSError: - continue - return - raise ConnectionError - - async def _async_connect(self): - """Connect to the remote.""" - try: - auth = await self.hass.async_add_executor_job(self._api.auth) - except OSError: - auth = False - if auth and not self._available: - _LOGGER.warning("Connected to the remote") - self._available = True - elif not auth and self._available: - _LOGGER.warning("Disconnected from the remote") - self._available = False - return auth diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 13b1530eb6d..da2bc534859 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -4,6 +4,7 @@ from ipaddress import ip_address import logging import broadlink as blk +from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -18,6 +19,7 @@ from homeassistant.const import ( TEMP_CELSIUS, UNIT_PERCENTAGE, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -27,10 +29,12 @@ from .const import ( A1_TYPES, DEFAULT_NAME, DEFAULT_PORT, + DEFAULT_RETRY, DEFAULT_TIMEOUT, RM4_TYPES, RM_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -60,7 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Broadlink device sensors.""" host = config[CONF_HOST] mac_addr = config[CONF_MAC] @@ -77,11 +81,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): check_sensors = api.check_sensors_raw api.timeout = timeout - broadlink_data = BroadlinkData(api, check_sensors, update_interval) - dev = [] - for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append(BroadlinkSensor(name, broadlink_data, variable)) - add_entities(dev, True) + device = BroadlinkDevice(hass, api) + + connected = await device.async_connect() + if not connected: + raise PlatformNotReady + + broadlink_data = BroadlinkData(device, check_sensors, update_interval) + sensors = [ + BroadlinkSensor(name, broadlink_data, variable) + for variable in config[CONF_MONITORED_CONDITIONS] + ] + async_add_entities(sensors, True) class BroadlinkSensor(Entity): @@ -91,7 +102,6 @@ class BroadlinkSensor(Entity): """Initialize the sensor.""" self._name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self._state = None - self._is_available = False self._type = sensor_type self._broadlink_data = broadlink_data self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -109,32 +119,27 @@ class BroadlinkSensor(Entity): @property def available(self): """Return True if entity is available.""" - return self._is_available + return self._broadlink_data.device.available @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement - def update(self): + async def async_update(self): """Get the latest data from the sensor.""" - self._broadlink_data.update() - if self._broadlink_data.data is None: - self._state = None - self._is_available = False - return - self._state = self._broadlink_data.data[self._type] - self._is_available = True + await self._broadlink_data.async_update() + self._state = self._broadlink_data.data.get(self._type) class BroadlinkData: """Representation of a Broadlink data object.""" - def __init__(self, api, check_sensors, interval): + def __init__(self, device, check_sensors, interval): """Initialize the data object.""" - self.api = api + self.device = device self.check_sensors = check_sensors - self.data = None + self.data = {} self._schema = vol.Schema( { vol.Optional("temperature"): vol.Range(min=-50, max=150), @@ -144,31 +149,21 @@ class BroadlinkData: vol.Optional("noise"): vol.Any(0, 1, 2), } ) - self.update = Throttle(interval)(self._update) - if not self._auth(): - _LOGGER.warning("Failed to connect to device") + self.async_update = Throttle(interval)(self._async_fetch_data) - def _update(self, retry=3): - try: - data = self.check_sensors() - if data is not None: - self.data = self._schema(data) + async def _async_fetch_data(self): + """Fetch sensor data.""" + for _ in range(DEFAULT_RETRY): + try: + data = await self.device.async_request(self.check_sensors) + except BroadlinkException: return - except OSError as error: - if retry < 1: - self.data = None - _LOGGER.error(error) + try: + data = self._schema(data) + except (vol.Invalid, vol.MultipleInvalid): + continue + else: + self.data = data return - except (vol.Invalid, vol.MultipleInvalid): - pass # Continue quietly if device returned malformed data - if retry > 0 and self._auth(): - self._update(retry - 1) - def _auth(self, retry=3): - try: - auth = self.api.auth() - except OSError: - auth = False - if not auth and retry > 0: - return self._auth(retry - 1) - return auth + _LOGGER.debug("Failed to update sensors: Device returned malformed data") diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 4173fa4adc6..b62da4ebb3e 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -2,9 +2,9 @@ from datetime import timedelta from ipaddress import ip_address import logging -import socket import broadlink as blk +from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_TYPE, STATE_ON, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import Throttle, slugify @@ -27,7 +28,6 @@ from . import async_setup_service, data_packet, hostname, mac_address from .const import ( DEFAULT_NAME, DEFAULT_PORT, - DEFAULT_RETRY, DEFAULT_TIMEOUT, MP1_TYPES, RM4_TYPES, @@ -35,6 +35,7 @@ from .const import ( SP1_TYPES, SP2_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -73,21 +74,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Broadlink switches.""" - devices = config.get(CONF_SWITCHES) - slots = config.get("slots", {}) - host = config.get(CONF_HOST) - mac_addr = config.get(CONF_MAC) - friendly_name = config.get(CONF_FRIENDLY_NAME) + host = config[CONF_HOST] + mac_addr = config[CONF_MAC] + friendly_name = config[CONF_FRIENDLY_NAME] model = config[CONF_TYPE] - retry_times = config.get(CONF_RETRY) + timeout = config[CONF_TIMEOUT] + slots = config[CONF_SLOTS] + devices = config[CONF_SWITCHES] def generate_rm_switches(switches, broadlink_device): """Generate RM switches.""" @@ -98,7 +98,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadlink_device, config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_OFF), - retry_times, ) for object_id, config in switches.items() ] @@ -110,58 +109,54 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return slots[f"slot_{slot}"] if model in RM_TYPES: - broadlink_device = blk.rm((host, DEFAULT_PORT), mac_addr, None) - hass.add_job(async_setup_service, hass, host, broadlink_device) + api = blk.rm((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in RM4_TYPES: - broadlink_device = blk.rm4((host, DEFAULT_PORT), mac_addr, None) - hass.add_job(async_setup_service, hass, host, broadlink_device) + api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in SP1_TYPES: - broadlink_device = blk.sp1((host, DEFAULT_PORT), mac_addr, None) - switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] + api = blk.sp1((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] elif model in SP2_TYPES: - broadlink_device = blk.sp2((host, DEFAULT_PORT), mac_addr, None) - switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] + api = blk.sp2((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] elif model in MP1_TYPES: - switches = [] - broadlink_device = blk.mp1((host, DEFAULT_PORT), mac_addr, None) - parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) - for i in range(1, 5): - slot = BroadlinkMP1Slot( - get_mp1_slot_name(friendly_name, i), - broadlink_device, - i, - parent_device, - retry_times, + api = blk.mp1((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + parent_device = BroadlinkMP1Switch(broadlink_device) + switches = [ + BroadlinkMP1Slot( + get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device, ) - switches.append(slot) + for i in range(1, 5) + ] - broadlink_device.timeout = config.get(CONF_TIMEOUT) - try: - broadlink_device.auth() - except OSError: - _LOGGER.error("Failed to connect to device") + api.timeout = timeout + connected = await broadlink_device.async_connect() + if not connected: + raise PlatformNotReady - add_entities(switches) + if model in RM_TYPES or model in RM4_TYPES: + hass.async_create_task(async_setup_service(hass, host, broadlink_device)) + + async_add_entities(switches) class BroadlinkRMSwitch(SwitchEntity, RestoreEntity): """Representation of an Broadlink switch.""" - def __init__( - self, name, friendly_name, device, command_on, command_off, retry_times - ): + def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" + self.device = device self.entity_id = f"{DOMAIN}.{slugify(name)}" self._name = friendly_name self._state = False self._command_on = command_on self._command_off = command_off - self._device = device - self._is_available = False - self._retry_times = retry_times - _LOGGER.debug("_retry_times : %s", self._retry_times) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -183,7 +178,7 @@ class BroadlinkRMSwitch(SwitchEntity, RestoreEntity): @property def available(self): """Return True if entity is available.""" - return not self.should_poll or self._is_available + return not self.should_poll or self.device.available @property def should_poll(self): @@ -195,68 +190,53 @@ class BroadlinkRMSwitch(SwitchEntity, RestoreEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + async def async_update(self): + """Update the state of the device.""" + if not self.available: + await self.device.async_connect() + + async def async_turn_on(self, **kwargs): """Turn the device on.""" - if self._sendpacket(self._command_on, self._retry_times): + if await self._async_send_packet(self._command_on): self._state = True - self.schedule_update_ha_state() + self.async_write_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - if self._sendpacket(self._command_off, self._retry_times): + if await self._async_send_packet(self._command_off): self._state = False - self.schedule_update_ha_state() + self.async_write_ha_state() - def _sendpacket(self, packet, retry): + async def _async_send_packet(self, packet): """Send packet to device.""" if packet is None: _LOGGER.debug("Empty packet") return True try: - self._device.send_data(packet) - except (ValueError, OSError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, retry - 1) + await self.device.async_request(self.device.api.send_data, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False return True - def _auth(self, retry): - _LOGGER.debug("_auth : retry=%s", retry) - try: - auth = self._device.auth() - except OSError: - auth = False - if retry < 1: - _LOGGER.error("Timeout during authorization") - if not auth and retry > 0: - return self._auth(retry - 1) - return auth - class BroadlinkSP1Switch(BroadlinkRMSwitch): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device, retry_times): + def __init__(self, friendly_name, device): """Initialize the switch.""" - super().__init__(friendly_name, friendly_name, device, None, None, retry_times) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._load_power = None - def _sendpacket(self, packet, retry): + async def _async_send_packet(self, packet): """Send packet to device.""" try: - self._device.set_power(packet) - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, retry - 1) + await self.device.async_request(self.device.api.set_power, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False return True @@ -281,37 +261,24 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): except (ValueError, TypeError): return None - def update(self): - """Synchronize state with switch.""" - self._update(self._retry_times) - - def _update(self, retry): + async def async_update(self): """Update the state of the device.""" - _LOGGER.debug("_update : retry=%s", retry) try: - state = self._device.check_power() - load_power = self._device.get_energy() - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during updating the state: %s", error) - self._is_available = False - return - if not self._auth(self._retry_times): - return - return self._update(retry - 1) - if state is None and retry > 0: - return self._update(retry - 1) + state = await self.device.async_request(self.device.api.check_power) + load_power = await self.device.async_request(self.device.api.get_energy) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to update state: %s", err_msg) + return self._state = state self._load_power = load_power - self._is_available = True class BroadlinkMP1Slot(BroadlinkRMSwitch): """Representation of a slot of Broadlink switch.""" - def __init__(self, friendly_name, device, slot, parent_device, retry_times): + def __init__(self, friendly_name, device, slot, parent_device): """Initialize the slot of switch.""" - super().__init__(friendly_name, friendly_name, device, None, None, retry_times) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._slot = slot @@ -322,44 +289,35 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Return true if unable to access real state of entity.""" return False - def _sendpacket(self, packet, retry): - """Send packet to device.""" - try: - self._device.set_power(self._slot, packet) - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - self._is_available = False - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, max(0, retry - 1)) - self._is_available = True - return True - @property def should_poll(self): """Return the polling state.""" return True - def update(self): - """Trigger update for all switches on the parent device.""" - self._parent_device.update() + async def async_update(self): + """Update the state of the device.""" + await self._parent_device.async_update() self._state = self._parent_device.get_outlet_status(self._slot) - if self._state is None: - self._is_available = False - else: - self._is_available = True + + async def _async_send_packet(self, packet): + """Send packet to device.""" + try: + await self.device.async_request( + self.device.api.set_power, self._slot, packet + ) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False + return True class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" - def __init__(self, device, retry_times): + def __init__(self, device): """Initialize the switch.""" - self._device = device + self.device = device self._states = None - self._retry_times = retry_times def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" @@ -368,31 +326,10 @@ class BroadlinkMP1Switch: return self._states[f"s{slot}"] @Throttle(TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for this device.""" - self._update(self._retry_times) - - def _update(self, retry): + async def async_update(self): """Update the state of the device.""" try: - states = self._device.check_power() - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during updating the state: %s", error) - return - if not self._auth(self._retry_times): - return - return self._update(max(0, retry - 1)) - if states is None and retry > 0: - return self._update(max(0, retry - 1)) + states = await self.device.async_request(self.device.api.check_power) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to update state: %s", err_msg) self._states = states - - def _auth(self, retry): - """Authenticate the device.""" - try: - auth = self._device.auth() - except OSError: - auth = False - if not auth and retry > 0: - return self._auth(retry - 1) - return auth diff --git a/requirements_all.txt b/requirements_all.txt index a1f6f8bfabc..1888d314c6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ boto3==1.9.252 bravia-tv==1.0.4 # homeassistant.components.broadlink -broadlink==0.13.2 +broadlink==0.14.0 # homeassistant.components.brother brother==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b25be57541..f601b5fff62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ bomradarloop==0.1.4 bravia-tv==1.0.4 # homeassistant.components.broadlink -broadlink==0.13.2 +broadlink==0.14.0 # homeassistant.components.brother brother==0.1.14 diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index c5477dff49f..5a359896bfa 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND +from homeassistant.components.broadlink.device import BroadlinkDevice from homeassistant.util.dt import utcnow from tests.async_mock import MagicMock, call, patch @@ -34,39 +35,37 @@ async def test_padding(hass): async def test_send(hass): """Test send service.""" - mock_device = MagicMock() - mock_device.send_data.return_value = None - - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() + mock_api = MagicMock() + mock_api.send_data.return_value = None + device = BroadlinkDevice(hass, mock_api) + await async_setup_service(hass, DUMMY_HOST, device) await hass.services.async_call( DOMAIN, SERVICE_SEND, {"host": DUMMY_HOST, "packet": (DUMMY_IR_PACKET)} ) await hass.async_block_till_done() - assert mock_device.send_data.call_count == 1 - assert mock_device.send_data.call_args == call(b64decode(DUMMY_IR_PACKET)) + assert device.api.send_data.call_count == 1 + assert device.api.send_data.call_args == call(b64decode(DUMMY_IR_PACKET)) async def test_learn(hass): """Test learn service.""" - mock_device = MagicMock() - mock_device.enter_learning.return_value = None - mock_device.check_data.return_value = b64decode(DUMMY_IR_PACKET) + mock_api = MagicMock() + mock_api.enter_learning.return_value = None + mock_api.check_data.return_value = b64decode(DUMMY_IR_PACKET) + device = BroadlinkDevice(hass, mock_api) with patch.object( hass.components.persistent_notification, "async_create" ) as mock_create: - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() - + await async_setup_service(hass, DUMMY_HOST, device) await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST}) await hass.async_block_till_done() - assert mock_device.enter_learning.call_count == 1 - assert mock_device.enter_learning.call_args == call() + assert device.api.enter_learning.call_count == 1 + assert device.api.enter_learning.call_args == call() assert mock_create.call_count == 1 assert mock_create.call_args == call( @@ -76,12 +75,12 @@ async def test_learn(hass): async def test_learn_timeout(hass): """Test learn service.""" - mock_device = MagicMock() - mock_device.enter_learning.return_value = None - mock_device.check_data.return_value = None + mock_api = MagicMock() + mock_api.enter_learning.return_value = None + mock_api.check_data.return_value = None + device = BroadlinkDevice(hass, mock_api) - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() + await async_setup_service(hass, DUMMY_HOST, device) now = utcnow() @@ -94,8 +93,8 @@ async def test_learn_timeout(hass): await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST}) await hass.async_block_till_done() - assert mock_device.enter_learning.call_count == 1 - assert mock_device.enter_learning.call_args == call() + assert device.api.enter_learning.call_count == 1 + assert device.api.enter_learning.call_args == call() assert mock_create.call_count == 1 assert mock_create.call_args == call(