Source Allies Logo

Sharing Our Passion for Technology

& Continuous Learning

<   Back to Blog

Making home assistant work with X10

Home assistant device sitting on shelf

Background

For the last 20 years, I’ve had some sort of automated home system. Around a year ago I added Google Home and voice controlled automation. To do this, I used HA Bridge to emulate a Phillips Hue Hub to connect my X10 lights to the Google Home.

Problem

In February, Google released an update to their hue bridge software. This update broke the ability for it to successfully connect to HA Bridge.

When things stopped working, I searched on Google and found out there was no way to make my old solution work. However, one solution I kept finding was Home Assistant, a package recommended by several teammates at Source Allies. I'd previously attempted to install and run Home Assistant, but kept running into issues getting it integrated with my lighting controls. This was a perfect opportunity to try again.

Investigation

I xen-create-image'd a new vm on my xen server and followed the install instructions for Home Assistant. The page came up, but it only detected one of the lights on my HA Bridge, so it still had the same problem. At that point I had 3 options.

  1. Fix the issue with it detecting my HA Bridge lights
  2. Use the X10 integration that it comes with
  3. Find/write another solution

I rejected the first option because I wanted to stop using emulating and other hacked together solutions that might be broken by the next update. I examined the second option and it wouldn't work for several reasons.

  1. It required the Home Assistant computer to also be the X10 control computer. At my house, currently those lived on different machines and there were PHP incompatibility issues keeping them separate (Home Assistant needed a newer version than software on the x10 box).
  2. It didn't seem to have dim support, just on and off.
  3. I wanted to learn about writing modules for Home Assistant. I had a feeling I'd run into more things it couldn’t do that I would want to do later.

I went with option 3, I already had another light control package running (domus.link) for an Android app, which provided a RESTful API. I reviewed the Home Assistant documentation and chose one of the example plugins to start my project.

Infrastructure

I created a new directory in my dev directory, wrote a quick Ansible script to deploy it to the home assistant server, checked it into my Git server, installed my standard Jenkins hook, and created a Jenkins job to deploy when changes are committed. (Probably overkill for such a small project, but as I said, I have a feeling I'm going to be writing other plugins. I'm a geek and enjoy doing things like that, so sue me).

Home Assistant Interface

The Home Assistant light interface is pretty simple and straightforward. I implemented it using Python (a language I've used before in other projects). It provides:

  • A setup_platform(hass, config, add_devices, discovery_info) call home assistant calls to initialize your platform (what home assistant calls plugins)
  • A class called Light you subclass to create your light
  • Lines you add to configuration.yaml to enable your new light

Setup platform

The first thing I did was set it up without any connection to real objects based on the example.


import logging
import requests

import voluptuous as vol

# Import the device class from the component that you want to support
from homeassistant.components.light import SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS, Light, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_HOST): cv.string,
    vol.Optional(CONF_PASSWORD): cv.string,
})

def setup_platform(hass, config, add_devices, discovery_info=None):

    # Assign configuration variables. The configuration check takes care they are
    # present.
    host = config.get(CONF_HOST)
    base_url="http://"+host+"/api.php"
    password = config.get(CONF_PASSWORD)

    # Add devices
    light_array = []

    light_array.append(DomusLight("fred"))
    light_array.append(DomusLight("george"))
    light_array.append(DomusLight("walter"))

    add_devices(light_array)

class DomusLight(Light):

    def __init__(self, alias):
        self._name = alias
        self._state = False
        self._brightness = 100

    @property
    def name(self):
        # Return the display name of this light.
        return self._name

    @property
    def brightness(self):
        # Return the brightness of the light.
        return self._brightness

    @property
    def is_on(self):
        # Return true if light is on.
        return self._state

    def turn_on(self, **kwargs):
        # Instruct the light to turn on.
        self._brightness=kwargs.get(ATTR_BRIGHTNESS, 255)
        self._state=True

    def turn_off(self, **kwargs):
        # Instruct the light to turn off.
        self._state=False

    def update(self):
        # Fetch new state data for this light.

Success!! Home Assistant now has 3 lights called fred, george, and walter that act like lights, but don't do anything. I'll be honest it took several iterations and checking of logs before I managed to get this to happen.

Pull list of lights from DOMUS.link

The next step was to replace the bogus list of lights with a real one pulled from the RESTful service so I added this code to the setup_platform call to replace the fake light creation.


    # Verify that passed in configuration works
    response = requests.get(base_url)
    if not response.ok:
        _LOGGER.error("Could not connect to Domus.Link")
        return False

    # Add devices
    response=requests.get(base_url+"/aliases/all",auth=("", password));
    if not response.ok:
        _LOGGER.error("Could not connect to Domus.Link")
        return False

    light_array = []

    for alias in response.json()['aliases']:
        if (alias['aliasMapElement']['elementType'].upper() == 'LIGHT'
             and
             alias['enabled']):
            light_array.append(DomusLight(alias))
    add_devices(light_array)

I discovered the fields in the JSON, by examining the result of sending the GET ...update the Home Assistant, and …SCORE! Finally, lots of lights appear (not that they do anything yet, but it's cool to see them.

Make the lights actually do something

Then I realized I needed the url and password in the DomusLight class so I added them to the constructor and passed them in.


    def __init__(self, alias, base_url, password):
        self._name = alias
        self._state = False
        self._brightness = 100
        self._base_url = base_url
        self._password = password



    light_array.append(DomusLight(alias,base_url,password))

Then implement the on, off, and update status calls


    def turn_on(self, **kwargs):
        # Instruct the light to turn on.
        request_string=self._base_url+"/on/"+self._alias

    def turn_off(self, **kwargs):
        # Instruct the light to turn off.
        response=requests.post(self._base_url+"/off/"+self._alias,auth=("", self._password));

    def update(self):
        # Fetch new state data for this light.
        response=requests.get(self._base_url+"/aliasstate/"+self._alias,auth=("", self._password));
        status=response.json()
        self._state = status['state']==1
        self._brightness = int(status['level'])

After a little debugging the lights could be turned on and off.

Next, enable dimming

I had to dig a bit to enable dimming as the example didn't actually implement it.

The class had to return the proper bitfield to the supported_features call to enable the dimmer slider on the UI.


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

Then I had to implement dimming in the class. Here I found out Home Assistant does brightness 0-255 and DOMUS.link is 0-100 so I had to make a few modifications.

    @property
    def brightness(self):
        # Return the brightness of the light.
        return int(float(self._brightness)*2.55)

    def turn_on(self, **kwargs):
        # Instruct the light to turn on.
        new_brightness=int(float(kwargs.get(ATTR_BRIGHTNESS, 255))/2.55)
        request_string=self._base_url+"/dimbright/"+self._alias+("/0","/1")[self._state]+"/"+str(self._brightness)+"/"+str(new_brightness)
        response=requests.post(request_string,auth=("", self._password));

Now, dimming mostly worked, however, there are still some problems. I found that dimming to 0 did nothing instead of dimming to minimum, some modules actually needed an 'on' call before being dimmed, and some of my modules couldn't dim. More complexity to handle that.

    def __init__(self, alias, base_url, password, capabilities):
        self._name = alias['label'].replace("_"," ")
        self._alias=alias['label']
        self._state = False
        self._brightness = 100
        self._base_url = base_url
        self._password = password
        self._capabilities = capabilities


    for alias in response.json()['aliases']:
        if (alias['aliasMapElement']['elementType'].upper() == 'LIGHT'
             and
             alias['enabled']):
           if ( alias['moduleType'].startswith("AM") ):
               light_array.append(DomusLight(alias,base_url,password,0))
           else:
               light_array.append(DomusLight(alias,base_url,password,SUPPORT_BRIGHTNESS))
    add_devices(light_array)



    @property
    def supported_features(self):
        # Flag supported features.
        return self._capabilities



    def turn_on(self, **kwargs):
        # Instruct the light to turn on.
        if(not self._state):
            request_string=self._base_url+"/on/"+self._alias
            response=requests.post(request_string,auth=("", self._password));
            self._state=True
            self._brightness=100
            if(self._capabilities == SUPPORT_BRIGHTNESS):
                new_brightness=int(float(kwargs.get(ATTR_BRIGHTNESS, 255))/2.55)
                if(new_brightness < 1):
                    new_brightness=1
                request_string=self._base_url+"/dimbright/"+self._alias+("/0","/1")[self._state]+"/"+str(self._brightness)+"/"+str(new_brightness)
            response=requests.post(request_string,auth=("", self._password));

Last add some error checking

I haven't yet decided if I'm going to make this code fit the model so it could be included in the master project. The interface functionality would have to be moved to a PyPI library to qualify for inclusion. I'm sure I'll end up tweaking it a bit, but I'm pretty happy with it for now. For my next trick I'm looking at adding push based presence detection by adding a callback to my DHCP server to detect when my phone pulls DHCP (I hate polling for things like that).

This project continues to evolve. For the latest version of the code, check out my project on GitHub.