diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py new file mode 100644 index 00000000000..6bffe9afd2c --- /dev/null +++ b/homeassistant/components/mysensors.py @@ -0,0 +1,150 @@ +""" +homeassistant.components.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +MySensors component that connects 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.html +""" +import logging + +from homeassistant.helpers import (validate_config) + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + TEMP_CELCIUS) + +CONF_PORT = 'port' +CONF_DEBUG = 'debug' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_VERSION = 'version' + +DOMAIN = 'mysensors' +DEPENDENCIES = [] +REQUIREMENTS = ['file:///home/martin/Dev/pymysensors-fifo_queue.zip' + '#pymysensors==0.3'] +_LOGGER = logging.getLogger(__name__) +ATTR_NODE_ID = 'node_id' +ATTR_CHILD_ID = 'child_id' + +PLATFORM_FORMAT = '{}.{}' +IS_METRIC = None +DEVICES = None +GATEWAY = None + +EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' +UPDATE_TYPE = 'update_type' +NODE_ID = 'nid' + +CONST = None + + +def setup(hass, config): + """ Setup the MySensors component. """ + + import mysensors.mysensors as mysensors + + if not validate_config(config, + {DOMAIN: [CONF_PORT]}, + _LOGGER): + return False + + version = config[DOMAIN].get(CONF_VERSION, '1.4') + + global CONST + if version == '1.4': + import mysensors.const_14 as const + CONST = const + _LOGGER.info('CONST = %s, 1.4', const) + elif version == '1.5': + import mysensors.const_15 as const + CONST = const + _LOGGER.info('CONST = %s, 1.5', const) + else: + import mysensors.const_14 as const + CONST = const + _LOGGER.info('CONST = %s, 1.4 default', const) + + global IS_METRIC + # 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) + global DEVICES + DEVICES = {} # keep track of devices added to HA + + def node_update(update_type, nid): + """ Callback for node updates from the MySensors gateway. """ + _LOGGER.info('update %s: node %s', update_type, nid) + + hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { + UPDATE_TYPE: update_type, + NODE_ID: nid + }) + + port = config[DOMAIN].get(CONF_PORT) + + persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + persistence_file = config[DOMAIN].get( + CONF_PERSISTENCE_FILE, hass.config.path('mysensors.pickle')) + + global GATEWAY + GATEWAY = mysensors.SerialGateway(port, node_update, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + GATEWAY.metric = IS_METRIC + GATEWAY.debug = config[DOMAIN].get(CONF_DEBUG, False) + GATEWAY.start() + + if persistence: + for nid in GATEWAY.sensors: + node_update('node_update', nid) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: GATEWAY.stop()) + + return True + + +def mysensors_update(platform_type): + """ + Decorator for callback function for sensor updates from the MySensors + component. + """ + def wrapper(gateway, devices, nid): + """Wrapper function in the decorator.""" + sensor = gateway.sensors[nid] + if sensor.sketch_name is None: + _LOGGER.info('No sketch_name: node %s', nid) + return + if nid not in devices: + devices[nid] = {} + node = devices[nid] + new_devices = [] + platform_def = platform_type(gateway, devices, nid) + platform_object = platform_def['platform_class'] + platform_v_types = platform_def['types_to_handle'] + add_devices = platform_def['add_devices'] + for child_id, child in sensor.children.items(): + 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) + if value_type in platform_v_types: + node[child_id][value_type] = \ + platform_object( + gateway, nid, child_id, name, value_type) + new_devices.append(node[child_id][value_type]) + else: + node[child_id][value_type].update_sensor( + value, sensor.battery_level) + _LOGGER.info('sensor_update: %s', new_devices) + if new_devices: + _LOGGER.info('adding new devices: %s', new_devices) + add_devices(new_devices) + return + return wrapper diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index cb959522134..b49fe706f78 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -11,102 +11,63 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, + ATTR_BATTERY_LEVEL, TEMP_CELCIUS, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) -CONF_PORT = "port" -CONF_DEBUG = "debug" -CONF_PERSISTENCE = "persistence" -CONF_PERSISTENCE_FILE = "persistence_file" -CONF_VERSION = "version" +import homeassistant.components.mysensors as mysensors ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' - 'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip' - '#pymysensors==0.3'] +DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the mysensors platform. """ + """ Setup the mysensors platform for sensors. """ - import mysensors.mysensors as mysensors - import mysensors.const_14 as const + v_types = [] + for _, member in mysensors.CONST.SetReq.__members__.items(): + if (member.value != mysensors.CONST.SetReq.V_STATUS and + member.value != mysensors.CONST.SetReq.V_LIGHT and + member.value != mysensors.CONST.SetReq.V_LOCK_STATUS): + v_types.append(member) - 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) + @mysensors.mysensors_update + def _sensor_update(gateway, devices, nid): + """Internal callback for sensor updates.""" + _LOGGER.info("sensor update = %s", devices) + return {'types_to_handle': v_types, + 'platform_class': MySensorsSensor, + 'add_devices': add_devices} - 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] = {} + def sensor_update(event): + """ Callback for sensor updates from the MySensors component. """ + _LOGGER.info( + 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], + event.data[mysensors.NODE_ID]) + _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, + event.data[mysensors.NODE_ID]) - node = devices[nid] - new_devices = [] - for child_id, child in sensor.children.items(): - 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, const) - 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: - _LOGGER.error("Missing required key 'port'") - return False - - persistence = config.get(CONF_PERSISTENCE, True) - persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle') - version = config.get(CONF_VERSION, '1.4') - - gateway = mysensors.SerialGateway(port, sensor_update, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - gateway.metric = is_metric - gateway.debug = config.get(CONF_DEBUG, False) - gateway.start() - - if persistence: - for nid in gateway.sensors: - sensor_update('sensor_update', nid) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) -class MySensorsNodeValue(Entity): +class MySensorsSensor(Entity): + """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, node_id, child_id, name, value_type, metric, const): + + def __init__(self, gateway, node_id, child_id, name, value_type): + self.gateway = gateway self._name = name self.node_id = node_id self.child_id = child_id self.battery_level = 0 self.value_type = value_type - self.metric = metric + self.metric = mysensors.IS_METRIC self._value = '' - self.const = const + self.const = mysensors.CONST @property def should_poll(self): @@ -144,7 +105,7 @@ class MySensorsNodeValue(Entity): } def update_sensor(self, value, battery_level): - """ Update a sensor with the latest value from the controller. """ + """ Update the controller with the latest value from a sensor. """ _LOGGER.info("%s value = %s", self._name, value) if self.value_type == self.const.SetReq.V_TRIPPED or \ self.value_type == self.const.SetReq.V_ARMED: diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py new file mode 100644 index 00000000000..9c67ed44b93 --- /dev/null +++ b/homeassistant/components/switch/mysensors.py @@ -0,0 +1,138 @@ +""" +homeassistant.components.sensor.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for MySensors switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mysensors.html +""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + TEMP_CELCIUS, TEMP_FAHRENHEIT, + STATE_ON, STATE_OFF) + +import homeassistant.components.mysensors as mysensors + +ATTR_NODE_ID = "node_id" +ATTR_CHILD_ID = "child_id" + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mysensors'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup the mysensors platform for switches. """ + + v_types = [] + for _, member in mysensors.CONST.SetReq.__members__.items(): + if (member.value == mysensors.CONST.SetReq.V_STATUS or + member.value == mysensors.CONST.SetReq.V_LIGHT or + member.value == mysensors.CONST.SetReq.V_LOCK_STATUS): + v_types.append(member) + + @mysensors.mysensors_update + def _sensor_update(gateway, devices, nid): + """Internal callback for sensor updates.""" + _LOGGER.info("sensor update = %s", devices) + return {'types_to_handle': v_types, + 'platform_class': MySensorsSwitch, + 'add_devices': add_devices} + + def sensor_update(event): + """ Callback for sensor updates from the MySensors component. """ + _LOGGER.info( + 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], + event.data[mysensors.NODE_ID]) + _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, + event.data[mysensors.NODE_ID]) + + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) + + +class MySensorsSwitch(SwitchDevice): + + """ Represents the value of a MySensors child node. """ + # pylint: disable=too-many-arguments, too-many-instance-attributes + + def __init__(self, gateway, node_id, child_id, name, value_type): + self.gateway = gateway + self._name = name + self.node_id = node_id + self.child_id = child_id + self.battery_level = 0 + self.value_type = value_type + self.metric = mysensors.IS_METRIC + self._value = STATE_OFF + self.const = mysensors.CONST + + @property + def should_poll(self): + """ MySensor gateway pushes its state to HA. """ + return False + + @property + def name(self): + """ The name of this sensor. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._value + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity. """ + if self.value_type == self.const.SetReq.V_TEMP: + return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT + elif self.value_type == self.const.SetReq.V_HUM or \ + self.value_type == self.const.SetReq.V_DIMMER or \ + self.value_type == self.const.SetReq.V_LIGHT_LEVEL: + return '%' + return None + + @property + def state_attributes(self): + """ Returns the state attributes. """ + return { + ATTR_NODE_ID: self.node_id, + ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } + + @property + def is_on(self): + """ Returns True if switch is on. """ + return self._value == STATE_ON + + def turn_on(self): + """ Turns the switch on. """ + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1) + self._value = STATE_ON + self.update_ha_state() + + def turn_off(self): + """ Turns the pin to low/off. """ + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0) + self._value = STATE_OFF + self.update_ha_state() + + def update_sensor(self, value, battery_level): + """ Update the controller with the latest value from a sensor. """ + _LOGGER.info("%s value = %s", self._name, value) + if self.value_type == self.const.SetReq.V_TRIPPED or \ + self.value_type == self.const.SetReq.V_ARMED or \ + self.value_type == self.const.SetReq.V_STATUS or \ + self.value_type == self.const.SetReq.V_LIGHT or \ + self.value_type == self.const.SetReq.V_LOCK_STATUS: + self._value = STATE_ON if int(value) == 1 else STATE_OFF + else: + self._value = value + self.battery_level = battery_level + self.update_ha_state()