hass-core/homeassistant/components/mysensors.py
Martin Hjelmare c8a53c564a Wait for future mysensors gateway ready (#14398)
* Wait for future mysensors gateway ready

* Add an asyncio future that is done when the gateway reports the
  gateway ready message, I_GATEWAY_READY.
* This will make sure that the gateway is ready before home assistant
  fires the home assistant start event. Automations can now send
  messages to the gateway when home assistant is started.
* Use async timeout to wait max 15 seconds for ready gateway.

* Address comments
2018-05-19 18:33:52 -04:00

705 lines
26 KiB
Python

"""
Connect to a MySensors gateway via pymysensors API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mysensors/
"""
import asyncio
from collections import defaultdict
import logging
import os
import socket
import sys
from timeit import default_timer as timer
import async_timeout
import voluptuous as vol
from homeassistant.components.mqtt import (
valid_publish_topic, valid_subscribe_topic)
from homeassistant.const import (
ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP,
STATE_OFF, STATE_ON)
from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.setup import async_setup_component
REQUIREMENTS = ['pymysensors==0.14.0']
_LOGGER = logging.getLogger(__name__)
ATTR_CHILD_ID = 'child_id'
ATTR_DESCRIPTION = 'description'
ATTR_DEVICE = 'device'
ATTR_DEVICES = 'devices'
ATTR_NODE_ID = 'node_id'
CONF_BAUD_RATE = 'baud_rate'
CONF_DEBUG = 'debug'
CONF_DEVICE = 'device'
CONF_GATEWAYS = 'gateways'
CONF_PERSISTENCE = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_RETAIN = 'retain'
CONF_TCP_PORT = 'tcp_port'
CONF_TOPIC_IN_PREFIX = 'topic_in_prefix'
CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix'
CONF_VERSION = 'version'
CONF_NODES = 'nodes'
CONF_NODE_NAME = 'name'
DEFAULT_BAUD_RATE = 115200
DEFAULT_TCP_PORT = 5003
DEFAULT_VERSION = '1.4'
DOMAIN = 'mysensors'
GATEWAY_READY_TIMEOUT = 15.0
MQTT_COMPONENT = 'mqtt'
MYSENSORS_GATEWAYS = 'mysensors_gateways'
MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}'
MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}'
PLATFORM = 'platform'
SCHEMA = 'schema'
SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}'
TYPE = 'type'
def is_socket_address(value):
"""Validate that value is a valid address."""
try:
socket.getaddrinfo(value, None)
return value
except OSError:
raise vol.Invalid('Device is not a valid domain name or ip address')
def has_parent_dir(value):
"""Validate that value is in an existing directory which is writeable."""
parent = os.path.dirname(os.path.realpath(value))
is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK)
if not is_dir_writable:
raise vol.Invalid(
'{} directory does not exist or is not writeable'.format(parent))
return value
def has_all_unique_files(value):
"""Validate that all persistence files are unique and set if any is set."""
persistence_files = [
gateway.get(CONF_PERSISTENCE_FILE) for gateway in value]
if None in persistence_files and any(
name is not None for name in persistence_files):
raise vol.Invalid(
'persistence file name of all devices must be set if any is set')
if not all(name is None for name in persistence_files):
schema = vol.Schema(vol.Unique())
schema(persistence_files)
return value
def is_persistence_file(value):
"""Validate that persistence file path ends in either .pickle or .json."""
if value.endswith(('.json', '.pickle')):
return value
else:
raise vol.Invalid(
'{} does not end in either `.json` or `.pickle`'.format(value))
def is_serial_port(value):
"""Validate that value is a windows serial port or a unix device."""
if sys.platform.startswith('win'):
ports = ('COM{}'.format(idx + 1) for idx in range(256))
if value in ports:
return value
else:
raise vol.Invalid('{} is not a serial port'.format(value))
else:
return cv.isdevice(value)
def deprecated(key):
"""Mark key as deprecated in configuration."""
def validator(config):
"""Check if key is in config, log warning and remove key."""
if key not in config:
return config
_LOGGER.warning(
'%s option for %s is deprecated. Please remove %s from your '
'configuration file', key, DOMAIN, key)
config.pop(key)
return config
return validator
NODE_SCHEMA = vol.Schema({
cv.positive_int: {
vol.Required(CONF_NODE_NAME): cv.string
}
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), {
vol.Required(CONF_GATEWAYS): vol.All(
cv.ensure_list, has_all_unique_files,
[{
vol.Required(CONF_DEVICE):
vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port),
vol.Optional(CONF_PERSISTENCE_FILE):
vol.All(cv.string, is_persistence_file, has_parent_dir),
vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE):
cv.positive_int,
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic,
vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic,
vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
}]
),
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean,
vol.Optional(CONF_RETAIN, default=True): cv.boolean,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
}))
}, extra=vol.ALLOW_EXTRA)
# MySensors const schemas
BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'}
CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'}
LIGHT_DIMMER_SCHEMA = {
PLATFORM: 'light', TYPE: 'V_DIMMER',
SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}}
LIGHT_PERCENTAGE_SCHEMA = {
PLATFORM: 'light', TYPE: 'V_PERCENTAGE',
SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}}
LIGHT_RGB_SCHEMA = {
PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: {
'V_RGB': cv.string, 'V_STATUS': cv.string}}
LIGHT_RGBW_SCHEMA = {
PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: {
'V_RGBW': cv.string, 'V_STATUS': cv.string}}
NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'}
DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'}
DUST_SCHEMA = [
{PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'},
{PLATFORM: 'sensor', TYPE: 'V_LEVEL'}]
SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'}
SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'}
MYSENSORS_CONST_SCHEMA = {
'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_SPRINKLER': [
BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}],
'S_WATER_LEAK': [
BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_SOUND': [
BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'},
{PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_VIBRATION': [
BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'},
{PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_MOISTURE': [
BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'},
{PLATFORM: 'switch', TYPE: 'V_ARMED'}],
'S_HVAC': [CLIMATE_SCHEMA],
'S_COVER': [
{PLATFORM: 'cover', TYPE: 'V_DIMMER'},
{PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'},
{PLATFORM: 'cover', TYPE: 'V_LIGHT'},
{PLATFORM: 'cover', TYPE: 'V_STATUS'}],
'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA],
'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA],
'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA],
'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}],
'S_GPS': [
DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}],
'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}],
'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}],
'S_BARO': [
{PLATFORM: 'sensor', TYPE: 'V_PRESSURE'},
{PLATFORM: 'sensor', TYPE: 'V_FORECAST'}],
'S_WIND': [
{PLATFORM: 'sensor', TYPE: 'V_WIND'},
{PLATFORM: 'sensor', TYPE: 'V_GUST'},
{PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}],
'S_RAIN': [
{PLATFORM: 'sensor', TYPE: 'V_RAIN'},
{PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}],
'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}],
'S_WEIGHT': [
{PLATFORM: 'sensor', TYPE: 'V_WEIGHT'},
{PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}],
'S_POWER': [
{PLATFORM: 'sensor', TYPE: 'V_WATT'},
{PLATFORM: 'sensor', TYPE: 'V_KWH'},
{PLATFORM: 'sensor', TYPE: 'V_VAR'},
{PLATFORM: 'sensor', TYPE: 'V_VA'},
{PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}],
'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}],
'S_LIGHT_LEVEL': [
{PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'},
{PLATFORM: 'sensor', TYPE: 'V_LEVEL'}],
'S_IR': [
{PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'},
{PLATFORM: 'switch', TYPE: 'V_IR_SEND',
SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}],
'S_WATER': [
{PLATFORM: 'sensor', TYPE: 'V_FLOW'},
{PLATFORM: 'sensor', TYPE: 'V_VOLUME'}],
'S_CUSTOM': [
{PLATFORM: 'sensor', TYPE: 'V_VAR1'},
{PLATFORM: 'sensor', TYPE: 'V_VAR2'},
{PLATFORM: 'sensor', TYPE: 'V_VAR3'},
{PLATFORM: 'sensor', TYPE: 'V_VAR4'},
{PLATFORM: 'sensor', TYPE: 'V_VAR5'},
{PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}],
'S_SCENE_CONTROLLER': [
{PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'},
{PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}],
'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}],
'S_MULTIMETER': [
{PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'},
{PLATFORM: 'sensor', TYPE: 'V_CURRENT'},
{PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}],
'S_GAS': [
{PLATFORM: 'sensor', TYPE: 'V_FLOW'},
{PLATFORM: 'sensor', TYPE: 'V_VOLUME'}],
'S_WATER_QUALITY': [
{PLATFORM: 'sensor', TYPE: 'V_TEMP'},
{PLATFORM: 'sensor', TYPE: 'V_PH'},
{PLATFORM: 'sensor', TYPE: 'V_ORP'},
{PLATFORM: 'sensor', TYPE: 'V_EC'},
{PLATFORM: 'switch', TYPE: 'V_STATUS'}],
'S_AIR_QUALITY': DUST_SCHEMA,
'S_DUST': DUST_SCHEMA,
'S_LIGHT': [SWITCH_LIGHT_SCHEMA],
'S_BINARY': [SWITCH_STATUS_SCHEMA],
'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}],
}
async def async_setup(hass, config):
"""Set up the MySensors component."""
import mysensors.mysensors as mysensors
version = config[DOMAIN].get(CONF_VERSION)
persistence = config[DOMAIN].get(CONF_PERSISTENCE)
async def setup_gateway(
device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix):
"""Return gateway after setup of the gateway."""
if device == MQTT_COMPONENT:
if not await async_setup_component(hass, MQTT_COMPONENT, config):
return None
mqtt = hass.components.mqtt
retain = config[DOMAIN].get(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(*args):
"""Call callback."""
sub_cb(*args)
hass.async_add_job(
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:
gateway = mysensors.AsyncTCPGateway(
device, port=tcp_port, loop=hass.loop, event_callback=None,
persistence=persistence, persistence_file=persistence_file,
protocol_version=version)
gateway.metric = hass.config.units.is_metric
gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC)
gateway.device = device
gateway.event_callback = gw_callback_factory(hass)
if persistence:
await gateway.start_persistence()
return gateway
# Setup all devices from config
gateways = {}
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
for index, gway in enumerate(conf_gateways):
device = gway[CONF_DEVICE]
persistence_file = gway.get(
CONF_PERSISTENCE_FILE,
hass.config.path('mysensors{}.pickle'.format(index + 1)))
baud_rate = gway.get(CONF_BAUD_RATE)
tcp_port = gway.get(CONF_TCP_PORT)
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '')
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '')
gateway = await setup_gateway(
device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix)
if gateway is not None:
gateway.nodes_config = gway.get(CONF_NODES)
gateways[id(gateway)] = gateway
if not gateways:
_LOGGER.error(
"No devices could be setup as gateways, check your configuration")
return False
hass.data[MYSENSORS_GATEWAYS] = gateways
hass.async_add_job(finish_setup(hass, gateways))
return True
async def finish_setup(hass, 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, 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, loop=hass.loop)
if start_tasks:
await asyncio.wait(start_tasks, loop=hass.loop)
async def gw_start(hass, gateway):
"""Start the gateway."""
@callback
def gw_stop(event):
"""Trigger to stop the gateway."""
hass.async_add_job(gateway.stop())
await gateway.start()
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, loop=hass.loop):
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)
@callback
def set_gateway_ready(hass, msg):
"""Set asyncio future result if gateway is ready."""
if (msg.type != msg.gateway.const.MessageType.internal or
msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY):
return
gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(
id(msg.gateway)))
if gateway_ready is None or gateway_ready.cancelled():
return
gateway_ready.set_result(True)
def validate_child(gateway, node_id, child):
"""Validate that a child has the correct values according to schema.
Return a dict of platform with a list of device ids for validated devices.
"""
validated = defaultdict(list)
if not child.values:
_LOGGER.debug(
"No child values for node %s child %s", node_id, child.id)
return validated
if gateway.sensors[node_id].sketch_name is None:
_LOGGER.debug("Node %s is missing sketch name", node_id)
return validated
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
s_name = next(
(member.name for member in pres if member.value == child.type), None)
if s_name not in MYSENSORS_CONST_SCHEMA:
_LOGGER.warning("Child type %s is not supported", s_name)
return validated
child_schemas = MYSENSORS_CONST_SCHEMA[s_name]
def msg(name):
"""Return a message for an invalid schema."""
return "{} requires value_type {}".format(
pres(child.type).name, set_req[name].name)
for schema in child_schemas:
platform = schema[PLATFORM]
v_name = schema[TYPE]
value_type = next(
(member.value for member in set_req if member.name == v_name),
None)
if value_type is None:
continue
_child_schema = child.get_schema(gateway.protocol_version)
vol_schema = _child_schema.extend(
{vol.Required(set_req[key].value, msg=msg(key)):
_child_schema.schema.get(set_req[key].value, val)
for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()},
extra=vol.ALLOW_EXTRA)
try:
vol_schema(child.values)
except vol.Invalid as exc:
level = (logging.WARNING if value_type in child.values
else logging.DEBUG)
_LOGGER.log(
level,
"Invalid values: %s: %s platform: node %s child %s: %s",
child.values, platform, node_id, child.id, exc)
continue
dev_id = id(gateway), node_id, child.id, value_type
validated[platform].append(dev_id)
return validated
@callback
def discover_mysensors_platform(hass, platform, new_devices):
"""Discover a MySensors platform."""
task = hass.async_add_job(discovery.async_load_platform(
hass, platform, DOMAIN,
{ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}))
return task
async def discover_persistent_devices(hass, gateway):
"""Discover platforms for devices loaded via persistence file."""
tasks = []
new_devices = defaultdict(list)
for node_id in gateway.sensors:
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, platform, dev_ids))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
def get_mysensors_devices(hass, domain):
"""Return MySensors devices for a platform."""
if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data:
hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)]
def gw_callback_factory(hass):
"""Return a new callback for the gateway."""
@callback
def mysensors_callback(msg):
"""Handle messages from a MySensors gateway."""
start = timer()
_LOGGER.debug(
"Node update: node %s child %s", msg.node_id, msg.child_id)
set_gateway_ready(hass, msg)
try:
child = msg.gateway.sensors[msg.node_id].children[msg.child_id]
except KeyError:
_LOGGER.debug("Not a child update for node %s", msg.node_id)
return
signals = []
# Update all platforms for the device via dispatcher.
# Add/update entity if schema validates to true.
validated = validate_child(msg.gateway, msg.node_id, child)
for platform, dev_ids in validated.items():
devices = get_mysensors_devices(hass, platform)
new_dev_ids = []
for dev_id in dev_ids:
if dev_id in devices:
signals.append(SIGNAL_CALLBACK.format(*dev_id))
else:
new_dev_ids.append(dev_id)
if new_dev_ids:
discover_mysensors_platform(hass, platform, new_dev_ids)
for signal in set(signals):
# Only one signal per device is needed.
# A device can have multiple platforms, ie multiple schemas.
# FOR LATER: Add timer to not signal if another update comes in.
async_dispatcher_send(hass, signal)
end = timer()
if end - start > 0.1:
_LOGGER.debug(
"Callback for node %s child %s took %.3f seconds",
msg.node_id, msg.child_id, end - start)
return mysensors_callback
def get_mysensors_name(gateway, node_id, child_id):
"""Return a name for a node child."""
node_name = '{} {}'.format(
gateway.sensors[node_id].sketch_name, node_id)
node_name = next(
(node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items()
if node.get(CONF_NODE_NAME) is not None and conf_id == node_id),
node_name)
return '{} {}'.format(node_name, child_id)
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)
@callback
def setup_mysensors_platform(
hass, domain, discovery_info, device_class, device_args=None,
async_add_devices=None):
"""Set up a MySensors platform."""
# Only act if called via mysensors by discovery event.
# Otherwise gateway is not setup.
if not discovery_info:
return
if device_args is None:
device_args = ()
new_devices = []
new_dev_ids = discovery_info[ATTR_DEVICES]
for dev_id in new_dev_ids:
devices = get_mysensors_devices(hass, domain)
if dev_id in devices:
continue
gateway_id, node_id, child_id, value_type = dev_id
gateway = get_mysensors_gateway(hass, gateway_id)
if not gateway:
continue
device_class_copy = device_class
if isinstance(device_class, dict):
child = gateway.sensors[node_id].children[child_id]
s_type = gateway.const.Presentation(child.type).name
device_class_copy = device_class[s_type]
name = get_mysensors_name(gateway, node_id, child_id)
args_copy = (*device_args, gateway, node_id, child_id, name,
value_type)
devices[dev_id] = device_class_copy(*args_copy)
new_devices.append(devices[dev_id])
if new_devices:
_LOGGER.info("Adding new devices: %s", new_devices)
if async_add_devices is not None:
async_add_devices(new_devices, True)
return new_devices
class MySensorsDevice(object):
"""Representation of a MySensors device."""
def __init__(self, gateway, node_id, child_id, name, value_type):
"""Set up the MySensors device."""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self._name = name
self.value_type = value_type
child = gateway.sensors[node_id].children[child_id]
self.child_type = child.type
self._values = {}
@property
def name(self):
"""Return the name of this entity."""
return self._name
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
attr = {
ATTR_BATTERY_LEVEL: node.battery_level,
ATTR_CHILD_ID: self.child_id,
ATTR_DESCRIPTION: child.description,
ATTR_DEVICE: self.gateway.device,
ATTR_NODE_ID: self.node_id,
}
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
attr[set_req(value_type).name] = value
return attr
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
set_req = self.gateway.const.SetReq
for value_type, value in child.values.items():
_LOGGER.debug(
"Entity update: %s: value_type %s, value = %s",
self._name, value_type, value)
if value_type in (set_req.V_ARMED, set_req.V_LIGHT,
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
self._values[value_type] = (
STATE_ON if int(value) == 1 else STATE_OFF)
elif value_type == set_req.V_DIMMER:
self._values[value_type] = int(value)
else:
self._values[value_type] = value
class MySensorsEntity(MySensorsDevice, Entity):
"""Representation of a MySensors entity."""
@property
def should_poll(self):
"""Return the polling state. The gateway pushes its states."""
return False
@property
def available(self):
"""Return true if entity is available."""
return self.value_type in self._values
@callback
def async_update_callback(self):
"""Update the entity."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register update callback."""
dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type
async_dispatcher_connect(
self.hass, SIGNAL_CALLBACK.format(*dev_id),
self.async_update_callback)