mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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:
parent
2a120d9045
commit
6464c94990
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
57
homeassistant/components/broadlink/device.py
Normal file
57
homeassistant/components/broadlink/device.py
Normal 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
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user