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
pull/35574/head
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/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

View File

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

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",
"name": "Broadlink",
"documentation": "https://www.home-assistant.io/integrations/broadlink",
"requirements": ["broadlink==0.13.2"],
"requirements": ["broadlink==0.14.0"],
"codeowners": ["@danielhiversen", "@felipediel"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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