Merge pull request #2368 from pvizeli/Homematic

Homematic Support (clean)
This commit is contained in:
Paulus Schoutsen 2016-06-25 20:37:53 -07:00 committed by GitHub
commit 446f998759
9 changed files with 1223 additions and 151 deletions

View file

@ -84,6 +84,9 @@ omit =
homeassistant/components/netatmo.py homeassistant/components/netatmo.py
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/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/arest.py

View file

@ -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

View file

@ -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})

View file

@ -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)

View file

@ -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})

View file

@ -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)

View file

@ -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)

View file

@ -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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.homematic/ 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 logging
import socket import homeassistant.components.homematic as homematic
from xmlrpc.client import ServerProxy
from xmlrpc.client import Error
from collections import namedtuple
from homeassistant.components.thermostat import ThermostatDevice from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN
REQUIREMENTS = [] DEPENDENCIES = ['homematic']
_LOGGER = logging.getLogger(__name__) _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', def setup_platform(hass, config, add_callback_devices, discovery_info=None):
['device_type', """Setup the platform."""
'platform_type', if discovery_info:
'channel', return homematic.setup_hmdevice_discovery_helper(HMThermostat,
'maint_channel']) discovery_info,
add_callback_devices)
HM_TYPE_MAPPING = { # Manual
'HM-CC-RT-DN': HomematicConfig('HM-CC-RT-DN', return homematic.setup_hmdevice_entity_helper(HMThermostat,
TYPE_HM_THERMOSTAT, config,
4, 4), add_callback_devices)
'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): # pylint: disable=abstract-method
"""Setup the Homematic thermostat.""" class HMThermostat(homematic.HMDevice, ThermostatDevice):
devices = [] """Represents a Homematic Thermostat in Home Assistant."""
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 __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
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -125,26 +50,22 @@ class HomematicThermostat(ThermostatDevice):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self._current_temperature if not self.available:
return None
return self._data["ACTUAL_TEMPERATURE"]
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the target temperature."""
return self._target_temperature if not self.available:
return None
return self._data["SET_TEMPERATURE"]
def set_temperature(self, temperature): def set_temperature(self, temperature):
"""Set new target temperature.""" """Set new target temperature."""
device = ServerProxy(self.address) if not self.available:
device.setValue(self._full_device_name, return None
PROPERTY_SET_TEMPERATURE, self._hmdevice.set_temperature(temperature)
temperature)
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {"valve": self._valve,
"battery": self._battery,
"mode": self._mode}
@property @property
def min_temp(self): def min_temp(self):
@ -156,39 +77,27 @@ class HomematicThermostat(ThermostatDevice):
"""Return the maximum temperature - 30.5 means on.""" """Return the maximum temperature - 30.5 means on."""
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
def update(self): def _check_hm_to_ha_object(self):
"""Update the data from the thermostat.""" """Check if possible to use the HM Object as this HA type."""
try: from pyhomematic.devicetypes.thermostats import HMThermostat\
device = ServerProxy(self.address) as pyHMThermostat
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, # Check compatibility from HMDevice
TYPE_HM_WALLTHERMOSTAT]: if not super()._check_hm_to_ha_object():
self._battery = device.getValue(self._maint_device_name, return False
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: # Check if the homematic device correct for this HA device
_LOGGER.exception("Did not receive any temperature data from the " if isinstance(self._hmdevice, pyHMThermostat):
"homematic API.") 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})

View file

@ -260,6 +260,9 @@ pyenvisalink==1.0
# homeassistant.components.ifttt # homeassistant.components.ifttt
pyfttt==0.3 pyfttt==0.3
# homeassistant.components.homematic
pyhomematic==0.1.6
# homeassistant.components.device_tracker.icloud # homeassistant.components.device_tracker.icloud
pyicloud==0.8.3 pyicloud==0.8.3