diff --git a/.coveragerc b/.coveragerc index 6526b2b1e3d252ca921db4e6744859ec4135c7e7..51d6d81179f8f5d516d0ea48f4b8cc5a9f2470b8 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 0000000000000000000000000000000000000000..08ea209944548ff172a0473ad751156fa78a8e8e --- /dev/null +++ b/homeassistant/components/binary_sensor/homematic.py @@ -0,0 +1,169 @@ +""" +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: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX" + name: "<User defined name>" (optional) + + +Configuration (multiple channels, like motion detector with buttons): + +binary_sensor: + - platform: homematic + address: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX" + param: <MOTION|PRESS_SHORT...> (device-dependent) (optional) + button: n (integer of channel to map, device-dependent) (optional) + name: "<User defined 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.""" + 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) + + +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 == "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 0000000000000000000000000000000000000000..c2c6c000fa262a4fd23843b5e0b6f49c835825de --- /dev/null +++ b/homeassistant/components/homematic.py @@ -0,0 +1,521 @@ +""" +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: "<IP of device running Home Assistant>" + local_port: <Port for connection with Home Assistant> + remote_ip: "<IP of Homegear / CCU>" + remote_port: <Port of Homegear / CCU XML-RPC Server> + autodetect: "<True/False>" (optional, experimental, detect all devices) +""" +import time +import logging +from functools import partial +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity + +DOMAIN = 'homematic' +REQUIREMENTS = ['pyhomematic==0.1.6'] + +HOMEMATIC = None +HOMEMATIC_DEVICES = {} + +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"], + DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], + DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion", + "GongSensor"], + 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", {}] +} + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the Homematic component.""" + global HOMEMATIC + + 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) + 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") + return False + + # Create server thread + bound_system_callback = partial(system_callback_handler, hass, config) + HOMEMATIC = HMConnection(local=local_ip, + localport=local_port, + 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 + 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(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 + 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) + else: + devices_not_created.append(dev) + + # If configuration allows autodetection of devices, + # all devices not configured are added. + autodetect = config[DOMAIN].get("autodetect", False) + if 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 + 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 + if found_devices: + # Fire discovery event + discovery.load_platform(hass, component_name, DOMAIN, { + 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.""" + 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__ not in HM_DEVICE_TYPES[device_type]: + continue + metadata = {} + + # Load metadata if needed to generate a param list + if device_type == DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type == 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) + 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 + + # 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 = {} + + # Search in sensor and binary metadata per elements + for channel in range(1, hmdevice.ELEMENT + 1): + param_chan = [] + 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 not param_chan: + 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 "{} {}".format(name, channel) + + # With multiple param first elements + if channel == 1 and param is not None: + return "{} {}".format(name, param) + + # Multiple param on object with multiple elements + if channel > 1 and param is not None: + 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: + _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, delay=0.5): + """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(): + 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 + # 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) + 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, + 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 0000000000000000000000000000000000000000..159f3e4dbdcd2bd48ff640e891c1beb42ab9fa86 --- /dev/null +++ b/homeassistant/components/light/homematic.py @@ -0,0 +1,117 @@ +""" +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: <Homematic addresss for device> # e.g. "JEQ0XXXXXXX" + name: <User defined 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.""" + 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) + + +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 0000000000000000000000000000000000000000..737a7eb017ddc0dc309effd8bca3aee7e11b3857 --- /dev/null +++ b/homeassistant/components/rollershutter/homematic.py @@ -0,0 +1,110 @@ +""" +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: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX" + name: "<User defined 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.""" + 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) + + +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 0000000000000000000000000000000000000000..f6f3825199b687ace3e77a1f9a0c55367a107ef8 --- /dev/null +++ b/homeassistant/components/sensor/homematic.py @@ -0,0 +1,124 @@ +""" +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: <Homematic address for device> # e.g. "JEQ0XXXXXXX" + name: <User defined name> (optional) + param: <Name of datapoint to us as sensor> (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.""" + 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) + + +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 0000000000000000000000000000000000000000..16cc63a6708ad93bbefb6e2888f98784a691d9a9 --- /dev/null +++ b/homeassistant/components/switch/homematic.py @@ -0,0 +1,116 @@ +""" +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: <Homematic address for device> # e.g. "JEQ0XXXXXXX" + name: <User defined 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.""" + 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) + + +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 6f1ff29c16c5c020233959d261cec13a00b10fee..d7675a5cd472c319b9c1942137e3a2f939d54b6b 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,121 +1,46 @@ """ -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: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX" + name: "<User defined name>" (optional) """ -import logging -import socket -from xmlrpc.client import ServerProxy -from xmlrpc.client import Error -from collections import namedtuple +import logging +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_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, abstract-method -class HomematicThermostat(ThermostatDevice): - """Representation of a Homematic thermostat.""" +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) - 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 +# pylint: disable=abstract-method +class HMThermostat(homematic.HMDevice, ThermostatDevice): + """Represents a Homematic Thermostat in Home Assistant.""" @property def unit_of_measurement(self): @@ -125,26 +50,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 +77,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) - - 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 - - except Error: - _LOGGER.exception("Did not receive any temperature data from the " - "homematic API.") + 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 + + # 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 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 7360074d8ab60e863de0ebc48458c781c985626a..62a87fcc368f610f113ff8c0e1d979ed5438f9e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,6 +260,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