I love home automation - I’ve spent far longer writing automation routines and hacking together my own devices and programs than I have ever saved by doing so, and that’s perfectly fine. One unique aspect of my setup, however, is that I cannot control the network I must use - in my Uni dorm, I am not allowed to run my own router, and so all IoT devices must connect to the school wireless network. There abound dozens of Google Home devices, Chromecasts, and so on, all accessible on the same network - but not from the wired connection that my server/desktop uses.
Here then is my solution: MQTT everything I can. From my lights, so light sensor, to coffee maker to desktop software, I bounce it through a MQTT server hosted on a VPS. I use Home Assistant, so automatic discovery is easy on most things, especially ESPHome. In fact, I disabled direct Home Assistant connectivity entirely on these devices, which works well enough for me to live with.
But how to flash? Link to heading
Given that my Home Assistant instance isn’t even on the same network as the IoT devices, how do I update the firmware? Using my laptop, I can connect to the wireless network that they reside on, and using IP address sensors reported by these devices, flash them directly without needing local discovery. In fact, I can easily automate this for myself using a couple scripts and a minimal number of hard-coded values:
hassio.sh
#!/bin/bash
export HASS_SERVER=https://<server_url>:443
export HASS_TOKEN='<HA_Token>'
hass-cli ${@}
exit
lamps.sh
#!/bin/bash
declare -A aa
aa["gosund_lb1_1.yaml"]="sensor.desk_lamp_ip_address"
aa["gosund_lb1_2.yaml"]="sensor.bed_lamp_ip_address"
aa["gosund_lb1_3.yaml"]="sensor.floor_lamp_girlfriend_ip_address"
aa["gosund_lb1_4.yaml"]="sensor.desk_lamp_girlfriend_ip_address"
aa["gosund_lb1_5.yaml"]="sensor.floor_lamp_ip_address"
__flash() {
__config="${1}"
__entity_name="${aa[${__config}]}"
echo "Getting ${__config} IP..."
__ip="$(
./hassio.sh -o yaml state get \
"${__entity_name}" |
grep -E '^ *state' | sed -e 's/.* //'
)"
echo "IP: ${__ip}"
if [ "${__ip}" == 'unavailable' ]; then
echo 'Ignoring...'
else
echo "Flashing..."
./esphome.sh "${__config}" run --upload-port="${__ip}"
fi
echo
}
if [ "${#}" -gt 0 ]; then
until [ "${#}" == 0 ]; do
__flash "${1}"
shift
done
else
for __config in ${!aa[@]}; do
__flash "${__config}"
done
fi
exit
this allows me to mostly painlessly flash my devices, though truth be told there is little need.
Custom software Link to heading
I developed for myself a tool in Golang to help tie more of my devices together.
It is rather uncreatively/cryptically named ha-mqtt-iot
- that is, “Home Assistant MQTT Internet of Things”.
I may rename this some day, but why bother.
It is similar to IOTLink (which is Windows only), and HASS Workstation Service - they are great projects, but this one is mine, even if it is poorly written.
The gist of the software is that most (all?) device types supported by Home Assistant may be implemented using a selection of user defined commands. The most prominent examples in my case are in order to enable/disable dark mode on my desktop. I automate this according to ambient light in my room, to better match the aesthetic I want. Additionally, I can use it to turn my desktop monitor off, without resorting to using a relay outlet, and even change the color temperature of my system. The script I use for this looks like the following:
#!/bin/bash
__monitor_i2c='dev:/dev/i2c-3'
__monitor_dpms='0xd6'
__monitor_brightness='0x10'
__monitor_standby='4'
__monitor_off='5'
__monitor_on='1'
__unknown() {
echo "Unknown ${1}"
}
f2i() {
awk 'BEGIN{for (i=1; i<ARGC;i++)
printf "%.0f\n", ARGV[i]}' "$@"
}
com="${1}"
arg="${2}"
case "${com}" in
"command")
case "${arg}" in
"ON")
xset dpms force on
;;
"OFF")
(
#xset dpms force off
#sleep 0.5s
#ddccontrol -r "${__monitor_dpms}" -w "${__monitor_standby}" "${__monitor_i2c}" -f
#sleep 2s
ddccontrol -r "${__monitor_dpms}" -w "${__monitor_off}" "${__monitor_i2c}" -f
) &
;;
*)
__unknown "${arg}"
;;
esac
;;
"command-state")
echo -n "$(xset q | grep 'Monitor is' | sed -e 's/.* //' | tr '[:lower:]' '[:upper:]')"
;;
"color-temp")
v="$(f2i "$(bc -l <<<"1000000/${arg}")")"
./scripts/run-in-user-session.sh gsettings set org.gnome.settings-daemon.plugins.color night-light-temperature "${v}"
;;
"color-temp-state")
v="$(./scripts/run-in-user-session.sh gsettings get org.gnome.settings-daemon.plugins.color night-light-temperature)"
echo -n "$(f2i "$(bc -l <<<"1000000/${v/* /}")")"
;;
"brightness")
ddccontrol -r "${__monitor_brightness}" "${__monitor_i2c}" -w "${arg}"
;;
"brightness-state")
echo -n "$(ddccontrol 2>/dev/null -r "${__monitor_brightness}" "${__monitor_i2c}" | tail -n 1 | grep -o '/[0-9]*/100' | sed -e 's|^/||' -e 's|/.*||')"
;;
*)
__unknown "root command ${com}"
;;
esac
exit
Note that the display doesn’t respond to being turned back on, so this is somewhat incomplete, but it’s good enough for my needs.
The corresponding portion of the config for ha-mqtt-iot
looks like the following:
"lights": [
{
"info": {
"name": "Desktop Monitor",
"id": "monitor"
},
"command": [
"./scripts/monitor.sh",
"command"
],
"command_state": [
"./scripts/monitor.sh",
"command-state"
],
"command_color_temp": [
"./scripts/monitor.sh",
"color-temp"
],
"command_color_temp_state": [
"./scripts/monitor.sh",
"color-temp-state"
],
"command_brightness": [
"./scripts/monitor.sh",
"brightness"
],
"command_brightness_state": [
"./scripts/monitor.sh",
"brightness-state"
],
"brightness_scale": 100,
"update_interval": 5
}
]
Pretty simple. This makes custom system sensors trivial. For example, to show my system IP, I use the following:
"sensors": [
{
"info": {
"name": "IP Address Desktop Solus",
"id": "ip-address-desktop-solus",
"icon": "mdi:ip-network"
},
"command_state": [
"/bin/bash",
"-c",
"ip -j address show eno1 | jq -r '.[0].addr_info[0].local'"
],
"update_interval": 10
}
]
Some common use cases are built in as well. Currently, this includes laptop displays (as lights) and batteries (as sensors), as well as Crypto prices (though the CoinGecko Golang library). These are really easy to call. An exhaustive example is quite short:
"builtin": {
"prefix": "Name Prefix ",
"backlight": {
"enable": true,
"temperature": false,
"range": {
"minimum": 0.025,
"maximum": 0.95
}
},
"battery": {
"enable": true
},
"crypto": [
{
"coin_name": "dogecoin",
"currency_name": "usd",
"update_interval": 1,
"icon": "mdi:currency-usd"
}
]
}
This lets me tailor my setup to each machine I’m using, while still enjoying the benefits of Home Assistant MQTT Discovery.
The primary limitation at present is the inability to signal to ha-mqtt-iot
from another process - it can only poll for changes.
This will be addressed one day, when it is important for my own needs.
How to host? Link to heading
But the question is now, how do I access my HomeAssistant instance if it’s also hosted at school? I most certainly don’t have a public IP, so in comes AutoSSH. I’m not sure which is the best one at this stage, but refer to this repo and check the various forks of the parent project.
I have configured on my VPS a docker image that accepts reverse SSH tunnelling, authorized only to the key of the HA addon.
From my docker-compose.yml
:
homeassistant:
image: "docker.io/panubo/sshd"
container_name: homeassistant
environment:
- TCP_FORWARDING=true
- GATEWAY_PORTS=true
- SSH_ENABLE_ROOT=true
- DISABLE_SFTP=true
volumes:
- "./hassio/authorized_keys:/root/.ssh/authorized_keys:ro"
- ./docker-config/hassio/data/:/data
- ./docker-config/hassio/keys/:/etc/ssh/keys
ports:
- "<MY_PORT>:22"
restart: unless-stopped
hostname: "homeassistant"
This is then reverse proxied to using Caddy, to expose the website on a subdomain of a website.
From my Caddyfile
:
<MY_SUBDOMAIN>.{$MY_DOMAIN} {
reverse_proxy homeassistant:8123
encode gzip
}
Pretty simple, but not without some hiccups now and again - I occasionally have to restart the sshd docker on my VPS if something goes wrong with HomeAssistant.
Bonus: Android tie in Link to heading
I use Sleep As Android to track my sleeping patterns, and as my alarm clock. Using Tasker, I can run an action when I begin sleep tracking, which (using a HomeAssistant plugin for Tasker) can call a script on my HomeAssistant instance to turn off my lights (only if I’m home, of course). Similarly, it turns on my bedhead light when my alarm goes off in the morning, and could optionally make me coffee…