Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Loxone plattform #9706

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fcb13b0
Added Loxone plattform
JoDehli Oct 5, 2017
4d2e80f
Fixed some code styling errors
JoDehli Oct 5, 2017
047a48d
Fixed some styling issues
JoDehli Oct 6, 2017
346cf2a
Fixed some code styling issues
JoDehli Oct 6, 2017
71da53f
Fixed some code styling issues
JoDehli Oct 6, 2017
725a0bf
Fixed some code styling issues
JoDehli Oct 6, 2017
bdb0ba7
Merge branch 'dev' into loxone
JoDehli Oct 7, 2017
2e239fe
updated from dev
JoDehli Oct 7, 2017
d36a411
Merge branch 'loxone' of https://github.com/JoDehli/home-assistant in…
JoDehli Oct 7, 2017
66203c9
Update loxone.py
JoDehli Oct 7, 2017
f090382
fixed pylint errors
JoDehli Oct 8, 2017
e0a13b6
Fixed pylint errors
JoDehli Oct 8, 2017
bc2252c
Merge remote-tracking branch 'origin/loxone' into loxone
JoDehli Oct 8, 2017
2eaef85
Fixed pylint issues.
JoDehli Oct 8, 2017
71de56d
Merge remote-tracking branch 'HomeAssistantUpstream/dev' into loxone
JoDehli Oct 8, 2017
b8abbe8
Merge remote-tracking branch 'HomeAssistantUpstream/dev' into loxone
JoDehli Oct 11, 2017
cd08986
Merge remote-tracking branch 'Home_Assistant_Main/dev' into loxone
JoDehli Oct 12, 2017
539285f
Merge remote-tracking branch 'Home_Assistant_Main/dev' into loxone
JoDehli Oct 12, 2017
139b81a
Merge remote-tracking branch 'origin/loxone' into loxone
JoDehli Oct 12, 2017
9ec1485
Uncommented pylint disable statement
JoDehli Oct 12, 2017
a89edc2
Updated requirements.
JoDehli Oct 12, 2017
36942fc
EVENT_HOMEASSISTANT_STOP added
JoDehli Oct 13, 2017
4f147c1
Merge remote-tracking branch 'Home_Assistant_Main/dev' into loxone
JoDehli Oct 13, 2017
8833f71
Merge remote-tracking branch 'HomeAssistantUpstream/dev' into loxone
JoDehli Oct 17, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
228 changes: 228 additions & 0 deletions homeassistant/components/loxone.py
@@ -0,0 +1,228 @@
"""
Component to create an interface to the Loxone Miniserver

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/loxone/
"""
# pylint: disable=unused-import, too-many-lines
Copy link
Contributor

Choose a reason for hiding this comment

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

Do not disable pylint

import asyncio
import codecs
import hashlib
import hmac
import logging
import uuid
import json
from datetime import timedelta
from struct import unpack

import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_USERNAME,
CONF_PASSWORD)
from homeassistant.helpers.event import async_track_time_interval

REQUIREMENTS = ['websockets==3.4']
Copy link
Member

Choose a reason for hiding this comment

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

You need to update the requirement of the other platform that uses websockets as well.

Copy link
Author

Choose a reason for hiding this comment

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

I think my biggest problem is that I do not know how to pull the latest changes from dev.
I tried it without a gui for github but I did not get it to work. Now I tried GitKraken. I fixed all of
the pylint issues. Maybe it is better you delete my pull request and I delete my fork. And I first try to get used with github and git.

Copy link
Author

Choose a reason for hiding this comment

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

And also I have big problems with the documentation page. But this is also a problem because I do not understand the github way to do it. I work personally with subversion and this is much easier for me. I believe that git is better but also more complicated.


_LOGGER = logging.getLogger(__name__)

DOMAIN = 'loxone'
DEFAULT_PORT = 80
EVENT = 'loxone_received'

KEEPALIVEINTERVAL = timedelta(seconds=120)

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)

DEFAULT = ""
ATTR_UUID = 'uuid'
ATTR_VALUE = 'value'

@asyncio.coroutine

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

def async_setup(hass, config):
"""Set up the Loxone component."""
user = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
host = config[DOMAIN][CONF_HOST]
port = config[DOMAIN][CONF_PORT]
api = LoxoneGateway(hass, user, password, host, port)
Copy link
Member

Choose a reason for hiding this comment

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

Will there be no exception if something is worn with the connection?

api.start_listener(api.get_process_message_callback())
async_track_time_interval(hass, api.send_keepalive, KEEPALIVEINTERVAL)

@asyncio.coroutine
def handle_event_bus_command(call):
"""Handle event bus services."""
value = call.data.get(ATTR_VALUE, DEFAULT)
device_uuid = call.data.get(ATTR_UUID, DEFAULT)
data = {device_uuid: value}
hass.bus.async_fire(EVENT, data)

hass.services.async_register(DOMAIN, 'event_bus_command',
handle_event_bus_command)

@asyncio.coroutine
def handle_websocket_command(call):
"""Handle websocket command services."""
value = call.data.get(ATTR_VALUE, DEFAULT)
device_uuid = call.data.get(ATTR_UUID, DEFAULT)
yield from api.send_websocket_command(device_uuid, value)

hass.services.async_register(DOMAIN, 'event_websocket_command',
handle_websocket_command)

return True


class LoxoneGateway:
""" Main class for the communication with the miniserver """
Copy link
Member

Choose a reason for hiding this comment

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

See PEP257 for details about proper docstrings and the docs about testing them.

def __init__(self, hass, user, password, host, port):
""" Username, password, host and port of a Loxone user """
self._hass = hass
self._user = user
self._password = password
self._host = host
self._port = port
self._ws = None
self._current_typ = None

@asyncio.coroutine
def send_websocket_command(self, device_uuid, value):
""" Send a websocket command to the Miniserver """
yield from self._ws.send(
"jdev/sps/io/{}/{}".format(device_uuid, value))

def get_process_message_callback(self):
"""Function to be called when data is received."""
return self._async_process_message

def start_listener(self, async_callback):
"""Start the websocket listener."""
try:
from asyncio import ensure_future
except ImportError:
from asyncio import async as ensure_future
# pylint: disable=deprecated-method
ensure_future(self._ws_listen(async_callback))

@asyncio.coroutine
def send_keepalive(self, _):
"""Send an keep alive to the Miniserver."""
_LOGGER.debug("Keep alive send")
yield from self._ws.send("keepalive")

@asyncio.coroutine
def _ws_read(self):
""" Establish the connection an read the messages"""
import websockets as wslib
try:
if not self._ws:
self._ws = yield from wslib.connect(
"ws://{}:{}/ws/rfc6455".format(self._host, self._port),
timeout=5)
yield from self._ws.send("jdev/sys/getkey")
yield from self._ws.recv()
key = yield from self._ws.recv()
yield from self._ws.send("authenticate/{}".format(get_hash(
key, self._user, self._password)))
yield from self._ws.recv()
yield from self._ws.send("jdev/sps/enablebinstatusupdate")
yield from self._ws.recv()
except Exception as ws_exc: # pylint: disable=broad-except
_LOGGER.error("Failed to connect to websocket: %s", ws_exc)
return

result = None
try:
result = yield from self._ws.recv()
except Exception as ws_exc: # pylint: disable=broad-except
_LOGGER.error("Failed to read from websocket: %s", ws_exc)
try:
yield from self._ws.close()
finally:
self._ws = None
return result

@asyncio.coroutine
def _ws_listen(self, async_callback):
""" Listen to all commands from the Miniserver"""
try:
while True:
result = yield from self._ws_read()
if result:
yield from _ws_process_message(result, async_callback)
else:
_LOGGER.debug("Trying again in 30 seconds.")
yield from asyncio.sleep(30)
finally:
print("CLOSED")
if self._ws:
yield from self._ws.close()

@asyncio.coroutine
def _async_process_message(self, message):
""" Process the messages """
if len(message) == 8:
unpacked_data = unpack('ccccI', message)
self._current_typ = int.from_bytes(unpacked_data[1],
byteorder='big')
else:
parsed_data = parse_loxone_message(self._current_typ, message)
_LOGGER.debug(parsed_data)
self._hass.bus.async_fire(EVENT, parsed_data)


@asyncio.coroutine

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

def _ws_process_message(message, async_callback):
""" Process the messages """
try:
yield from async_callback(message)
except: # pylint: disable=bare-except
_LOGGER.exception("Exception in callback, ignoring.")


def get_hash(key, username, password):
""" Get the login data from username and password """
key_dict = json.loads(key)
key_value = key_dict['LL']['value']
data = "{}:{}".format(username, password)
decoded_key = codecs.decode(key_value.encode("ascii"), "hex")
hmac_obj = hmac.new(decoded_key, data.encode('UTF-8'), hashlib.sha1)
return hmac_obj.hexdigest()


def parse_loxone_message(typ, message):
""" Parser of the Loxone message """
event_dict = {}
if typ == 0:
_LOGGER.debug("Text Message received!!")
event_dict = message
elif typ == 1:
_LOGGER.debug("Binary Message received!!")
elif typ == 2:
_LOGGER.debug("Event-Table of Value-States received!!")
length = len(message)
num = length / 24
start = 0
end = 24
# pylint: disable=unused-variable
for i in range(int(num)):
packet = message[start:end]
event_uuid = uuid.UUID(bytes_le=packet[0:16])
fields = event_uuid.urn.replace("urn:uuid:", "").split("-")
uuidstr = "{}-{}-{}-{}-{}".format(fields[0], fields[1], fields[2],
fields[3], fields[4])

Choose a reason for hiding this comment

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

continuation line over-indented for visual indent

Choose a reason for hiding this comment

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

continuation line over-indented for visual indent

value = unpack('d', packet[16:24])[0]
event_dict[uuidstr] = value
start += 24
end += 24
elif typ == 3:
pass
elif typ == 6:
_LOGGER.debug("Keep alive Message received!")
return event_dict
89 changes: 89 additions & 0 deletions homeassistant/components/sensor/loxone.py
@@ -0,0 +1,89 @@
"""
Loxone simple sensor

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.loxone/
"""

import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.const import (CONF_UNIT_OF_MEASUREMENT)

DOMAIN = 'loxone'
EVENT = 'loxone_received'

_LOGGER = logging.getLogger(__name__)

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Done

CONF_SENSOR_NAME = 'sensorname'
CONF_SENSOR_TYP = 'sensortyp'
CONF_UUID = 'uuid'

CONFIG_SCHEMA = vol.Schema({
Copy link
Member

Choose a reason for hiding this comment

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

You have to extend the sensor validation. Get the details from the link I provided in the previous comment about the configuration validation.

DOMAIN: vol.Schema({
vol.Required(CONF_UUID): cv.string,
vol.Required(CONF_SENSOR_NAME): cv.string,
vol.Required(CONF_SENSOR_TYP): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
sensor_name = config.get(CONF_SENSOR_NAME)
uuid = config.get(CONF_UUID)
sensor_typ = config.get(CONF_SENSOR_TYP)
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
sensor = Loxonesensor(sensor_name, uuid, sensor_typ, unit_of_measurement)
add_devices([sensor])
hass.bus.listen(EVENT, sensor.event_handler)


class Loxonesensor(Entity):
"""Representation of a Sensor."""

def __init__(self, name, uuid, sensor_typ, unit_of_measurement):
"""Initialize the sensor."""
self._state = None
self._name = name
self._uuid = uuid
self._sensor_typ = sensor_typ
self._unit_of_measurement = unit_of_measurement

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def should_poll(self):
""" Disable polling"""
return False

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement

def event_handler(self, event):
""" Event_handler """
if self._uuid in event.data:
if self._sensor_typ == "InfoOnlyAnalog":
self._state = round(event.data[self._uuid], 1)
elif self._sensor_typ == "InfoOnlyDigital":
self._state = event.data[self._uuid]
if self._state == 1:
self._state = "on"
else:
self._state = "off"
else:
self._state = event.data[self._uuid]
self.update()
self.schedule_update_ha_state()