""" homeassistant.components.zwave ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Connects Home Assistant to a Z-Wave network. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zwave/ """ import sys import os.path from pprint import pprint from homeassistant.util import slugify, convert from homeassistant import bootstrap from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, ATTR_BATTERY_LEVEL, ATTR_LOCATION, ATTR_ENTITY_ID, CONF_CUSTOMIZE) DOMAIN = "zwave" REQUIREMENTS = ['pydispatcher==2.0.5'] CONF_USB_STICK_PATH = "usb_path" DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" CONF_DEBUG = "debug" CONF_POLLING_INTERVAL = "polling_interval" CONF_POLLING_INTENSITY = "polling_intensity" DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share', 'python-openzwave', 'config') SERVICE_ADD_NODE = "add_node" SERVICE_REMOVE_NODE = "remove_node" DISCOVER_SENSORS = "zwave.sensors" DISCOVER_SWITCHES = "zwave.switch" DISCOVER_LIGHTS = "zwave.light" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" COMMAND_CLASS_SWITCH_MULTILEVEL = 38 COMMAND_CLASS_SWITCH_BINARY = 37 COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_ALARM = 113 # 0x71 GENRE_WHATEVER = None GENRE_USER = "User" TYPE_WHATEVER = None TYPE_BYTE = "Byte" TYPE_BOOL = "Bool" TYPE_DECIMAL = "Decimal" # list of tuple (DOMAIN, discovered service, supported command # classes, value type) DISCOVERY_COMPONENTS = [ ('sensor', DISCOVER_SENSORS, [COMMAND_CLASS_SENSOR_BINARY, COMMAND_CLASS_SENSOR_MULTILEVEL, COMMAND_CLASS_METER, COMMAND_CLASS_ALARM], TYPE_WHATEVER, GENRE_USER), ('light', DISCOVER_LIGHTS, [COMMAND_CLASS_SWITCH_MULTILEVEL], TYPE_BYTE, GENRE_USER), ('switch', DISCOVER_SWITCHES, [COMMAND_CLASS_SWITCH_BINARY], TYPE_BOOL, GENRE_USER), ] ATTR_NODE_ID = "node_id" ATTR_VALUE_ID = "value_id" ATTR_SCENE_ID = "scene_id" NETWORK = None def _obj_to_dict(obj): """ Converts an obj into a hash for debug. """ return {key: getattr(obj, key) for key in dir(obj) if key[0] != '_' and not hasattr(getattr(obj, key), '__call__')} def _node_name(node): """ Returns the name of the node. """ return node.name or "{} {}".format( node.manufacturer_name, node.product_name) def _value_name(value): """ Returns the name of the value. """ return "{} {}".format(_node_name(value.node), value.label) def _object_id(value): """ Returns the object_id of the device value. The object_id contains node_id and value instance id to not collide with other entity_ids""" object_id = "{}_{}".format(slugify(_value_name(value)), value.node.node_id) # Add the instance id if there is more than one instance for the value if value.instance > 1: return "{}_{}".format(object_id, value.instance) return object_id def nice_print_node(node): """ Prints a nice formatted node to the output (debug method). """ node_dict = _obj_to_dict(node) node_dict['values'] = {value_id: _obj_to_dict(value) for value_id, value in node.values.items()} print("\n\n\n") print("FOUND NODE", node.product_name) pprint(node_dict) print("\n\n\n") def get_config_value(node, value_index): """ Returns the current config value for a specific index. """ try: for value in node.values.values(): # 112 == config command class if value.command_class == 112 and value.index == value_index: return value.data except RuntimeError: # If we get an runtime error the dict has changed while # we was looking for a value, just do it again return get_config_value(node, value_index) def setup(hass, config): """ Setup Z-wave. Will automatically load components to support devices found on the network. """ # pylint: disable=global-statement, import-error global NETWORK from pydispatch import dispatcher from openzwave.option import ZWaveOption from openzwave.network import ZWaveNetwork # Load configuration use_debug = str(config[DOMAIN].get(CONF_DEBUG)) == '1' customize = config[DOMAIN].get(CONF_CUSTOMIZE, {}) # Setup options options = ZWaveOption( config[DOMAIN].get(CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH), user_path=hass.config.config_dir, config_path=config[DOMAIN].get('config_path', DEFAULT_ZWAVE_CONFIG_PATH),) options.set_console_output(use_debug) options.lock() NETWORK = ZWaveNetwork(options, autostart=False) if use_debug: def log_all(signal, value=None): """ Log all the signals. """ print("") print("SIGNAL *****", signal) if value and signal in (ZWaveNetwork.SIGNAL_VALUE_CHANGED, ZWaveNetwork.SIGNAL_VALUE_ADDED): pprint(_obj_to_dict(value)) print("") dispatcher.connect(log_all, weak=False) def value_added(node, value): """ Called when a value is added to a node on the network. """ for (component, discovery_service, command_ids, value_type, value_genre) in DISCOVERY_COMPONENTS: if value.command_class not in command_ids: continue if value_type is not None and value_type != value.type: continue if value_genre is not None and value_genre != value.genre: continue # Ensure component is loaded bootstrap.setup_component(hass, component, config) # Configure node name = "{}.{}".format(component, _object_id(value)) node_config = customize.get(name, {}) polling_intensity = convert( node_config.get(CONF_POLLING_INTENSITY), int) if polling_intensity is not None: value.enable_poll(polling_intensity) # Fire discovery event hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { ATTR_SERVICE: discovery_service, ATTR_DISCOVERED: { ATTR_NODE_ID: node.node_id, ATTR_VALUE_ID: value.value_id, } }) def scene_activated(node, scene_id): """ Called when a scene is activated on any node in the network. """ name = _node_name(node) object_id = "{}_{}".format(slugify(name), node.node_id) hass.bus.fire(EVENT_SCENE_ACTIVATED, { ATTR_ENTITY_ID: object_id, ATTR_SCENE_ID: scene_id }) dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT, weak=False) def add_node(event): """ Switch into inclusion mode """ NETWORK.controller.begin_command_add_device() def remove_node(event): """ Switch into exclusion mode""" NETWORK.controller.begin_command_remove_device() def stop_zwave(event): """ Stop Z-wave. """ NETWORK.stop() def start_zwave(event): """ Called when Home Assistant starts up. """ NETWORK.start() polling_interval = convert( config[DOMAIN].get(CONF_POLLING_INTERVAL), int) if polling_interval is not None: NETWORK.set_poll_interval(polling_interval, False) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave) # register add / remove node services for zwave sticks without # hardware inclusion button hass.services.register(DOMAIN, SERVICE_ADD_NODE, add_node) hass.services.register(DOMAIN, SERVICE_REMOVE_NODE, remove_node) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) return True class ZWaveDeviceEntity: """ Represents a ZWave node entity within Home Assistant. """ def __init__(self, value, domain): self._value = value self.entity_id = "{}.{}".format(domain, self._object_id()) @property def should_poll(self): """ False because we will push our own state to HA when changed. """ return False @property def unique_id(self): """ Returns a unique id. """ return "ZWAVE-{}-{}".format(self._value.node.node_id, self._value.object_id) @property def name(self): """ Returns the name of the device. """ return _value_name(self._value) def _object_id(self): """ Returns the object_id of the device value. The object_id contains node_id and value instance id to not collide with other entity_ids""" return _object_id(self._value) @property def device_state_attributes(self): """ Returns device specific state attributes. """ attrs = { ATTR_NODE_ID: self._value.node.node_id, } battery_level = self._value.node.get_battery_level() if battery_level is not None: attrs[ATTR_BATTERY_LEVEL] = battery_level location = self._value.node.location if location: attrs[ATTR_LOCATION] = location return attrs