Search This Blog

Wednesday, October 30, 2019

Waking up devices in Hass.io (Home Assistant)


This is a quick post on a challenge I had to overcome while integrating my SmartTV (an LG TV 55UJ620V) with Hass.io.

I wanted Hass.io to be able to turn on the TV (as such allowing automations to be built on top of it, or for example turning it on through a voice command via Google Assistant). As such I first resorted to using the Home Assistant Wake on LAN built in integration (https://www.home-assistant.io/integrations/wake_on_lan/). It kind of worked, but was not very reliable (perhaps 2 out of 5 times it would work).

I knew that by definition, the way that (Wake-up On LAN) WOL is implemented is inherently unreliable: essentially the target (dormant) device is expecting a frame with a specific pattern of bytes. If it receives that frame, it wakes up the host, otherwise nothing happens. The device will normally scan for that pattern of bytes in the frame regardless of the type of transport level protocol it may be on top of. In the case of WiFi in particular, there is the probability (high or low, depending on the network conditions) of that single frame not reaching the destination. This probability increases with the more hops we have in between.

With this Hass.io implementation I was requiring the magic packet to be sent from a host (the Raspberry Pi where I keep Hass.io running) that is in a separate router vlan, from where the TV is (these are connected via Ethernet and WiFi respectively):



In order to target reliability I scrapped this approach altogether. As both my router and repeater are OpenWRT based, I started by installing the etherwake package onto the repeater, given that this is the closest note to the TV set:


Tested the tool manually and confirmed that it worked:


It was now a matter of scripting it, and for improved reliability, make sure that the script tests the connection after it tries to turn on the TV.

As a quick dirty approach and knowing that the OpenWRT repeater runs a web server, I went for writing a simple cgi-bin shell script that could be called from another host (making a very basic REST web service). This script would just have to send the WOL packet, and for a while check if the TV comes online. If it doesn't it sends the WOL packet a few more times, and tests the TV again:

root@griffinnet-zh-repeater:/www/cgi-bin# cat wol.sh
#!/bin/sh

MAX_RETRIES=10
NUM_PINGS=20

echo "Content-type: application/json"
echo ""

if [ -z "$QUERY_STRING" ]; then
    echo "{\"status\":\"error\"; \"message\":\"MAC and IP addresses not specified.\"}"
    exit 1
else
    MAC_ADDR=$(echo "$QUERY_STRING" | sed -n 's/^.*mac=\([^&]*\).*$/\1/p' | sed "s/%20/ /g")
    IP_ADDR=$(echo "$QUERY_STRING" | sed -n 's/^.*ip=\([^&]*\).*$/\1/p' | sed "s/%20/ /g")
fi

i=0

while [ $i -lt "$MAX_RETRIES" ]; do
    WOL_RESULT=$(etherwake -D -i br-lan $MAC_ADDR)

    if [ ! $? -eq 0 ]; then
        echo "{\"status\":\"error\"; \"message\":\"Wake-up On LAN command failed.\"}"
        exit 1
    fi

    ping -c $NUM_PINGS $IP_ADDR > /dev/null

    PING_RESULT=$?

    if [ $PING_RESULT -eq 0 ]; then
        echo "{ \"status\" : \"success\"; \"message\" : \"WOL: $WOL_RESULT; Ping Result: $PING_RESULT\" }"
        exit 0
    else
        i=$(($i+1))
    fi
done

echo "{\"status\":\"error\"; \"message\":\"Exhausted retries. Unable to wake up device.\"}"
exit 1

On the Hass.io side, it was a matter of registering for this REST endpoint, and use it as the service that turns on the TV, instead of the built-in Wake on LAN integration:

rest_command:
  wake_up_device:
    url: 'http://192.168.1.200/cgi-bin/wol.sh?mac={{tv_mac}}&ip={{tv_ip}}'
    method: GET
    headers:
      accept: 'application/json, text/html'
    content_type: 'application/json; charset=utf-8'
    timeout: 60

media_player:
  - platform: webostv
    host: 192.168.1.30
    name: TV Sala
    filename: webostv.conf
    timeout: 5
    turn_on_action:
      - service: rest_command.wake_up_device
        data:
          tv_mac: 'B4:E6:2A:??:??:??'
          tv_ip: '192.168.1.30'
    customize:
      sources:
        - livetv
        - youtube
        - netflix

Given that the this REST endpoint takes some time to respond (due to the number of pings it performs to test the connection to the TV), the response timeout was increased to 60 seconds.

This is all that it was needed, and so far the feature have been working flawlessly.

No comments: