Make mysensors component async (#13641)

* Make mysensors component async

* Use async dispatcher and discovery.
* Run I/O in executor.
* Make mysensors actuator methods async.
* Upgrade pymysensors to 0.13.0.
* Use async serial gateway.
* Use async TCP gateway.
* Use async mqtt gateway.

* Start gateway before hass start event

* Make sure gateway is started after discovery of persistent devices
  and after corresponding platforms have been loaded.
* Don't wait to start gateway until after hass start.

* Bump pymysensors to 0.14.0
This commit is contained in:
Martin Hjelmare 2018-05-11 09:39:18 +02:00 committed by GitHub
parent ef8fc1f201
commit be3b227a87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 80 deletions

View file

@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""List of available fan modes."""
return ['Auto', 'Min', 'Normal', 'Max']
def set_temperature(self, **kwargs):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
set_req = self.gateway.const.SetReq
temp = kwargs.get(ATTR_TEMPERATURE)
@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic:
# Optimistically assume that device has changed state
self._values[value_type] = value
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def set_fan_mode(self, fan_mode):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic:
# Optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan_mode
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
async def async_set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type,
@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic:
# Optimistically assume that device has changed state
self._values[self.value_type] = operation_mode
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
async def async_update(self):
"""Update the controller with the latest value from a sensor."""

View file

@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_DIMMER)
def open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs):
"""Move the cover up."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
self._values[set_req.V_DIMMER] = 100
else:
self._values[set_req.V_LIGHT] = STATE_ON
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs):
"""Move the cover down."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
self._values[set_req.V_DIMMER] = 0
else:
self._values[set_req.V_LIGHT] = STATE_OFF
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION)
set_req = self.gateway.const.SetReq
@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
if self.gateway.optimistic:
# Optimistically assume that cover has changed state.
self._values[set_req.V_DIMMER] = position
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs):
"""Stop the device."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(

View file

@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
self._white = white
self._values[self.value_type] = hex_color
def turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(
@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
# optimistically assume that light has changed state
self._state = False
self._values[value_type] = STATE_OFF
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def _async_update_light(self):
"""Update the controller with values from light child."""
@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
if self.gateway.optimistic:
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight):
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
return SUPPORT_COLOR
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs)
if self.gateway.optimistic:
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
@ -225,10 +225,10 @@ class MySensorsLightRGBW(MySensorsLightRGB):
return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW
return SUPPORT_MYSENSORS_RGBW
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs)
if self.gateway.optimistic:
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()

View file

@ -4,6 +4,7 @@ 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
@ -16,17 +17,17 @@ 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_START,
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, dispatcher_send)
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.setup import setup_component
from homeassistant.setup import async_setup_component
REQUIREMENTS = ['pymysensors==0.11.1']
REQUIREMENTS = ['pymysensors==0.14.0']
_LOGGER = logging.getLogger(__name__)
@ -280,67 +281,62 @@ MYSENSORS_CONST_SCHEMA = {
}
def setup(hass, config):
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)
def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix,
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 setup_component(hass, MQTT_COMPONENT, config):
return
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.publish(topic, payload, qos, retain)
mqtt.async_publish(topic, payload, qos, retain)
def sub_callback(topic, sub_cb, qos):
"""Call MQTT subscribe function."""
mqtt.subscribe(topic, sub_cb, qos)
gateway = mysensors.MQTTGateway(
pub_callback, sub_callback,
@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, in_prefix=in_prefix,
out_prefix=out_prefix, retain=retain)
protocol_version=version)
else:
try:
is_serial_port(device)
gateway = mysensors.SerialGateway(
device, event_callback=None, persistence=persistence,
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, baud=baud_rate)
protocol_version=version)
except vol.Invalid:
try:
socket.getaddrinfo(device, None)
# valid ip address
gateway = mysensors.TCPGateway(
device, event_callback=None, persistence=persistence,
persistence_file=persistence_file,
protocol_version=version, port=tcp_port)
except OSError:
# invalid ip address
return
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)
def gw_start(event):
"""Trigger to start of the gateway and any persistence."""
if persistence:
discover_persistent_devices(hass, gateway)
gateway.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start)
await gateway.start_persistence()
return gateway
@ -357,7 +353,7 @@ def setup(hass, config):
tcp_port = gway.get(CONF_TCP_PORT)
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '')
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '')
ready_gateway = setup_gateway(
ready_gateway = await setup_gateway(
device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix)
if ready_gateway is not None:
@ -371,9 +367,36 @@ def setup(hass, config):
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)
def validate_child(gateway, node_id, child):
"""Validate that a child has the correct values according to schema.
@ -431,14 +454,18 @@ def validate_child(gateway, node_id, child):
return validated
@callback
def discover_mysensors_platform(hass, platform, new_devices):
"""Discover a MySensors platform."""
discovery.load_platform(
hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})
task = hass.async_add_job(discovery.async_load_platform(
hass, platform, DOMAIN,
{ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}))
return task
def discover_persistent_devices(hass, gateway):
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]
@ -447,7 +474,9 @@ def discover_persistent_devices(hass, gateway):
for platform, dev_ids in validated.items():
new_devices[platform].extend(dev_ids)
for platform, dev_ids in new_devices.items():
discover_mysensors_platform(hass, platform, dev_ids)
tasks.append(discover_mysensors_platform(hass, platform, dev_ids))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
def get_mysensors_devices(hass, domain):
@ -459,6 +488,7 @@ def get_mysensors_devices(hass, 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()
@ -489,7 +519,7 @@ def gw_callback_factory(hass):
# 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.
dispatcher_send(hass, signal)
async_dispatcher_send(hass, signal)
end = timer()
if end - start > 0.1:
_LOGGER.debug(

View file

@ -42,7 +42,7 @@ class MySensorsNotificationService(BaseNotificationService):
"""Initialize the service."""
self.devices = mysensors.get_mysensors_devices(hass, DOMAIN)
def send_message(self, message="", **kwargs):
async def async_send_message(self, message="", **kwargs):
"""Send a message to a user."""
target_devices = kwargs.get(ATTR_TARGET)
devices = [device for device in self.devices.values()

View file

@ -42,7 +42,7 @@ async def async_setup_platform(
hass, DOMAIN, discovery_info, device_class_map,
async_add_devices=async_add_devices)
def send_ir_code_service(service):
async def async_send_ir_code_service(service):
"""Set IR code as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
ir_code = service.data.get(ATTR_IR_CODE)
@ -58,10 +58,10 @@ async def async_setup_platform(
kwargs = {ATTR_IR_CODE: ir_code}
for device in _devices:
device.turn_on(**kwargs)
await device.async_turn_on(**kwargs)
hass.services.async_register(
DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service,
DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service,
schema=SEND_IR_CODE_SERVICE_SCHEMA)
@ -84,23 +84,23 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice):
"""Return True if switch is on."""
return self._values.get(self.value_type) == STATE_ON
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1)
if self.gateway.optimistic:
# optimistically assume that switch has changed state
self._values[self.value_type] = STATE_ON
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
def turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0)
if self.gateway.optimistic:
# optimistically assume that switch has changed state
self._values[self.value_type] = STATE_OFF
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
class MySensorsIRSwitch(MySensorsSwitch):
@ -117,7 +117,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_LIGHT) == STATE_ON
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the IR switch on."""
set_req = self.gateway.const.SetReq
if ATTR_IR_CODE in kwargs:
@ -130,11 +130,11 @@ class MySensorsIRSwitch(MySensorsSwitch):
# optimistically assume that switch has changed state
self._values[self.value_type] = self._ir_code
self._values[set_req.V_LIGHT] = STATE_ON
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
# turn off switch after switch was turned on
self.turn_off()
await self.async_turn_off()
def turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the IR switch off."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
@ -142,7 +142,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
if self.gateway.optimistic:
# optimistically assume that switch has changed state
self._values[set_req.V_LIGHT] = STATE_OFF
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
async def async_update(self):
"""Update the controller with the latest value from a sensor."""

View file

@ -869,7 +869,7 @@ pymusiccast==0.1.6
pymyq==0.0.8
# homeassistant.components.mysensors
pymysensors==0.11.1
pymysensors==0.14.0
# homeassistant.components.lock.nello
pynello==1.5.1