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

Add lightwave components for switches and lights #18026

Merged
merged 25 commits into from Dec 2, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
154bfcc
Added lightwave components for switches and lights.
GeoffAtHome Oct 30, 2018
3810820
Address warnings raised by Hound
Oct 31, 2018
102bf58
Correcting lint messages and major typo. This time tested before commit.
GeoffAtHome Oct 31, 2018
c366c5d
Trying to fix author
GeoffAtHome Oct 31, 2018
451a40f
Minor lint changes
GeoffAtHome Oct 31, 2018
6b12336
Attempt to correct other lint error.
GeoffAtHome Nov 2, 2018
bc0a6c3
Another lint attempt.
GeoffAtHome Nov 2, 2018
a921147
More lint issues.
GeoffAtHome Nov 3, 2018
d93e6cf
Last two lint errors! Hurrah.
GeoffAtHome Nov 3, 2018
faeef0e
Changes after review from fabaff.
GeoffAtHome Nov 12, 2018
3bd6f5c
Moved device dependent code to PyPi.
GeoffAtHome Nov 24, 2018
9667ae0
Replaced DEPENDENCIES with REQUIREMENTS
GeoffAtHome Nov 25, 2018
ef25b9a
Updated following code review from Martin Hjelmare.
GeoffAtHome Nov 25, 2018
369f410
Added lightwave to requirements_all.txt
GeoffAtHome Nov 25, 2018
073592b
Omit lightwave from tests.
GeoffAtHome Nov 25, 2018
e728ab5
Updated requirements_all.txt
GeoffAtHome Nov 26, 2018
f5c2de6
Refactored how lightwave lights and switches load.
GeoffAtHome Nov 30, 2018
037c626
Removed imports that were no longer required.
GeoffAtHome Nov 30, 2018
c9d8ae0
Add guard for no discovery_info.
GeoffAtHome Dec 1, 2018
b08f076
Make it a guard clause and save indentation. Rename LRFxxx to LWRFxxx.
GeoffAtHome Dec 1, 2018
533e1e9
Sorted imports to match style guidelines.
GeoffAtHome Dec 1, 2018
1dee677
Correct return value.
GeoffAtHome Dec 1, 2018
1b7d3a6
Update requirements_all.txt
GeoffAtHome Dec 1, 2018
115a929
Catch case where we have no lights or switches configured.
GeoffAtHome Dec 1, 2018
f84a9fa
Improve configuration validation.
GeoffAtHome Dec 2, 2018
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
121 changes: 121 additions & 0 deletions homeassistant/components/light/lightwave.py
@@ -0,0 +1,121 @@
"""
homeassistant.components.light.lightwave
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements LightwaveRF lights.


My understanding of the LightWave Hub is that devices cannot be discovered so must be
registered manually. This is done in the configuration file:

switch:
- platform: lightwave
devices:
R1D2:
name: Room one Device two
R2D1:
name: Room two Device one

Each device requires an id and a name. THe id takes the from R#D# where R# is the room number
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
and D# is the device number.

If devices are missing the default is to generate 15 rooms with 8 lights. From this you will
be able to determine the room and device number for each light.

TODO:
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
Add a registration button. Until then the following command needs to be sent to the LightwaveRF hub:
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
echo -ne "100,\!F*p." | nc -u -w1 LW_HUB_IP_ADDRESS 9760

When this is sent you have 12 seconds to acknowledge the message on the hub.

For more details on the api see: https://api.lightwaverf.com/
"""
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved

import asyncio
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
import logging
import voluptuous as vol
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
from homeassistant.const import CONF_DEVICES, CONF_NAME
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved

DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
})

_LOGGER = logging.getLogger(__name__)

LIGHTWAVE_LINK = 'lightwave_link'
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
DEPENDENCIES = ['lightwave']


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
""" Find and return LightWave lights """
lights = []
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
lwlink = hass.data[LIGHTWAVE_LINK]

for device_id, device_config in config.get(CONF_DEVICES, {}).items():
name = device_config[CONF_NAME]
lights.append(LRFLight(name, device_id, lwlink))

async_add_entities(lights)


class LRFLight(Light):
""" Provides a LightWave light. """

def __init__(self, name, device_id, lwlink):
self._name = name
self._device_id = device_id
self._state = None
self._brightness = 255
self._lwlink = lwlink

@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS

@property
def should_poll(self):
""" No polling needed for a LightWave light. """
return False

@property
def name(self):
""" Returns the name of the LightWave light. """
return self._name

@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness

@property
def is_on(self):
""" True if the LightWave light is on. """
return self._state

async def async_turn_on(self, **kwargs):
""" Turn the LightWave light on. """
self._state = True

if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]

if not self._brightness == 255:
self._lwlink.turn_on_with_brightness(
self._device_id, self._name, self._brightness)
else:
self._lwlink.turn_on_light(self._device_id, self._name)

self.async_schedule_update_ha_state()

async def async_turn_off(self, **kwargs):
""" Turn the LightWave light off. """
self._state = False
self._lwlink.turn_off(self._device_id, self._name)
self.async_schedule_update_ha_state()
136 changes: 136 additions & 0 deletions homeassistant/components/lightwave.py
@@ -0,0 +1,136 @@
"""
Support for MQTT message handling.

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

import asyncio
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
import queue
import threading
import socket
import time
import logging
import voluptuous as vol

from homeassistant.const import CONF_HOST
from homeassistant.helpers import config_validation as cv

_LOGGER = logging.getLogger(__name__)

LIGHTWAVE_LINK = 'lightwave_link'
DOMAIN = 'lightwave'
LWRF_REGISTRATION = '100,!F*p'
LWRF_DEREGISTRATION = '100,!F*xP'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
})
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Try to start embedded Lightwave broker."""
host = config[DOMAIN].get(CONF_HOST)
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
hass.data[LIGHTWAVE_LINK] = LWLink(host)
return True


class LWLink():
SOCKET_TIMEOUT = 2.0
RX_PORT = 9761
TX_PORT = 9760

the_queue = queue.Queue()
thread = None
link_ip = ''

# msg = "100,!F*p."

def __init__(self, link_ip=None):
if link_ip != None:

Choose a reason for hiding this comment

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

comparison to None should be 'if cond is not None:'

LWLink.link_ip = link_ip

# methods
def _send_message(self, msg):
LWLink.the_queue.put_nowait(msg)
if LWLink.thread == None or not self.thread.isAlive():

Choose a reason for hiding this comment

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

comparison to None should be 'if cond is None:'

LWLink.thread = threading.Thread(target=self._sendQueue)
LWLink.thread.start()

def turn_on_light(self, device_id, name):
msg = '321,!%sFdP32|Turn On|%s' % (device_id, name)
self._send_message(msg)

def turn_on_switch(self, device_id, name):
msg = '321,!%sF1|Turn On|%s' % (device_id, name)
self._send_message(msg)

def turn_on_with_brightness(self, device_id, name, brightness):
"""Scale brightness from 0..255 to 1..32"""
brightness_value = round((brightness * 31) / 255) + 1
# F1 = Light on and F0 = light off. FdP[0..32] is brightness. 32 is
# full. We want that when turning the light on.
msg = '321,!%sFdP%d|Lights %d|%s' % (
device_id, brightness_value, brightness_value, name)
self._send_message(msg)
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved

def turn_off(self, device_id, name):
msg = "321,!%sF0|Turn Off|%s" % (device_id, name)
self._send_message(msg)

def _sendQueue(self):
while not LWLink.the_queue.empty():
self._send_reliable_message(LWLink.the_queue.get_nowait())

def _send_reliable_message(self, msg):
""" Send msg to LightwaveRF hub and only returns after:
an OK is received | timeout | exception | max_retries """
result = False
max_retries = 15
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as write_sock, socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as read_sock:

Choose a reason for hiding this comment

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

line too long (143 > 79 characters)

write_sock.setsockopt(
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
read_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
read_sock.settimeout(LWLink.SOCKET_TIMEOUT)
read_sock.bind(('0.0.0.0', LWLink.RX_PORT))
while max_retries:
max_retries -= 1
write_sock.sendto(msg.encode(
'UTF-8'), (LWLink.link_ip, LWLink.TX_PORT))
result = False
while True:
response, dummy = read_sock.recvfrom(1024)
response = response.decode('UTF-8')
if "Not yet registered." in response:
_LOGGER.error("Not yet registered")
self._send_message(LWRF_REGISTRATION)
result = True
break

response.split(',')[1]
if response.startswith('OK'):
result = True
break
if response.startswith('ERR'):
break

if result:
break

time.sleep(0.25)

except socket.timeout:
_LOGGER.error("LW broker timeout!")
return result

except:

Choose a reason for hiding this comment

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

do not use bare except'

_LOGGER.error("LW broker something went wrong!")

if result:
_LOGGER.info("LW broker OK!")
else:
_LOGGER.error("LW broker fail!")
return result
99 changes: 99 additions & 0 deletions homeassistant/components/switch/lightwave.py
@@ -0,0 +1,99 @@
"""
homeassistant.components.switch.lightwave
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements LightwaveRF switches.


My understanding of the LightWave Hub is that devices cannot be discovered

Choose a reason for hiding this comment

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

trailing whitespace

so must be registered manually. This is done in the configuration file

switch:
- platform: lightwave
devices:
R1D2:
name: Room one Device two
R2D1:
name: Room two Device one

Each device requires an id and a name. THe id takes the from R#D# where R# is the room number

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)
trailing whitespace

and D# is the device number.

If devices are missing the default is to generate 15 rooms with 8 lights. From this you will
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
be able to determine the room and device number for each light.

TODO:
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
Add a registration button. Until then the following command needs to be sent to the LightwaveRF hub:
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
echo -ne "100,\!F*p." | nc -u -w1 LW_HUB_IP_ADDRESS 9760

When this is sent you have 12 seconds to acknowledge the message on the hub.

For more details on the api see: https://api.lightwaverf.com/
"""
import asyncio
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
import logging
import voluptuous as vol
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
from homeassistant.const import CONF_DEVICES, CONF_NAME
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved

DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}
})

_LOGGER = logging.getLogger(__name__)

LIGHTWAVE_LINK = 'lightwave_link'
DEPENDENCIES = ['lightwave']


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
""" Find and return LightWave switches """
switches = []
GeoffAtHome marked this conversation as resolved.
Show resolved Hide resolved
lwlink = hass.data[LIGHTWAVE_LINK]

for device_id, device_config in config.get(CONF_DEVICES, {}).items():
name = device_config[CONF_NAME]
switches.append(LRFSwitch(name, device_id, lwlink))

async_add_entities(switches)


class LRFSwitch(SwitchDevice):
""" Provides a LightWave switch. """

def __init__(self, name, device_id, lwlink):
self._name = name
self._device_id = device_id
self._state = None
self._lwlink = lwlink

@property
def should_poll(self):
""" No polling needed for a LightWave light. """
return False

@property
def name(self):
""" Returns the name of the LightWave switch. """
return self._name

@property
def is_on(self):
""" True if LightWave switch is on. """
return self._state

async def async_turn_on(self, **kwargs):
""" Turn the LightWave switch on. """
self._state = True
self._lwlink.turn_on_switch(self._device_id, self._name)
self.async_schedule_update_ha_state()

async def async_turn_off(self, **kwargs):
""" Turn the LightWave switch off. """
self._state = False
self._lwlink.turn_off(self._device_id, self._name)
self.async_schedule_update_ha_state()