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 variablepull/35574/head
parent
2a120d9045
commit
6464c94990
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue