Home Assistant: Scripts para Personalizar el Sistema

Home Assistant: Scripts for Customization No API's No Chat Bots

En este artículo, profundizaremos en la personalización de Home Assistant, creando nuestros propios scripts para recopilar datos de sensores remotos y otros dispositivos de control. También veremos varias formas de comunicarnos con los sensores remotos.

Obtener datos de temperatura en remoto

Supongamos que tiene este problema: tienes varios sensores de temperatura, como el DS1820 en tu casa, conectados a varios ODROID y deseas enviar los datos a Home Assistant, que se ejecuta en un único dispositivo. Necesitas decidirte por un protocolo de transporte de datos y escribir un script para recopilar las lecturas de temperatura y pasarlas a Home Assistant.

Vamos a analizar algunas técnicas:

  • Sondeo por HTTP
  • Conexión a través de la API de Home Assistant
  • Conexión a través de MQTT

Sondeo por HTTP

Si estás acostumbrado a desarrollar web, probablemente habrás usado la denominada CGI (Interfaz Común de Comunicaciones), la forma más antigua de generar contenido dinámico utilizando un servidor web (http://bit.ly/2jNVkjT). Básicamente, carga un script en el servidor, independientemente del lenguaje, que es requerido por el servidor web y que a su vez sirve el resultado del script al cliente. Obviamente, primero necesitas instalar un servidor HTTP en tu host remoto y activar el soporte CGI. Usaremos Apache 2.4:

$ sudo apt-get install apache2
$ sudo a2enmod cgi
La configuración por defecto asigna /cgi-bin/URL a /usr/lib/cgi-bin en tu sistema de archivos. Cualquier script ejecutable que coloques en esta ubicación puede ser requerido por el servidor web. Vamos a suponer que puedes obtener los datos de temperatura del host remoto con estos comandos shell:
$ cat /sys/devices/w1_bus_master1/28-05168661eaff/w1_slave
c6 01 4b 46 7f ff 0c 10 bd : crc=bd YES
c6 01 4b 46 7f ff 0c 10 bd t=28375
En el resultado anterior, la primera línea valida la lectura del valor (si coincide con el CRC) y la segunda línea devuelve el valor en mili-celsius. Vamos a crear dos scripts (no olvide marcarlos como ejecutables) para ilustrar el código en dos lenguajes diferentes: BASH y Python. Los archivos se almacenarán en /usr/lib/cgi-bin/temperature.sh y /usr/lib/cgi-bin/temperature.py.
#!/bin/bash 

filename='/sys/devices/w1_bus_master1/28-05168661eaff/w1_slave' 
valid=0 

echo "Content-Type: text/plain" 
echo  

# read line by line, parse each line 
while read -r line 
do 
    if [[ $line =~ crc=.*YES ]]; then 
        # the CRC is valid. Continue processing 
        valid=1 
        continue 
    fi 
    if [[ "$valid" == "1" ]] && [[ $line =~ t=[0-9]+ ]]; then 
        # extract the temperature value 
        rawtemperature=`echo "$line" | cut -d "=" -f 2` 
        # convert to degrees celsius and keep 1 digit of accuracy 
        echo "scale=1;$rawtemperature/1000" | bc 
    fi 
#read line by line from $filename 
done < "$filename"
Figura 1a Hay dos formas de leer la misma temperatura, esta en bash
#!/usr/bin/python 
import re 

filename = '/sys/devices/w1_bus_master1/28-05168661eaff/w1_slave' 
valid = False 

print "Content-Type: text/plain" 
print "" 

# execute the command and parse each line of output 
with open(filename) as f: 
    for line in f: 

        if re.search('crc=.*YES', line): 
            # the CRC is valid. Continue processing 
            valid = True 
            continue 

        if valid and re.search('t=[0-9]+', line): 
            # extract the temperature value 
            temperature = re.search('t=([0-9]+)', line) 
            # convert to degrees celsius and keep 1 digit of accuracy 
            output = "%.1f" % (float(temperature.group(1))/1000.0) 
            print output 
Figura 1b – Y esta en Python

Analicemos un poco los scripts. Ambos scripts empiezan con una línea shebang que le indica al llamador qué intérprete usar para ejecutar el script (línea 1). A continuación, definimos dos variables que apuntan al archivo que va a leer (línea 4) y una variable para recordar si la lectura es válida o no (línea 5). En las líneas 7 y 8, grabamos los encabezados HTTP. El script CGI tiene que devolver los encabezados HTTP en las primeras líneas, separados por una línea en blanco del resto del resultado. El servidor web necesita al menos el encabezado Content-Type para procesar la solicitud. Si omites esto, recibirás un error de HTTP 500. En la línea 11 empezamos a leer las líneas del archivo para analizarlas. Buscamos un CRC válido con una expresión regular en la línea 14 y si es correcto, fijamos valid = true. En la línea 19, si el CRC es true y la línea contiene una temperatura, extraemos la temperatura en bruto (línea 21) y la convertimos a Celsius, con un digito de precisión (línea 23), y la mostramos como resultado estándar. Para acceder a los datos, puede usar cualquier cliente HTTP, como wget, tal y como se muestra en la Figura 2.

Figura 2 - Extrayendo los datos del host remoto

Puede haber ligeras diferencias en el resultado que se devuelva debido a los diferentes métodos de redondeo utilizados, o por variaciones en el periodo de tiempo en el que se realiza la consulta, lo cual puede provocar que los datos del sensor fluctúen un poco.

Por razones de seguridad, puedes activar la autenticación básica HTTP en la configuración de tu servidor. Necesitarás SSL/HTTPS con certificados válidos para protegerte de cualquiera que quiera analizar tu tráfico, aunque esta cuestión está fuera del alcance de este artículo. Puedes leer más sobre este tema aquí y aquí.

Para añadir el sensor a Home Assistant podemos usar el sensor REST dentro de configuration.yaml :

sensor:
 ...
 - platform: rest
   resource: http://192.168.1.13/cgi-bin/temperature.sh
   name: Temperature REST Bash
   unit_of_measurement: C
 - platform: rest
   resource: http://192.168.1.13/cgi-bin/temperature.py
   name: Temperature REST Python
  unit_of_measurement: C
Puedes conseguir el código aquí.

Pros de este método:

  • Es fácil de implementar si estás familiarizado con el desarrollo web.
  • En Home Assistant se reinician los nuevos datos que se sondean

Contras de este método:

  • El uso de un servidor web lo expone a posibles vulnerabilidades.
  • El servidor web puede usar muchos recursos en comparación con lo que realmente necesitas hacer.

Conexión a través de la API de HA

Una técnica diferente en la que no interviene un servidor web es mover los datos del sensor a Home Assistant desde el sistema remoto. Podemos usar una Plantilla Sensor para almacenar y presentar los datos. Para hacer esto, puedes hacer que el script de la Figura 3 sea requerido periódicamente por cron en el sistema remoto

#!/bin/bash 

filename='/sys/devices/w1_bus_master1/28-05168661eaff/w1_slave' 
homeassistantip='192.168.1.9' 
haport=8123 
api_password='odroid' 
sensor_name='sensor.temperature_via_api' 
valid=0 

# read line by line, parse each line 
while read -r line 
do 

    if [[ $line =~ crc=.*YES ]]; then 
        # the CRC is valid. Continue processing 
        valid=1 
        continue 
    fi 
    if [[ "$valid" == "1" ]] && [[ $line =~ t=[0-9]+ ]]; then 
        # extract the temperature value 
        rawtemperature=`echo "$line" | cut -d "=" -f 2` 
        # convert to degrees celsius and keep 1 digit of accuracy 
        temperature=`echo "scale=1;$rawtemperature/1000" | bc` 
        # push the data to the Home Assistant entity via the API 
        curl -X POST -H "x-ha-access: $api_password" -H "Content-Type: application/json" \ 
        --data "{\"state\": \"$temperature\"}" http://$homeassistantip:$haport/api/states/$sensor_name 
    fi 
#read line by line from $filename 
done < "$filename"
Figura 3: Enviando datos a través de la API de HA

Como puede ver, el código es similar al ejemplo anterior, excepto que en la línea 25 utiliza la API REST de Home Assistant para enviar la lectura de la temperatura. La API REST requiere que envíes la clave de la API de Home Assistant dentro de un encabezado HTTP, y los datos que quieras cambiar deben estar en una carga JSON en la solicitud POST. La URL que envías es tu instancia de Home Assistant /api/states/sensor.name. Para activar esto y enviar los datos cada 5 minutos, añade la siguiente entrada cron:

$ crontab -e
*/5 * * * * /bin/bash /path/to/script/temperature-HA-API.sh > /dev/null 2>&1
La configuración de Home Assistant debería parecerse a esto:
sensor:
…
- platform: template
   sensors:
     temperature_via_api:
       value_template: '{{ states.sensor.temperature_via_api.state }}'
       friendly_name: Temperature via API
       unit_of_measurement: C
La plantilla sensor generalmente se utiliza para extraer los datos de otras entidades de Home Assistant y, en este caso, la utilizamos para extraer datos de sí mismo. Este truco te evita que se eliminen datos de estado tras una actualización externa. Antes de configurar la temperatura, el estado del sensor estará en blanco. Una vez que cron ejecute el script por primera vez, obtendrás datos de temperatura. Puedes descargar el código desde here

Pros de este método:

  • Permite controlar cuando se envían los datos
  • El uso de recursos es muy bajo.

Contras de este método:

  • Tu script necesita tener claramente tu contraseña secreta de Home Assistant
  • Cuando se reinicia Home Assistant, el sensor no tendrá ningún valor hasta la primera actualización

Conexión a través de MQTT

El protocolo MQTT es un protocolo de máquina a máquina diseñado para la eficiencia (y para entornos de baja potencia) y ya ha sido tratado en anteriores artículos de ODROID Magazine. El modo de funcionamiento parte de un servidor central llamado broker que transmite mensajes a clientes que se suscriben a un tema común. Piensa en un tema como si fuera algo así como un canal IRC donde los clientes se conectan y se envían mensajes específicos.

Home Assistant tiene un MQTT Broker, integrado, pero en mis pruebas lo encontré poco fiable, así que usé un broker dedicado llamado Mosquitto. Se puede instalar en el mismo sistema que Home Assistant o en un sistema diferente. Para instalarlo, sigue estos pasos:

$ sudo apt-get install mosquitto mosquitto-clients
$ sudo systemctl enable mosquitto
La versión 3.11 de MQTT soporta autenticación, de modo que, debes configurar un nombre de usuario y una contraseña que serán compartidos por el bróker y los clientes y opcionalmente, Encriptación SSL. En mi configuración, utilicé la autenticación usuario-contraseña y añadí el usuario 'ODROID'
$ sudo mosquitto_passwd -c /etc/mosquitto/passwd ODROID
$ sudo vi /etc/mosquitto/conf.d/default.conf
allow_anonymous false
password_file /etc/mosquitto/passwd
Puedes activar el soporte genérico para MQTT en Home Assistant añadiendo una plataforma MQTT en configuration.yaml (recuerda que el parámetro mqtt_password se define en secrets.yaml):
mqtt:
  broker: 127.0.0.1
  port: 1883
  client_id: home-assistant
  keepalive: 60
  username: ODROID
  password: !secret mqtt_password
Para enviar datos de temperatura a Home Assistant, nuestro script necesitará la librería Python Paho-MQTT. Para analizar los datos de configuración necesitaremos también la librería python-yaml:
$ sudo apt-get install python-pip python-yaml
$ sudo pip install paho-mqtt
El script se ejecuta como un demonio, realizando lecturas periódicas de temperatura en segundo plano y enviando los cambios a través de MQTT. El código que lee la temperatura en sí (línea 40) es el mismo que aparecen en la Figura 1b y no se muestra en la Figura 4 para simplificar. El único cambio es que, en lugar de mostrar la temperatura, la devuelve como una cadena.

El código comienza importando algunos módulos auxiliares, definiendo las funciones para analizar la configuración YAML en un diccionario. La lectura de la temperatura y la ejecución empiezan en la línea 57. Se define y se activa un nuevo objeto cliente MQTT con los detalles necesarios para acceder al bróker MQTT. En la línea 61, hay un subproceso en segundo plano iniciado por la llamada loop_start () que asegura que el cliente permanezca conectado al bróker MQTT. Sin él, la conexión expiraría tras un tiempo y tendrías que volver a conectarte manualmente. Tienes más información sobre la API MQTT en Python disponible aquí. En la línea 65, aparece un bucle que lee los datos de temperatura, los compara con la última lectura de temperatura y, si hay un cambio, publica un mensaje MQTT al bróker con la nueva temperatura. Después, el código está inactivo durante un tiempo antes de la próxima lectura. Cuando se publican datos en el bróker (en la línea 71), debes especificar el tema MQTT, el valor que se envía y también si estos datos deben ser persistentes o no. Los datos persistentes son muy ventajosos, ya que puedes obtener la última lectura de temperatura desde MQTT cuando inicias Home Assistant y leer la temperatura desde el principio. Puedes conseguir el código completo aquí.

#!/usr/bin/python 
import paho.mqtt.client as mqtt 
import re 
import time 
import sys 
import yaml 

# Prerequisites: 
# * pip: sudo apt-get install python-pip 
# * paho-mqtt: pip install paho-mqtt 
# * python-yaml: sudo apt-get install python-yaml 

# Configuration file goes in /etc/temperature-mqtt-agent.yaml and should contain your mqtt broker details 

# For startup copy temperature-mqtt-agent.service to /etc/systemd/system/ 
# Startup is done via systemd with 
#  sudo systemctl enable temperature-mqtt-agent 
#  sudo systemctl start temperature-mqtt-agent 

filename = '/sys/devices/w1_bus_master1/28-05168661eaff/w1_slave' 
valid = False 
oldValue = 0 

""" Parse and load the configuration file to get MQTT credentials """ 

conf = {} 

def parseConfig(): 
    global conf 
    with open("/etc/temperature-mqtt-agent.yaml", 'r') as stream: 
        try: 
            conf = yaml.load(stream) 
        except yaml.YAMLError as exc: 
            print(exc) 
            print("Unable to parse configuration file /etc/temperature-mqtt-agent.yaml") 
            sys.exit(1) 

""" Read temperature from sysfs and return it as a string """ 

    def readTemperature(): 
        with open(filename) as f: 
            for line in f: 
                if re.search('crc=.*YES', line): 
                    # the CRC is valid. Continue processing 
                    valid = True 
                    continue 
                if valid and re.search('t=[0-9]+', line): 
                    # extract the temperature value 
                    temperature = re.search('t=([0-9]+)', line) 
                    # convert to degrees celsius and keep 1 digit of accuracy 
                    output = "%.1f" % (float(temperature.group(1)) / 1000.0) 
                    # print("Temperature is "+str(output)) 
                    return output 

""" Initialize the MQTT object and connect to the server """ 
parseConfig() 
client = mqtt.Client() 
if conf['mqttUser'] and conf['mqttPass']: 
    client.username_pw_set(username=conf['mqttUser'], password=conf['mqttPass']) 
    client.connect(conf['mqttServer'], conf['mqttPort'], 60) 
    client.loop_start() 

""" Do an infinite loop reading temperatures and sending them via MQTT """ 

    while (True): 
        newValue = readTemperature() 
        # publish the output value via MQTT if the value has changed 
        if oldValue != newValue: 
            print("Temperature changed from %f to %f" % (float(oldValue), float(newValue))) 
            sys.stdout.flush() 
            client.publish(conf['mqttTopic'], newValue, 0, conf['mqttPersistent']) 
            oldValue = newValue 
        # sleep for a while 
        # print("Sleeping...") 
        time.sleep(conf['sleep']) 
Figura 4: Enviando datos de temperatura a través de MQTT

El script también necesitará un archivo de configuración donde guardar las credenciales MQTT, ubicado en /etc/temperature-mqtt-agent.yaml:

mqttServer: 192.168.1.9
mqttPort: 1883
mqttUser: ODROID
mqttPass: ODROID
mqttTopic: ha/kids_room/temperature
mqttPersistent: True
sleep: 10
También hay un script de arranque systemd para que inicie tu script con cada arranque. Cópialo en /etc/systemd/system:
$ cat /etc/systemd/system/temperature-mqtt-agent.service
[Unit]
Description=Temperature MQTT Agent
After=network.target
[Service]
ExecStart=/usr/local/bin/temperature-mqtt-agent.py
Type=simple
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Para que se active al arrancar, ejecuta los siguientes comandos:
$ sudo systemctl enable temperature-mqtt-agent.service
$ sudo systemctl start temperature-mqtt-agent.service
En lo que respecta a Home Assistant, necesitamos definir un sendor MQTT con la siguiente configuración:
sensor:
...
 - platform: mqtt
    state_topic: 'ha/kids_room/temperature'
    name: 'Temperature via MQTT'
    unit_of_measurement: C

Pros de este método:

  • El uso de recursos es bajo
  • API estándar de bajo coste operativo diseñada para la comunicación máquina a máquina.

Contras de este método:

  • El sistema remoto necesita tener claramente la contraseña de MQTT
  • Cuando se reinicia Home Assistant, el sensor no tendrá ningún valor hasta la primera actualización a menos que se use la opción de persistencia de MQTT

Ahora que has visto varios ejemplos de cómo obtener datos en Home Assistant, deberá elegir cual es el mejor para tu configuración. De ahora en adelante yo usare MQTT porque, aunque parezca más difícil al principio, permite un mejor escalado con tareas más complejas.

Controlar una Smart TV con un componente personalizado

Aquí tenemos un nuevo problema que queremos resolver. Queremos obtener el número de canal actual, el nombre del programa y el estado de TV de un televisor Samsung con firmware SamyGO. El televisor revela esta información a través de una API REST que se puede instalar en el televisor desde aquí. La API devuelve información en formato JSON sobre el estado actual del televisor. Se puede inyectar códigos de control remoto y también se puede enviar de vuelta capturas de pantalla con lo que aparece actualmente en pantalla. La llamada y los resultados de la información actual se ven así:

$ wget -O - "http://tv-ip:1080/cgi-bin/samygo-web-api.cgi?challenge=oyd4uIz5WWAkWPo5MzfxBFraI05C3FDorSPE7xiMLCVAQ40a&action=CHANNELINFO"
{"source":"TV (0)", "pvr_status":"NONE", "powerstate":"Normal", "tv_mode":"Cable (1)", "volume":"9", "channel_number":"45", "channel_name":"Nat Geo HD", "program_name":"Disaster planet", "resolution":"1920x1080", "error":false}

En teoría, podríamos configurar los sensores REST para hacer la consulta anterior y utilizar la plantilla para conservar solo la información deseada, algo así como esto:

sensor:
...
  - platform: rest
  resource: http://tv-ip:1080/cgi-bin/samygo-web-api.cgi?challenge=oyd4uIz5WWAkWPo5MzfxBFraI05C3FDorSPE7xiMLCVAQ40a&action=CHANNELINFO
  method: GET
  value_template: '{{ value_json.channel_name }}'
  name: TV Channel Name
Pero el problema es que, para obtener toda la información de diferentes sensores, debes hacer la misma consulta, descartar una gran cantidad de datos y mantener solo lo que necesitas para ese sensor en particular. Esto es ineficaz y en este caso, no funcionará porque, para obtener y presentar esta información, la API web que se ejecuta en el televisor inyecta varias librerías en los procesos que se ejecutan en el televisor para captar algunas llamadas de funciones y obtener los datos aquí. La fase de la inyección es muy crítica, y hacer varias inyecciones al mismo tiempo podría causar que se bloquee el proceso, lo cual bloquearía a su vez el TV. Esta es la razón por la cual la API web realiza las consultas de forma progresiva y no responde a una consulta antes de que se complete la anterior, aunque esto genere tiempos de espera.

Lo que se necesita en este caso es que el componente del sensor almacene todos los datos JSON y tenga una plantilla de sensores para extraer los datos necesarios y presentarlo. Para hacer esto, necesitamos un componente personalizado, que deriva del sensor REST y que actúa justamente como el sensor REST, pero cuando recibe datos JSON, los almacena como atributos de la entidad en lugar de descartarlos.

Los componentes personalizados se encuentran en el directorio ~ homeassistant/.homeassistant/custom_components y mantienen la estructura de los componentes habituales (lo que significa que nuestro sensor estará en el subdirectorio del sensor). Se cargarán en el Inicio de Home Assistant antes de que se analice la configuración. La Figura 5 muestra las diferencias entre el sensor REST y el nuevo sensor JsonRest personalizado.

Figura 5: cambios para almacenar y presentar los atributos

Para entender los cambios realizados, deberías seguir la guía de componentes personalizada http://bit.ly/2fvc1PT. El código hace algunos cambios en el nombre de las clases del módulo para evitar enfrentamientos con el componente REST, y activa y gestiona una lista de atributos que se analizan desde la entrada JSON. Estos se mostrarán como atributos en la vista de estados. El nombre del nuevo componente es JsonRest, igual que el nombre del archivo.

Para instalar el componente JsonRest, puede seguir estos pasos:

mkdir -p ~homeassistant/.homeassistant/custom_components/sensor/
wget -O ~homeassistant/.homeassistant/custom_components/sensor/jsonrest.py https://raw.githubusercontent.com/mad-ady/home-assistant-customizations/master/custom_components/sensor/jsonrest.py
TPara configurar el nuevo componente, una vez que se almacene en el directorio custom_components/sensor, podemos usar esta configuración para sondear el televisor cada 5 minutos:
sensor:
…
 - platform: jsonrest
    resource: http://tv-ip:1080/cgi-bin/samygo-web-api.cgi?challenge=oyd4uIz5WWAkWPo5MzfxBFraI05C3FDorSPE7xiMLCVAQ40a&action=CHANNELINFO
    method: GET
    name: TV Living ChannelInfo
    scan_interval: '00:05'
 - platform: template
    sensors:
     tv_living_powerstate:
        value_template: '{{ states.sensor.tv_living_channelinfo.attributes.power_state }}'
        friendly_name: TV Living Power
     tv_living_channel_number:
        value_template: '{{ states.sensor.tv_living_channelinfo.attributes.channel_number }}'
        friendly_name: TV Living Channel Number
      tv_living_channel_name:
        value_template: '{{ states.sensor.tv_living_channelinfo.attributes.channel_name }}'
        friendly_name: TV Living Channel Name
      tv_living_program_name:
        value_template: '{{ states.sensor.tv_living_channelinfo.attributes.program_name }}'
        friendly_name: TV Living Program Name
Ahora solo el componente JsonRest será el que sondee el televisor para obtener la información, y la plantilla sensores extraerán los datos necesarios de los atributos, reduciendo la carga en el televisor.

Puesto que la API web del TV permite tomar capturas de pantalla, vamos añadir esta función también a Home Assistant (para no perder de vista lo que los niños ven el TV). La API devuelve una imagen JPEG cada vez que preguntes con el parámetro URL action=SNAPSHOT. Puedes usar un a Componente de Cámara IP genérica:

camera 2:
  platform: generic
  name: TV Living Image
  still_image_url: http://tv-ip:1080/cgi-bin/samygo-web-api.cgi?challenge=oyd4uIz5WWAkWPo5MzfxBFraI05C3FDorSPE7xiMLCVAQ40a&action=SNAPSHOT
La API web TV también te permite enviar acciones de control remoto, que se pueden configurar a través del Componente de commandos Restful:
rest_command:
  tv_living_power_on:
    url: !secret samygo_tv_living_power_on
  tv_living_power_off:
    url: !secret samygo_tv_living_power_off
Tras agrupar un poco, el resultado final lo puedes ver  aquí. Tienes un enlace a la configuración disponible aquí, y un ejemplo para el archivo secrets aquí. Puedes encontrar el código y la configuración en la pçagina de GitHub.

Figura 6 – Echando un ojo a la TV

Be the first to comment

Leave a Reply