131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""The broadlink component."""
|
|
import asyncio
|
|
from base64 import b64decode, b64encode
|
|
from binascii import unhexlify
|
|
import logging
|
|
import re
|
|
import socket
|
|
|
|
from datetime import timedelta
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import CONF_HOST
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
from .const import CONF_PACKET, DOMAIN, SERVICE_LEARN, SERVICE_SEND
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_RETRY = 3
|
|
|
|
|
|
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)
|
|
|
|
|
|
def hostname(value):
|
|
"""Validate a hostname."""
|
|
host = str(value).lower()
|
|
if len(host) > 253:
|
|
raise ValueError
|
|
if host[-1] == ".":
|
|
host = host[:-1]
|
|
allowed = re.compile(r"(?!-)[a-z\d-]{1,63}(?<!-)$")
|
|
if not all(allowed.match(elem) for elem in host.split(".")):
|
|
raise ValueError
|
|
return host
|
|
|
|
|
|
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})
|
|
|
|
|
|
def async_setup_service(hass, host, device):
|
|
"""Register a device for given host for use in services."""
|
|
hass.data.setdefault(DOMAIN, {})[host] = device
|
|
|
|
if not hass.services.has_service(DOMAIN, SERVICE_LEARN):
|
|
|
|
async def _learn_command(call):
|
|
"""Learn a packet from remote."""
|
|
device = hass.data[DOMAIN][call.data[CONF_HOST]]
|
|
|
|
try:
|
|
auth = await hass.async_add_executor_job(device.auth)
|
|
except socket.timeout:
|
|
_LOGGER.error("Failed to connect to device, timeout")
|
|
return
|
|
if not auth:
|
|
_LOGGER.error("Failed to connect to device")
|
|
return
|
|
|
|
await hass.async_add_executor_job(device.enter_learning)
|
|
|
|
_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:
|
|
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
|
|
await asyncio.sleep(1)
|
|
_LOGGER.error("No signal was 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
|
|
)
|
|
|
|
if not hass.services.has_service(DOMAIN, SERVICE_SEND):
|
|
|
|
async def _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")
|
|
|
|
hass.services.async_register(
|
|
DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA
|
|
)
|