home-assistant / core

:house_with_garden: Open source home automation that puts local control and privacy first.
https://www.home-assistant.io
Apache License 2.0
71.16k stars 29.83k forks source link

LEDENET component support? #530

Closed avaidyam closed 8 years ago

avaidyam commented 8 years ago

This LED controller API makes the combination of a 30$ controller and a 15$ LED strip much more appealing than a Philips Hue LED strip for more than double the cost. @sidoh has already reverse engineered its API and provided a very simple wrapper. It should be simple to integrate. Any thoughts?

The following Ruby files are the pertinent ones: api.rb and device_discovery.rb, and if translated to Python would be simple to plug in.

maddox commented 8 years ago

I saw this on reddit today. Can't wait to work on integrating it with the strip under my kitchen cabinets :)

I just gotta get one first.

avaidyam commented 8 years ago

Just ordered it! Should be here in a day or two, so I'll let you all know how it goes. For now, I'm sure a scripting bridge from HA would do the trick to the Ruby API.

avaidyam commented 8 years ago

I've been able to confirm that the API works from the given ruby scripts. Any interest in porting it?

maddox commented 8 years ago

Someone needs the device to properly do something like that.

avaidyam commented 8 years ago

@maddox I was indicating that I had the device, and was willing to attempt porting it... 😎 Was wondering if anyone would be interested in the port.

maddox commented 8 years ago

@avaidyam AHH. Yes OF COURSE. Who wouldn't be interested in having this heh.

Good luck!

avaidyam commented 8 years ago

Thanks! Have you ordered yours? It's pretty handy, and for 30$ + 15$ for a 5m light strip, it's an amazing Philips Hue replacement.

I'm just struggling to have banana clips stick properly in the thing.

happyleavesaoc commented 8 years ago

Milight/Limitless LED also supports LED strips. Already has an HA component as well.

avaidyam commented 8 years ago

@maddox I've managed to get the ruby script successfully working, so I'll be porting that tonight. @happyleavesaoc I can probably model the component the same way then.

avaidyam commented 8 years ago

I've ported the ruby scripts as best I could to python. Any issues here?

API_PORT = 5577

class LimitlessLED(Light):
    def __init__(self, device_address, reuse_connection=False, max_retries=3):
        self.device_address = device_address
        self.reuse_connection = reuse_connection
        self.max_retries = max_retries

    @property
    def is_on(self):
        return (status.bytes[13] & 0x01) == 1

    @property
    def turn_on(self):
        send_bytes_action(0x71, 0x23, 0x0F, 0xA3)
        return True

    @property
    def turn_off(self):
        send_bytes_action(0x71, 0x24 ,0x0F, 0xA4)
        return True

    def update_color(self, r, g, b):
        checksum = (r + g + b + 0x3F) % 0x100
        send_bytes_action(0x31, r, g, b, 0xFF, 0, 0x0F, checksum)

    def current_color(self):
        status.bytes[6, 3]

    def reconnect(self):
        create_socket()

    # Example response:
    # [129, 4, 35, 97, 33, 9, 11, 22, 33, 255, 3, 0, 0, 119]
    #                         R   G   B   WW            ^--- LSB indicates on/off
    def status(self):
        socket_action do
            send_bytes(0x81, 0x8A, 0x8B, 0x96)
            flush_response(14)

    def flush_response(self, msg_length):
        self.socket.recv(msg_length, socket.MSG_WAITALL)

    def send_bytes(self, *b):
        self.socket.write(b.pack('c*'))

    def send_bytes_action(self, *b):
        socket_action { send_bytes(self, *b) }

    def create_socket(self):
        if not ((self.socket is None) or (self.socket.closed)):
            self.socket.close()
        self.socket = socket()
        self.socket.connect(self.device_address, API_PORT)

    def socket_action(self):
        tries = 0
        try:
            if not ((self.socket is None) or (self.socket.closed)):
                create_socket()
            yield
        catch Errno::EPIPE, IOError => e:
            tries += 1

                if tries <= self.max_retries
                        reconnect()
                        retry
                else
                        throw e
            finally:
            if not (self.socket.closed or self.reuse_connection)
                self.socket.close()

def discover_devices(expected_devices=1, timeout=5):
    send_socket = socket(AF_INET, SOCK_DGRAM)
    send_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    send_socket.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
    send_socket.sendto('HF-A11ASSISTHREAD', ('<broadcast>', 48899))

    discovered_devices = []
    try:
        Timeout::timeout(timeout) do:
                while true:
                    data = send_socket.recv(1024)
                    discovered_devices.push(data.split(','))
                if discovered_devices.count >= expected_devices
                    throw Timeout::Error
    except Timeout::Error:
        # expected
    finally:
        send_socket.close()
    return discovered_devices
avaidyam commented 8 years ago

@tomduijf Do you think you could proofread the Python translation here?

tomduijf commented 8 years ago

Looks ok as a stand-alone module. Don't have such a device, so cant comment on it workings.

For HASS integration it'll probably need some more work. So this works with some kind of broadcast discovery? Perhaps it's already natively discovered by netdisco.

I think i'll order me one of these ledenet devices, looks fun :)

sidoh commented 8 years ago

The wifi module the LEDENET devices use (documentation) supports discovery via UDP broadcast. It doesn't seem like a standard discovery protocol.

@avaidyam -- maybe good to link to my repo in this code so there's a sensible place to report issues?

avaidyam commented 8 years ago

@sidoh Hey! Of course, that's a great idea, will do that. Could you help out with the Python translation into a single component?

@tomduijf Like @sidoh said, it's a non-standard UDP broadcast discovery, so unless we explicitly bake that support in, it doesn't seem likely.

sidoh commented 8 years ago

My Python's not strong enough to be of much help with that. What you have looks enough like the Ruby that I suspect it wouldn't work. Might be best to restructure it a bit in a way that's better suited for Python.

avaidyam commented 8 years ago

@sidoh @maddox I've spent a little time in getting a good port working here.

Essentially, brightness can be achieved with (RGBW * % brightness), and a kelvin scale for plain RGB strips can be achieved using the ktorgb() function, and you can also adjust the brightness from there.

I've only barely tested it, but it should have API-parity with @sidoh's stuff, and it works on my strip. I'll work on getting a component in for this for HA.

sidoh commented 8 years ago

RGB*%brightness is destructive, right? If you make something darker, you can't make it brighter again?

I ended up doing RGB -> HSL, adjusting luminosity, and converting back to RGB. That seemed to work pretty well. Code here:

https://github.com/sidoh/ha_gateway/blob/master/ha_gateway.rb#L41

avaidyam commented 8 years ago

@sidoh That's a smart idea, I'll try that. Have you taken a look at the additional Kelvin scale? It's a fake incandescent lighting, but it does the job alright.

avaidyam commented 8 years ago

@sidoh @maddox Here's what I've got for the LEDENET component. It's not working that well, and I can't figure out how to display an RGB color selector in the frontend...

import logging
from ledenet import *

from colorsys import *
from homeassistant.components.light import (
    Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR)

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = []
#REQUIREMENTS = ['https://github.com/avaidyam/LEDENET/']

def setup_platform(hass, config, add_devices_callback, discovery_info=None):
    """ Find and return LEDENET lights. """
    bulbs = LEDENETDevice.discover_devices(expected_devices=999, timeout=5)
    add_devices_callback([LEDENETLight(n) for n in bulbs])

class LEDENETLight(Light):
    """ Provides an LEDENET bulb. """

    def __init__(self, bulb):
        self._bulb = bulb
        self._name = self._bulb.ip.decode('utf-8')

    @property
    def should_poll(self):
        return False

    @property
    def name(self):
        """ Returns the name of the device if any. """
        return self._name

    @property
    def brightness(self):
        """ Brightness of this light between 0..255. """
        rgb = self._bulb.get_color()
        hls = rgb_to_hls(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0)
        return int(hls[1] * 255)

    @property
    def rgb_color(self):
        """ rgb color value. """
        return self._bulb.get_color()

    @property
    def is_on(self):
        """ True if device is on. """
        return self._bulb.is_on()

    def turn_on(self, **kwargs):
        """ Turn the device on. """
        self._bulb.set_on(False)

        bright = 1.0
        if ATTR_BRIGHTNESS in kwargs:
            bright = float(kwargs[ATTR_BRIGHTNESS]) / 255.0

        rgb = [1.0, 1.0, 1.0]
        if ATTR_RGB_COLOR in kwargs:
            rgb = kwargs[ATTR_RGB_COLOR]

        hls = rgb_to_hls(rgb[0], rgb[1], rgb[2])
        hls[1] = bright
        rgb = hls_to_rgb(hls[0], hls[1], hls[2])

        self._bulb.set_color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255, 0x00)
        self.update_ha_state()

    def turn_off(self, **kwargs):
        """ Turn the device off. """
        self._bulb.set_on(False)
        self.update_ha_state()
balloob commented 8 years ago

For this platform, I think you want to enable polling.

avaidyam commented 8 years ago

@balloob Gotcha, thanks. It's not really working right to begin with, so I'll continue debugging it.

ryanlaux commented 8 years ago

Any updates on support for this ? I have a similar LEDNET wifi strip adapter, trying to integrate somehow as well. Best I've found so far is a python script that sends commands as hex codes. For now I can turn on/ off with a command line strip, but would like a bit more control over it eventually.

danielolson13 commented 8 years ago

Here's a python project for Lednet bulbs. https://github.com/beville/flux_led This may be a good place to start. I've only used it with a single bulb

avaidyam commented 8 years ago

Unfortunately, the code here is as-is. I've switched away from this platform, which I found fairly unreliable, to a SmartThings hub.

balloob commented 8 years ago

Closing this issue as feature requests should be posted in the forum.