Fix connection problems in the Broadlink integration (#34670)

* Use helper functions for exception handling

* Create a separate class to handle communication with the device

* Update manifest

* Use coroutine for service setup

* Fix sensor update

* Update tests

* Fix MP1 switch

* Add device.py to .coveragerc

* Remove unnecessary blocking from test_learn_timeout

* Change access method for entries with default values

* Make the changes suggested by MartinHjelmare

* Remove dot from debug message

* Use underscore for unused variable
This commit is contained in:
Felipe Martins Diel 2020-05-13 05:36:32 -03:00 committed by GitHub
parent 2a120d9045
commit 6464c94990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 278 additions and 315 deletions

View File

@ -94,6 +94,7 @@ omit =
homeassistant/components/braviatv/const.py homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/media_player.py
homeassistant/components/broadlink/const.py homeassistant/components/broadlink/const.py
homeassistant/components/broadlink/device.py
homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/remote.py
homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/sensor.py
homeassistant/components/broadlink/switch.py homeassistant/components/broadlink/switch.py

View File

@ -5,12 +5,11 @@ from binascii import unhexlify
from datetime import timedelta from datetime import timedelta
import logging import logging
import re import re
import socket
from broadlink.exceptions import BroadlinkException, ReadError
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow 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}) SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
@callback async def async_setup_service(hass, host, device):
def async_setup_service(hass, host, device):
"""Register a device for given host for use in services.""" """Register a device for given host for use in services."""
hass.data.setdefault(DOMAIN, {})[host] = device hass.data.setdefault(DOMAIN, {})[host] = device
if hass.services.has_service(DOMAIN, SERVICE_LEARN): if hass.services.has_service(DOMAIN, SERVICE_LEARN):
return return
async def _learn_command(call): async def async_learn_command(call):
"""Learn a packet from remote.""" """Learn a packet from remote."""
device = hass.data[DOMAIN][call.data[CONF_HOST]] device = hass.data[DOMAIN][call.data[CONF_HOST]]
for retry in range(DEFAULT_RETRY): try:
try: await device.async_request(device.api.enter_learning)
await hass.async_add_executor_job(device.enter_learning) except BroadlinkException as err_msg:
break _LOGGER.error("Failed to enter learning mode: %s", err_msg)
except (socket.timeout, ValueError): return
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
_LOGGER.info("Press the key you want Home Assistant to learn") _LOGGER.info("Press the key you want Home Assistant to learn")
start_time = utcnow() start_time = utcnow()
while (utcnow() - start_time) < timedelta(seconds=20): while (utcnow() - start_time) < timedelta(seconds=20):
packet = await hass.async_add_executor_job(device.check_data) try:
if packet: 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") data = b64encode(packet).decode("utf8")
log_msg = f"Received packet is: {data}" log_msg = f"Received packet is: {data}"
_LOGGER.info(log_msg) _LOGGER.info(log_msg)
@ -102,32 +100,26 @@ def async_setup_service(hass, host, device):
log_msg, title="Broadlink switch" log_msg, title="Broadlink switch"
) )
return return
await asyncio.sleep(1) _LOGGER.error("Failed to learn: No signal received")
_LOGGER.error("No signal was received")
hass.components.persistent_notification.async_create( hass.components.persistent_notification.async_create(
"No signal was received", title="Broadlink switch" "No signal was received", title="Broadlink switch"
) )
hass.services.async_register( 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.""" """Send a packet."""
device = hass.data[DOMAIN][call.data[CONF_HOST]] device = hass.data[DOMAIN][call.data[CONF_HOST]]
packets = call.data[CONF_PACKET] packets = call.data[CONF_PACKET]
for packet in packets: for packet in packets:
for retry in range(DEFAULT_RETRY): try:
try: await device.async_request(device.api.send_data, packet)
await hass.async_add_executor_job(device.send_data, packet) except BroadlinkException as err_msg:
break _LOGGER.error("Failed to send packet: %s", err_msg)
except (socket.timeout, ValueError): return
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")
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA DOMAIN, SERVICE_SEND, async_send_packet, schema=SERVICE_SEND_SCHEMA
) )

View File

@ -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

View File

@ -2,6 +2,6 @@
"domain": "broadlink", "domain": "broadlink",
"name": "Broadlink", "name": "Broadlink",
"documentation": "https://www.home-assistant.io/integrations/broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink",
"requirements": ["broadlink==0.13.2"], "requirements": ["broadlink==0.14.0"],
"codeowners": ["@danielhiversen", "@felipediel"] "codeowners": ["@danielhiversen", "@felipediel"]
} }

View File

@ -9,6 +9,12 @@ from itertools import product
import logging import logging
import broadlink as blk import broadlink as blk
from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
DeviceOfflineError,
ReadError,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.components.remote import ( from homeassistant.components.remote import (
@ -36,11 +42,11 @@ from .const import (
DEFAULT_LEARNING_TIMEOUT, DEFAULT_LEARNING_TIMEOUT,
DEFAULT_NAME, DEFAULT_NAME,
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_RETRY,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
RM4_TYPES, RM4_TYPES,
RM_TYPES, RM_TYPES,
) )
from .device import BroadlinkDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -103,17 +109,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
else: else:
api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) api = blk.rm4((host, DEFAULT_PORT), mac_addr, None)
api.timeout = timeout api.timeout = timeout
device = BroadlinkDevice(hass, api)
code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes") code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes")
flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags") 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) remote = BroadlinkRemote(name, unique_id, device, code_storage, flag_storage)
try:
connected, loaded = await asyncio.gather( connected, loaded = await asyncio.gather(
hass.async_add_executor_job(api.auth), remote.async_load_storage_files() device.async_connect(), remote.async_load_storage_files()
) )
except OSError:
pass
if not connected: if not connected:
hass.data[DOMAIN][COMPONENT].remove(unique_id) hass.data[DOMAIN][COMPONENT].remove(unique_id)
raise PlatformNotReady raise PlatformNotReady
@ -127,11 +132,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class BroadlinkRemote(RemoteEntity): class BroadlinkRemote(RemoteEntity):
"""Representation of a Broadlink remote.""" """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.""" """Initialize the remote."""
self.device = device
self._name = name self._name = name
self._unique_id = unique_id self._unique_id = unique_id
self._api = api
self._code_storage = code_storage self._code_storage = code_storage
self._flag_storage = flag_storage self._flag_storage = flag_storage
self._codes = {} self._codes = {}
@ -157,7 +162,7 @@ class BroadlinkRemote(RemoteEntity):
@property @property
def available(self): def available(self):
"""Return True if the remote is available.""" """Return True if the remote is available."""
return self._available return self.device.available
@property @property
def supported_features(self): def supported_features(self):
@ -182,9 +187,9 @@ class BroadlinkRemote(RemoteEntity):
self._state = False self._state = False
async def async_update(self): async def async_update(self):
"""Update the availability of the remote.""" """Update the availability of the device."""
if not self.available: if not self.available:
await self._async_connect() await self.device.async_connect()
async def async_load_storage_files(self): async def async_load_storage_files(self):
"""Load codes and toggle flags from storage files.""" """Load codes and toggle flags from storage files."""
@ -213,8 +218,10 @@ class BroadlinkRemote(RemoteEntity):
should_delay = await self._async_send_code( should_delay = await self._async_send_code(
cmd, device, delay if should_delay else 0 cmd, device, delay if should_delay else 0
) )
except ConnectionError: except (AuthorizationError, DeviceOfflineError):
break break
except BroadlinkException:
pass
self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY)
@ -227,7 +234,7 @@ class BroadlinkRemote(RemoteEntity):
try: try:
code = self._codes[device][command] code = self._codes[device][command]
except KeyError: 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 return False
if isinstance(code, list): if isinstance(code, list):
@ -238,12 +245,14 @@ class BroadlinkRemote(RemoteEntity):
await asyncio.sleep(delay) await asyncio.sleep(delay)
try: 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: 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 return False
except ConnectionError: except BroadlinkException as err_msg:
_LOGGER.error("Failed to send '%s/%s': remote is offline", command, device) _LOGGER.error("Failed to send '%s/%s': %s", command, device, err_msg)
raise raise
if should_alternate: if should_alternate:
@ -268,8 +277,10 @@ class BroadlinkRemote(RemoteEntity):
should_store |= await self._async_learn_code( should_store |= await self._async_learn_code(
command, device, toggle, timeout command, device, toggle, timeout
) )
except ConnectionError: except (AuthorizationError, DeviceOfflineError):
break break
except BroadlinkException:
pass
if should_store: if should_store:
await self._code_storage.async_save(self._codes) 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),
await self._async_capture_code(command, timeout), await self._async_capture_code(command, timeout),
] ]
except (ValueError, TimeoutError): except TimeoutError:
_LOGGER.error( _LOGGER.error("Failed to learn '%s/%s': No code received", command, device)
"Failed to learn '%s/%s': no signal received", command, device
)
return False return False
except ConnectionError: except BroadlinkException as err_msg:
_LOGGER.error("Failed to learn '%s/%s': remote is offline", command, device) _LOGGER.error("Failed to learn '%s/%s': %s", command, device, err_msg)
raise raise
self._codes.setdefault(device, {}).update({command: code}) self._codes.setdefault(device, {}).update({command: code})
return True return True
async def _async_capture_code(self, command, timeout): async def _async_capture_code(self, command, timeout):
"""Enter learning mode and capture a code from a remote.""" """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( self.hass.components.persistent_notification.async_create(
f"Press the '{command}' button.", f"Press the '{command}' button.",
@ -313,44 +321,18 @@ class BroadlinkRemote(RemoteEntity):
code = None code = None
start_time = utcnow() start_time = utcnow()
while (utcnow() - start_time) < timedelta(seconds=timeout): while (utcnow() - start_time) < timedelta(seconds=timeout):
code = await self.hass.async_add_executor_job(self._api.check_data) try:
if code: code = await self.device.async_request(self.device.api.check_data)
except ReadError:
await asyncio.sleep(1)
else:
break break
await asyncio.sleep(1)
self.hass.components.persistent_notification.async_dismiss( self.hass.components.persistent_notification.async_dismiss(
notification_id="learn_command" notification_id="learn_command"
) )
if not code: if code is None:
raise TimeoutError raise TimeoutError
if all(not value for value in code):
raise ValueError
return b64encode(code).decode("utf8") 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

View File

@ -4,6 +4,7 @@ from ipaddress import ip_address
import logging import logging
import broadlink as blk import broadlink as blk
from broadlink.exceptions import BroadlinkException
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
@ -18,6 +19,7 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -27,10 +29,12 @@ from .const import (
A1_TYPES, A1_TYPES,
DEFAULT_NAME, DEFAULT_NAME,
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_RETRY,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
RM4_TYPES, RM4_TYPES,
RM_TYPES, RM_TYPES,
) )
from .device import BroadlinkDevice
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Broadlink device sensors."""
host = config[CONF_HOST] host = config[CONF_HOST]
mac_addr = config[CONF_MAC] 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 check_sensors = api.check_sensors_raw
api.timeout = timeout api.timeout = timeout
broadlink_data = BroadlinkData(api, check_sensors, update_interval) device = BroadlinkDevice(hass, api)
dev = []
for variable in config[CONF_MONITORED_CONDITIONS]: connected = await device.async_connect()
dev.append(BroadlinkSensor(name, broadlink_data, variable)) if not connected:
add_entities(dev, True) 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): class BroadlinkSensor(Entity):
@ -91,7 +102,6 @@ class BroadlinkSensor(Entity):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self._name = f"{name} {SENSOR_TYPES[sensor_type][0]}"
self._state = None self._state = None
self._is_available = False
self._type = sensor_type self._type = sensor_type
self._broadlink_data = broadlink_data self._broadlink_data = broadlink_data
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -109,32 +119,27 @@ class BroadlinkSensor(Entity):
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._is_available return self._broadlink_data.device.available
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
def update(self): async def async_update(self):
"""Get the latest data from the sensor.""" """Get the latest data from the sensor."""
self._broadlink_data.update() await self._broadlink_data.async_update()
if self._broadlink_data.data is None: self._state = self._broadlink_data.data.get(self._type)
self._state = None
self._is_available = False
return
self._state = self._broadlink_data.data[self._type]
self._is_available = True
class BroadlinkData: class BroadlinkData:
"""Representation of a Broadlink data object.""" """Representation of a Broadlink data object."""
def __init__(self, api, check_sensors, interval): def __init__(self, device, check_sensors, interval):
"""Initialize the data object.""" """Initialize the data object."""
self.api = api self.device = device
self.check_sensors = check_sensors self.check_sensors = check_sensors
self.data = None self.data = {}
self._schema = vol.Schema( self._schema = vol.Schema(
{ {
vol.Optional("temperature"): vol.Range(min=-50, max=150), vol.Optional("temperature"): vol.Range(min=-50, max=150),
@ -144,31 +149,21 @@ class BroadlinkData:
vol.Optional("noise"): vol.Any(0, 1, 2), vol.Optional("noise"): vol.Any(0, 1, 2),
} }
) )
self.update = Throttle(interval)(self._update) self.async_update = Throttle(interval)(self._async_fetch_data)
if not self._auth():
_LOGGER.warning("Failed to connect to device")
def _update(self, retry=3): async def _async_fetch_data(self):
try: """Fetch sensor data."""
data = self.check_sensors() for _ in range(DEFAULT_RETRY):
if data is not None: try:
self.data = self._schema(data) data = await self.device.async_request(self.check_sensors)
except BroadlinkException:
return return
except OSError as error: try:
if retry < 1: data = self._schema(data)
self.data = None except (vol.Invalid, vol.MultipleInvalid):
_LOGGER.error(error) continue
else:
self.data = data
return 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): _LOGGER.debug("Failed to update sensors: Device returned malformed data")
try:
auth = self.api.auth()
except OSError:
auth = False
if not auth and retry > 0:
return self._auth(retry - 1)
return auth

View File

@ -2,9 +2,9 @@
from datetime import timedelta from datetime import timedelta
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
import socket
import broadlink as blk import broadlink as blk
from broadlink.exceptions import BroadlinkException
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity
@ -19,6 +19,7 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
STATE_ON, STATE_ON,
) )
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import Throttle, slugify from homeassistant.util import Throttle, slugify
@ -27,7 +28,6 @@ from . import async_setup_service, data_packet, hostname, mac_address
from .const import ( from .const import (
DEFAULT_NAME, DEFAULT_NAME,
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_RETRY,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
MP1_TYPES, MP1_TYPES,
RM4_TYPES, RM4_TYPES,
@ -35,6 +35,7 @@ from .const import (
SP1_TYPES, SP1_TYPES,
SP2_TYPES, SP2_TYPES,
) )
from .device import BroadlinkDevice
_LOGGER = logging.getLogger(__name__) _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_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TYPE, default=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES), 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_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.""" """Set up the Broadlink switches."""
devices = config.get(CONF_SWITCHES) host = config[CONF_HOST]
slots = config.get("slots", {}) mac_addr = config[CONF_MAC]
host = config.get(CONF_HOST) friendly_name = config[CONF_FRIENDLY_NAME]
mac_addr = config.get(CONF_MAC)
friendly_name = config.get(CONF_FRIENDLY_NAME)
model = config[CONF_TYPE] 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): def generate_rm_switches(switches, broadlink_device):
"""Generate RM switches.""" """Generate RM switches."""
@ -98,7 +98,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
broadlink_device, broadlink_device,
config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_ON),
config.get(CONF_COMMAND_OFF), config.get(CONF_COMMAND_OFF),
retry_times,
) )
for object_id, config in switches.items() 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}"] return slots[f"slot_{slot}"]
if model in RM_TYPES: if model in RM_TYPES:
broadlink_device = blk.rm((host, DEFAULT_PORT), mac_addr, None) api = blk.rm((host, DEFAULT_PORT), mac_addr, None)
hass.add_job(async_setup_service, hass, host, broadlink_device) broadlink_device = BroadlinkDevice(hass, api)
switches = generate_rm_switches(devices, broadlink_device) switches = generate_rm_switches(devices, broadlink_device)
elif model in RM4_TYPES: elif model in RM4_TYPES:
broadlink_device = blk.rm4((host, DEFAULT_PORT), mac_addr, None) api = blk.rm4((host, DEFAULT_PORT), mac_addr, None)
hass.add_job(async_setup_service, hass, host, broadlink_device) broadlink_device = BroadlinkDevice(hass, api)
switches = generate_rm_switches(devices, broadlink_device) switches = generate_rm_switches(devices, broadlink_device)
elif model in SP1_TYPES: elif model in SP1_TYPES:
broadlink_device = blk.sp1((host, DEFAULT_PORT), mac_addr, None) api = blk.sp1((host, DEFAULT_PORT), mac_addr, None)
switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] broadlink_device = BroadlinkDevice(hass, api)
switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)]
elif model in SP2_TYPES: elif model in SP2_TYPES:
broadlink_device = blk.sp2((host, DEFAULT_PORT), mac_addr, None) api = blk.sp2((host, DEFAULT_PORT), mac_addr, None)
switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] broadlink_device = BroadlinkDevice(hass, api)
switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)]
elif model in MP1_TYPES: elif model in MP1_TYPES:
switches = [] api = blk.mp1((host, DEFAULT_PORT), mac_addr, None)
broadlink_device = blk.mp1((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api)
parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) parent_device = BroadlinkMP1Switch(broadlink_device)
for i in range(1, 5): switches = [
slot = BroadlinkMP1Slot( BroadlinkMP1Slot(
get_mp1_slot_name(friendly_name, i), get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device,
broadlink_device,
i,
parent_device,
retry_times,
) )
switches.append(slot) for i in range(1, 5)
]
broadlink_device.timeout = config.get(CONF_TIMEOUT) api.timeout = timeout
try: connected = await broadlink_device.async_connect()
broadlink_device.auth() if not connected:
except OSError: raise PlatformNotReady
_LOGGER.error("Failed to connect to device")
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): class BroadlinkRMSwitch(SwitchEntity, RestoreEntity):
"""Representation of an Broadlink switch.""" """Representation of an Broadlink switch."""
def __init__( def __init__(self, name, friendly_name, device, command_on, command_off):
self, name, friendly_name, device, command_on, command_off, retry_times
):
"""Initialize the switch.""" """Initialize the switch."""
self.device = device
self.entity_id = f"{DOMAIN}.{slugify(name)}" self.entity_id = f"{DOMAIN}.{slugify(name)}"
self._name = friendly_name self._name = friendly_name
self._state = False self._state = False
self._command_on = command_on self._command_on = command_on
self._command_off = command_off 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): async def async_added_to_hass(self):
"""Call when entity about to be added to hass.""" """Call when entity about to be added to hass."""
@ -183,7 +178,7 @@ class BroadlinkRMSwitch(SwitchEntity, RestoreEntity):
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """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 @property
def should_poll(self): def should_poll(self):
@ -195,68 +190,53 @@ class BroadlinkRMSwitch(SwitchEntity, RestoreEntity):
"""Return true if device is on.""" """Return true if device is on."""
return self._state 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.""" """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._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.""" """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._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.""" """Send packet to device."""
if packet is None: if packet is None:
_LOGGER.debug("Empty packet") _LOGGER.debug("Empty packet")
return True return True
try: try:
self._device.send_data(packet) await self.device.async_request(self.device.api.send_data, packet)
except (ValueError, OSError) as error: except BroadlinkException as err_msg:
if retry < 1: _LOGGER.error("Failed to send packet: %s", err_msg)
_LOGGER.error("Error during sending a packet: %s", error) return False
return False
if not self._auth(self._retry_times):
return False
return self._sendpacket(packet, retry - 1)
return True 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): class BroadlinkSP1Switch(BroadlinkRMSwitch):
"""Representation of an Broadlink switch.""" """Representation of an Broadlink switch."""
def __init__(self, friendly_name, device, retry_times): def __init__(self, friendly_name, device):
"""Initialize the switch.""" """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_on = 1
self._command_off = 0 self._command_off = 0
self._load_power = None self._load_power = None
def _sendpacket(self, packet, retry): async def _async_send_packet(self, packet):
"""Send packet to device.""" """Send packet to device."""
try: try:
self._device.set_power(packet) await self.device.async_request(self.device.api.set_power, packet)
except (socket.timeout, ValueError) as error: except BroadlinkException as err_msg:
if retry < 1: _LOGGER.error("Failed to send packet: %s", err_msg)
_LOGGER.error("Error during sending a packet: %s", error) return False
return False
if not self._auth(self._retry_times):
return False
return self._sendpacket(packet, retry - 1)
return True return True
@ -281,37 +261,24 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
except (ValueError, TypeError): except (ValueError, TypeError):
return None return None
def update(self): async def async_update(self):
"""Synchronize state with switch."""
self._update(self._retry_times)
def _update(self, retry):
"""Update the state of the device.""" """Update the state of the device."""
_LOGGER.debug("_update : retry=%s", retry)
try: try:
state = self._device.check_power() state = await self.device.async_request(self.device.api.check_power)
load_power = self._device.get_energy() load_power = await self.device.async_request(self.device.api.get_energy)
except (socket.timeout, ValueError) as error: except BroadlinkException as err_msg:
if retry < 1: _LOGGER.error("Failed to update state: %s", err_msg)
_LOGGER.error("Error during updating the state: %s", error) return
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)
self._state = state self._state = state
self._load_power = load_power self._load_power = load_power
self._is_available = True
class BroadlinkMP1Slot(BroadlinkRMSwitch): class BroadlinkMP1Slot(BroadlinkRMSwitch):
"""Representation of a slot of Broadlink switch.""" """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.""" """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_on = 1
self._command_off = 0 self._command_off = 0
self._slot = slot self._slot = slot
@ -322,44 +289,35 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch):
"""Return true if unable to access real state of entity.""" """Return true if unable to access real state of entity."""
return False 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 @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
return True return True
def update(self): async def async_update(self):
"""Trigger update for all switches on the parent device.""" """Update the state of the device."""
self._parent_device.update() await self._parent_device.async_update()
self._state = self._parent_device.get_outlet_status(self._slot) self._state = self._parent_device.get_outlet_status(self._slot)
if self._state is None:
self._is_available = False async def _async_send_packet(self, packet):
else: """Send packet to device."""
self._is_available = True 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: class BroadlinkMP1Switch:
"""Representation of a Broadlink switch - To fetch states of all slots.""" """Representation of a Broadlink switch - To fetch states of all slots."""
def __init__(self, device, retry_times): def __init__(self, device):
"""Initialize the switch.""" """Initialize the switch."""
self._device = device self.device = device
self._states = None self._states = None
self._retry_times = retry_times
def get_outlet_status(self, slot): def get_outlet_status(self, slot):
"""Get status of outlet from cached status list.""" """Get status of outlet from cached status list."""
@ -368,31 +326,10 @@ class BroadlinkMP1Switch:
return self._states[f"s{slot}"] return self._states[f"s{slot}"]
@Throttle(TIME_BETWEEN_UPDATES) @Throttle(TIME_BETWEEN_UPDATES)
def update(self): async def async_update(self):
"""Fetch new state data for this device."""
self._update(self._retry_times)
def _update(self, retry):
"""Update the state of the device.""" """Update the state of the device."""
try: try:
states = self._device.check_power() states = await self.device.async_request(self.device.api.check_power)
except (socket.timeout, ValueError) as error: except BroadlinkException as err_msg:
if retry < 1: _LOGGER.error("Failed to update state: %s", err_msg)
_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))
self._states = states 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

View File

@ -374,7 +374,7 @@ boto3==1.9.252
bravia-tv==1.0.4 bravia-tv==1.0.4
# homeassistant.components.broadlink # homeassistant.components.broadlink
broadlink==0.13.2 broadlink==0.14.0
# homeassistant.components.brother # homeassistant.components.brother
brother==0.1.14 brother==0.1.14

View File

@ -159,7 +159,7 @@ bomradarloop==0.1.4
bravia-tv==1.0.4 bravia-tv==1.0.4
# homeassistant.components.broadlink # homeassistant.components.broadlink
broadlink==0.13.2 broadlink==0.14.0
# homeassistant.components.brother # homeassistant.components.brother
brother==0.1.14 brother==0.1.14

View File

@ -6,6 +6,7 @@ import pytest
from homeassistant.components.broadlink import async_setup_service, data_packet 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.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND
from homeassistant.components.broadlink.device import BroadlinkDevice
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from tests.async_mock import MagicMock, call, patch from tests.async_mock import MagicMock, call, patch
@ -34,39 +35,37 @@ async def test_padding(hass):
async def test_send(hass): async def test_send(hass):
"""Test send service.""" """Test send service."""
mock_device = MagicMock() mock_api = MagicMock()
mock_device.send_data.return_value = None mock_api.send_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)
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_SEND, {"host": DUMMY_HOST, "packet": (DUMMY_IR_PACKET)} DOMAIN, SERVICE_SEND, {"host": DUMMY_HOST, "packet": (DUMMY_IR_PACKET)}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_device.send_data.call_count == 1 assert device.api.send_data.call_count == 1
assert mock_device.send_data.call_args == call(b64decode(DUMMY_IR_PACKET)) assert device.api.send_data.call_args == call(b64decode(DUMMY_IR_PACKET))
async def test_learn(hass): async def test_learn(hass):
"""Test learn service.""" """Test learn service."""
mock_device = MagicMock() mock_api = MagicMock()
mock_device.enter_learning.return_value = None mock_api.enter_learning.return_value = None
mock_device.check_data.return_value = b64decode(DUMMY_IR_PACKET) mock_api.check_data.return_value = b64decode(DUMMY_IR_PACKET)
device = BroadlinkDevice(hass, mock_api)
with patch.object( with patch.object(
hass.components.persistent_notification, "async_create" hass.components.persistent_notification, "async_create"
) as mock_create: ) as mock_create:
async_setup_service(hass, DUMMY_HOST, mock_device) await async_setup_service(hass, DUMMY_HOST, device)
await hass.async_block_till_done()
await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST}) await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST})
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_device.enter_learning.call_count == 1 assert device.api.enter_learning.call_count == 1
assert mock_device.enter_learning.call_args == call() assert device.api.enter_learning.call_args == call()
assert mock_create.call_count == 1 assert mock_create.call_count == 1
assert mock_create.call_args == call( assert mock_create.call_args == call(
@ -76,12 +75,12 @@ async def test_learn(hass):
async def test_learn_timeout(hass): async def test_learn_timeout(hass):
"""Test learn service.""" """Test learn service."""
mock_device = MagicMock() mock_api = MagicMock()
mock_device.enter_learning.return_value = None mock_api.enter_learning.return_value = None
mock_device.check_data.return_value = None mock_api.check_data.return_value = None
device = BroadlinkDevice(hass, mock_api)
async_setup_service(hass, DUMMY_HOST, mock_device) await async_setup_service(hass, DUMMY_HOST, device)
await hass.async_block_till_done()
now = utcnow() 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.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST})
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_device.enter_learning.call_count == 1 assert device.api.enter_learning.call_count == 1
assert mock_device.enter_learning.call_args == call() assert device.api.enter_learning.call_args == call()
assert mock_create.call_count == 1 assert mock_create.call_count == 1
assert mock_create.call_args == call( assert mock_create.call_args == call(