Skip to content

Commit

Permalink
update pyhomematic and homematic use now events from HA for remotes
Browse files Browse the repository at this point in the history
  • Loading branch information
pvizeli committed Jun 29, 2016
1 parent 3c5c018 commit bb0f484
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 80 deletions.
37 changes: 5 additions & 32 deletions homeassistant/components/binary_sensor/homematic.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@
"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."""
Expand Down Expand Up @@ -78,20 +73,17 @@ def _check_hm_to_ha_object(self):
_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:
if self._state and self._state not in self._hmdevice.BINARYNODE:
_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:
if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
_LOGGER.critical("%s have multible binary params. It use all " +
"binary nodes as one. Possible param values: %s",
self._name, str(available_bin))
self._name, str(self._hmdevice.BINARYNODE))
return False

return True
Expand All @@ -100,32 +92,13 @@ 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:
if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
for value in self._hmdevice.BINARYNODE:
self._state = value

# 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
150 changes: 103 additions & 47 deletions homeassistant/components/homematic.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
from homeassistant.helpers.entity import Entity

DOMAIN = 'homematic'
REQUIREMENTS = ['pyhomematic==0.1.6']
REQUIREMENTS = ['pyhomematic==0.1.8']

HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5
HOMEMATIC_DEVICES = {}

DISCOVER_SWITCHES = "homematic.switch"
DISCOVER_LIGHTS = "homematic.light"
Expand All @@ -34,18 +33,22 @@
DISCOVER_THERMOSTATS = "homematic.thermostat"

ATTR_DISCOVER_DEVICES = "devices"
ATTR_DISCOVER_CONFIG = "config"
ATTR_PARAM = "param"
ATTR_CHANNEL = "channel"
ATTR_NAME = "name"
ATTR_ADDRESS = "address"

EVENT_KEYPRESS = "homematic.keypress"

HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"],
DISCOVER_LIGHTS: ["Dimmer"],
DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2",
"RemoteMotion", "ThermostatWall", "AreaThermostat",
"RotaryHandleSensor"],
"RotaryHandleSensor", "WaterSensor"],
DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"],
DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2",
"Motion", "MotionV2", "RemoteMotion",
"GongSensor"],
DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2",
"Motion", "MotionV2", "RemoteMotion"],
DISCOVER_ROLLERSHUTTER: ["Blind"]
}

Expand All @@ -65,6 +68,13 @@
"VOLTAGE": ["Voltage", {}]
}

HM_PRESS_EVENTS = [
"PRESS_SHORT",
"PRESS_LONG",
"PRESS_CONT",
"PRESS_LONG_RELEASE"
]

_LOGGER = logging.getLogger(__name__)


Expand All @@ -80,6 +90,8 @@ 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)
username = config[DOMAIN].get("username", "Admin")
password = config[DOMAIN].get("password", "")
HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5)

if remote_ip is None or local_ip is None:
Expand All @@ -88,12 +100,15 @@ def setup(hass, config):

# Create server thread
bound_system_callback = partial(system_callback_handler, hass, config)
# pylint: disable=unexpected-keyword-arg
HOMEMATIC = HMConnection(local=local_ip,
localport=local_port,
remote=remote_ip,
remoteport=remote_port,
systemcallback=bound_system_callback,
resolvenames=resolvenames,
rpcusername=username,
rpcpassword=password,
interface_id="homeassistant")

# Start server thread, connect to peer, initialize to receive events
Expand All @@ -118,6 +133,20 @@ def system_callback_handler(hass, config, src, *args):
for dev in dev_descriptions:
key_dict[dev['ADDRESS'].split(':')[0]] = True

# Register EVENTS
# Search all device with a EVENTNODE that include data
bound_event_callback = partial(_hm_event_handler, hass)
for dev in key_dict:
if dev not in HOMEMATIC.devices:
continue

hmdevice = HOMEMATIC.devices.get(dev)
# have events?
if len(hmdevice.EVENTNODE) > 0:
_LOGGER.debug("Register Events from %s", dev)
hmdevice.setEventCallback(callback=bound_event_callback,
bequeath=True)

# If configuration allows autodetection of devices,
# all devices not configured are added.
if key_dict:
Expand All @@ -142,29 +171,24 @@ def system_callback_handler(hass, config, src, *args):

def _get_devices(device_type, keys):
"""Get devices."""
from homeassistant.components.binary_sensor.homematic import \
SUPPORT_HM_EVENT_AS_BINMOD

# run
device_arr = []
for key in keys:
device = HOMEMATIC.devices[key]
if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]:
continue
class_name = device.__class__.__name__
metadata = {}

# is class supported by discovery type
if class_name not in HM_DEVICE_TYPES[device_type]:
continue

# 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)
params = _create_params_list(device, metadata, device_type)
if params:
# Generate options for 1...n elements with 1...n params
for channel in range(1, device.ELEMENT + 1):
Expand All @@ -177,9 +201,9 @@ def _get_devices(device_type, keys):
device_dict = dict(platform="homematic",
address=key,
name=name,
button=channel)
channel=channel)
if param is not None:
device_dict["param"] = param
device_dict[ATTR_PARAM] = param

# Add new device
device_arr.append(device_dict)
Expand All @@ -192,15 +216,22 @@ def _get_devices(device_type, keys):
return device_arr


def _create_params_list(hmdevice, metadata):
def _create_params_list(hmdevice, metadata, device_type):
"""Create a list from HMDevice with all possible parameters in config."""
params = {}
merge = False

# use merge?
if device_type == DISCOVER_SENSORS:
merge = True
elif device_type == DISCOVER_BINARY_SENSORS:
merge = True

# 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():
for node, meta_chan in metadata.items():
try:
# Is this attribute ignored?
if node in HM_IGNORE_DISCOVERY_NODE:
continue
Expand All @@ -210,15 +241,17 @@ def _create_params_list(hmdevice, metadata):
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:
except (TypeError, ValueError):
_LOGGER.error("Exception generating %s (%s)",
hmdevice.ADDRESS, str(metadata))

# default parameter is merge is off
if len(param_chan) == 0 and not merge:
param_chan.append(None)

# Add to channel
params.update({channel: param_chan})
if len(param_chan) > 0:
params.update({channel: param_chan})

_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
str(params))
Expand Down Expand Up @@ -247,7 +280,7 @@ def _create_ha_name(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"]:
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
_LOGGER.debug("Add device %s from config: %s",
str(hmdevicetype), str(config))

Expand All @@ -261,16 +294,41 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
return True


def _hm_event_handler(hass, device, caller, attribute, value):
"""Handle all pyhomematic device events."""
channel = device.split(":")[1]
address = device.split(":")[0]
hmdevice = HOMEMATIC.devices.get(address)

# is not a event?
if attribute not in hmdevice.EVENTNODE:
return

_LOGGER.debug("Event %s for %s channel %s", attribute,
hmdevice.NAME, channel)

# a keypress event
if attribute in HM_PRESS_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_PARAM: attribute,
ATTR_CHANNEL: channel
})
return

_LOGGER.warning("Event is unknown and not forwarded to HA")


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._name = config.get(ATTR_NAME, None)
self._address = config.get(ATTR_ADDRESS, None)
self._channel = config.get(ATTR_CHANNEL, 1)
self._state = config.get(ATTR_PARAM, None)
self._data = {}
self._hmdevice = None
self._connected = False
Expand Down Expand Up @@ -311,13 +369,20 @@ def device_state_attributes(self):
"""Return device specific state attributes."""
attr = {}

# no data available to create
if not self.available:
return 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

# static attributes
attr["ID"] = self._hmdevice.ADDRESS

return attr

def link_homematic(self):
Expand Down Expand Up @@ -382,18 +447,12 @@ def _hm_event_callback(self, device, caller, attribute, value):
self._available = bool(value)
have_change = True

# If it has changed, update HA
# If it has changed data point, 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 = {}
Expand Down Expand Up @@ -441,18 +500,15 @@ def _load_init_data_from_hm(self):
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):
"""Set data to main datapoint."""
if self._state in self._data:
self._data[self._state] = value

def _hm_get_state(self):
"""Get data from main datapoint."""
if self._state in self._data:
return self._data[self._state]
return None
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ pyenvisalink==1.0
pyfttt==0.3

# homeassistant.components.homematic
pyhomematic==0.1.6
pyhomematic==0.1.8

# homeassistant.components.device_tracker.icloud
pyicloud==0.8.3
Expand Down

5 comments on commit bb0f484

@pvizeli
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@balloob is that code for handle HA events okay? (L310-316)

@balloob
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know that's what pull requests are for ;-)

@balloob
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's fine 👍

@pvizeli
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) thanks

@balloob
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw for transparency, please open a PR to get changes into the dev branch . Since you're changing Homematic stuff you can merge it yourself right away but that way it is easier for the people that watch this repo to keep track of what is happening. And also makes sure that our tests pass 👍

Please sign in to comment.