hass-core/homeassistant/components/mysensors/gateway.py
Ville Skyttä b4bac0f7a0
Exception chaining and wrapping improvements (#39320)
* Remove unnecessary exception re-wraps

* Preserve exception chains on re-raise

We slap "from cause" to almost all possible cases here. In some cases it
could conceivably be better to do "from None" if we really want to hide
the cause. However those should be in the minority, and "from cause"
should be an improvement over the corresponding raise without a "from"
in all cases anyway.

The only case where we raise from None here is in plex, where the
exception for an original invalid SSL cert is not the root cause for
failure to validate a newly fetched one.

Follow local convention on exception variable names if there is a
consistent one, otherwise `err` to match with majority of codebase.

* Fix mistaken re-wrap in homematicip_cloud/hap.py

Missed the difference between HmipConnectionError and
HmipcConnectionError.

* Do not hide original error on plex new cert validation error

Original is not the cause for the new one, but showing old in the
traceback is useful nevertheless.
2020-08-28 13:50:32 +02:00

250 lines
8 KiB
Python

"""Handle MySensors gateways."""
import asyncio
from collections import defaultdict
import logging
import socket
import sys
import async_timeout
from mysensors import mysensors
import voluptuous as vol
from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.setup import async_setup_component
from .const import (
CONF_BAUD_RATE,
CONF_DEVICE,
CONF_GATEWAYS,
CONF_NODES,
CONF_PERSISTENCE,
CONF_PERSISTENCE_FILE,
CONF_RETAIN,
CONF_TCP_PORT,
CONF_TOPIC_IN_PREFIX,
CONF_TOPIC_OUT_PREFIX,
CONF_VERSION,
DOMAIN,
MYSENSORS_GATEWAY_READY,
MYSENSORS_GATEWAYS,
)
from .handler import HANDLERS
from .helpers import discover_mysensors_platform, validate_child, validate_node
_LOGGER = logging.getLogger(__name__)
GATEWAY_READY_TIMEOUT = 15.0
MQTT_COMPONENT = "mqtt"
def is_serial_port(value):
"""Validate that value is a windows serial port or a unix device."""
if sys.platform.startswith("win"):
ports = (f"COM{idx + 1}" for idx in range(256))
if value in ports:
return value
raise vol.Invalid(f"{value} is not a serial port")
return cv.isdevice(value)
def is_socket_address(value):
"""Validate that value is a valid address."""
try:
socket.getaddrinfo(value, None)
return value
except OSError as err:
raise vol.Invalid("Device is not a valid domain name or ip address") from err
def get_mysensors_gateway(hass, gateway_id):
"""Return MySensors gateway."""
if MYSENSORS_GATEWAYS not in hass.data:
hass.data[MYSENSORS_GATEWAYS] = {}
gateways = hass.data.get(MYSENSORS_GATEWAYS)
return gateways.get(gateway_id)
async def setup_gateways(hass, config):
"""Set up all gateways."""
conf = config[DOMAIN]
gateways = {}
for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]):
persistence_file = gateway_conf.get(
CONF_PERSISTENCE_FILE,
hass.config.path(f"mysensors{index + 1}.pickle"),
)
ready_gateway = await _get_gateway(hass, config, gateway_conf, persistence_file)
if ready_gateway is not None:
gateways[id(ready_gateway)] = ready_gateway
return gateways
async def _get_gateway(hass, config, gateway_conf, persistence_file):
"""Return gateway after setup of the gateway."""
conf = config[DOMAIN]
persistence = conf[CONF_PERSISTENCE]
version = conf[CONF_VERSION]
device = gateway_conf[CONF_DEVICE]
baud_rate = gateway_conf[CONF_BAUD_RATE]
tcp_port = gateway_conf[CONF_TCP_PORT]
in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, "")
out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, "")
if device == MQTT_COMPONENT:
if not await async_setup_component(hass, MQTT_COMPONENT, config):
return None
mqtt = hass.components.mqtt
retain = conf[CONF_RETAIN]
def pub_callback(topic, payload, qos, retain):
"""Call MQTT publish function."""
mqtt.async_publish(topic, payload, qos, retain)
def sub_callback(topic, sub_cb, qos):
"""Call MQTT subscribe function."""
@callback
def internal_callback(msg):
"""Call callback."""
sub_cb(msg.topic, msg.payload, msg.qos)
hass.async_create_task(mqtt.async_subscribe(topic, internal_callback, qos))
gateway = mysensors.AsyncMQTTGateway(
pub_callback,
sub_callback,
in_prefix=in_prefix,
out_prefix=out_prefix,
retain=retain,
loop=hass.loop,
event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version,
)
else:
try:
await hass.async_add_job(is_serial_port, device)
gateway = mysensors.AsyncSerialGateway(
device,
baud=baud_rate,
loop=hass.loop,
event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version,
)
except vol.Invalid:
try:
await hass.async_add_job(is_socket_address, device)
# valid ip address
gateway = mysensors.AsyncTCPGateway(
device,
port=tcp_port,
loop=hass.loop,
event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version,
)
except vol.Invalid:
# invalid ip address
return None
gateway.metric = hass.config.units.is_metric
gateway.optimistic = conf[CONF_OPTIMISTIC]
gateway.device = device
gateway.event_callback = _gw_callback_factory(hass, config)
gateway.nodes_config = gateway_conf[CONF_NODES]
if persistence:
await gateway.start_persistence()
return gateway
async def finish_setup(hass, hass_config, gateways):
"""Load any persistent devices and platforms and start gateway."""
discover_tasks = []
start_tasks = []
for gateway in gateways.values():
discover_tasks.append(_discover_persistent_devices(hass, hass_config, gateway))
start_tasks.append(_gw_start(hass, gateway))
if discover_tasks:
# Make sure all devices and platforms are loaded before gateway start.
await asyncio.wait(discover_tasks)
if start_tasks:
await asyncio.wait(start_tasks)
async def _discover_persistent_devices(hass, hass_config, gateway):
"""Discover platforms for devices loaded via persistence file."""
tasks = []
new_devices = defaultdict(list)
for node_id in gateway.sensors:
if not validate_node(gateway, node_id):
continue
node = gateway.sensors[node_id]
for child in node.children.values():
validated = validate_child(gateway, node_id, child)
for platform, dev_ids in validated.items():
new_devices[platform].extend(dev_ids)
for platform, dev_ids in new_devices.items():
tasks.append(discover_mysensors_platform(hass, hass_config, platform, dev_ids))
if tasks:
await asyncio.wait(tasks)
async def _gw_start(hass, gateway):
"""Start the gateway."""
# Don't use hass.async_create_task to avoid holding up setup indefinitely.
connect_task = hass.loop.create_task(gateway.start())
@callback
def gw_stop(event):
"""Trigger to stop the gateway."""
hass.async_create_task(gateway.stop())
if not connect_task.done():
connect_task.cancel()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop)
if gateway.device == "mqtt":
# Gatways connected via mqtt doesn't send gateway ready message.
return
gateway_ready = asyncio.Future()
gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway))
hass.data[gateway_ready_key] = gateway_ready
try:
with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
await gateway_ready
except asyncio.TimeoutError:
_LOGGER.warning(
"Gateway %s not ready after %s secs so continuing with setup",
gateway.device,
GATEWAY_READY_TIMEOUT,
)
finally:
hass.data.pop(gateway_ready_key, None)
def _gw_callback_factory(hass, hass_config):
"""Return a new callback for the gateway."""
@callback
def mysensors_callback(msg):
"""Handle messages from a MySensors gateway."""
_LOGGER.debug("Node update: node %s child %s", msg.node_id, msg.child_id)
msg_type = msg.gateway.const.MessageType(msg.type)
msg_handler = HANDLERS.get(msg_type.name)
if msg_handler is None:
return
hass.async_create_task(msg_handler(hass, hass_config, msg))
return mysensors_callback