From 2e1b1635b1768012152c8a728f51a31d2a4cae09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Aug 2017 08:44:02 -0700 Subject: [PATCH 01/11] Version bump to 0.52 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 19ce7e470c2..a8fefcf26c4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 52 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 1d615ea6c3a5f9b3ae6481b827919f04f23c09ce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 25 Aug 2017 17:58:05 +0200 Subject: [PATCH 02/11] Refactor mysensors callback and add validation (#9069) * Refactor mysensors callback and add validation * Add mysensors entity class. The mysensors entity class inherits from a more general mysensors device class. * Extract mysensors name function. * Add setup_mysensors_platform for mysensors platforms. * Add mysensors const schemas. * Update mysensors callback and add child validation. * Remove gateway wrapper class. * Add better logging for mysensors callback. * Add discover_persistent_devices function. * Remove discovery in mysensors component setup. * Clean up gateway storage in hass.data. * Update all mysensors platforms. * Add repr for MySensorsNotificationDevice. * Fix bug in mysensors climate target temperatures. * Clean up platforms. Child validation simplifies assumptions in platforms. * Remove not needed try except statements. All messages are validated already in pymysensors. * Clean up logging. * Add timer debug logging if callback is slow. * Upgrade pymysensors to 0.11.0. * Make dispatch callback async * Pass tuple device_args and optional add_devices * Also return new_devices as list instead of dictionary. --- .../components/binary_sensor/mysensors.py | 49 +- homeassistant/components/climate/mysensors.py | 94 +--- homeassistant/components/cover/mysensors.py | 34 +- .../components/device_tracker/mysensors.py | 90 ++-- homeassistant/components/light/mysensors.py | 172 ++----- homeassistant/components/mysensors.py | 480 +++++++++++++----- homeassistant/components/notify/mysensors.py | 45 +- homeassistant/components/sensor/mysensors.py | 79 +-- homeassistant/components/switch/mysensors.py | 116 +---- requirements_all.txt | 2 +- 10 files changed, 492 insertions(+), 669 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 767ed858ec7..4b83f0c8f2d 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -4,62 +4,27 @@ Support for MySensors binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mysensors/ """ -import logging - from homeassistant.components import mysensors -from homeassistant.components.binary_sensor import (DEVICE_CLASSES, +from homeassistant.components.binary_sensor import (DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for sensors.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DOOR: [set_req.V_TRIPPED], - pres.S_MOTION: [set_req.V_TRIPPED], - pres.S_SMOKE: [set_req.V_TRIPPED], - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_SPRINKLER: [set_req.V_TRIPPED], - pres.S_WATER_LEAK: [set_req.V_TRIPPED], - pres.S_SOUND: [set_req.V_TRIPPED], - pres.S_VIBRATION: [set_req.V_TRIPPED], - pres.S_MOISTURE: [set_req.V_TRIPPED], - }) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsBinarySensor, add_devices)) + """Setup the mysensors platform for binary sensors.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsBinarySensor, + add_devices=add_devices) class MySensorsBinarySensor( - mysensors.MySensorsDeviceEntity, BinarySensorDevice): + mysensors.MySensorsEntity, BinarySensorDevice): """Represent the value of a MySensors Binary Sensor child node.""" @property def is_on(self): """Return True if the binary sensor is on.""" - if self.value_type in self._values: - return self._values[self.value_type] == STATE_ON - return False + return self._values.get(self.value_type) == STATE_ON @property def device_class(self): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 82ed8a94e2b..d4316c2cfba 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -4,15 +4,11 @@ MySensors platform that offers a Climate (MySensors-HVAC) component. For more details about this platform, please refer to the documentation https://home-assistant.io/components/climate.mysensors/ """ -import logging - from homeassistant.components import mysensors from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_OFF, STATE_AUTO, ClimateDevice, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE - -_LOGGER = logging.getLogger(__name__) + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, + STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT DICT_HA_TO_MYS = { STATE_AUTO: 'AutoChangeOver', @@ -29,28 +25,12 @@ DICT_MYS_TO_HA = { def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the mysensors climate.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - if float(gateway.protocol_version) < 1.5: - continue - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_HVAC: [set_req.V_HVAC_FLOW_STATE], - } - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsHVAC, add_devices)) + """Setup the mysensors climate.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) -class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): +class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): """Representation of a MySensors HVAC.""" @property @@ -84,26 +64,28 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) if temp is None: temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) - return float(temp) + return float(temp) if temp is not None else None @property def target_temperature_high(self): """Return the highbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_HEAT in self._values: - return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL)) + temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + return float(temp) if temp is not None else None @property def target_temperature_low(self): """Return the lowbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_COOL in self._values: - return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT)) + temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + return float(temp) if temp is not None else None @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._values.get(self.gateway.const.SetReq.V_HVAC_FLOW_STATE) + return self._values.get(self.value_type) @property def operation_list(self): @@ -128,7 +110,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): high = kwargs.get(ATTR_TARGET_TEMP_HIGH) heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL) - updates = () + updates = [] if temp is not None: if heat is not None: # Set HEAT Target temperature @@ -146,7 +128,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # optimistically assume that device has changed state self._values[value_type] = value self.schedule_update_ha_state() @@ -156,54 +138,22 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" - set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_FLOW_STATE, + self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[operation_mode]) if self.gateway.optimistic: - # optimistically assume that switch has changed state - self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode + # optimistically assume that device has changed state + self._values[self.value_type] = operation_mode self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - set_req = self.gateway.const.SetReq - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == set_req.V_HVAC_FLOW_STATE: - self._values[value_type] = DICT_MYS_TO_HA[value] - else: - self._values[value_type] = value - - def set_humidity(self, humidity): - """Set new target humidity.""" - _LOGGER.error("Service Not Implemented yet") - - def set_swing_mode(self, swing_mode): - """Set new target swing operation.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_away_mode_on(self): - """Turn away mode on.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_away_mode_off(self): - """Turn away mode off.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_aux_heat_on(self): - """Turn auxillary heater on.""" - _LOGGER.error("Service Not Implemented yet") - - def turn_aux_heat_off(self): - """Turn auxillary heater off.""" - _LOGGER.error("Service Not Implemented yet") + super().update() + self._values[self.value_type] = DICT_MYS_TO_HA[ + self._values[self.value_type]] diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index f48a2110eca..cd4ff62b3e9 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -4,42 +4,18 @@ Support for MySensors covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mysensors/ """ -import logging - from homeassistant.components import mysensors -from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.cover import CoverDevice, ATTR_POSITION, DOMAIN from homeassistant.const import STATE_ON, STATE_OFF -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = [] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for covers.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT], - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS], - }) - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsCover, add_devices)) + """Setup the mysensors platform for covers.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices) -class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): +class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index 4503c4d1b26..f68eb361ca0 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -4,61 +4,51 @@ Support for tracking MySensors devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mysensors/ """ -import logging - from homeassistant.components import mysensors +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.util import slugify -DEPENDENCIES = ['mysensors'] - -_LOGGER = logging.getLogger(__name__) - def setup_scanner(hass, config, see, discovery_info=None): - """Set up the MySensors tracker.""" - def mysensors_callback(gateway, msg): - """Set up callback for mysensors platform.""" - node = gateway.sensors[msg.node_id] - if node.sketch_name is None: - _LOGGER.debug("No sketch_name: node %s", msg.node_id) - return + """Set up the MySensors device scanner.""" + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsDeviceScanner, + device_args=(see, )) + if not new_devices: + return False - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - - child = node.children.get(msg.child_id) - if child is None: - return - position = child.values.get(set_req.V_POSITION) - if child.type != pres.S_GPS or position is None: - return - try: - latitude, longitude, _ = position.split(',') - except ValueError: - _LOGGER.error("Payload for V_POSITION %s is not of format " - "latitude, longitude, altitude", position) - return - name = '{} {} {}'.format( - node.sketch_name, msg.node_id, child.id) - attr = { - mysensors.ATTR_CHILD_ID: child.id, - mysensors.ATTR_DESCRIPTION: child.description, - mysensors.ATTR_DEVICE: gateway.device, - mysensors.ATTR_NODE_ID: msg.node_id, - } - see( - dev_id=slugify(name), - host_name=name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=attr - ) - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - - for gateway in gateways: - if float(gateway.protocol_version) < 2.0: - continue - gateway.platform_callbacks.append(mysensors_callback) + for device in new_devices: + dev_id = ( + id(device.gateway), device.node_id, device.child_id, + device.value_type) + dispatcher_connect( + hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), + device.update_callback) return True + + +class MySensorsDeviceScanner(mysensors.MySensorsDevice): + """Represent a MySensors scanner.""" + + def __init__(self, see, *args): + """Set up instance.""" + super().__init__(*args) + self.see = see + + def update_callback(self): + """Update the device.""" + self.update() + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + position = child.values[self.value_type] + latitude, longitude, _ = position.split(',') + + self.see( + dev_id=slugify(self.name), + host_name=self.name, + gps=(latitude, longitude), + battery=node.battery_level, + attributes=self.device_state_attributes + ) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 203119e5e51..c41f480c67e 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,64 +4,35 @@ Support for MySensors lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mysensors/ """ -import logging - from homeassistant.components import mysensors from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list -_LOGGER = logging.getLogger(__name__) -ATTR_VALUE = 'value' -ATTR_VALUE_TYPE = 'value_type' - SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for lights.""" - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DIMMER: [set_req.V_DIMMER], - } - device_class_map = { - pres.S_DIMMER: MySensorsLightDimmer, - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_RGB_LIGHT: [set_req.V_RGB], - pres.S_RGBW_LIGHT: [set_req.V_RGBW], - }) - map_sv_types[pres.S_DIMMER].append(set_req.V_PERCENTAGE) - device_class_map.update({ - pres.S_RGB_LIGHT: MySensorsLightRGB, - pres.S_RGBW_LIGHT: MySensorsLightRGBW, - }) - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, device_class_map, add_devices)) + """Setup the mysensors platform for lights.""" + device_class_map = { + 'S_DIMMER': MySensorsLightDimmer, + 'S_RGB_LIGHT': MySensorsLightRGB, + 'S_RGBW_LIGHT': MySensorsLightRGBW, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + add_devices=add_devices) -class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): +class MySensorsLight(mysensors.MySensorsEntity, Light): """Representation of a MySensors Light child node.""" def __init__(self, *args): """Initialize a MySensors Light.""" - mysensors.MySensorsDeviceEntity.__init__(self, *args) + super().__init__(*args) self._state = None self._brightness = None self._rgb = None @@ -101,7 +72,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): """Turn on light child device.""" set_req = self.gateway.const.SetReq - if self._state or set_req.V_LIGHT not in self._values: + if self._state: return self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 1) @@ -110,7 +81,6 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._state = True self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() def _turn_on_dimmer(self, **kwargs): """Turn on dimmer child device.""" @@ -130,7 +100,6 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._brightness = brightness self._values[set_req.V_DIMMER] = percent - self.schedule_update_ha_state() def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" @@ -144,16 +113,11 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): return if new_rgb is not None: rgb = list(new_rgb) - if rgb is None: - return if hex_template == '%02x%02x%02x%02x': if new_white is not None: rgb.append(new_white) - elif white is not None: - rgb.append(white) else: - _LOGGER.error("White value is not updated for RGBW light") - return + rgb.append(white) hex_color = hex_template % tuple(rgb) if len(rgb) > 3: white = rgb.pop() @@ -164,104 +128,40 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._rgb = rgb self._white = white - if hex_color: - self._values[self.value_type] = hex_color - self.schedule_update_ha_state() + self._values[self.value_type] = hex_color - def _turn_off_light(self, value_type=None, value=None): - """Turn off light child device.""" - set_req = self.gateway.const.SetReq - value_type = ( - set_req.V_LIGHT - if set_req.V_LIGHT in self._values else value_type) - value = 0 if set_req.V_LIGHT in self._values else value - return {ATTR_VALUE_TYPE: value_type, ATTR_VALUE: value} - - def _turn_off_dimmer(self, value_type=None, value=None): - """Turn off dimmer child device.""" - set_req = self.gateway.const.SetReq - value_type = ( - set_req.V_DIMMER - if set_req.V_DIMMER in self._values else value_type) - value = 0 if set_req.V_DIMMER in self._values else value - return {ATTR_VALUE_TYPE: value_type, ATTR_VALUE: value} - - def _turn_off_rgb_or_w(self, value_type=None, value=None): - """Turn off RGB or RGBW child device.""" - if float(self.gateway.protocol_version) >= 1.5: - set_req = self.gateway.const.SetReq - if self.value_type == set_req.V_RGB: - value = '000000' - elif self.value_type == set_req.V_RGBW: - value = '00000000' - return {ATTR_VALUE_TYPE: self.value_type, ATTR_VALUE: value} - - def _turn_off_main(self, value_type=None, value=None): + def turn_off(self): """Turn the device off.""" - set_req = self.gateway.const.SetReq - if value_type is None or value is None: - _LOGGER.warning( - "%s: value_type %s, value = %s, None is not valid argument " - "when setting child value", self._name, value_type, value) - return + value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value( - self.node_id, self.child_id, value_type, value) + self.node_id, self.child_id, value_type, 0) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False - self._values[value_type] = ( - STATE_OFF if set_req.V_LIGHT in self._values else value) + self._values[value_type] = STATE_OFF self.schedule_update_ha_state() def _update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT - if value_type in self._values: - self._values[value_type] = ( - STATE_ON if int(self._values[value_type]) == 1 else STATE_OFF) - self._state = self._values[value_type] == STATE_ON + self._state = self._values[value_type] == STATE_ON def _update_dimmer(self): """Update the controller with values from dimmer child.""" - set_req = self.gateway.const.SetReq - value_type = set_req.V_DIMMER + value_type = self.gateway.const.SetReq.V_DIMMER if value_type in self._values: self._brightness = round(255 * int(self._values[value_type]) / 100) if self._brightness == 0: self._state = False - if set_req.V_LIGHT not in self._values: - self._state = self._brightness > 0 def _update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" - set_req = self.gateway.const.SetReq value = self._values[self.value_type] - if len(value) != 6 and len(value) != 8: - _LOGGER.error( - "Wrong value %s for %s", value, set_req(self.value_type).name) - return color_list = rgb_hex_to_rgb_list(value) - if set_req.V_LIGHT not in self._values and \ - set_req.V_DIMMER not in self._values: - self._state = max(color_list) > 0 if len(color_list) > 3: - if set_req.V_RGBW != self.value_type: - _LOGGER.error( - "Wrong value %s for %s", - value, set_req(self.value_type).name) - return self._white = color_list.pop() self._rgb = color_list - def _update_main(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - self._values[value_type] = value - class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" @@ -270,18 +170,12 @@ class MySensorsLightDimmer(MySensorsLight): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) - - def turn_off(self, **kwargs): - """Turn the device off.""" - ret = self._turn_off_dimmer() - ret = self._turn_off_light( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - self._turn_off_main( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) + if self.gateway.optimistic: + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - self._update_main() + super().update() self._update_light() self._update_dimmer() @@ -294,20 +188,12 @@ class MySensorsLightRGB(MySensorsLight): self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) - - def turn_off(self, **kwargs): - """Turn the device off.""" - ret = self._turn_off_rgb_or_w() - ret = self._turn_off_dimmer( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - ret = self._turn_off_light( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - self._turn_off_main( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) + if self.gateway.optimistic: + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" - self._update_main() + super().update() self._update_light() self._update_dimmer() self._update_rgb_or_w() @@ -316,8 +202,12 @@ class MySensorsLightRGB(MySensorsLight): class MySensorsLightRGBW(MySensorsLightRGB): """RGBW child class to MySensorsLightRGB.""" + # pylint: disable=too-many-ancestors + def 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() diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index ef863bfb34f..63bd1f6faac 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,30 +4,37 @@ Connect to a MySensors gateway via pymysensors API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ +import asyncio +from collections import defaultdict import logging import os import socket import sys +from timeit import default_timer as timer import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.setup import setup_component 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, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component +from homeassistant.setup import setup_component -REQUIREMENTS = ['pymysensors==0.10.0'] +REQUIREMENTS = ['pymysensors==0.11.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' @@ -44,11 +51,16 @@ CONF_VERSION = 'version' DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 -DEFAULT_VERSION = 1.4 +DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' +MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +PLATFORM = 'platform' +SCHEMA = 'schema' +SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' +TYPE = 'type' def is_socket_address(value): @@ -144,11 +156,127 @@ CONFIG_SCHEMA = vol.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): vol.Coerce(float), + 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'}], +} + + def setup(hass, config): """Set up the MySensors component.""" import mysensors.mysensors as mysensors @@ -197,20 +325,14 @@ def setup(hass, config): # invalid ip address return gateway.metric = hass.config.units.is_metric - optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) - gateway = GatewayWrapper(gateway, optimistic, device) - # pylint: disable=attribute-defined-outside-init - gateway.event_callback = gateway.callback_factory() + 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: - for node_id in gateway.sensors: - node = gateway.sensors[node_id] - for child_id in node.children: - msg = mysensors.Message().modify( - node_id=node_id, child_id=child_id) - gateway.event_callback(msg) + discover_persistent_devices(hass, gateway) gateway.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) @@ -219,15 +341,8 @@ def setup(hass, config): return gateway - gateways = hass.data.get(MYSENSORS_GATEWAYS) - if gateways is not None: - _LOGGER.error( - "%s already exists in %s, will not setup %s component", - MYSENSORS_GATEWAYS, hass.data, DOMAIN) - return False - # Setup all devices from config - gateways = [] + gateways = {} conf_gateways = config[DOMAIN][CONF_GATEWAYS] for index, gway in enumerate(conf_gateways): @@ -243,7 +358,7 @@ def setup(hass, config): device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) if ready_gateway is not None: - gateways.append(ready_gateway) + gateways[id(ready_gateway)] = ready_gateway if not gateways: _LOGGER.error( @@ -252,115 +367,187 @@ def setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways - for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate', - 'cover']: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - discovery.load_platform( - hass, 'device_tracker', DOMAIN, {}, config) - - discovery.load_platform( - hass, 'notify', DOMAIN, {CONF_NAME: DOMAIN}, config) - return True -def pf_callback_factory(map_sv_types, devices, entity_class, add_devices=None): - """Return a new callback for the platform.""" - def mysensors_callback(gateway, msg): - """Run when a message from the gateway arrives.""" - if gateway.sensors[msg.node_id].sketch_name is None: - _LOGGER.debug("No sketch_name: node %s", msg.node_id) - return - child = gateway.sensors[msg.node_id].children.get(msg.child_id) +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 + + +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}) + + +def discover_persistent_devices(hass, gateway): + """Discover platforms for devices loaded via persistence file.""" + 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(): + discover_mysensors_platform(hass, platform, dev_ids) + + +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.""" + def mysensors_callback(msg): + """Default callback for a mysensors gateway.""" + start = timer() + _LOGGER.debug( + "Node update: node %s child %s", msg.node_id, msg.child_id) + + child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) if child is None: + _LOGGER.debug( + "Not a child update for node %s", msg.node_id) return - for value_type in child.values: - key = msg.node_id, child.id, value_type - if child.type not in map_sv_types or \ - value_type not in map_sv_types[child.type]: - continue - if key in devices: - if add_devices: - devices[key].schedule_update_ha_state(True) - else: - devices[key].update() - continue - name = '{} {} {}'.format( - gateway.sensors[msg.node_id].sketch_name, msg.node_id, - child.id) - if isinstance(entity_class, dict): - device_class = entity_class[child.type] - else: - device_class = entity_class - devices[key] = device_class( - gateway, msg.node_id, child.id, name, value_type) - if add_devices: - _LOGGER.info("Adding new devices: %s", [devices[key]]) - add_devices([devices[key]], True) - else: - devices[key].update() + + 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) + for idx, dev_id in enumerate(list(dev_ids)): + if dev_id in devices: + dev_ids.pop(idx) + signals.append(SIGNAL_CALLBACK.format(*dev_id)) + if dev_ids: + discover_mysensors_platform(hass, platform, 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. + 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 -class GatewayWrapper(object): - """Gateway wrapper class.""" - - def __init__(self, gateway, optimistic, device): - """Set up the class attributes on instantiation. - - Args: - gateway (mysensors.SerialGateway): Gateway to wrap. - optimistic (bool): Send values to actuators without feedback state. - device (str): Path to serial port, ip adress or mqtt. - - Attributes: - _wrapped_gateway (mysensors.SerialGateway): Wrapped gateway. - platform_callbacks (list): Callback functions, one per platform. - optimistic (bool): Send values to actuators without feedback state. - device (str): Device configured as gateway. - __initialised (bool): True if GatewayWrapper is initialised. - - """ - self._wrapped_gateway = gateway - self.platform_callbacks = [] - self.optimistic = optimistic - self.device = device - self.__initialised = True - - def __getattr__(self, name): - """See if this object has attribute name.""" - # Do not use hasattr, it goes into infinite recurrsion - if name in self.__dict__: - # This object has the attribute. - return getattr(self, name) - # The wrapped object has the attribute. - return getattr(self._wrapped_gateway, name) - - def __setattr__(self, name, value): - """See if this object has attribute name then set to value.""" - if '_GatewayWrapper__initialised' not in self.__dict__: - return object.__setattr__(self, name, value) - elif name in self.__dict__: - object.__setattr__(self, name, value) - else: - object.__setattr__(self._wrapped_gateway, name, value) - - def callback_factory(self): - """Return a new callback function.""" - def node_update(msg): - """Handle node updates from the MySensors gateway.""" - _LOGGER.debug( - "Update: node %s, child %s sub_type %s", - msg.node_id, msg.child_id, msg.sub_type) - for callback in self.platform_callbacks: - callback(self, msg) - - return node_update +def get_mysensors_name(gateway, node_id, child_id): + """Return a name for a node child.""" + return '{} {} {}'.format( + gateway.sensors[node_id].sketch_name, node_id, child_id) -class MySensorsDeviceEntity(object): - """Representation of a MySensors entity.""" +def get_mysensors_gateway(hass, gateway_id): + """Return gateway.""" + if MYSENSORS_GATEWAYS not in hass.data: + hass.data[MYSENSORS_GATEWAYS] = {} + gateways = hass.data.get(MYSENSORS_GATEWAYS) + return gateways.get(gateway_id) + + +def setup_mysensors_platform( + hass, domain, discovery_info, device_class, device_args=None, + 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) + + # python 3.4 cannot unpack inside tuple, but combining tuples works + 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 add_devices is not None: + 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.""" @@ -373,11 +560,6 @@ class MySensorsDeviceEntity(object): self.child_type = child.type self._values = {} - @property - def should_poll(self): - """Mysensor gateway pushes its state to HA.""" - return False - @property def name(self): """Return the name of this entity.""" @@ -399,18 +581,9 @@ class MySensorsDeviceEntity(object): set_req = self.gateway.const.SetReq for value_type, value in self._values.items(): - try: - attr[set_req(value_type).name] = value - except ValueError: - _LOGGER.error("Value_type %s is not valid for mysensors " - "version %s", value_type, - self.gateway.protocol_version) - return attr + attr[set_req(value_type).name] = value - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values + return attr def update(self): """Update the controller with the latest value from a sensor.""" @@ -419,7 +592,8 @@ class MySensorsDeviceEntity(object): set_req = self.gateway.const.SetReq for value_type, value in child.values.items(): _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) + "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] = ( @@ -428,3 +602,29 @@ class MySensorsDeviceEntity(object): 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): + """Mysensor gateway pushes its state to HA.""" + return False + + @property + def available(self): + """Return true if entity is available.""" + return self.value_type in self._values + + def _async_update_callback(self): + """Update the entity.""" + self.hass.async_add_job(self.async_update_ha_state(True)) + + @asyncio.coroutine + 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) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index d9576767f25..8ae697048f5 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -6,35 +6,19 @@ https://home-assistant.io/components/notify.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) + ATTR_TARGET, DOMAIN, BaseNotificationService) def get_service(hass, config, discovery_info=None): """Get the MySensors notification service.""" - if discovery_info is None: + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsNotificationDevice) + if not new_devices: return - platform_devices = [] - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - if float(gateway.protocol_version) < 2.0: - continue - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_INFO: [set_req.V_TEXT], - } - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsNotificationDevice)) - platform_devices.append(devices) - - return MySensorsNotificationService(platform_devices) + return MySensorsNotificationService(hass) -class MySensorsNotificationDevice(mysensors.MySensorsDeviceEntity): +class MySensorsNotificationDevice(mysensors.MySensorsDevice): """Represent a MySensors Notification device.""" def send_msg(self, msg): @@ -44,24 +28,25 @@ class MySensorsNotificationDevice(mysensors.MySensorsDeviceEntity): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, sub_msg) + def __repr__(self): + """Return the representation.""" + return "".format(self.name) + class MySensorsNotificationService(BaseNotificationService): - """Implement MySensors notification service.""" + """Implement a MySensors notification service.""" # pylint: disable=too-few-public-methods - def __init__(self, platform_devices): + def __init__(self, hass): """Initialize the service.""" - self.platform_devices = platform_devices + self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) def send_message(self, message="", **kwargs): """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) - devices = [] - for gw_devs in self.platform_devices: - for device in gw_devs.values(): - if target_devices is None or device.name in target_devices: - devices.append(device) + devices = [device for device in self.devices.values() + if target_devices is None or device.name in target_devices] for device in devices: device.send_msg(message) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index d46680c7b66..a8daf212e57 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -4,89 +4,18 @@ Support for MySensors sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ -import logging - from homeassistant.components import mysensors +from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_TEMP: [set_req.V_TEMP], - pres.S_HUM: [set_req.V_HUM], - pres.S_BARO: [set_req.V_PRESSURE, set_req.V_FORECAST], - pres.S_WIND: [set_req.V_WIND, set_req.V_GUST, set_req.V_DIRECTION], - pres.S_RAIN: [set_req.V_RAIN, set_req.V_RAINRATE], - pres.S_UV: [set_req.V_UV], - pres.S_WEIGHT: [set_req.V_WEIGHT, set_req.V_IMPEDANCE], - pres.S_POWER: [set_req.V_WATT, set_req.V_KWH], - pres.S_DISTANCE: [set_req.V_DISTANCE], - pres.S_LIGHT_LEVEL: [set_req.V_LIGHT_LEVEL], - pres.S_IR: [set_req.V_IR_RECEIVE], - pres.S_WATER: [set_req.V_FLOW, set_req.V_VOLUME], - pres.S_CUSTOM: [set_req.V_VAR1, - set_req.V_VAR2, - set_req.V_VAR3, - set_req.V_VAR4, - set_req.V_VAR5], - pres.S_SCENE_CONTROLLER: [set_req.V_SCENE_ON, - set_req.V_SCENE_OFF], - } - if float(gateway.protocol_version) < 1.5: - map_sv_types.update({ - pres.S_AIR_QUALITY: [set_req.V_DUST_LEVEL], - pres.S_DUST: [set_req.V_DUST_LEVEL], - }) - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_COLOR_SENSOR: [set_req.V_RGB], - pres.S_MULTIMETER: [set_req.V_VOLTAGE, - set_req.V_CURRENT, - set_req.V_IMPEDANCE], - pres.S_SOUND: [set_req.V_LEVEL], - pres.S_VIBRATION: [set_req.V_LEVEL], - pres.S_MOISTURE: [set_req.V_LEVEL], - pres.S_AIR_QUALITY: [set_req.V_LEVEL], - pres.S_DUST: [set_req.V_LEVEL], - }) - map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL) - - if float(gateway.protocol_version) >= 2.0: - map_sv_types.update({ - pres.S_INFO: [set_req.V_TEXT], - pres.S_GAS: [set_req.V_FLOW, set_req.V_VOLUME], - pres.S_GPS: [set_req.V_POSITION], - pres.S_WATER_QUALITY: [set_req.V_TEMP, set_req.V_PH, - set_req.V_ORP, set_req.V_EC] - }) - map_sv_types[pres.S_CUSTOM].append(set_req.V_CUSTOM) - map_sv_types[pres.S_POWER].extend( - [set_req.V_VAR, set_req.V_VA, set_req.V_POWER_FACTOR]) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, MySensorsSensor, add_devices)) + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsSensor, add_devices=add_devices) -class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): +class MySensorsSensor(mysensors.MySensorsEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 38f67ee3ee9..131ec58ae67 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -4,7 +4,6 @@ Support for MySensors switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mysensors/ """ -import logging import os import voluptuous as vol @@ -15,9 +14,6 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.config import load_yaml_config_file from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - ATTR_IR_CODE = 'V_IR_SEND' SERVICE_SEND_IR_CODE = 'mysensors_send_ir_code' @@ -29,82 +25,37 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the mysensors platform for switches.""" - # Only act if loaded via mysensors by discovery event. - # Otherwise gateway is not setup. - if discovery_info is None: - return - - gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) - if not gateways: - return - - platform_devices = [] - - for gateway in gateways: - # Define the S_TYPES and V_TYPES that the platform should handle as - # states. Map them in a dict of lists. - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - map_sv_types = { - pres.S_DOOR: [set_req.V_ARMED], - pres.S_MOTION: [set_req.V_ARMED], - pres.S_SMOKE: [set_req.V_ARMED], - pres.S_LIGHT: [set_req.V_LIGHT], - pres.S_LOCK: [set_req.V_LOCK_STATUS], - pres.S_IR: [set_req.V_IR_SEND], - } - device_class_map = { - pres.S_DOOR: MySensorsSwitch, - pres.S_MOTION: MySensorsSwitch, - pres.S_SMOKE: MySensorsSwitch, - pres.S_LIGHT: MySensorsSwitch, - pres.S_LOCK: MySensorsSwitch, - pres.S_IR: MySensorsIRSwitch, - } - if float(gateway.protocol_version) >= 1.5: - map_sv_types.update({ - pres.S_BINARY: [set_req.V_STATUS, set_req.V_LIGHT], - pres.S_SPRINKLER: [set_req.V_STATUS], - pres.S_WATER_LEAK: [set_req.V_ARMED], - pres.S_SOUND: [set_req.V_ARMED], - pres.S_VIBRATION: [set_req.V_ARMED], - pres.S_MOISTURE: [set_req.V_ARMED], - }) - map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS) - device_class_map.update({ - pres.S_BINARY: MySensorsSwitch, - pres.S_SPRINKLER: MySensorsSwitch, - pres.S_WATER_LEAK: MySensorsSwitch, - pres.S_SOUND: MySensorsSwitch, - pres.S_VIBRATION: MySensorsSwitch, - pres.S_MOISTURE: MySensorsSwitch, - }) - if float(gateway.protocol_version) >= 2.0: - map_sv_types.update({ - pres.S_WATER_QUALITY: [set_req.V_STATUS], - }) - device_class_map.update({ - pres.S_WATER_QUALITY: MySensorsSwitch, - }) - - devices = {} - gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, device_class_map, add_devices)) - platform_devices.append(devices) + device_class_map = { + 'S_DOOR': MySensorsSwitch, + 'S_MOTION': MySensorsSwitch, + 'S_SMOKE': MySensorsSwitch, + 'S_LIGHT': MySensorsSwitch, + 'S_LOCK': MySensorsSwitch, + 'S_IR': MySensorsIRSwitch, + 'S_BINARY': MySensorsSwitch, + 'S_SPRINKLER': MySensorsSwitch, + 'S_WATER_LEAK': MySensorsSwitch, + 'S_SOUND': MySensorsSwitch, + 'S_VIBRATION': MySensorsSwitch, + 'S_MOISTURE': MySensorsSwitch, + 'S_WATER_QUALITY': MySensorsSwitch, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + add_devices=add_devices) def 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) + devices = mysensors.get_mysensors_devices(hass, DOMAIN) if entity_ids: - _devices = [device for gw_devs in platform_devices - for device in gw_devs.values() + _devices = [device for device in devices.values() if isinstance(device, MySensorsIRSwitch) and device.entity_id in entity_ids] else: - _devices = [device for gw_devs in platform_devices - for device in gw_devs.values() + _devices = [device for device in devices.values() if isinstance(device, MySensorsIRSwitch)] kwargs = {ATTR_IR_CODE: ir_code} @@ -120,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=SEND_IR_CODE_SERVICE_SCHEMA) -class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): +class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Representation of the value of a MySensors Switch child node.""" @property @@ -131,9 +82,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): @property def is_on(self): """Return True if switch is on.""" - if self.value_type in self._values: - return self._values[self.value_type] == STATE_ON - return False + return self._values.get(self.value_type) == STATE_ON def turn_on(self, **kwargs): """Turn the switch on.""" @@ -159,24 +108,18 @@ class MySensorsIRSwitch(MySensorsSwitch): def __init__(self, *args): """Set up instance attributes.""" - MySensorsSwitch.__init__(self, *args) + super().__init__(*args) self._ir_code = None @property def is_on(self): """Return True if switch is on.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT in self._values: - return self._values[set_req.V_LIGHT] == STATE_ON - return False + return self._values.get(set_req.V_LIGHT) == STATE_ON def turn_on(self, **kwargs): """Turn the IR switch on.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT not in self._values: - _LOGGER.error('missing value_type: %s at node: %s, child: %s', - set_req.V_LIGHT.name, self.node_id, self.child_id) - return if ATTR_IR_CODE in kwargs: self._ir_code = kwargs[ATTR_IR_CODE] self.gateway.set_child_value( @@ -194,10 +137,6 @@ class MySensorsIRSwitch(MySensorsSwitch): def turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq - if set_req.V_LIGHT not in self._values: - _LOGGER.error('missing value_type: %s at node: %s, child: %s', - set_req.V_LIGHT.name, self.node_id, self.child_id) - return self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0) if self.gateway.optimistic: @@ -207,6 +146,5 @@ class MySensorsIRSwitch(MySensorsSwitch): def update(self): """Update the controller with the latest value from a sensor.""" - MySensorsSwitch.update(self) - if self.value_type in self._values: - self._ir_code = self._values[self.value_type] + super().update() + self._ir_code = self._values.get(self.value_type) diff --git a/requirements_all.txt b/requirements_all.txt index cf6325e6572..f1fc40a2187 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ pymodbus==1.3.1 pymyq==0.0.8 # homeassistant.components.mysensors -pymysensors==0.10.0 +pymysensors==0.11.0 # homeassistant.components.lock.nello pynello==1.5 From 10e3c00f0797471b1d1e87e0b755318069785323 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Aug 2017 09:11:11 -0700 Subject: [PATCH 03/11] Version bump to 0.52.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a8fefcf26c4..15079b11992 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 52 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 8a896433387827a7b3237309969ee5c43a5dbadb Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Sat, 26 Aug 2017 23:56:39 +0700 Subject: [PATCH 04/11] Close stream request once we end up with proxy (#9110) * Close stream request once we end up with proxy * Update aiohttp_client.py * Update aiohttp_client.py * Removed trailing whitespace --- homeassistant/helpers/aiohttp_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index a8b18351021..29e2a6260fd 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -90,8 +90,15 @@ def async_aiohttp_proxy_web(hass, request, web_coro, buffer_size=102400, # Something went wrong with the connection raise HTTPBadGateway() from err - yield from async_aiohttp_proxy_stream(hass, request, req.content, - req.headers.get(CONTENT_TYPE)) + try: + yield from async_aiohttp_proxy_stream( + hass, + request, + req.content, + req.headers.get(CONTENT_TYPE) + ) + finally: + req.close() @asyncio.coroutine From 0ae1f85f9fb5df4e60fb7310f1dc156e6b48c38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 26 Aug 2017 09:36:54 +0200 Subject: [PATCH 05/11] Fix issue #9116 in pushbullet (#9128) * Fix issue #9116 in pushbullet --- homeassistant/components/notify/pushbullet.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 6d97d544905..353e833ae51 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -89,7 +89,7 @@ class PushBulletNotificationService(BaseNotificationService): if not targets: # Backward compatibility, notify all devices in own account - self._push_data(filepath, message, title, url) + self._push_data(filepath, message, title, self.pushbullet, url) _LOGGER.info("Sent notification to self") return @@ -104,7 +104,8 @@ class PushBulletNotificationService(BaseNotificationService): # Target is email, send directly, don't use a target object # This also seems works to send to all devices in own account if ttype == 'email': - self._push_data(filepath, message, title, url, tname) + self._push_data(filepath, message, title, url, + self.pushbullet, tname) _LOGGER.info("Sent notification to email %s", tname) continue @@ -123,27 +124,27 @@ class PushBulletNotificationService(BaseNotificationService): # Attempt push_note on a dict value. Keys are types & target # name. Dict pbtargets has all *actual* targets. try: - if url: - self.pbtargets[ttype][tname].push_link( - title, url, body=message) - else: - self.pbtargets[ttype][tname].push_note(title, message) + self._push_data(filepath, message, title, url, + self.pbtargets[ttype][tname]) _LOGGER.info("Sent notification to %s/%s", ttype, tname) except KeyError: _LOGGER.error("No such target: %s/%s", ttype, tname) continue - except self.pushbullet.errors.PushError: - _LOGGER.error("Notify failed to: %s/%s", ttype, tname) - continue - def _push_data(self, filepath, message, title, url, tname=None): - if url: - self.pushbullet.push_link( - title, url, body=message, email=tname) - elif filepath and self.hass.config.is_allowed_path(filepath): - with open(filepath, "rb") as fileh: - filedata = self.pushbullet.upload_file(fileh, filepath) - self.pushbullet.push_file(title=title, body=message, - **filedata) - else: - self.pushbullet.push_note(title, message, email=tname) + def _push_data(self, filepath, message, title, url, pusher, tname=None): + from pushbullet import PushError + try: + if url: + pusher.push_link(title, url, body=message, email=tname) + elif filepath and self.hass.config.is_allowed_path(filepath): + with open(filepath, "rb") as fileh: + filedata = self.pushbullet.upload_file(fileh, filepath) + if filedata.get('file_type') == 'application/x-empty': + _LOGGER.error("Failed to send an empty file.") + return + pusher.push_file(title=title, body=message, **filedata) + else: + pusher.push_note(title, message, email=tname) + + except PushError as err: + _LOGGER.error("Notify failed: %s", err) From 422be25d222d1de4d211b3059d4f14aabbcea2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 26 Aug 2017 18:12:51 +0200 Subject: [PATCH 06/11] bug fix pushbullet (#9139) --- homeassistant/components/notify/pushbullet.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 353e833ae51..0d596fb41ba 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -133,9 +133,13 @@ class PushBulletNotificationService(BaseNotificationService): def _push_data(self, filepath, message, title, url, pusher, tname=None): from pushbullet import PushError + from pushbullet import Device try: if url: - pusher.push_link(title, url, body=message, email=tname) + if isinstance(pusher, Device): + pusher.push_link(title, url, body=message) + else: + pusher.push_link(title, url, body=message, email=tname) elif filepath and self.hass.config.is_allowed_path(filepath): with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) @@ -144,7 +148,9 @@ class PushBulletNotificationService(BaseNotificationService): return pusher.push_file(title=title, body=message, **filedata) else: - pusher.push_note(title, message, email=tname) - + if isinstance(pusher, Device): + pusher.push_note(title, message) + else: + pusher.push_note(title, message, email=tname) except PushError as err: _LOGGER.error("Notify failed: %s", err) From e6892a4077be135e72eb208a20a7bbe1208fa0db Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sat, 26 Aug 2017 12:08:37 -0400 Subject: [PATCH 07/11] Fix import for foscam (#9140) While waiting for a new pyfoscam release, we can fix this for users just by changing the import. Foscam devices a pretty widely deployed, so a regression here is definitely no fun. Fixes Bug #8940 --- homeassistant/components/camera/foscam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 15138e2c253..8ea90d5a44e 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -53,7 +53,7 @@ class FoscamCam(Camera): self._name = device_info.get(CONF_NAME) self._motion_status = False - from foscam import FoscamCamera + from foscam.foscam import FoscamCamera self._foscam_session = FoscamCamera(ip_address, port, self._username, self._password, verbose=False) From d986b8f4c2e589ee0e6fd4d61e03c606714e0f37 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sat, 26 Aug 2017 17:09:57 -0400 Subject: [PATCH 08/11] Bump aioautomatic to prevent leaking exceptions (#9148) --- homeassistant/components/device_tracker/automatic.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 071edf42642..a4495926f82 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['aioautomatic==0.6.0'] +REQUIREMENTS = ['aioautomatic==0.6.2'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f1fc40a2187..9349c884272 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -42,7 +42,7 @@ TwitterAPI==2.4.6 abodepy==0.7.1 # homeassistant.components.device_tracker.automatic -aioautomatic==0.6.0 +aioautomatic==0.6.2 # homeassistant.components.sensor.dnsip aiodns==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74717aa7d7b..f286555833e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -27,7 +27,7 @@ PyJWT==1.5.2 SoCo==0.12 # homeassistant.components.device_tracker.automatic -aioautomatic==0.6.0 +aioautomatic==0.6.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 308b822832b423765fdd6ecd2209d4cd7dae4eed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Aug 2017 17:00:59 -0700 Subject: [PATCH 09/11] Wrap state when iterating a domain in templates (#9157) --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index aa6ca186a8e..bdef5541983 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -209,7 +209,7 @@ class DomainStates(object): def __iter__(self): """Return the iteration over all the states.""" return iter(sorted( - (state for state in self._hass.states.async_all() + (_wrap_state(state) for state in self._hass.states.async_all() if state.domain == self._domain), key=lambda state: state.entity_id)) From 3509ecf07ff7fca7a3409e28462ac39ba12cc948 Mon Sep 17 00:00:00 2001 From: mjj4791 Date: Mon, 28 Aug 2017 18:09:36 +0200 Subject: [PATCH 10/11] Prevent iCloud exceptions in logfile (#9179) * Prevent iCloud exceptions in logfile With this change ValueError exceptions in the logfile caused by this component will disappear. These errors are caused by the iCloud API returning an HTTP 450 error and the external lib throwing a ValueError because of it. A PR has been raised against the external library, but that fix did not yet make it into a new version of the library. This will catch the exception in the mean time.... https://github.com/picklepete/pyicloud/pull/138 * Align log messages --- .../components/device_tracker/icloud.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index f20dad1fceb..e670287dd87 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -307,12 +307,15 @@ class Icloud(DeviceScanner): self.api.authenticate() currentminutes = dt_util.now().hour * 60 + dt_util.now().minute - for devicename in self.devices: - interval = self._intervals.get(devicename, 1) - if ((currentminutes % interval == 0) or - (interval > 10 and - currentminutes % interval in [2, 4])): - self.update_device(devicename) + try: + for devicename in self.devices: + interval = self._intervals.get(devicename, 1) + if ((currentminutes % interval == 0) or + (interval > 10 and + currentminutes % interval in [2, 4])): + self.update_device(devicename) + except ValueError: + _LOGGER.debug("iCloud API returned an error") def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" @@ -397,7 +400,7 @@ class Icloud(DeviceScanner): self.see(**kwargs) self.seen_devices[devicename] = True except PyiCloudNoDevicesException: - _LOGGER.error('No iCloud Devices found!') + _LOGGER.error("No iCloud Devices found") def lost_iphone(self, devicename): """Call the lost iPhone function if the device is found.""" From 0ccff6c03e2be946dd9c090772c485fb327401b1 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Mon, 28 Aug 2017 11:12:21 -0400 Subject: [PATCH 11/11] bump ecobee version to fix issue 9190 (#9191) --- homeassistant/components/ecobee.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 9e74299e6bc..c4b0f2e9546 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle -REQUIREMENTS = ['python-ecobee-api==0.0.8'] +REQUIREMENTS = ['python-ecobee-api==0.0.9'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9349c884272..7ec81558945 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -716,7 +716,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.12 # homeassistant.components.ecobee -python-ecobee-api==0.0.8 +python-ecobee-api==0.0.9 # homeassistant.components.climate.eq3btsmart # python-eq3bt==0.1.5