From e87652e95f7da667ce4e68a3dbbf32546854905c Mon Sep 17 00:00:00 2001 From: theolind Date: Thu, 2 Apr 2015 19:59:44 +0200 Subject: [PATCH 1/7] added HA module for mysensors --- .gitmodules | 3 + homeassistant/components/sensor/mysensors.py | 77 ++++++++++++++++++++ homeassistant/external/pymysensors | 1 + 3 files changed, 81 insertions(+) create mode 100644 homeassistant/components/sensor/mysensors.py create mode 160000 homeassistant/external/pymysensors diff --git a/.gitmodules b/.gitmodules index ae38be7c61b..ca0b1f024b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "homeassistant/external/nzbclients"] path = homeassistant/external/nzbclients url = https://github.com/jamespcole/home-assistant-nzb-clients.git +[submodule "homeassistant/external/pymysensors"] + path = homeassistant/external/pymysensors + url = https://github.com/theolind/pymysensors diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py new file mode 100644 index 00000000000..357aa05331c --- /dev/null +++ b/homeassistant/components/sensor/mysensors.py @@ -0,0 +1,77 @@ +import homeassistant.external.pymysensors.mysensors as mysensors +from homeassistant.helpers.entity import Entity +import logging + +# +# Config: +# sensor: +# platform: mysensors +# gateway: serial +# port: '/dev/ttyACM0' +# + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL ) + +_LOGGER = logging.getLogger(__name__) + +devices = {} # keep track of devices added to HA +gw = mysensors.Gateway(); + +def setup_platform(hass, config, add_devices, discovery_info=None): + + # Passed to pymysensors and called when a sensor is updated + def sensor_update(type, nid, cid = None, value = None): + s = gw.sensors[nid] + if s.sketch_name is not None: + if nid in devices: + devices[nid]._battery_level = s.battery_level + for c in s.children: + child = s.children[c] + devices[nid]._children[child.id] = MySensorsChildSensor(child.type, child.value) + else: + devices[nid] = MySensorsSensor(s.sketch_name) + add_devices([devices[nid]]) + + if config['gateway'] == 'serial': + gw = mysensors.SerialGateway('/dev/ttyACM0', sensor_update) + else: + _LOGGER.error('mysensors gateway type: ' + config.gateway + ' not supported') + + gw.listen() + + +class MySensorsSensor(Entity): + def __init__(self, name): + self._name = name + self._state = '' + self._battery_level = 0 + self._children = {} + + @property + def name(self): + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + """ Returns the state attributes. """ + attrs = {} + attrs[ATTR_BATTERY_LEVEL] = self._battery_level + + for c in self._children.values(): + attrs[c._type] = c._value + + return attrs + + #def update(self): + # get latest values from gateway + +class MySensorsChildSensor(): + def __init__(self, type, value): + self._type = type + self._value = value \ No newline at end of file diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors new file mode 160000 index 00000000000..9b86a46c245 --- /dev/null +++ b/homeassistant/external/pymysensors @@ -0,0 +1 @@ +Subproject commit 9b86a46c245630e30149ba4e3b0729ef2803a702 From c41d7b8f6d27a23310703ba2ffb244678fdb8c62 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Thu, 2 Apr 2015 20:04:28 -0500 Subject: [PATCH 2/7] Refactor and update to use latest pymysensors. --- homeassistant/components/sensor/mysensors.py | 104 +++++++++++-------- homeassistant/external/pymysensors | 2 +- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 357aa05331c..5b772182d9c 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -1,77 +1,93 @@ -import homeassistant.external.pymysensors.mysensors as mysensors -from homeassistant.helpers.entity import Entity +""" +homeassistant.components.sensor.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MySensors sensors. + +Config: + sensor: + - platform: mysensors + port: '/dev/ttyACM0' +""" import logging -# -# Config: -# sensor: -# platform: mysensors -# gateway: serial -# port: '/dev/ttyACM0' -# +# pylint: disable=no-name-in-module, import-error +import homeassistant.external.pymysensors.mysensors.mysensors as mysensors +import homeassistant.external.pymysensors.mysensors.const as const +from homeassistant.helpers.entity import Entity -from homeassistant.const import ( - ATTR_BATTERY_LEVEL ) +from homeassistant.const import ATTR_BATTERY_LEVEL + +CONF_PORT = "port" _LOGGER = logging.getLogger(__name__) -devices = {} # keep track of devices added to HA -gw = mysensors.Gateway(); def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup the mysensors platform. """ - # Passed to pymysensors and called when a sensor is updated - def sensor_update(type, nid, cid = None, value = None): - s = gw.sensors[nid] - if s.sketch_name is not None: - if nid in devices: - devices[nid]._battery_level = s.battery_level - for c in s.children: - child = s.children[c] - devices[nid]._children[child.id] = MySensorsChildSensor(child.type, child.value) - else: - devices[nid] = MySensorsSensor(s.sketch_name) - add_devices([devices[nid]]) + devices = {} # keep track of devices added to HA - if config['gateway'] == 'serial': - gw = mysensors.SerialGateway('/dev/ttyACM0', sensor_update) - else: - _LOGGER.error('mysensors gateway type: ' + config.gateway + ' not supported') + def sensor_update(update_type, nid): + """ Callback for sensor updates from the MySensors gateway. """ + sensor = gateway.sensors[nid] + if sensor.sketch_name is None: + return + if nid not in devices: + devices[nid] = MySensorsNode(sensor.sketch_name) + add_devices([devices[nid]]) - gw.listen() + devices[nid].battery_level = sensor.battery_level + for child_id, child in sensor.children.items(): + devices[nid].update_child(child_id, child) + + port = config.get(CONF_PORT) + if port is None: + _LOGGER.error("Missing required key 'port'") + return False + + gateway = mysensors.SerialGateway(port, sensor_update) + gateway.start() -class MySensorsSensor(Entity): +class MySensorsNode(Entity): + """ Represents a MySensors node. """ def __init__(self, name): self._name = name - self._state = '' - self._battery_level = 0 - self._children = {} + self.battery_level = 0 + self.children = {} @property def name(self): + """ The name of this sensor. """ return self._name @property def state(self): """ Returns the state of the device. """ - return self._state + return '' @property def state_attributes(self): """ Returns the state attributes. """ attrs = {} - attrs[ATTR_BATTERY_LEVEL] = self._battery_level - - for c in self._children.values(): - attrs[c._type] = c._value + attrs[ATTR_BATTERY_LEVEL] = self.battery_level + for child in self.children.values(): + for value_type, value in child.values.items(): + attrs[value_type] = value return attrs - #def update(self): - # get latest values from gateway + def update_child(self, child_id, child): + """ Sets the values of a child sensor. """ + self.children[child_id] = MySensorsChildSensor( + const.Presentation(child.type).name, + {const.SetReq(t).name: v for t, v in child.values.items()}) + class MySensorsChildSensor(): - def __init__(self, type, value): - self._type = type - self._value = value \ No newline at end of file + """ Represents a MySensors child sensor. """ + # pylint: disable=too-few-public-methods + def __init__(self, child_type, values): + self.type = child_type + self.values = values diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors index 9b86a46c245..b3937f3ac15 160000 --- a/homeassistant/external/pymysensors +++ b/homeassistant/external/pymysensors @@ -1 +1 @@ -Subproject commit 9b86a46c245630e30149ba4e3b0729ef2803a702 +Subproject commit b3937f3ac15f47f57ed0cfb95709a77b7b7bed06 From c72a7358515b6442ee532cee2b3846a7ec699901 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Sun, 5 Apr 2015 17:02:48 -0500 Subject: [PATCH 3/7] Use stop method for mysensors on shutdown. --- homeassistant/components/sensor/mysensors.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 5b772182d9c..4451b7d7755 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -16,7 +16,7 @@ import homeassistant.external.pymysensors.mysensors.mysensors as mysensors import homeassistant.external.pymysensors.mysensors.const as const from homeassistant.helpers.entity import Entity -from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.const import (ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP) CONF_PORT = "port" @@ -49,6 +49,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): gateway = mysensors.SerialGateway(port, sensor_update) gateway.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: gateway.stop()) + class MySensorsNode(Entity): """ Represents a MySensors node. """ From 0e9d826d41a52ce15387353593b76dc29a8f3eb5 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Sun, 5 Apr 2015 17:12:07 -0500 Subject: [PATCH 4/7] Push mysensor state instead of polling. --- homeassistant/components/sensor/mysensors.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 4451b7d7755..d678e735292 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -37,9 +37,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices[nid] = MySensorsNode(sensor.sketch_name) add_devices([devices[nid]]) - devices[nid].battery_level = sensor.battery_level + node = devices[nid] + node.battery_level = sensor.battery_level for child_id, child in sensor.children.items(): - devices[nid].update_child(child_id, child) + node.update_child(child_id, child) + node.update_ha_state() port = config.get(CONF_PORT) if port is None: @@ -60,6 +62,11 @@ class MySensorsNode(Entity): self.battery_level = 0 self.children = {} + @property + def should_poll(self): + """ MySensor gateway pushes its state to HA. """ + return False + @property def name(self): """ The name of this sensor. """ From 4d47d313f945f1420810995d8f38896550fe543c Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Sun, 5 Apr 2015 17:15:29 -0500 Subject: [PATCH 5/7] Add mysensors support for metric/imperial units. --- homeassistant/components/sensor/mysensors.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index d678e735292..5c608d57907 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -16,7 +16,8 @@ import homeassistant.external.pymysensors.mysensors.mysensors as mysensors import homeassistant.external.pymysensors.mysensors.const as const from homeassistant.helpers.entity import Entity -from homeassistant.const import (ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, TEMP_CELCIUS) CONF_PORT = "port" @@ -51,6 +52,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): gateway = mysensors.SerialGateway(port, sensor_update) gateway.start() + # Just assume celcius means that the user wants metric for now. + # It may make more sense to make this a global config option in the future. + gateway.metric = (hass.config.temperature_unit == TEMP_CELCIUS) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) From dfbeddf1f71b65f73498a8f9b4af35cc3c058894 Mon Sep 17 00:00:00 2001 From: andythigpen Date: Sat, 11 Apr 2015 11:29:23 -0500 Subject: [PATCH 6/7] Update .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index f41886aaa0f..0dc59d895b9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,7 @@ omit = homeassistant/components/light/hue.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/mysensors.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushover.py homeassistant/components/media_player/cast.py From 3cff05ef91f81b1559ae1072751ecdc1a8055f40 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Sat, 25 Apr 2015 20:07:44 -0500 Subject: [PATCH 7/7] Create entity for each sensor variable. Refactors to create a separate entity for each variable associated with a child sensor. --- homeassistant/components/sensor/mysensors.py | 100 ++++++++++++------- homeassistant/external/pymysensors | 2 +- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 5c608d57907..807ddb6b485 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -17,10 +17,15 @@ import homeassistant.external.pymysensors.mysensors.const as const from homeassistant.helpers.entity import Entity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, TEMP_CELCIUS) + ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, + TEMP_CELCIUS, TEMP_FAHRENHEIT, + STATE_ON, STATE_OFF) CONF_PORT = "port" +ATTR_NODE_ID = "node_id" +ATTR_CHILD_ID = "child_id" + _LOGGER = logging.getLogger(__name__) @@ -28,21 +33,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the mysensors platform. """ devices = {} # keep track of devices added to HA + # Just assume celcius means that the user wants metric for now. + # It may make more sense to make this a global config option in the future. + is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) def sensor_update(update_type, nid): """ Callback for sensor updates from the MySensors gateway. """ + _LOGGER.info("sensor_update %s: node %s", update_type, nid) sensor = gateway.sensors[nid] if sensor.sketch_name is None: return if nid not in devices: - devices[nid] = MySensorsNode(sensor.sketch_name) - add_devices([devices[nid]]) + devices[nid] = {} node = devices[nid] - node.battery_level = sensor.battery_level + new_devices = [] for child_id, child in sensor.children.items(): - node.update_child(child_id, child) - node.update_ha_state() + if child_id not in node: + node[child_id] = {} + for value_type, value in child.values.items(): + if value_type not in node[child_id]: + name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id) + node[child_id][value_type] = \ + MySensorsNodeValue( + nid, child_id, name, value_type, is_metric) + new_devices.append(node[child_id][value_type]) + else: + node[child_id][value_type].update_sensor( + value, sensor.battery_level) + + if new_devices: + _LOGGER.info("adding new devices: %s", new_devices) + add_devices(new_devices) port = config.get(CONF_PORT) if port is None: @@ -51,21 +73,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): gateway = mysensors.SerialGateway(port, sensor_update) gateway.start() - - # Just assume celcius means that the user wants metric for now. - # It may make more sense to make this a global config option in the future. - gateway.metric = (hass.config.temperature_unit == TEMP_CELCIUS) + gateway.metric = is_metric hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) -class MySensorsNode(Entity): - """ Represents a MySensors node. """ - def __init__(self, name): +class MySensorsNodeValue(Entity): + """ Represents the value of a MySensors child node. """ + # pylint: disable=too-many-arguments + def __init__(self, node_id, child_id, name, value_type, metric): self._name = name + self.node_id = node_id + self.child_id = child_id self.battery_level = 0 - self.children = {} + self.value_type = value_type + self.metric = metric + self._value = '' @property def should_poll(self): @@ -80,29 +104,35 @@ class MySensorsNode(Entity): @property def state(self): """ Returns the state of the device. """ - return '' + return self._value + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity. """ + if self.value_type == const.SetReq.V_TEMP: + return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT + elif self.value_type == const.SetReq.V_HUM or \ + self.value_type == const.SetReq.V_DIMMER or \ + self.value_type == const.SetReq.V_LIGHT_LEVEL: + return '%' + return None @property def state_attributes(self): """ Returns the state attributes. """ - attrs = {} - attrs[ATTR_BATTERY_LEVEL] = self.battery_level + return { + ATTR_NODE_ID: self.node_id, + ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } - for child in self.children.values(): - for value_type, value in child.values.items(): - attrs[value_type] = value - return attrs - - def update_child(self, child_id, child): - """ Sets the values of a child sensor. """ - self.children[child_id] = MySensorsChildSensor( - const.Presentation(child.type).name, - {const.SetReq(t).name: v for t, v in child.values.items()}) - - -class MySensorsChildSensor(): - """ Represents a MySensors child sensor. """ - # pylint: disable=too-few-public-methods - def __init__(self, child_type, values): - self.type = child_type - self.values = values + def update_sensor(self, value, battery_level): + """ Update a sensor with the latest value from the controller. """ + _LOGGER.info("%s value = %s", self._name, value) + if self.value_type == const.SetReq.V_TRIPPED or \ + self.value_type == const.SetReq.V_ARMED: + self._value = STATE_ON if int(value) == 1 else STATE_OFF + else: + self._value = value + self.battery_level = battery_level + self.update_ha_state() diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors index b3937f3ac15..75e0f505715 160000 --- a/homeassistant/external/pymysensors +++ b/homeassistant/external/pymysensors @@ -1 +1 @@ -Subproject commit b3937f3ac15f47f57ed0cfb95709a77b7b7bed06 +Subproject commit 75e0f505715b0cbf5a38e63749ea1d6ccf90773a