Skip to content

Commit

Permalink
Implement config flow in the Broadlink integration (home-assistant#36914
Browse files Browse the repository at this point in the history
)

* Implement config flow in the Broadlink integration

* General improvements to the Broadlink config flow

* Remove unnecessary else after return

* Fix translations

* Rename device to device_entry

* Add tests for the config flow

* Improve docstrings

* Test we do not accept more than one config entry per device

* Improve helpers

* Allow empty packets

* Allow multiple config files for switches related to the same device

* Rename mock_device to mock_api

* General improvements

* Make new attempts before marking the device as unavailable

* Let the name be the template for the entity_id

* Handle OSError

* Test network unavailable in the configuration flow

* Rename lock attribute

* Update manifest.json

* Import devices from platforms

* Test import flow

* Add deprecation warnings

* General improvements

* Rename deprecate to discontinue

* Test device setup

* Add type attribute to mock api

* Test we handle an update failure at startup

* Remove BroadlinkDevice from tests

* Remove device.py from .coveragerc

* Add tests for the config flow

* Add tests for the device

* Test device registry and update listener

* Test MAC address validation

* Add tests for the device

* Extract domains and types to a helper function

* Do not patch integration details

* Add tests for the device

* Set device classes where appropriate

* Set an appropriate connection class

* Do not set device class for custom switches

* Fix tests and improve code readability

* Use RM4 to test authentication errors

* Handle BroadlinkException in the authentication
  • Loading branch information
felipediel authored and weissm committed Aug 28, 2020
1 parent be7d999 commit 048ac87
Show file tree
Hide file tree
Showing 21 changed files with 2,468 additions and 766 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ omit =
homeassistant/components/braviatv/__init__.py
homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py
homeassistant/components/broadlink/__init__.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
homeassistant/components/broadlink/updater.py
homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/*
homeassistant/components/brunt/cover.py
Expand Down
135 changes: 22 additions & 113 deletions homeassistant/components/broadlink/__init__.py
Original file line number Diff line number Diff line change
@@ -1,125 +1,34 @@
"""The broadlink component."""
import asyncio
from base64 import b64decode, b64encode
from binascii import unhexlify
"""The Broadlink integration."""
from dataclasses import dataclass, field
import logging
import re

from broadlink.exceptions import BroadlinkException, ReadError, StorageError
import voluptuous as vol
from .const import DOMAIN
from .device import BroadlinkDevice

from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
LOGGER = logging.getLogger(__name__)

from .const import CONF_PACKET, DOMAIN, LEARNING_TIMEOUT, SERVICE_LEARN, SERVICE_SEND

_LOGGER = logging.getLogger(__name__)
@dataclass
class BroadlinkData:
"""Class for sharing data within the Broadlink integration."""

DEFAULT_RETRY = 3
devices: dict = field(default_factory=dict)
platforms: dict = field(default_factory=dict)


def data_packet(value):
"""Decode a data packet given for broadlink."""
value = cv.string(value)
extra = len(value) % 4
if extra > 0:
value = value + ("=" * (4 - extra))
return b64decode(value)
async def async_setup(hass, config):
"""Set up the Broadlink integration."""
hass.data[DOMAIN] = BroadlinkData()
return True


def hostname(value):
"""Validate a hostname."""
host = str(value)
if len(host) > 253:
raise ValueError
if host[-1] == ".":
host = host[:-1]
allowed = re.compile(r"(?![_-])[a-z\d_-]{1,63}(?<![_-])$", flags=re.IGNORECASE)
if not all(allowed.match(elem) for elem in host.split(".")):
raise ValueError
return host
async def async_setup_entry(hass, entry):
"""Set up a Broadlink device from a config entry."""
device = BroadlinkDevice(hass, entry)
return await device.async_setup()


def mac_address(value):
"""Validate and coerce a 48-bit MAC address."""
mac = str(value).lower()
if len(mac) == 17:
mac = mac[0:2] + mac[3:5] + mac[6:8] + mac[9:11] + mac[12:14] + mac[15:17]
elif len(mac) == 14:
mac = mac[0:2] + mac[2:4] + mac[5:7] + mac[7:9] + mac[10:12] + mac[12:14]
elif len(mac) != 12:
raise ValueError
return unhexlify(mac)


SERVICE_SEND_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]),
}
)

SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})


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 async_learn_command(call):
"""Learn a packet from remote."""

device = hass.data[DOMAIN][call.data[CONF_HOST]]

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) < LEARNING_TIMEOUT:
await asyncio.sleep(1)
try:
packet = await device.async_request(device.api.check_data)
except (ReadError, StorageError):
continue
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)
hass.components.persistent_notification.async_create(
log_msg, title="Broadlink switch"
)
return
_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, async_learn_command, schema=SERVICE_LEARN_SCHEMA
)

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:
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, async_send_packet, schema=SERVICE_SEND_SCHEMA
)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
device = hass.data[DOMAIN].devices.pop(entry.entry_id)
return await device.async_unload()
Loading

0 comments on commit 048ac87

Please sign in to comment.