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 1 commit
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
253 changes: 253 additions & 0 deletions homeassistant/components/loxone.py
@@ -0,0 +1,253 @@
"""
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)

Choose a reason for hiding this comment

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

line too long (84 > 79 characters)

from homeassistant.helpers.event import async_track_time_interval

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

Choose a reason for hiding this comment

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

If needed then you could go with websockets 3.4. I guess that we haven't update the requirement because it's not heavily used.


_LOGGER = logging.getLogger(__name__)

DOMAIN = 'loxone'
DEFAULT_HOST = '127.0.0.1'
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't make much sense. Or isn't the Miniserver an independent hardware unit only?

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.Optional(CONF_HOST, default=DEFAULT_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)

Choose a reason for hiding this comment

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

line too long (87 > 79 characters)


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

Choose a reason for hiding this comment

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

line too long (93 > 79 characters)


return True


class LoxoneGateway:
"""
Loxone Gateway
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.

Please go for an one-line docstring.


# pylint: disable=too-many-arguments, too-many-instance-attributes
Copy link
Member

Choose a reason for hiding this comment

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

Those are already disabled globally.

def __init__(self, hass, user, password, host, port):
"""
:param hass: Reference to the hass object
:param user: Username to communicate with the Miniserver
:param password: Password of the user
:param host: Address where the Loxone Miniserver runs
:param port: Port of the Loxone Miniserver on the host
Copy link
Member

Choose a reason for hiding this comment

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

Please replace the parameter with a simple docstring.

"""
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
:param device_uuid: Device id of a sensor or input
:param value: Value to be sent
"""
yield from self._ws.send("jdev/sps/io/{}/{}".format(device_uuid, value))

Choose a reason for hiding this comment

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

line too long (80 > 79 characters)


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

Choose a reason for hiding this comment

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

line too long (86 > 79 characters)

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/" + get_hash(key, self._user, self._password))

Choose a reason for hiding this comment

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

line too long (80 > 79 characters)

Copy link
Member

Choose a reason for hiding this comment

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

Use string formatting too -> "".format() please.

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):
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):
if len(message) == 8:
unpacked_data = unpack('ccccI', message)
self._current_typ = int.from_bytes(unpacked_data[1], byteorder='big')

Choose a reason for hiding this comment

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

line too long (81 > 79 characters)

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):
"""
:param message: Received message
:param async_callback: Callback function
"""
try:
yield from async_callback(message)
except: # pylint: disable=bare-except
_LOGGER.exception("Exception in callback, ignoring.")


def get_hash(key, username, password):
"""
:param key: Key from the Miniserver
:param username: Username
:param password: Password of the user
:return: Hashed Key
"""
key_dict = json.loads(key)
key_value = key_dict['LL']['value']
data = username + ":" + password
Copy link
Member

Choose a reason for hiding this comment

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

String formatting.

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
:param typ: Typ of the Message
:param message: Message from the Miniserver
:return: Parsed message as dict
"""
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 = fields[0] + "-" + \
fields[1] + "-" + \

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

fields[2] + "-" + \

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

fields[3] + \

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

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

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
74 changes: 74 additions & 0 deletions homeassistant/components/sensor/loxone.py
@@ -0,0 +1,74 @@
"""
Loxone simple sensor

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

Choose a reason for hiding this comment

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

Needs to point to the sensor documentation.

"""

import logging
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


def setup_platform(hass, config, add_devices):
"""Setup the sensor platform."""
sensor_name = config.get("sensorname")
uuid = config.get("uuid")
sensor_typ = config.get("sensortyp")
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 = 0.0
Copy link
Member

Choose a reason for hiding this comment

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

Set it to None and let Home Assistant handle the rest.

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