Componente genérico en HomeAssistant


Tras conseguir el «hola mundo» en una entrada anterior, ahora diseñaré mi primer componente. El ejemplo mostrado pretendo realizar un controlador para una pantalla LCD de texto. Independientemente del objetivo, el ejemplo lo planteo de forma genérica para cualquier tipo de componente. La idea de este tutorial es trabajar a través de plantillas de código sin entrar en detalle toda la maquinaria que hay detrás, para eso ya está la documentación oficial que voy enlazando.

Como HomeAssistant está diseñado por capas, en esta entrada sólo voy a tratar la realización de un servicio, posteriormente me pelearé con la interfaz de usuario, a la que hay que dedicar otro tiempo. En las páginas de desarrollo de HomeAssistant está este enlace [Using Services] donde se explican los servicios, muy escueto para variar, por eso me animo a escribir estas entradas.

Componente genérico

La primera decisión es si se va a crear un componente genérico o dedicado. Realmente con genérico, me refiero a realizar una interfaz mediante una clase abstracta. El ejemplo que planteo es realizar un controlador para pantallas LCD de texto genérico que servirá de interfaz para la realizar implementaciones concretas para cada tipo de LCD. La prueba la he realizado con una Raspberry Pi3 y un LCD del modelo [NHD-0420D3Z-FL-GBW-V3] el cual tiene una interfaz serie.

En HomeAssistant cuando se diseña un componente de este tipo, a cada una de las implementaciones para un tipo de dispositivo se le llama plataforma. Para crearlas son necesarias estas consideraciones:

  1. Se debe crear un directorio dentro de custom_components con el nombre del controlador genérico. El nombre de carpeta es recomendable que coincida con la entrada que se quiere usar en el fichero de configuración YAML.
  2. Como en cualquier proyecto python, se debe crear el fichero init.py en la nueva carpeta. Aquí irá una clase abstracta que represente el tipo de componente.

Para el componente que estoy realizando lo llamaré display_lcd, por tanto se debe crear la carpeta y el fichero: custom_components/display_lcd/init.py. Este fichero contener la definición DOMAIN=’display_lcd’ y esta es la definición clave que hará que pueda cargar desde el fichero YAML, para ello se añade como entrada de primer nivel lo siguiente (fichero .homeassistant/configuration.yaml):

display_lcd:

Al realizar el desarrollo de forma genérica se consigue que la configuración YAML pueda contener entradas tipo platform por ejemplo (fichero .homeassistant/configuration.yaml):

display_lcd:
    - platform: display_rs232
      port: /dev/ttyS0

    - platform: display_i2c

Para implementar las plataformas indicadas, hay que crear un fichero exactamente con el nombre de la plataforma en el directorio en el que estamos trabajando. Quedaría la siguiente estructura:

custom_components
  |
  +-- display_lcd
      |
      +-- __init__.py
      +-- display_rs232.py
      +-- display_i2c.py

Probando los servicios

Los servicios forman el API de HomeAssistant, y se pueden llamar desde la red o desde el código. Lo que hace HomeAssistant es exponerlos para que un Frontend los llame. De hecho el frontend por defecto [HomeAssistantPolymer] incluye una herramienta para hacer llamadas manualmente a cualquiera de los servicios disponibles. Todas estas llamadas usan el formato JSON quedando así la capa de presentación totalmente separada del núcleo de HomeAssistant.

Para probar los servicios se debe usar el primer icono de las herramientas de desarrolladores mostrados en la siguiente figura.

img

Si se despliega el cuadro de servicios y se selecciona alguno, se muestra en la descripción los parámetros requeridos para la llamada. En el ejemplo anterior ya he implementado una llamada para el componente display que consiste en cambiar la luminosidad de la pantalla, pero no he añadido la descripción, y la llamada en este caso requiere dos parámetros quedando:

{"entity_id" : "display_lcd.pantallita", "brightness" : "1"}

En el ejemplo original [Using Services] se añade un servicio mediante la llamada hass.services.register especificando solo el nombre y la función que maneja la llamada, pero no es la forma en que se debe hacer, basta con repasar el código de los componentes de HomeAssistant. La forma correcta y segura de realizarlo es mediante la definición de un esquema donde se especifican los parámetros que admite el servicio, el tipo de parámetros que son y si son obligatorios o no.

Para que funcione este esquema es necesario definirlo de la siguiente manera (fichero display_lcd/init.py):

import voluptuous as vol

...

ATTR_BRIGHTNESS = 'brightness'
ATTR_TEXT = 'text'

DISPLAY_BRIGHTNESS_SCHEMA = vol.Schema({
    vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
    vol.Required(ATTR_BRIGHTNESS): cv.positive_int,
    })

Otro detalle importante es que HomeAssistant recomienda usar programación asíncrona, por lo que el ejemplo original donde se usa la función setup se debe cambiar lo indicado en el código, donde se registra el servicio (fichero display_lcd/init.py):

...

async def async_setup(hass,config):
    hass.services.async_register(
        DOMAIN, SERVICE_BACKLIGHT, async_handle_display_service,
        schema=DISPLAY_BRIGHTNESS_SCHEMA)

async_handle_display_service es la retrollamada (callback) que se ejecuta cuando se llama al servicio. Esta retrollamada se puede implementar usando como plantilla el código existente en cualquiera de los componentes de HomeAssistant. Usándola en forma de plantilla (fichero display_lcd/init.py):

async def async_handle_display_service(service):
        """Handle calls of the display services."""
        targets = component.async_extract_from_service(service)

        update_tasks = []
        for display in targets:
            if service.service == SERVICE_BACKLIGHT:
                brightness = service.data.get(ATTR_BRIGHTNESS)
                #display.set_backlight(brightness)
                _LOGGER.info('Changed brightness to %s on display' % brightness)

            if not display.should_poll:
                continue
            update_tasks.append(display.async_update_ha_state(True))

        if update_tasks:
            await asyncio.wait(update_tasks, loop=hass.loop)

En el código anterior he comentado la llamada real que hace cambiar el brillo del display ya que todavía no he indicado como implementarlo, pero el código se puede probar y ver por el log como aparece la cadena «Changed brithtness …«. En el código anterior de la retrollamada se debe considerar lo siguiente:

  1. No es obligatorio que sea asíncrona pero recomendable cambiar el código.
  2. La llamada se ha realizado sobre una instancia de un display, la función async_extract_from_service permite obtener las entidades afectadas y por ello hay un bucle.
  3. El objeto service permite obtener el nombre de la llamada realizada y los atributos se obtienen con service.data.get.
  4. El resto del código se puede dejar tal cual, en modo plantilla, y cuando se profundice en HomeAssitant se enterá el por qué.
#HomeAssistant