From 67a04c2a0eb1018024c89b728d962619cdbfa2c4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 24 Jun 2016 10:06:58 +0200 Subject: [PATCH 01/23] Initial clean import --- .coveragerc | 3 + .../components/binary_sensor/homematic.py | 164 ++++++ homeassistant/components/homematic.py | 528 ++++++++++++++++++ homeassistant/components/light/homematic.py | 112 ++++ .../components/rollershutter/homematic.py | 105 ++++ homeassistant/components/sensor/homematic.py | 119 ++++ homeassistant/components/switch/homematic.py | 111 ++++ .../components/thermostat/homematic.py | 205 ++----- requirements_all.txt | 3 + 9 files changed, 1199 insertions(+), 151 deletions(-) create mode 100644 homeassistant/components/binary_sensor/homematic.py create mode 100644 homeassistant/components/homematic.py create mode 100644 homeassistant/components/light/homematic.py create mode 100644 homeassistant/components/rollershutter/homematic.py create mode 100644 homeassistant/components/sensor/homematic.py create mode 100644 homeassistant/components/switch/homematic.py diff --git a/.coveragerc b/.coveragerc index a1b63cf0559..5932ed9a0d0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -84,6 +84,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/homematic.py + homeassistant/components/*/homematic.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/binary_sensor/arest.py diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py new file mode 100644 index 00000000000..fe50a5ef48c --- /dev/null +++ b/homeassistant/components/binary_sensor/homematic.py @@ -0,0 +1,164 @@ +""" +The homematic binary sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration (single channel, simple device): + +binary_sensor: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) + + +Configuration (multiple channels, like motion detector with buttons): + +binary_sensor: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + param: (device-dependent) (optional) + button: n (integer of channel to map, device-dependent) (optional) + name: "" (optional) +binary_sensor: + - platform: homematic + ... +""" + +import logging +from homeassistant.const import STATE_UNKNOWN +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +SENSOR_TYPES_CLASS = { + "Remote": None, + "ShutterContact": "opening", + "Smoke": "smoke", + "SmokeV2": "smoke", + "Motion": "motion", + "MotionV2": "motion", + "RemoteMotion": None +} + +SUPPORT_HM_EVENT_AS_BINMOD = [ + "PRESS_LONG", + "PRESS_SHORT" +] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMBinarySensor, + config, + add_callback_devices) + + +class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): + """Represents diverse binary Homematic units in Home Assistant.""" + + @property + def is_on(self): + """Return True if switch is on.""" + if not self.available: + return False + # no binary is defined, check all! + if self._state is None: + available_bin = self._create_binary_list_from_hm() + for binary in available_bin: + try: + if binary in self._data and self._data[binary] == 1: + return True + except (ValueError, TypeError): + _LOGGER.warning("%s datatype error!", self._name) + return False + + # single binary + return bool(self._hm_get_state()) + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + if not self.available: + return None + + # If state is MOTION (RemoteMotion works only) + if self._state in "MOTION": + return "motion" + return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMBinarySensor\ + as pyHMBinarySensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # check if the homematic device correct for this HA device + if not isinstance(self._hmdevice, pyHMBinarySensor): + _LOGGER.critical("This %s can't be use as binary!", self._name) + return False + + # load possible binary sensor + available_bin = self._create_binary_list_from_hm() + + # if exists user value? + if self._state and self._state not in available_bin: + _LOGGER.critical("This %s have no binary with %s!", self._name, + self._state) + return False + + # only check and give a warining to User + if self._state is None and len(available_bin) > 1: + _LOGGER.warning("%s have multible binary params. It use all " + + "binary nodes as one. Possible param values: %s", + self._name, str(available_bin)) + + return True + + def _init_data_struct(self): + """Generate a data struct (self._data) from hm metadata.""" + super()._init_data_struct() + + # load possible binary sensor + available_bin = self._create_binary_list_from_hm() + + # object have 1 binary + if self._state is None and len(available_bin) == 1: + for value in available_bin: + self._state = value + + # no binary is definit, use all binary for state + if self._state is None and len(available_bin) > 1: + for node in available_bin: + self._data.update({node: STATE_UNKNOWN}) + + # add state to data struct + if self._state: + _LOGGER.debug("%s init datastruct with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + + def _create_binary_list_from_hm(self): + """Generate a own metadata for binary_sensors.""" + bin_data = {} + if not self._hmdevice: + return bin_data + + # copy all data from BINARYNODE + bin_data.update(self._hmdevice.BINARYNODE) + + # copy all hm event they are supportet by this object + for event, channel in self._hmdevice.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + bin_data.update({event: channel}) + + return bin_data diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py new file mode 100644 index 00000000000..751db436def --- /dev/null +++ b/homeassistant/components/homematic.py @@ -0,0 +1,528 @@ +""" +Support for Homematic Devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematic/ + +Configuration: + +homematic: + local_ip: "" + local_port: + remote_ip: "" + remote_port: + autodetect: "" (optional, experimental, detect all devices) +""" +import time +import logging +from collections import OrderedDict +from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ + EVENT_PLATFORM_DISCOVERED,\ + ATTR_SERVICE,\ + ATTR_DISCOVERED,\ + STATE_UNKNOWN +from homeassistant.loader import get_component +from homeassistant.helpers.entity import Entity +import homeassistant.bootstrap + +DOMAIN = 'homematic' +REQUIREMENTS = ['pyhomematic==0.1.6'] + +HOMEMATIC = None +HOMEMATIC_DEVICES = {} +HOMEMATIC_AUTODETECT = False + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_ROLLERSHUTTER = "homematic.rollershutter" +DISCOVER_THERMOSTATS = "homematic.thermostat" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_DISCOVER_CONFIG = "config" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], + DISCOVER_LIGHTS: ["Dimmer"], + DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", + "RemoteMotion", "ThermostatWall", "AreaThermostat", + "RotaryHandleSensor", "GongSensor"], + DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], + DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion"], + DISCOVER_ROLLERSHUTTER: ["Blind"] +} + +HM_IGNORE_DISCOVERY_NODE = [ + "ACTUAL_TEMPERATURE" +] + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["Battery", {0: "High", 1: "Low"}], + "ERROR": ["Sabotage", {0: "No", 1: "Yes"}], + "RSSI_DEVICE": ["RSSI", {}], + "VALVE_STATE": ["Valve", {}], + "BATTERY_STATE": ["Battery", {}], + "CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}], + "POWER": ["Power", {}], + "CURRENT": ["Current", {}], + "VOLTAGE": ["Voltage", {}] +} + +_HM_DISCOVER_HASS = None +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the Homematic component.""" + global HOMEMATIC, HOMEMATIC_AUTODETECT, _HM_DISCOVER_HASS + + from pyhomematic import HMConnection + + local_ip = config[DOMAIN].get("local_ip", None) + local_port = config[DOMAIN].get("local_port", 8943) + remote_ip = config[DOMAIN].get("remote_ip", None) + remote_port = config[DOMAIN].get("remote_port", 2001) + autodetect = config[DOMAIN].get("autodetect", False) + + if remote_ip is None or local_ip is None: + _LOGGER.error("Missing remote CCU/Homegear or local address") + return False + + # Create server thread + HOMEMATIC_AUTODETECT = autodetect + _HM_DISCOVER_HASS = hass + HOMEMATIC = HMConnection(local=local_ip, + localport=local_port, + remote=remote_ip, + remoteport=remote_port, + systemcallback=system_callback_handler, + interface_id="homeassistant") + + # Start server thread, connect to peer, initialize to receive events + HOMEMATIC.start() + + # Stops server when Homeassistant is shutting down + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop) + hass.config.components.append(DOMAIN) + + return True + + +# pylint: disable=too-many-branches +def system_callback_handler(src, *args): + """Callback handler.""" + if src == 'newDevices': + # pylint: disable=unused-variable + (interface_id, dev_descriptions) = args + key_dict = {} + # Get list of all keys of the devices (ignoring channels) + for dev in dev_descriptions: + key_dict[dev['ADDRESS'].split(':')[0]] = True + # Connect devices already created in HA to pyhomematic and + # add remaining devices to list + devices_not_created = [] + for dev in key_dict: + try: + if dev in HOMEMATIC_DEVICES: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + else: + devices_not_created.append(dev) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed to setup device %s: %s", str(dev), + str(err)) + # If configuration allows autodetection of devices, + # all devices not configured are added. + if HOMEMATIC_AUTODETECT and devices_not_created: + for component_name, discovery_type in ( + ('switch', DISCOVER_SWITCHES), + ('light', DISCOVER_LIGHTS), + ('rollershutter', DISCOVER_ROLLERSHUTTER), + ('binary_sensor', DISCOVER_BINARY_SENSORS), + ('sensor', DISCOVER_SENSORS), + ('thermostat', DISCOVER_THERMOSTATS)): + # Get all devices of a specific type + try: + found_devices = _get_devices(discovery_type, + devices_not_created) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed generate opt %s with error '%s'", + component_name, str(err)) + + # When devices of this type are found + # they are setup in HA and an event is fired + if found_devices: + try: + component = get_component(component_name) + config = {component.DOMAIN: found_devices} + + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + _HM_DISCOVER_HASS, + component.DOMAIN, + config) + + # Fire discovery event + _HM_DISCOVER_HASS.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' + } + } + ) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed to autotetect %s with" + + "error '%s'", component_name, str(err)) + for dev in devices_not_created: + if dev in HOMEMATIC_DEVICES: + try: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + # Need to wait, if you have a lot devices we don't + # to overload CCU/Homegear + time.sleep(1) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed link %s with" + + "error '%s'", dev, str(err)) + + +def _get_devices(device_type, keys): + """Get devices.""" + from homeassistant.components.binary_sensor.homematic import \ + SUPPORT_HM_EVENT_AS_BINMOD + + # run + device_arr = [] + if not keys: + keys = HOMEMATIC.devices + for key in keys: + device = HOMEMATIC.devices[key] + if device.__class__.__name__ in HM_DEVICE_TYPES[device_type]: + elements = device.ELEMENT + 1 + metadata = {} + + # Load metadata if needed to generate a param list + if device_type is DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type is DISCOVER_BINARY_SENSORS: + metadata.update(device.BINARYNODE) + + # Also add supported events as binary type + for event, channel in device.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + metadata.update({event: channel}) + + params = _create_params_list(device, metadata) + + # Generate options for 1...n elements with 1...n params + for channel in range(1, elements): + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + ordered_device_dict = OrderedDict() + ordered_device_dict["platform"] = "homematic" + ordered_device_dict["address"] = key + ordered_device_dict["name"] = name + ordered_device_dict["button"] = channel + if param is not None: + ordered_device_dict["param"] = param + + # Add new device + device_arr.append(ordered_device_dict) + _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) + return device_arr + + +def _create_params_list(hmdevice, metadata): + """Create a list from HMDevice with all possible parameters in config.""" + params = {} + elements = hmdevice.ELEMENT + 1 + + # Search in sensor and binary metadata per elements + for channel in range(1, elements): + param_chan = [] + for node, meta_chan in metadata.items(): + # Is this attribute ignored? + if node in HM_IGNORE_DISCOVERY_NODE: + continue + if meta_chan == 'c' or meta_chan is None: + # Only channel linked data + param_chan.append(node) + elif channel == 1: + # First channel can have other data channel + param_chan.append(node) + + # Default parameter + if len(param_chan) == 0: + param_chan.append(None) + # Add to channel + params.update({channel: param_chan}) + + _LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS, + str(params)) + return params + + +def _create_ha_name(name, channel, param): + """Generate a unique object name.""" + # HMDevice is a simple device + if channel == 1 and param is None: + return name + + # Has multiple elements/channels + if channel > 1 and param is None: + return name + " " + str(channel) + + # With multiple param first elements + if channel == 1 and param is not None: + return name + " " + param + + # Multiple param on object with multiple elements + if channel > 1 and param is not None: + return name + " " + str(channel) + " " + param + + +def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): + """Helper to setup Homematic devices.""" + if HOMEMATIC is None: + _LOGGER.error('Error setting up HMDevice: Server not configured.') + return False + + address = config.get('address', None) + if address is None: + _LOGGER.error("Error setting up device '%s': " + + "'address' missing in configuration.", address) + return False + + # Create a new HA homematic object + new_device = hmdevicetype(config) + if address not in HOMEMATIC_DEVICES: + HOMEMATIC_DEVICES[address] = [] + HOMEMATIC_DEVICES[address].append(new_device) + + # Add to HA + add_callback_devices([new_device]) + return True + + +class HMDevice(Entity): + """Homematic device base object.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, config): + """Initialize generic HM device.""" + self._name = config.get("name", None) + self._address = config.get("address", None) + self._channel = config.get("button", 1) + self._state = config.get("param", None) + self._hidden = config.get("hidden", False) + self._data = {} + self._hmdevice = None + self._connected = False + self._available = False + + # Set param to uppercase + if self._state: + self._state = self._state.upper() + + # Generate name + if not self._name: + self._name = _create_ha_name(name=self._address, + channel=self._channel, + param=self._state) + + @property + def should_poll(self): + """Return False. Homematic states are pushed by the XML RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def assumed_state(self): + """Return True if unable to access real state of the device.""" + return not self._available + + @property + def available(self): + """Return True if device is available.""" + return self._available + + @property + def hidden(self): + """Return True if the entity should be hidden from UIs.""" + return self._hidden + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate an attributes list + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attributes and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + return attr + + def link_homematic(self): + """Connect to homematic.""" + # Does a HMDevice from pyhomematic exist? + if self._address in HOMEMATIC.devices: + # Init + self._hmdevice = HOMEMATIC.devices[self._address] + self._connected = True + + # Check if HM class is okay for HA class + _LOGGER.info("Start linking %s to %s", self._address, self._name) + if self._check_hm_to_ha_object(): + # Init datapoints of this object + self._init_data_struct() + self._load_init_data_from_hm() + _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) + + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + else: + _LOGGER.critical("Delink %s object from HM!", self._name) + self._connected = False + self._available = False + + # Update HA + _LOGGER.debug("%s linking down, send update_ha_state", self._name) + self.update_ha_state() + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s receive event '%s' value: %s", self._name, + attribute, value) + have_change = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + have_change = True + + # If available it has changed + if attribute is "UNREACH": + self._available = bool(value) + have_change = True + + # If it has changed, update HA + if have_change: + _LOGGER.debug("%s update_ha_state after '%s'", self._name, + attribute) + self.update_ha_state() + + # Reset events + if attribute in self._hmdevice.EVENTNODE: + _LOGGER.debug("%s reset event", self._name) + self._data[attribute] = False + self.update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = {} + + # Push data to channels_to_sub from hmdevice metadata + for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE): + for node, channel in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if channel == 'c' or channel is None: + channel = self._channel + # Prepare for subscription + try: + if int(channel) > 0: + channels_to_sub.update({int(channel): True}) + except (ValueError, TypeError): + _LOGGER("Invalid channel in metadata from %s", + self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %s from %s", + str(channel), self._name) + self._hmdevice.setEventCallback(callback=self._hm_event_callback, + bequeath=False, + channel=channel) + + def _load_init_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, + self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)): + for node in metadata: + if node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + # Set events to False + for node in self._hmdevice.EVENTNODE: + if node in self._data: + self._data[node] = False + + return True + + def _hm_set_state(self, value): + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + if self._state in self._data: + return self._data[self._state] + return None + + def _check_hm_to_ha_object(self): + """Check if it is possible to use the HM Object as this HA type. + + NEEDS overwrite by inherit! + """ + if not self._connected or self._hmdevice is None: + _LOGGER.error("HA object is not linked to homematic.") + return False + + # Check if button option is correctly set for this object + if self._channel > self._hmdevice.ELEMENT: + _LOGGER.critical("Button option is not correct for this object!") + return False + + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata. + + NEEDS overwrite by inherit! + """ + # Add all attributes to data dict + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: STATE_UNKNOWN}) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py new file mode 100644 index 00000000000..94dabb0f00a --- /dev/null +++ b/homeassistant/components/light/homematic.py @@ -0,0 +1,112 @@ +""" +The homematic light platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +light: + - platform: homematic + addresss: # e.g. "JEQ0XXXXXXX" + name: (optional) + button: n (integer of channel to map, device-dependent) +""" + +import logging +from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +# List of component names (string) your component depends upon. +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMLight, + config, + add_callback_devices) + + +class HMLight(homematic.HMDevice, Light): + """Represents a Homematic Light in Home Assistant.""" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if not self.available: + return None + # Is dimmer? + if self._state is "LEVEL": + return int(self._hm_get_state() * 255) + else: + return None + + @property + def is_on(self): + """Return True if light is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + def turn_on(self, **kwargs): + """Turn the light on.""" + if not self.available: + return + + if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL": + percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 + self._hmdevice.set_level(percent_bright, self._channel) + else: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the light off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as light!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py new file mode 100644 index 00000000000..e0dd5e5469f --- /dev/null +++ b/homeassistant/components/rollershutter/homematic.py @@ -0,0 +1,105 @@ +""" +The homematic rollershutter platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +rollershutter: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) +""" + +import logging +from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) +from homeassistant.components.rollershutter import RollershutterDevice,\ + ATTR_CURRENT_POSITION +import homeassistant.components.homematic as homematic + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMRollershutter, + config, + add_callback_devices) + + +class HMRollershutter(homematic.HMDevice, RollershutterDevice): + """Represents a Homematic Rollershutter in Home Assistant.""" + + @property + def current_position(self): + """ + Return current position of rollershutter. + + None is unknown, 0 is closed, 100 is fully open. + """ + if self.available: + return int((1 - self._hm_get_state()) * 100) + return None + + def position(self, **kwargs): + """Move to a defined position: 0 (closed) and 100 (open).""" + if self.available: + if ATTR_CURRENT_POSITION in kwargs: + position = float(kwargs[ATTR_CURRENT_POSITION]) + position = min(100, max(0, position)) + level = (100 - position) / 100.0 + self._hmdevice.set_level(level, self._channel) + + @property + def state(self): + """Return the state of the rollershutter.""" + current = self.current_position + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 100 else STATE_OPEN + + def move_up(self, **kwargs): + """Move the rollershutter up.""" + if self.available: + self._hmdevice.move_up(self._channel) + + def move_down(self, **kwargs): + """Move the rollershutter down.""" + if self.available: + self._hmdevice.move_down(self._channel) + + def stop(self, **kwargs): + """Stop the device if in motion.""" + if self.available: + self._hmdevice.stop(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Blind + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Blind): + return True + + _LOGGER.critical("This %s can't be use as rollershutter!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._state = "LEVEL" + self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py new file mode 100644 index 00000000000..52ece78f59e --- /dev/null +++ b/homeassistant/components/sensor/homematic.py @@ -0,0 +1,119 @@ +""" +The homematic sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +sensor: + - platform: homematic + address: # e.g. "JEQ0XXXXXXX" + name: (optional) + param: (optional) +""" + +import logging +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +HM_STATE_HA_CAST = { + "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"} +} + +HM_UNIT_HA_CAST = { + "HUMIDITY": "%", + "TEMPERATURE": "°C", + "BRIGHTNESS": "#", + "POWER": "W", + "CURRENT": "mA", + "VOLTAGE": "V", + "ENERGY_COUNTER": "Wh" +} + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMSensor, + config, + add_callback_devices) + + +class HMSensor(homematic.HMDevice): + """Represents various Homematic sensors in Home Assistant.""" + + @property + def state(self): + """Return the state of the sensor.""" + if not self.available: + return STATE_UNKNOWN + + # Does a cast exist for this class? + name = self._hmdevice.__class__.__name__ + if name in HM_STATE_HA_CAST: + return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None) + + # No cast, return original value + return self._hm_get_state() + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if not self.available: + return None + + return HM_UNIT_HA_CAST.get(self._state, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMSensor as pyHMSensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if not isinstance(self._hmdevice, pyHMSensor): + _LOGGER.critical("This %s can't be use as sensor!", self._name) + return False + + # Does user defined value exist? + if self._state and self._state not in self._hmdevice.SENSORNODE: + # pylint: disable=logging-too-many-args + _LOGGER.critical("This %s have no sensor with %s! Values are", + self._name, self._state, + str(self._hmdevice.SENSORNODE.keys())) + return False + + # No param is set and more than 1 sensor nodes are present + if self._state is None and len(self._hmdevice.SENSORNODE) > 1: + _LOGGER.critical("This %s has multiple sensor nodes. " + + "Please us param. Values are: %s", self._name, + str(self._hmdevice.SENSORNODE.keys())) + return False + + _LOGGER.debug("%s is okay for linking", self._name) + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + if self._state is None and len(self._hmdevice.SENSORNODE) == 1: + for value in self._hmdevice.SENSORNODE: + self._state = value + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init sensor %s.", self._name) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py new file mode 100644 index 00000000000..5a630f43022 --- /dev/null +++ b/homeassistant/components/switch/homematic.py @@ -0,0 +1,111 @@ +""" +The homematic switch platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +switch: + - platform: homematic + address: # e.g. "JEQ0XXXXXXX" + name: (optional) + button: n (integer of channel to map, device-dependent) (optional) +""" + +import logging +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMSwitch, + config, + add_callback_devices) + + +class HMSwitch(homematic.HMDevice, SwitchDevice): + """Represents a Homematic Switch in Home Assistant.""" + + @property + def is_on(self): + """Return True if switch is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + @property + def current_power_mwh(self): + """Return the current power usage in mWh.""" + if "ENERGY_COUNTER" in self._data: + try: + return self._data["ENERGY_COUNTER"] / 1000 + except ZeroDivisionError: + return 0 + + return None + + def turn_on(self, **kwargs): + """Turn the switch on.""" + if self.available: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as switch!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + from pyhomematic.devicetypes.actors import Dimmer,\ + Switch, SwitchPowermeter + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Need sensor values for SwitchPowermeter + if isinstance(self._hmdevice, SwitchPowermeter): + for node in self._hmdevice.SENSORNODE: + self._data.update({node: STATE_UNKNOWN}) + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init data dict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index b4ecc6c166b..a1ed06bc4bd 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,121 +1,40 @@ """ -Support for Homematic (HM-TC-IT-WM-W-EU, HM-CC-RT-DN) thermostats. +The Homematic thermostat platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +thermostat: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) """ + import logging -import socket -from xmlrpc.client import ServerProxy -from xmlrpc.client import Error -from collections import namedtuple - +import homeassistant.components.homematic as homematic from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.temperature import convert +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN -REQUIREMENTS = [] +DEPENDENCIES = ['homematic'] _LOGGER = logging.getLogger(__name__) -CONF_ADDRESS = 'address' -CONF_DEVICES = 'devices' -CONF_ID = 'id' -PROPERTY_SET_TEMPERATURE = 'SET_TEMPERATURE' -PROPERTY_VALVE_STATE = 'VALVE_STATE' -PROPERTY_ACTUAL_TEMPERATURE = 'ACTUAL_TEMPERATURE' -PROPERTY_BATTERY_STATE = 'BATTERY_STATE' -PROPERTY_LOWBAT = 'LOWBAT' -PROPERTY_CONTROL_MODE = 'CONTROL_MODE' -PROPERTY_BURST_MODE = 'BURST_RX' -TYPE_HM_THERMOSTAT = 'HOMEMATIC_THERMOSTAT' -TYPE_HM_WALLTHERMOSTAT = 'HOMEMATIC_WALLTHERMOSTAT' -TYPE_MAX_THERMOSTAT = 'MAX_THERMOSTAT' -HomematicConfig = namedtuple('HomematicConfig', - ['device_type', - 'platform_type', - 'channel', - 'maint_channel']) - -HM_TYPE_MAPPING = { - 'HM-CC-RT-DN': HomematicConfig('HM-CC-RT-DN', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-CC-RT-DN-BoM': HomematicConfig('HM-CC-RT-DN-BoM', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-TC-IT-WM-W-EU': HomematicConfig('HM-TC-IT-WM-W-EU', - TYPE_HM_WALLTHERMOSTAT, - 2, 2), - 'BC-RT-TRX-CyG': HomematicConfig('BC-RT-TRX-CyG', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-2': HomematicConfig('BC-RT-TRX-CyG-2', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-3': HomematicConfig('BC-RT-TRX-CyG-3', - TYPE_MAX_THERMOSTAT, - 1, 0) -} +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMThermostat, + config, + add_callback_devices) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Homematic thermostat.""" - devices = [] - try: - address = config[CONF_ADDRESS] - homegear = ServerProxy(address) - - for name, device_cfg in config[CONF_DEVICES].items(): - # get device description to detect the type - device_type = homegear.getDeviceDescription( - device_cfg[CONF_ID] + ':-1')['TYPE'] - - if device_type in HM_TYPE_MAPPING.keys(): - devices.append(HomematicThermostat( - HM_TYPE_MAPPING[device_type], - address, - device_cfg[CONF_ID], - name)) - else: - raise ValueError( - "Device Type '{}' currently not supported".format( - device_type)) - except socket.error: - _LOGGER.exception("Connection error to homematic web service") - return False - - add_devices(devices) - - return True - - -# pylint: disable=too-many-instance-attributes -class HomematicThermostat(ThermostatDevice): - """Representation of a Homematic thermostat.""" - - def __init__(self, hm_config, address, _id, name): - """Initialize the thermostat.""" - self._hm_config = hm_config - self.address = address - self._id = _id - self._name = name - self._full_device_name = '{}:{}'.format(self._id, - self._hm_config.channel) - self._maint_device_name = '{}:{}'.format(self._id, - self._hm_config.maint_channel) - self._current_temperature = None - self._target_temperature = None - self._valve = None - self._battery = None - self._mode = None - self.update() - - @property - def name(self): - """Return the name of the Homematic device.""" - return self._name +class HMThermostat(homematic.HMDevice, ThermostatDevice): + """Represents a Homematic Thermostat in Home Assistant.""" @property def unit_of_measurement(self): @@ -125,26 +44,22 @@ class HomematicThermostat(ThermostatDevice): @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + if not self.available: + return None + return self._data["ACTUAL_TEMPERATURE"] @property def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature + """Return the target temperature.""" + if not self.available: + return None + return self._data["SET_TEMPERATURE"] def set_temperature(self, temperature): """Set new target temperature.""" - device = ServerProxy(self.address) - device.setValue(self._full_device_name, - PROPERTY_SET_TEMPERATURE, - temperature) - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return {"valve": self._valve, - "battery": self._battery, - "mode": self._mode} + if not self.available: + return None + self._hmdevice.set_temperature(temperature) @property def min_temp(self): @@ -156,39 +71,27 @@ class HomematicThermostat(ThermostatDevice): """Return the maximum temperature - 30.5 means on.""" return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) - def update(self): - """Update the data from the thermostat.""" - try: - device = ServerProxy(self.address) - self._current_temperature = device.getValue( - self._full_device_name, - PROPERTY_ACTUAL_TEMPERATURE) - self._target_temperature = device.getValue( - self._full_device_name, - PROPERTY_SET_TEMPERATURE) - self._valve = device.getValue( - self._full_device_name, - PROPERTY_VALVE_STATE) - self._mode = device.getValue( - self._full_device_name, - PROPERTY_CONTROL_MODE) + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.thermostats import HMThermostat\ + as pyHMThermostat - if self._hm_config.platform_type in [TYPE_HM_THERMOSTAT, - TYPE_HM_WALLTHERMOSTAT]: - self._battery = device.getValue(self._maint_device_name, - PROPERTY_BATTERY_STATE) - elif self._hm_config.platform_type == TYPE_MAX_THERMOSTAT: - # emulate homematic battery voltage, - # max reports lowbat if voltage < 2.2V - # while homematic battery_state should - # be between 1.5V and 4.6V - lowbat = device.getValue(self._maint_device_name, - PROPERTY_LOWBAT) - if lowbat: - self._battery = 1.5 - else: - self._battery = 4.6 + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False - except Error: - _LOGGER.exception("Did not receive any temperature data from the " - "homematic API.") + # Check if the homematic device correct for this HA device + if isinstance(self._hmdevice, pyHMThermostat): + return True + + _LOGGER.critical("This %s can't be use as thermostat", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._data.update({"CONTROL_MODE": STATE_UNKNOWN, + "SET_TEMPERATURE": STATE_UNKNOWN, + "ACTUAL_TEMPERATURE": STATE_UNKNOWN}) diff --git a/requirements_all.txt b/requirements_all.txt index a5ef32201c5..f3670ea35da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,6 +257,9 @@ pyenvisalink==1.0 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.homematic +pyhomematic==0.1.6 + # homeassistant.components.device_tracker.icloud pyicloud==0.8.3 From dfe1b8d9344fbed665896bf6c2b65de8fbb5f280 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 24 Jun 2016 19:46:42 +0200 Subject: [PATCH 02/23] Fixed minor feature-detection bug with incomplet configuration --- homeassistant/components/binary_sensor/homematic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index fe50a5ef48c..d2005f99ba5 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -89,7 +89,7 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): return None # If state is MOTION (RemoteMotion works only) - if self._state in "MOTION": + if self._state == "MOTION": return "motion" return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) From 04748e3ad168113ad86fe1d546369ac3766e9b95 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 15:10:19 +0200 Subject: [PATCH 03/23] First batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 101 ++++++++++++-------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 751db436def..81b4fde9596 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,7 +15,6 @@ homematic: """ import time import logging -from collections import OrderedDict from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ EVENT_PLATFORM_DISCOVERED,\ ATTR_SERVICE,\ @@ -157,31 +156,27 @@ def system_callback_handler(src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - try: - component = get_component(component_name) - config = {component.DOMAIN: found_devices} + component = get_component(component_name) + config = {component.DOMAIN: found_devices} - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, - component.DOMAIN, - config) + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + _HM_DISCOVER_HASS, + component.DOMAIN, + config) - # Fire discovery event - _HM_DISCOVER_HASS.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } + # Fire discovery event + _HM_DISCOVER_HASS.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' } - ) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed to autotetect %s with" + - "error '%s'", component_name, str(err)) + } + ) + for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: try: @@ -207,39 +202,37 @@ def _get_devices(device_type, keys): keys = HOMEMATIC.devices for key in keys: device = HOMEMATIC.devices[key] - if device.__class__.__name__ in HM_DEVICE_TYPES[device_type]: - elements = device.ELEMENT + 1 - metadata = {} + if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: + continue + elements = device.ELEMENT + 1 + metadata = {} - # Load metadata if needed to generate a param list - if device_type is DISCOVER_SENSORS: - metadata.update(device.SENSORNODE) - elif device_type is DISCOVER_BINARY_SENSORS: - metadata.update(device.BINARYNODE) + # Load metadata if needed to generate a param list + if device_type is DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type is DISCOVER_BINARY_SENSORS: + metadata.update(device.BINARYNODE) - # Also add supported events as binary type - for event, channel in device.EVENTNODE.items(): - if event in SUPPORT_HM_EVENT_AS_BINMOD: - metadata.update({event: channel}) + # Also add supported events as binary type + for event, channel in device.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + metadata.update({event: channel}) - params = _create_params_list(device, metadata) + params = _create_params_list(device, metadata) - # Generate options for 1...n elements with 1...n params - for channel in range(1, elements): - for param in params[channel]: - name = _create_ha_name(name=device.NAME, - channel=channel, - param=param) - ordered_device_dict = OrderedDict() - ordered_device_dict["platform"] = "homematic" - ordered_device_dict["address"] = key - ordered_device_dict["name"] = name - ordered_device_dict["button"] = channel - if param is not None: - ordered_device_dict["param"] = param + # Generate options for 1...n elements with 1...n params + for channel in range(1, elements): + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + device_dict = dict(platform="homematic", address=key, + name=name, button=channel) + if param is not None: + device_dict["param"] = param - # Add new device - device_arr.append(ordered_device_dict) + # Add new device + device_arr.append(device_dict) _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) return device_arr @@ -282,15 +275,15 @@ def _create_ha_name(name, channel, param): # Has multiple elements/channels if channel > 1 and param is None: - return name + " " + str(channel) + return "{} {}".format(name, channel) # With multiple param first elements if channel == 1 and param is not None: - return name + " " + param + return "{} {}".format(name, param) # Multiple param on object with multiple elements if channel > 1 and param is not None: - return name + " " + str(channel) + " " + param + return "{} {} {}".format(name, channel, param) def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): From 5ca26fc13fb6ceed3282bf9eb68dc0c6abc56613 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 16:25:33 +0200 Subject: [PATCH 04/23] Moved try/except-block and moved delay to link_homematic --- homeassistant/components/homematic.py | 58 ++++++++++++++------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 81b4fde9596..7db0d926dda 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -124,16 +124,12 @@ def system_callback_handler(src, *args): # add remaining devices to list devices_not_created = [] for dev in key_dict: - try: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - else: - devices_not_created.append(dev) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed to setup device %s: %s", str(dev), - str(err)) + if dev in HOMEMATIC_DEVICES: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + else: + devices_not_created.append(dev) + # If configuration allows autodetection of devices, # all devices not configured are added. if HOMEMATIC_AUTODETECT and devices_not_created: @@ -179,16 +175,8 @@ def system_callback_handler(src, *args): for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: - try: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - # Need to wait, if you have a lot devices we don't - # to overload CCU/Homegear - time.sleep(1) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed link %s with" + - "error '%s'", dev, str(err)) + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic(delay=1) def _get_devices(device_type, keys): @@ -374,7 +362,7 @@ class HMDevice(Entity): return attr - def link_homematic(self): + def link_homematic(self, delay=0): """Connect to homematic.""" # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: @@ -385,14 +373,26 @@ class HMDevice(Entity): # Check if HM class is okay for HA class _LOGGER.info("Start linking %s to %s", self._address, self._name) if self._check_hm_to_ha_object(): - # Init datapoints of this object - self._init_data_struct() - self._load_init_data_from_hm() - _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) + try: + # Init datapoints of this object + self._init_data_struct() + if delay: + # We delay / pause loading of data to avoid overloading + # of CCU / Homegear when doing auto detection + time.sleep(delay) + self._load_init_data_from_hm() + _LOGGER.debug("%s datastruct: %s", + self._name, str(self._data)) - # Link events from pyhomatic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + # pylint: disable=broad-except + except Exception as err: + self._connected = False + self._available = False + _LOGGER.error("Exception while linking %s: %s" % + (self._address, str(err))) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False @@ -401,6 +401,8 @@ class HMDevice(Entity): # Update HA _LOGGER.debug("%s linking down, send update_ha_state", self._name) self.update_ha_state() + else: + _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" From 43faeff42a60f9b14cd1e19e5c1926b75ca80956 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 18:19:05 +0200 Subject: [PATCH 05/23] Moved trx/except, added debug messages, minor fixes --- homeassistant/components/homematic.py | 112 ++++++++++++++------------ 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 7db0d926dda..317397c0b9c 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,11 +15,11 @@ homematic: """ import time import logging -from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ - EVENT_PLATFORM_DISCOVERED,\ - ATTR_SERVICE,\ - ATTR_DISCOVERED,\ - STATE_UNKNOWN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ + EVENT_PLATFORM_DISCOVERED, \ + ATTR_SERVICE, \ + ATTR_DISCOVERED, \ + STATE_UNKNOWN from homeassistant.loader import get_component from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -141,13 +141,8 @@ def system_callback_handler(src, *args): ('sensor', DISCOVER_SENSORS), ('thermostat', DISCOVER_THERMOSTATS)): # Get all devices of a specific type - try: - found_devices = _get_devices(discovery_type, - devices_not_created) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed generate opt %s with error '%s'", - component_name, str(err)) + found_devices = _get_devices(discovery_type, + devices_not_created) # When devices of this type are found # they are setup in HA and an event is fired @@ -157,21 +152,21 @@ def system_callback_handler(src, *args): # Ensure component is loaded homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, - component.DOMAIN, - config) + _HM_DISCOVER_HASS, + component.DOMAIN, + config) # Fire discovery event _HM_DISCOVER_HASS.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' } } - ) + ) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: @@ -192,13 +187,12 @@ def _get_devices(device_type, keys): device = HOMEMATIC.devices[key] if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: continue - elements = device.ELEMENT + 1 metadata = {} # Load metadata if needed to generate a param list - if device_type is DISCOVER_SENSORS: + if device_type == DISCOVER_SENSORS: metadata.update(device.SENSORNODE) - elif device_type is DISCOVER_BINARY_SENSORS: + elif device_type == DISCOVER_BINARY_SENSORS: metadata.update(device.BINARYNODE) # Also add supported events as binary type @@ -207,45 +201,57 @@ def _get_devices(device_type, keys): metadata.update({event: channel}) params = _create_params_list(device, metadata) + if params: + # Generate options for 1...n elements with 1...n params + for channel in range(1, device.ELEMENT + 1): + _LOGGER.debug("Handling %s:%i", key, channel) + if channel in params: + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + device_dict = dict(platform="homematic", + address=key, + name=name, + button=channel) + if param is not None: + device_dict["param"] = param - # Generate options for 1...n elements with 1...n params - for channel in range(1, elements): - for param in params[channel]: - name = _create_ha_name(name=device.NAME, - channel=channel, - param=param) - device_dict = dict(platform="homematic", address=key, - name=name, button=channel) - if param is not None: - device_dict["param"] = param - - # Add new device - device_arr.append(device_dict) - _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) + # Add new device + device_arr.append(device_dict) + else: + _LOGGER.debug("Channel %i not in params", channel) + else: + _LOGGER.debug("Got no params for %s", key) + _LOGGER.debug("%s autodiscovery: %s", + device_type, str(device_arr)) return device_arr def _create_params_list(hmdevice, metadata): """Create a list from HMDevice with all possible parameters in config.""" params = {} - elements = hmdevice.ELEMENT + 1 # Search in sensor and binary metadata per elements - for channel in range(1, elements): + for channel in range(1, hmdevice.ELEMENT + 1): param_chan = [] - for node, meta_chan in metadata.items(): - # Is this attribute ignored? - if node in HM_IGNORE_DISCOVERY_NODE: - continue - if meta_chan == 'c' or meta_chan is None: - # Only channel linked data - param_chan.append(node) - elif channel == 1: - # First channel can have other data channel - param_chan.append(node) - + try: + for node, meta_chan in metadata.items(): + # Is this attribute ignored? + if node in HM_IGNORE_DISCOVERY_NODE: + continue + if meta_chan == 'c' or meta_chan is None: + # Only channel linked data + param_chan.append(node) + elif channel == 1: + # First channel can have other data channel + param_chan.append(node) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Exception generating %s (%s): %s", + hmdevice.ADDRESS, str(metadata), str(err)) # Default parameter - if len(param_chan) == 0: + if not param_chan: param_chan.append(None) # Add to channel params.update({channel: param_chan}) From 30b7c6b6943cb338fbd1c5f298c6f12944e3a4c2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:34:35 +0200 Subject: [PATCH 06/23] Second batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 317397c0b9c..32ac7ac80eb 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,6 +15,7 @@ homematic: """ import time import logging +from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ EVENT_PLATFORM_DISCOVERED, \ ATTR_SERVICE, \ @@ -29,7 +30,6 @@ REQUIREMENTS = ['pyhomematic==0.1.6'] HOMEMATIC = None HOMEMATIC_DEVICES = {} -HOMEMATIC_AUTODETECT = False DISCOVER_SWITCHES = "homematic.switch" DISCOVER_LIGHTS = "homematic.light" @@ -69,14 +69,13 @@ HM_ATTRIBUTE_SUPPORT = { "VOLTAGE": ["Voltage", {}] } -_HM_DISCOVER_HASS = None _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" - global HOMEMATIC, HOMEMATIC_AUTODETECT, _HM_DISCOVER_HASS + global HOMEMATIC from pyhomematic import HMConnection @@ -84,20 +83,18 @@ def setup(hass, config): local_port = config[DOMAIN].get("local_port", 8943) remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) - autodetect = config[DOMAIN].get("autodetect", False) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") return False # Create server thread - HOMEMATIC_AUTODETECT = autodetect - _HM_DISCOVER_HASS = hass + bound_system_callback = partial(system_callback_handler, hass, config) HOMEMATIC = HMConnection(local=local_ip, localport=local_port, remote=remote_ip, remoteport=remote_port, - systemcallback=system_callback_handler, + systemcallback=bound_system_callback, interface_id="homeassistant") # Start server thread, connect to peer, initialize to receive events @@ -111,8 +108,9 @@ def setup(hass, config): # pylint: disable=too-many-branches -def system_callback_handler(src, *args): +def system_callback_handler(hass, config, src, *args): """Callback handler.""" + delay = config[DOMAIN].get("delay", 0.5) if src == 'newDevices': # pylint: disable=unused-variable (interface_id, dev_descriptions) = args @@ -126,13 +124,14 @@ def system_callback_handler(src, *args): for dev in key_dict: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() + hm_element.link_homematic(delay=delay) else: devices_not_created.append(dev) # If configuration allows autodetection of devices, # all devices not configured are added. - if HOMEMATIC_AUTODETECT and devices_not_created: + autodetect = config[DOMAIN].get("autodetect", False) + if autodetect and devices_not_created: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), @@ -152,12 +151,12 @@ def system_callback_handler(src, *args): # Ensure component is loaded homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, + hass, component.DOMAIN, config) # Fire discovery event - _HM_DISCOVER_HASS.bus.fire( + hass.bus.fire( EVENT_PLATFORM_DISCOVERED, { ATTR_SERVICE: discovery_type, ATTR_DISCOVERED: { @@ -171,7 +170,7 @@ def system_callback_handler(src, *args): for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=1) + hm_element.link_homematic(delay=delay) def _get_devices(device_type, keys): @@ -368,7 +367,7 @@ class HMDevice(Entity): return attr - def link_homematic(self, delay=0): + def link_homematic(self, delay=0.5): """Connect to homematic.""" # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: From a19f7bff28d47975b0ae50318190362fdea25120 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:36:52 +0200 Subject: [PATCH 07/23] fix false autodetect with HM GongSensor types --- homeassistant/components/homematic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 32ac7ac80eb..84a3ae33c52 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -46,10 +46,11 @@ HM_DEVICE_TYPES = { DISCOVER_LIGHTS: ["Dimmer"], DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", "RemoteMotion", "ThermostatWall", "AreaThermostat", - "RotaryHandleSensor", "GongSensor"], + "RotaryHandleSensor"], DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", - "Motion", "MotionV2", "RemoteMotion"], + "Motion", "MotionV2", "RemoteMotion", + "GongSensor"], DISCOVER_ROLLERSHUTTER: ["Blind"] } From b3acd7d21d43f7b201b5d041ba9f3a0b5347ffa8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:54:14 +0200 Subject: [PATCH 08/23] add resolvenames function support from pyhomematic (homegear only) --- homeassistant/components/homematic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 84a3ae33c52..6662c6bbe0d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -84,6 +84,7 @@ def setup(hass, config): local_port = config[DOMAIN].get("local_port", 8943) remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) + resolvenames = config[DOMAIN].get("resolvenames", False) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") @@ -96,6 +97,7 @@ def setup(hass, config): remote=remote_ip, remoteport=remote_port, systemcallback=bound_system_callback, + resolvenames=resolvenames, interface_id="homeassistant") # Start server thread, connect to peer, initialize to receive events From 87c138c5593ae7c9c7e4b1a20858c96d5a9f6b82 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 19:25:59 +0200 Subject: [PATCH 09/23] Third batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6662c6bbe0d..040a15a368e 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,6 +22,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ ATTR_DISCOVERED, \ STATE_UNKNOWN from homeassistant.loader import get_component +from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -150,25 +151,11 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: component = get_component(component_name) - config = {component.DOMAIN: found_devices} - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - hass, - component.DOMAIN, - config) - - # Fire discovery event - hass.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } - } - ) + # HA discovery event + discovery.load_platform(hass, component, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 86ccf26a1a41a73ecb5d4514d66f4aacce4bf349 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 20:12:49 +0200 Subject: [PATCH 10/23] fix autodiscovery --- homeassistant/components/binary_sensor/homematic.py | 2 ++ homeassistant/components/homematic.py | 5 +---- homeassistant/components/light/homematic.py | 2 ++ homeassistant/components/rollershutter/homematic.py | 2 ++ homeassistant/components/sensor/homematic.py | 2 ++ homeassistant/components/switch/homematic.py | 2 ++ homeassistant/components/thermostat/homematic.py | 2 ++ 7 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index d2005f99ba5..acbc2eafe69 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,8 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 040a15a368e..3bc23bbdb71 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,7 +21,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN -from homeassistant.loader import get_component from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -150,10 +149,8 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - component = get_component(component_name) - # HA discovery event - discovery.load_platform(hass, component, DOMAIN, { + discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 94dabb0f00a..6ccc2f636ba 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,8 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index e0dd5e5469f..55a86be0bf6 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,8 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 52ece78f59e..c07faedbf5b 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,8 @@ HM_UNIT_HA_CAST = { def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 5a630f43022..ca639b95ecb 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,8 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index a1ed06bc4bd..d98b674c692 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From be72b048551a2a7b86c8ccb8f15bc55c1081eac2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 20:30:02 +0200 Subject: [PATCH 11/23] fix discovery function --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 3bc23bbdb71..c42058a0424 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -150,9 +150,9 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: # HA discovery event - discovery.load_platform(hass, component_name, DOMAIN, { + discovery.load_platform(hass, discovery_type, { ATTR_DISCOVER_DEVICES: found_devices - }, config) + }, component_name, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 57754cd2ff8b9e05b1c0c593973c74ad1bdbfd8f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:33 +0200 Subject: [PATCH 12/23] Revert "fix discovery function" This reverts commit be72b048551a2a7b86c8ccb8f15bc55c1081eac2. --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index c42058a0424..3bc23bbdb71 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -150,9 +150,9 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: # HA discovery event - discovery.load_platform(hass, discovery_type, { + discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices - }, component_name, config) + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 199fbc7a15275b8a449ab8fd737cdd6bef0a3fed Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:37 +0200 Subject: [PATCH 13/23] Revert "fix autodiscovery" This reverts commit 86ccf26a1a41a73ecb5d4514d66f4aacce4bf349. --- homeassistant/components/binary_sensor/homematic.py | 2 -- homeassistant/components/homematic.py | 5 ++++- homeassistant/components/light/homematic.py | 2 -- homeassistant/components/rollershutter/homematic.py | 2 -- homeassistant/components/sensor/homematic.py | 2 -- homeassistant/components/switch/homematic.py | 2 -- homeassistant/components/thermostat/homematic.py | 2 -- 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index acbc2eafe69..d2005f99ba5 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,8 +55,6 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 3bc23bbdb71..040a15a368e 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,6 +21,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN +from homeassistant.loader import get_component from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -149,8 +150,10 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: + component = get_component(component_name) + # HA discovery event - discovery.load_platform(hass, component_name, DOMAIN, { + discovery.load_platform(hass, component, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 6ccc2f636ba..94dabb0f00a 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,8 +29,6 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index 55a86be0bf6..e0dd5e5469f 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,8 +29,6 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index c07faedbf5b..52ece78f59e 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,8 +41,6 @@ HM_UNIT_HA_CAST = { def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index ca639b95ecb..5a630f43022 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,8 +28,6 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index d98b674c692..a1ed06bc4bd 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,8 +28,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From a687bdb388ff03cbe5e253ea0802c06358f8f92d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:41 +0200 Subject: [PATCH 14/23] Revert "Third batch of (minor) fixes as suggested by @balloob" This reverts commit 87c138c5593ae7c9c7e4b1a20858c96d5a9f6b82. --- homeassistant/components/homematic.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 040a15a368e..6662c6bbe0d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,7 +22,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ ATTR_DISCOVERED, \ STATE_UNKNOWN from homeassistant.loader import get_component -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -151,11 +150,25 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: component = get_component(component_name) + config = {component.DOMAIN: found_devices} - # HA discovery event - discovery.load_platform(hass, component, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices - }, config) + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + hass, + component.DOMAIN, + config) + + # Fire discovery event + hass.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' + } + } + ) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From e0e9d3c57b6e61525b026b1f504b85a6a3de5fd4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:37:51 +0200 Subject: [PATCH 15/23] change autodiscovery --- .../components/binary_sensor/homematic.py | 5 +++ homeassistant/components/homematic.py | 37 ++++++++----------- homeassistant/components/light/homematic.py | 5 +++ .../components/rollershutter/homematic.py | 5 +++ homeassistant/components/sensor/homematic.py | 5 +++ homeassistant/components/switch/homematic.py | 5 +++ .../components/thermostat/homematic.py | 5 +++ 7 files changed, 45 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index d2005f99ba5..08ea2099445 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,11 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6662c6bbe0d..5c23462e98d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,9 @@ import time import logging from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - EVENT_PLATFORM_DISCOVERED, \ - ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN -from homeassistant.loader import get_component +from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -149,26 +147,10 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - component = get_component(component_name) - config = {component.DOMAIN: found_devices} - - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - hass, - component.DOMAIN, - config) - # Fire discovery event - hass.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } - } - ) + discovery.load_platform(hass, component_name, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: @@ -282,6 +264,17 @@ def _create_ha_name(name, channel, param): return "{} {} {}".format(name, channel, param) +def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, + add_callback_devices): + """Helper to setup Homematic devices with discovery info.""" + for config in discovery_info["devices"]: + ret = setup_hmdevice_entity_helper(hmdevicetype, config, + add_callback_devices) + if not ret: + _LOGGER.error("Setup discovery error with config %s", str(config)) + return True + + def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): """Helper to setup Homematic devices.""" if HOMEMATIC is None: diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 94dabb0f00a..159f3e4dbdc 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,11 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMLight, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index e0dd5e5469f..737a7eb017d 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,11 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMRollershutter, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 52ece78f59e..f6f3825199b 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,11 @@ HM_UNIT_HA_CAST = { def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMSensor, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 5a630f43022..16cc63a6708 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,11 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMSwitch, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index a1ed06bc4bd..e654379d56e 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,11 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMThermostat, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From 4ecd7245784d0f6717b4249eef3eaf5d4464848b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:10:47 +0200 Subject: [PATCH 16/23] fix linter errors --- homeassistant/components/homematic.py | 3 +-- homeassistant/components/thermostat/homematic.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 5c23462e98d..749f372596b 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,10 @@ import time import logging from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - ATTR_DISCOVERED, \ + ATTR_DISCOVER_DEVICES, \ STATE_UNKNOWN from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -import homeassistant.bootstrap DOMAIN = 'homematic' REQUIREMENTS = ['pyhomematic==0.1.6'] diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index e654379d56e..d7675a5cd47 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -38,6 +38,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): add_callback_devices) +# pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ThermostatDevice): """Represents a Homematic Thermostat in Home Assistant.""" From f3199e7daeb083a196d945875531a2013a61b83f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:13:29 +0200 Subject: [PATCH 17/23] fix wrong import --- homeassistant/components/homematic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 749f372596b..ec092baf80b 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -16,9 +16,7 @@ homematic: import time import logging from functools import partial -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - ATTR_DISCOVER_DEVICES, \ - STATE_UNKNOWN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity From c3b25f2cd5997139150e14d4545f9dad5768611e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:20:09 +0200 Subject: [PATCH 18/23] fix logging-not-lazy --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index ec092baf80b..c2c6c000fa2 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -389,8 +389,8 @@ class HMDevice(Entity): except Exception as err: self._connected = False self._available = False - _LOGGER.error("Exception while linking %s: %s" % - (self._address, str(err))) + _LOGGER.error("Exception while linking %s: %s", + self._address, str(err)) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False From 206e7d7a678fe4ecd1dd18497992af251cf1d78d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 Jun 2016 16:40:33 -0700 Subject: [PATCH 19/23] Extend persistent notification support (#2371) --- homeassistant/bootstrap.py | 5 +- homeassistant/components/demo.py | 6 ++ .../components/persistent_notification.py | 70 +++++++++++++++++-- .../test_persistent_notification.py | 65 +++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 tests/components/test_persistent_notification.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 754d4f4f5aa..ff7e73a00f1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -11,7 +11,7 @@ from threading import RLock import voluptuous as vol import homeassistant.components as core_components -import homeassistant.components.group as group +from homeassistant.components import group, persistent_notification import homeassistant.config as config_util import homeassistant.core as core import homeassistant.helpers.config_validation as cv @@ -262,9 +262,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, if not core_components.setup(hass, config): _LOGGER.error('Home Assistant core failed to initialize. ' 'Further initialization aborted.') - return hass + persistent_notification.setup(hass, config) + _LOGGER.info('Home Assistant core initialized') # Give event decorators access to HASS diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 148c57a12c3..f083a96f5b2 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -37,6 +37,7 @@ def setup(hass, config): """Setup a demo environment.""" group = loader.get_component('group') configurator = loader.get_component('configurator') + persistent_notification = loader.get_component('persistent_notification') config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) @@ -59,6 +60,11 @@ def setup(hass, config): demo_config[component] = {CONF_PLATFORM: 'demo'} bootstrap.setup_component(hass, component, demo_config) + # Setup example persistent notification + persistent_notification.create( + hass, 'This is an example of a persistent notification.', + title='Example Notification') + # Setup room groups lights = sorted(hass.states.entity_ids('light')) switches = sorted(hass.states.entity_ids('switch')) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 6c784eaf5ca..66a634616fa 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -4,15 +4,77 @@ A component which is collecting configuration errors. For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ +import logging -DOMAIN = "persistent_notification" +import voluptuous as vol + +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template, config_validation as cv +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util import slugify + +DOMAIN = 'persistent_notification' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SERVICE_CREATE = 'create' +ATTR_TITLE = 'title' +ATTR_MESSAGE = 'message' +ATTR_NOTIFICATION_ID = 'notification_id' + +SCHEMA_SERVICE_CREATE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, + vol.Optional(ATTR_NOTIFICATION_ID): cv.string, +}) -def create(hass, entity, msg): - """Create a state for an error.""" - hass.states.set('{}.{}'.format(DOMAIN, entity), msg) +DEFAULT_OBJECT_ID = 'notification' +_LOGGER = logging.getLogger(__name__) + + +def create(hass, message, title=None, notification_id=None): + """Turn all or specified light off.""" + data = { + key: value for key, value in [ + (ATTR_TITLE, title), + (ATTR_MESSAGE, message), + (ATTR_NOTIFICATION_ID, notification_id), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_CREATE, data) def setup(hass, config): """Setup the persistent notification component.""" + def create_service(call): + """Handle a create notification service call.""" + title = call.data.get(ATTR_TITLE) + message = call.data.get(ATTR_MESSAGE) + notification_id = call.data.get(ATTR_NOTIFICATION_ID) + + if notification_id is not None: + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + else: + entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, + hass=hass) + attr = {} + if title is not None: + try: + title = template.render(hass, title) + except TemplateError as ex: + _LOGGER.error('Error rendering title %s: %s', title, ex) + + attr[ATTR_TITLE] = title + + try: + message = template.render(hass, message) + except TemplateError as ex: + _LOGGER.error('Error rendering message %s: %s', message, ex) + + hass.states.set(entity_id, message, attr) + + hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {}, + SCHEMA_SERVICE_CREATE) + return True diff --git a/tests/components/test_persistent_notification.py b/tests/components/test_persistent_notification.py new file mode 100644 index 00000000000..6f6d8b8e1b0 --- /dev/null +++ b/tests/components/test_persistent_notification.py @@ -0,0 +1,65 @@ +"""The tests for the persistent notification component.""" +import homeassistant.components.persistent_notification as pn + +from tests.common import get_test_home_assistant + + +class TestPersistentNotification: + """Test persistent notification component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + pn.setup(self.hass, {}) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_create(self): + """Test creating notification without title or notification id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'Hello World {{ 1 + 1 }}', + title='{{ 1 + 1 }} beers') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == 'Hello World 2' + assert state.attributes.get('title') == '2 beers' + + def test_create_notification_id(self): + """Ensure overwrites existing notification with same id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'test', notification_id='Beer 2') + self.hass.pool.block_till_done() + + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test' + + pn.create(self.hass, 'test 2', notification_id='Beer 2') + self.hass.pool.block_till_done() + + # We should have overwritten old one + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test 2' + + def test_create_template_error(self): + """Ensure we output templates if contain error.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, '{{ message + 1 }}', '{{ title + 1 }}') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == '{{ message + 1 }}' + assert state.attributes.get('title') == '{{ title + 1 }}' From d13cc227cc769c444e46900c1d12185e84f88b84 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Sun, 26 Jun 2016 01:33:23 -0600 Subject: [PATCH 20/23] Push State (#2365) * Add ability to push state changes * Add tests for push state changes * Fix style issues * Use better name to force an update --- homeassistant/components/api.py | 3 ++- homeassistant/core.py | 5 +++-- homeassistant/helpers/entity.py | 12 +++++++++++- homeassistant/remote.py | 9 +++++---- tests/components/test_api.py | 21 +++++++++++++++++++++ tests/test_core.py | 14 ++++++++++++++ tests/test_remote.py | 17 ++++++++++++++++- 7 files changed, 72 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ad8f21f069b..b538a62d008 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -204,11 +204,12 @@ class APIEntityStateView(HomeAssistantView): return self.json_message('No state specified', HTTP_BAD_REQUEST) attributes = request.json.get('attributes') + force_update = request.json.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # Write state - self.hass.states.set(entity_id, new_state, attributes) + self.hass.states.set(entity_id, new_state, attributes, force_update) # Read the state back for our response resp = self.json(self.hass.states.get(entity_id)) diff --git a/homeassistant/core.py b/homeassistant/core.py index ffaccdeae43..d3eed6ce5e0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -456,7 +456,7 @@ class StateMachine(object): return True - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -472,7 +472,8 @@ class StateMachine(object): old_state = self._states.get(entity_id) is_existing = old_state is not None - same_state = is_existing and old_state.state == new_state + same_state = (is_existing and old_state.state == new_state and + not force_update) same_attr = is_existing and old_state.attributes == attributes if same_state and same_attr: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e4ccf11e168..d120a3b2cf6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -125,6 +125,15 @@ class Entity(object): """Return True if unable to access real state of the entity.""" return False + @property + def force_update(self): + """Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return False + def update(self): """Retrieve latest state.""" pass @@ -190,7 +199,8 @@ class Entity(object): state, attr[ATTR_UNIT_OF_MEASUREMENT]) state = str(state) - return self.hass.states.set(self.entity_id, state, attr) + return self.hass.states.set( + self.entity_id, state, attr, self.force_update) def _attr_setter(self, name, typ, attr, attrs): """Helper method to populate attributes based on properties.""" diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 4bfb01890cf..b2dfc3ae18f 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -259,9 +259,9 @@ class StateMachine(ha.StateMachine): """ return remove_state(self._api, entity_id) - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Call set_state on remote API.""" - set_state(self._api, entity_id, new_state, attributes) + set_state(self._api, entity_id, new_state, attributes, force_update) def mirror(self): """Discard current data and mirrors the remote state machine.""" @@ -450,7 +450,7 @@ def remove_state(api, entity_id): return False -def set_state(api, entity_id, new_state, attributes=None): +def set_state(api, entity_id, new_state, attributes=None, force_update=False): """Tell API to update state for entity_id. Return True if success. @@ -458,7 +458,8 @@ def set_state(api, entity_id, new_state, attributes=None): attributes = attributes or {} data = {'state': new_state, - 'attributes': attributes} + 'attributes': attributes, + 'force_update': force_update} try: req = api(METHOD_POST, diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 60ff19d4a43..8d1ee1c4ad5 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -136,6 +136,27 @@ class TestAPI(unittest.TestCase): self.assertEqual(400, req.status_code) + # pylint: disable=invalid-name + def test_api_state_change_push(self): + """Test if we can push a change the state of an entity.""" + hass.states.set("test.test", "not_to_be_set") + + events = [] + hass.bus.listen(const.EVENT_STATE_CHANGED, events.append) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set"}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set", + "force_update": True}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + # pylint: disable=invalid-name def test_api_fire_event_with_no_data(self): """Test if the API allows us to fire an event.""" diff --git a/tests/test_core.py b/tests/test_core.py index 4930bcef6ed..cb698cdc53c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,20 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(state.last_changed, self.states.get('light.Bowl').last_changed) + def test_force_update(self): + """Test force update option.""" + self.pool.add_worker() + events = [] + self.bus.listen(EVENT_STATE_CHANGED, events.append) + + self.states.set('light.bowl', 'on') + self.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + self.states.set('light.bowl', 'on', None, True) + self.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + class TestServiceCall(unittest.TestCase): """Test ServiceCall class.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index 58b2f9b359d..893f02bea31 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -8,7 +8,7 @@ import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http -from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.const import HTTP_HEADER_HA_AUTH, EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_instance_port, get_test_home_assistant @@ -155,6 +155,21 @@ class TestRemoteMethods(unittest.TestCase): self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) + def test_set_state_with_push(self): + """TestPython API set_state with push option.""" + events = [] + hass.bus.listen(EVENT_STATE_CHANGED, events.append) + + remote.set_state(master_api, 'test.test', 'set_test_2') + remote.set_state(master_api, 'test.test', 'set_test_2') + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + + remote.set_state( + master_api, 'test.test', 'set_test_2', force_update=True) + hass.bus._pool.block_till_done() + self.assertEqual(2, len(events)) + def test_is_state(self): """Test Python API is_state.""" self.assertTrue( From 254b1c46ac9381415dd5e1699954b59ab7f2ae62 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 26 Jun 2016 19:13:52 +0200 Subject: [PATCH 21/23] Remove lxml dependency (#2374) --- homeassistant/components/sensor/swiss_hydrological_data.py | 4 ++-- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index ddc31bb56ec..2589bd44955 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.4.1'] _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://www.hydrodaten.admin.ch/en/' @@ -148,7 +148,7 @@ class HydrologicalData(object): try: tables = BeautifulSoup(response.content, - 'lxml').findChildren('table') + 'html.parser').findChildren('table') rows = tables[0].findChildren(['th', 'tr']) details = [] diff --git a/requirements_all.txt b/requirements_all.txt index 62a87fcc368..a131813edbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,9 +180,6 @@ lightify==1.0.3 # homeassistant.components.light.limitlessled limitlessled==1.0.0 -# homeassistant.components.sensor.swiss_hydrological_data -lxml==3.6.0 - # homeassistant.components.notify.message_bird messagebird==1.2.0 From fb3e388f0441240fe207274f0167f01e034ba4b6 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 26 Jun 2016 14:49:46 -0400 Subject: [PATCH 22/23] Depreciate ssl2/3 (#2375) * Depreciate ssl2/3 Following the best practices as defind here: https://mozilla.github.io/server-side-tls/ssl-config-generator/ * Updated comment with better decription Links to the rational rather than the config generator; explains link. * add comment mentioning intermediate --- homeassistant/components/http.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index d7ce8e78013..1f77aac5ad4 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -10,6 +10,7 @@ import logging import mimetypes import threading import re +import ssl import voluptuous as vol import homeassistant.core as ha @@ -36,6 +37,24 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins' DATA_API_PASSWORD = 'api_password' +# TLS configuation follows the best-practice guidelines +# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS +# Intermediate guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_TLSv1 +CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ + "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ + "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ + "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ + "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ + "AES256-SHA:DES-CBC3-SHA:!DSS" + _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _LOGGER = logging.getLogger(__name__) @@ -294,7 +313,8 @@ class HomeAssistantWSGI(object): sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, - keyfile=self.ssl_key, server_side=True) + keyfile=self.ssl_key, server_side=True, + ssl_version=SSL_VERSION, ciphers=CIPHERS) wsgi.server(sock, self, log=_LOGGER) def dispatch_request(self, request): From 3afc566be11ec3d715415a5d4ff11271c5bdc234 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 26 Jun 2016 23:18:18 +0200 Subject: [PATCH 23/23] Fix timing bug while linking HM device to HA object https://github.com/danielperna84/home-assistant/issues/14 --- homeassistant/components/homematic.py | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index c2c6c000fa2..7b3e265a9dd 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -24,6 +24,7 @@ DOMAIN = 'homematic' REQUIREMENTS = ['pyhomematic==0.1.6'] HOMEMATIC = None +HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_DEVICES = {} DISCOVER_SWITCHES = "homematic.switch" @@ -71,7 +72,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" - global HOMEMATIC + global HOMEMATIC, HOMEMATIC_LINK_DELAY from pyhomematic import HMConnection @@ -80,6 +81,7 @@ def setup(hass, config): remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) resolvenames = config[DOMAIN].get("resolvenames", False) + HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") @@ -108,27 +110,30 @@ def setup(hass, config): # pylint: disable=too-many-branches def system_callback_handler(hass, config, src, *args): """Callback handler.""" - delay = config[DOMAIN].get("delay", 0.5) if src == 'newDevices': + _LOGGER.debug("newDevices with: %s", str(args)) # pylint: disable=unused-variable (interface_id, dev_descriptions) = args key_dict = {} # Get list of all keys of the devices (ignoring channels) for dev in dev_descriptions: key_dict[dev['ADDRESS'].split(':')[0]] = True + # Connect devices already created in HA to pyhomematic and # add remaining devices to list devices_not_created = [] for dev in key_dict: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=delay) + hm_element.link_homematic() else: devices_not_created.append(dev) # If configuration allows autodetection of devices, # all devices not configured are added. autodetect = config[DOMAIN].get("autodetect", False) + _LOGGER.debug("Autodetect is %s / unknown device: %s", str(autodetect), + str(devices_not_created)) if autodetect and devices_not_created: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), @@ -149,11 +154,6 @@ def system_callback_handler(hass, config, src, *args): ATTR_DISCOVER_DEVICES: found_devices }, config) - for dev in devices_not_created: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=delay) - def _get_devices(device_type, keys): """Get devices.""" @@ -269,6 +269,7 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, add_callback_devices) if not ret: _LOGGER.error("Setup discovery error with config %s", str(config)) + return True @@ -284,6 +285,8 @@ def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): "'address' missing in configuration.", address) return False + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), str(config)) # Create a new HA homematic object new_device = hmdevicetype(config) if address not in HOMEMATIC_DEVICES: @@ -292,6 +295,10 @@ def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): # Add to HA add_callback_devices([new_device]) + + # HM is connected + if address in HOMEMATIC.devices: + return new_device.link_homematic() return True @@ -360,8 +367,12 @@ class HMDevice(Entity): return attr - def link_homematic(self, delay=0.5): + def link_homematic(self): """Connect to homematic.""" + # device is already linked + if self._connected: + return True + # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: # Init @@ -374,10 +385,10 @@ class HMDevice(Entity): try: # Init datapoints of this object self._init_data_struct() - if delay: + if HOMEMATIC_LINK_DELAY: # We delay / pause loading of data to avoid overloading # of CCU / Homegear when doing auto detection - time.sleep(delay) + time.sleep(HOMEMATIC_LINK_DELAY) self._load_init_data_from_hm() _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) @@ -388,23 +399,21 @@ class HMDevice(Entity): # pylint: disable=broad-except except Exception as err: self._connected = False - self._available = False _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False - self._available = False # Update HA - _LOGGER.debug("%s linking down, send update_ha_state", self._name) + _LOGGER.debug("%s linking done, send update_ha_state", self._name) self.update_ha_state() else: _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" - _LOGGER.debug("%s receive event '%s' value: %s", self._name, + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) have_change = False