Home Assistant component code description

This is my document about the development of a component for the Home Assistant project. This page will fill in over the coming days.



W800rf32 component code

"""
Support for w800rf32 components.

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

This is the configuration for our component, it is from the configuration.yaml file. "w800rf32" is the DOMAIN as set below and device is the serial port our w800 hardware is connected to.

# Example configuration.yaml entry
w800rf32:
  device: PATH_TO_DEVICE

Imports

The next section shows the imports we need, remeber this is a fairly simple component, more complex components will have more imports. The logging and voluptuous imports are fairly normal python libraries.

"""
import logging
import voluptuous as vol


These are pretty obvious ~/homeassistant/const.py holds the constants for the Home Assistant project. Almost all of the constants are strings so you could just use the string, however, if the HA project changes a constant it is global, so best to use them.

    1. ATTR_ENTITY_ID = 'entity_id' <- the platform ID of an entity, ie: a light, switch, sensor etc
    2. ATTR_STATE = 'state' <- the state (on/off) of the entity
    3. CONF_DEVICE = 'device'<- the component device from config, in this case the serial port
    4. EVENT_HOMEASSISTANT_START = 'homeassistant_start'<- event that fires upon HA startup
    5. EVENT_HOMEASSISTANT_STOP = 'homeassistant_stop'<- event that fires upon HA shutdown

from homeassistant.const import (
     ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICE,
     EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)

Import the config_validation, explained below.

import homeassistant.helpers.config_validation as cv

Import HA Entity class which holds methods and properties such as entity name, state, device_class, icon etc. This class can be inherited and methods and properties can be overridden in your component, you will often see this in components to add functionality specific to that component.

See this for an example at the bottom of the file.


from homeassistant.helpers.entity import Entity

Requirements

HA covers the REQUIREMENTS section well, here is the link.
In a nutshell REQUIREMENTS is a project on PYPI that gets pulled into HA (pip install) and is responsible for encoding and decoding data from the hardware.


REQUIREMENTS = ['pyW800rf32']

Domain

DOMAIN is how your component will be known under HA. Since my hardware is a W800rf32A, I opted to call my DOMAIN 'w800rf32'.
The DOMAIN is a value stored in a dictionary called config and holds such items as config[DOMAIN][ATTR_DEVICE], which in this case, is the serial port the w800rf32 is connected to.

The code in ~/homeassistant/config.py reads the configuration.yaml file and stores the information in config[DOMAIN].


DOMAIN = 'w800rf32'

Constants

These are constants that we use, that are not defined in the ~/homeassistant/const.py file. They are specific to this component.


CONF_FIRE_EVENT = 'fire_event'
CONF_DEBUG = 'debug'
CONF_OFF_DELAY = 'off_delay'
EVENT_BUTTON_PRESSED = 'button_pressed'
RECEIVED_EVT_SUBSCRIBERS = []
W800_DEVICES = {}

Logger

This is the python logger and just returns the name of the module for logging. HA covers the LOGGING section well, except it seems it's not well linked to. here is the link.


_LOGGER = logging.getLogger(__name__)

Configuration

The voluptuous library is used to validate the schema of the configuration data, set in the configuration.yaml file (or possibly an included file).
In this component we have two variables allowed in the configuration.yaml file:
    1. "device": which is CONF_DEVICE
    2. "debug": which is CONF_DEBUG

Just to refresh your memory, the configuration.yaml file looks like so:
w800rf32:
  device: /dev/ttyUSB0
  debug: False
device is a required parameter, in other words it must be present. It's value, in this case '/dev/ttyUSB0',is also checked to be a string value (cv.string).
debug is optional and has a default value if it isn't present in the configuration.yaml file and is checked, if present, for a boolean value.

The extra=vol.ALLOW_EXTRA is there to allow additional keys in the data that are not listed in the schema, from throwing an exception.


CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_DEVICE): cv.string,
        vol.Optional(CONF_DEBUG, default=False): cv.boolean,
    }),
}, extra=vol.ALLOW_EXTRA)

setup()

Called from: homeassistant.setup
Returns: True

Setup is where we actually setup our component for HA.


First we should come up with a check list of things to do:
    1. Define a setup function and pass in the hass object and the config dictionary
    2. Setup an event handler to deal with in coming data
    3. Import the W800rf32 module
    4. Get the device from config, call connect and return an w800_object
    5. Setup two event handlers to deal with HA start and stop events
    6. Hand our w800_object over to hass
    7. Return if all is well

Setup is pretty simple, just define a function passing in the hass object and the config dictionary.
The hass object is described here. It is the HA instamce that has access to all the parts of the running HA system.

def setup(hass, config):
    """Set up the w800rf32 component."""

Define an event handler to handle data coming from the hardware. This is fairly simple, it just logs the event and then runs a handler for the specific entity. This could be simplified, however, it's a good way to write it, if you want expanded functionality in a more complex component.
handle_receive gets added to the HA event loop when HA starts running, see code below.

    # Declare the Handle event
    def handle_receive(event):
        """Handle received messages from w800rf32 gateway."""
        # Log event
        if not event.device:
            return
        _LOGGER.debug("Receive W800rf32 event in handle_receive")

This code is a list of event functions (the subscriber), which is added in the platform code.
When data is received from hardware, it triggers the handle_receive function which logs it and then runs the subscriber(event), in this case binary_sensor_update() from the platform code.

        # Callback to HA registered components.
        for subscriber in RECEIVED_EVT_SUBSCRIBERS:
            subscriber(event)

Load the W800rf32 module (REQUIREMENTS) which was installed from the PYPI repository. The HA documentation requests that modules are loaded in functions and not globaly at the top.

    # Try to load the W800rf32 module.
    import W800rf32 as w800

Right, next thing we need to do is grab the serial port device form the configuration.yaml file which was parsed into the config dictionary.
If we break this down it would look like so: device = config['w800rf32']['device']

Now call the Connect function in the PYPI module code that opens the serial port and setups the event handlers, returning an object. Remeber HA doesn't want us to do any lowlevel handling of the hardware in HA code, it must be a third party PYPI module.

    # device --> /dev/ttyUSB0
    device = config[DOMAIN][CONF_DEVICE]
    w800_object = w800.Connect(device, None)

Define two small functions and add them to the HA event loop, for events that get triggered on startup and shutdown of HA.
_start_w800rf32() is where the handle_receive function listed above gets added to the HA event loop.

    def _start_w800rf32(event):
        w800_object.event_callback = handle_receive
    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_w800rf32)

_shutdown_w800rf32() should be fairly obvious.

    def _shutdown_w800rf32(event):
        """Close connection with w800rf32."""
        w800_object.close_connection()
    hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_w800rf32)

The last task in setup is to add our object to the global hass.data dictionary and then return True

    hass.data['w800object'] = w800_object
    return True

apply_received_command()

Called from: (platform) w800rf32.binary_sensor_update()
Returns: None

apply_received_command applies a command received from an event, from the hardware through the platform and PYPI code.

def apply_received_command(event):
    """Apply command from w800rf32."""
Get the device and command from the event, in this case the device will an X10 code, example: A1 or G4 and either 'On' or 'Off' for the command.
    device_id = event.device.lower()
    command = event.command
    # Check if entity exists or previously added automatically
    if device_id not in W800_DEVICES:
        return

    _LOGGER.debug(
        "Device_id: %s, Command: %s",
        device_id,
        command
    )

Check if command is valid and then call update_state which is part of the w800rf32BinarySensor class within our platform code. This in turn will schedule a state update in HA.
    if command == 'On' or command == 'Off':
        # Update the w800rf32 device state
        is_on = command == 'On'
        W800_DEVICES[device_id].update_state(is_on)
If we need to fire and event, which we would configure in configuration.yaml then run this code and log it.
    # Fire event
    if W800_DEVICES[device_id].should_fire_event:
        W800_DEVICES[device_id].hass.bus.fire(
            EVENT_BUTTON_PRESSED, {
                ATTR_ENTITY_ID:
                    W800_DEVICES[device_id].entity_id,
                ATTR_STATE: command.lower()
            }
        )
        _LOGGER.debug(
            "w800rf32 fired event: (event_type: %s, %s: %s, %s: %s)",
            EVENT_BUTTON_PRESSED,
            ATTR_ENTITY_ID,
            W800_DEVICES[device_id].entity_id,
            ATTR_STATE,
            command.lower()
        )