Home Assistant on Vero V with Docker
Eventually I got tired of constantly re-signing and re-uploading Kodi to my Apple TV (and being stuck with an old version). So I got a Vero V instead, and immediately it became clear that I should have done so from the beginning. Buying Apple TV was a mistake: like I said before, if not for running Kodi, Apple TV is a pretty useless device (especially now that I have a TV that supports AirPlay).
Around the same time I entered the domain of home automation. At first it was just Homely (a norwegian home security system), which comes with their own hub and a (not very large) set of supported devices/sensors. But soon enough I discovered that it’s all just a Zigbee network wrapped into a user-friendly package, so I could do more with my own hub and a much wider (and cheaper) collection of devices and sensors. Shortly after that realization I stumbled upon Home Assistant, as all roads lead to it, and, you guessed it right, I decided to host and run it on that same Vero V device.
Is something wrong with Homely
I said that I started with Homely before getting into Home Assistant, and that might have given you an impression that I switched from one to another. But no, I still use Homely, it just serves a different purpose, so it is not that I have replaced it with Home Assistant.
Homely isn’t a yet another automation hub with some devices. Its main purpose is to connect your home to their 24/7 monitoring system, and if something happens - smoke sensor detects fire, or alarm is triggered by door/window/motion sensors - monitoring will get notified, and they will call firefighters/police/whoever for you. As a bonus to that, you also get some basic home automation.
As you can imagine, such a system cannot depend on a random 1 USD sensor from uncle Lao, as it all needs to be certified/approved, which I guess is why their list of supported devices is not very long. Mostly it’s Frient/Develco/Onesti devices (they all seem to be the same thing, just re-branded), but there are also IKEA bulbs and other vendors there too, although some of them also seem to be just re-branded variants of the same things, so the “actual” list might be even shorter:
I cannot say how good (or bad) their 24/7 monitoring is, as I never had any accidents that would trigger an alarm (except for when I forgot about the alarm and opened the balcony door during the night time, while the alarm was armed, and they called me right away), and hopefully I will never know how good (or bad) their monitoring is. I can only say that out of all the security systems available in Norway, Homely’s monthly subscription fee is the lowest (in 2024 it was plain 199 NOK per month, without cheeky offers like a “free” smart lock installation), and they are recognized by insurance companies, so you can get a discount on your house insurance too. They do overprice the devices/sensors though, so you can most likely find several of those cheaper in other places.
The mobile application is okay, although looks like it’s not their own but a customized (re-styled) variant of Keyfree. The nimly application seems to be using the same. Or maybe there is some other common “parent” for all of them, but either way, they all definitely look the same.
Surprisingly, there is no web-version, so you are limited to controlling it from the mobile application only. And by the way, there is no proper iPad version, it just uses the same iPhone application, which isn’t optimized for non-iPhone screens.
Coming back to home automation, it’s actually not the small number of supported devices that made me look elsewhere, but the very limited set of automation rules/triggers. For example, you cannot create a rule to turn on the lights when movement sensor detects you entering the room, which seems to be a very common routine to have. That’s how I got to Home Assistant, where automation capabilities are practically limitless, and I left Homely to handle just the security/safety stuff.
Why not a dedicated device
Yes, I could’ve gotten a dedicated device to run Home Assistant - Raspberry Pi or maybe their own Green/Yellow boxes - and they are rather inexpensive, but that’s yet another box cluttering my shelves.
I already have Vero V, and it is quite a powerful device - 2 GHz ARM CPU (4 cores), 4 GB RAM, 32GB eMMC. Kodi is barely taking 10% of resources even while playing 4K movies, so there is a lot of room for doing something else, such as running Home Assistant, and it would be wasteful not to do so.
Environment and versions
Just in case, here’s my environment and versions of the software/firmware that I have:
- Vero V:
- Debian:
11.10
- OSMC:
2024.05-1
- Debian:
- Home Assistant:
2024.7.3
- Zigbee adapter/coordinator:
SONOFF Zigbee 3.0 USB Dongle Plus-E
- type:
EZSP v8
- revision:
6.10.3.0 build 297
- type:
- Zigbee2MQTT:
1.39.0
, commit0326926
Installation
On the first connect, before anything else, change the default osmc
password to a proper one with passwd
, deploy an SSH key and disable passwords authentication.
Docker
Out of all the possible ways of installing Home Assistant I chose Docker. Mainly because I have never used Docker for anything (aside from doing some tutorials), so this seemed like a good opportunity to finally try it out for something actually useful. Secondly, from the little I knew about Docker, one of its distinguishing features is the ability to run stuff in an isolated environment, which was just the thing I needed, as I didn’t want to disrupt the default Vero/OSMC environment too much, especially given that some of the Home Assistant components are run with godawful things like Node.js.
One thing you should be aware of is that going with Docker way of installing Home Assistant you will lose certain features such as addons out of the box. That would be a major disadvantage or even a show-stopper, but actually it’s no worries, because those addons turned out to be… also Docker containers, which in other types of Home Assistant installation are just managed by the Home Assistant itself, so you absolutely can do the same on your own (and probably better too).
First you will need to install Docker, as it isn’t preinstalled in OSMC by default. Following the trend of running random Bash scripts from the internet, Docker installation is supposed to be carried out that way too:
$ mkdir ~/downloads && cd $_
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ less ./get-docker.sh
I scrolled the script contents and didn’t see anything concerning enough, so I gave up and just ran it, but I certainly condemn this retarded practice. The “installation”:
$ sudo sh ./get-docker.sh --dry-run
$ sudo sh ./get-docker.sh
$ sudo systemctl enable docker.service
$ sudo systemctl enable containerd.service
$ sudo docker run hello-world
After the installation it is probably a good idea to limit Docker logs size right away. The following should limit logs to the maximum of 5 files, each being 10 MB of size:
$ sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
$ dockerd --validate --config-file=/etc/docker/daemon.json
configuration OK
Also, you might want to run Docker from a non-root user, but for simplicity I decided to go with root, at least in the beginning, and I will probably make it non-root later.
Home Assistant
The installation or rather deployment procedure for anything Docker-based is to create a container from a specified image. If you don’t have the image available locally already, it will get fetched from Docker Hub. Which begs the question whether one can get images and host them somewhere locally and independently from Docker Hub, and fortunately that seems to be supported too.
The process of creating/running a Home Assistant container is the following:
$ sudo docker run -d \
--name homeassistant \
--restart=unless-stopped \
-e TZ=Europe/Amsterdam \
-v /root/homeassistant:/config \
ghcr.io/home-assistant/home-assistant:stable
The -v
argument here tells the container that host paths (what’s on the left from :
) will be mapped to container paths (what’s on the right from :
). Here Home Assistant settings (/config/
path inside container) will be stored in /root/homeassistant/
path in the host filesystem. So when container will be deleted/re-created (during an update, for example), the data (volume) will be preserved between container reincarnations. In addition to that, we can edit the configs in the host filesystem without logging-in to the container.
In their manual they also set --privileged
and --network=host
, but there is no need for that, as well as there is probably no need for -v /run/dbus:/run/dbus:ro
(if you don’t intend to use Bluetooth). The Home Assistant team probably added those options to reduce the number of users complaining that “nothing works”, but in general these parameters certainly aren’t great for something that is supposed to be running isolated from the main/host environment.
IP address of the running container
In order to interact with Home Assistant, you will need to know the local IP address of its container. One of the ways to get it is to inspect the container:
$ sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' homeassistant
172.17.0.2
$ ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.294 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.263 ms
^C
You can also try to query it with cURL by sending a HEAD
request to the default Home Assistant port 8123
, and it should reply that HEAD
method is not allowed (if you haven’t performed the initial configuration/onboarding already):
$ curl -I 172.17.0.2:8123
HTTP/1.1 405 Method Not Allowed
Content-Type: text/plain; charset=utf-8
Allow: GET
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
Server:
X-Frame-Options: SAMEORIGIN
Content-Length: 23
Date: Mon, 22 Jul 2024 19:42:29 GMT
The GET
request should return something like this:
$ curl -v 172.17.0.2:8123
* Trying 172.17.0.2:8123...
* Connected to 172.17.0.2 (172.17.0.2) port 8123 (#0)
> GET / HTTP/1.1
> Host: 172.17.0.2:8123
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< location: /onboarding.html
< Referrer-Policy: no-referrer
< X-Content-Type-Options: nosniff
< Server:
< X-Frame-Options: SAMEORIGIN
< Content-Length: 0
< Content-Type: application/octet-stream
< Date: Mon, 22 Jul 2024 19:42:45 GMT
<
* Connection #0 to host 172.17.0.2 left intact
So yes, this IP address is definitely from the running Home Assistant container.
However, on the next restart/reboot the container might get a different IP address, but we need it to persist. Better yet, it would be great if there was some “internal” DNS - to be able to address containers by their names instead of IP addresses, especially when it comes to other containers “talking” to each other within their network. Sadly, by default the name resolution is disabled, and you can test it right now by spawning a new container:
$ sudo docker run -it --rm alpine /bin/ash
/ # ip route get 1 | awk '{print $NF;exit}'
172.17.0.3
/ # apk --no-cache add curl
/ # curl -I http://172.17.0.2:8123 -v
* Trying 172.17.0.2:8123...
* Connected to 172.17.0.2 (172.17.0.2) port 8123
> HEAD / HTTP/1.1
> Host: 172.17.0.2:8123
> User-Agent: curl/8.8.0
/ # curl -I http://homeassistant:8123 -v
* Could not resolve host: homeassistant
* Closing connection
curl: (6) Could not resolve host: homeassistant
/ # exit
As you can see, this new temporary (because of --rm
) container got an IP address in the same network (172.17.0.3
) and it could reach the homeassistant
container by IP address, but could not reach it by hostname.
Fortunately, the name resolution can be enabled after all, it just isn’t enabled in the default network. Check what networks you already have:
$ sudo docker network list
NETWORK ID NAME DRIVER SCOPE
f7dc4c461ccb bridge bridge local
e6e7d7a437b4 host host local
ecc68fac9a4b none null local
The one with bridge
driver is the default network (172.17.x.x
) where new containers are created, and that is where homeassistant
container is running now too.
But if you create a new network with the same bridge
driver:
$ sudo docker network create --driver bridge hub
5dfbe1989378e089d5548ae21e4b48b055108b570be7ac717a0d9ad034b79817
$ sudo docker network list
NETWORK ID NAME DRIVER SCOPE
f7dc4c461ccb bridge bridge local
e6e7d7a437b4 host host local
5dfbe1989378 hub bridge local
ecc68fac9a4b none null local
then it will be a “user-created network”, and these have “automatic service discovery” - that very same DNS that we need. Now you can re-create your container in that new network (--network hub
) or you can just move it from the default network (172.17.x.x
) to this new one (172.18.x.x
):
$ sudo docker network inspect bridge
[
{
"Name": "bridge",
"Id": "f7dc4c461ccb9d518abcdec62207554d809bc973d874c21f55b1d92427223d0d",
"Created": "2024-07-22T21:16:52.221175914+02:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"7e9bc60ef0f894eec68eaf784b06c0d4605ee5dc52d5733c9f0394de67a3943c": {
"Name": "homeassistant",
"EndpointID": "922f23693b311435906df168af5c669c606b9c41e8bca061d01f44056573922f",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
$ sudo docker network inspect hub
[
{
"Name": "hub",
"Id": "5dfbe1989378e089d5548ae21e4b48b055108b570be7ac717a0d9ad034b79817",
"Created": "2024-07-23T09:44:48.167021383+02:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
$ sudo docker network disconnect bridge homeassistant
$ sudo docker network connect hub homeassistant
$ sudo docker network inspect bridge
[
{
"Name": "bridge",
"...": "...",
"Containers": {},
"...": {}
}
]
$ sudo docker network inspect hub
[
{
"Name": "hub",
"...": "...",
"Containers": {
"7e9bc60ef0f894eec68eaf784b06c0d4605ee5dc52d5733c9f0394de67a3943c": {
"Name": "homeassistant",
"EndpointID": "db73973bfdc5d9b8e1807252da52cbfd6fda9c17899918377f28d9a21d54dfb2",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"...": {}
}
]
If you now create a new temporary container in the hub
network, then you should be able to reach the homeassistant
container from it by both IP and hostname:
$ sudo docker run -it --rm --network hub alpine /bin/ash
/ # ip route get 1 | awk '{print $NF;exit}'
172.18.0.3
/ # apk --no-cache add curl
/ # curl -I http://172.18.0.2:8123 -v
* Trying 172.18.0.2:8123...
* Connected to 172.18.0.2 (172.18.0.2) port 8123
> HEAD / HTTP/1.1
> Host: 172.18.0.2:8123
> User-Agent: curl/8.8.0
/ # curl -I http://homeassistant:8123 -v
* Host homeassistant:8123 was resolved.
* IPv6: (none)
* IPv4: 172.18.0.2
* Trying 172.18.0.2:8123...
* Connected to homeassistant (172.18.0.2) port 8123
> HEAD / HTTP/1.1
> Host: homeassistant:8123
> User-Agent: curl/8.8.0
/ # exit
While that works, it doesn’t mean that you will also be able to reach that container by hostname from your host environment too, because you won’t:
$ curl -I http://homeassistant:8123 -v
* Could not resolve host: homeassistant
* Closing connection 0
curl: (6) Could not resolve host: homeassistant
as containers name resolution can work only within their networks.
NGINX
Alright, Home Assistant is running and other containers can reach it. But you as a user are not able to reach it yet, because its port 8123
isn’t exposed from the host.
There are several ways of exposing a container port from the host, and I prefer the one with NGINX being a reverse proxy. So let’s install NGINX:
$ sudo apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring
$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
$ gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
$ echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" | sudo tee /etc/apt/preferences.d/99nginx
$ sudo apt update
$ sudo apt install nginx
$ sudo systemctl status nginx.service
$ sudo chown -R www-data:www-data /var/www
The site config for Home Assistant would be the following:
$ sudo nano /etc/nginx/sites-available/homeassistant
# this is supposed to be used for WebSocket connections,
# although it might require a separate `location` block
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# doesn't have to be exactly `8123`, you can set it to the "usual" `80`
listen 8123;
location / {
# `172.18.0.2` is the IP address of the Home Assistant container
proxy_pass http://172.18.0.2:8123;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
$ sudo ln -s /etc/nginx/sites-{available,enabled}/homeassistant
$ sudo systemctl restart nginx.service
$ ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p'
192.168.1.35
Now you should be able to open http://192.168.1.35:8123/ (or whichever IP address you have for your device/server) from a web-browser on your computer/tablet/phone (as long as they are in the same network). Obviously, this IP address needs to be statically assigned to the device/server (Vero V in my case) in your home network DHCP server (a router, usually).
What if container IP address will change
But what to do if container IP will change on the next restart/reboot? Since the container’s hostname isn’t resolving in the host environment (as we know, it resolves only within a user-defined Docker network), we’ll have to rely in the “bare” IP address. But we at least can make it “static”, so container doesn’t get a different address on the next restart/reboot:
$ sudo docker network disconnect hub homeassistant
$ sudo docker network connect --ip 172.18.0.100 hub homeassistant
$ sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' homeassistant
172.18.0.100
Here it is assumed that you have already moved the container from the default bridge
network to the new user-defined hub
network.
Don’t forget to edit NGINX config and change the IP address from 172.18.0.2
to 172.18.0.100
.
HTTPS
I don’t plan to expose my Home Assistant instance to the internet, at least not without a VPN, so I intend to use it from the local home network only, but even there it is still worth to encrypt the traffic, so credentials aren’t sent in plain text.
A self-signed certificate should be enough for this purpose:
$ sudo mkdir /opt/certificates
$ mkdir /tmp/crts && cd $_
$ openssl req -x509 -newkey rsa:2048 -nodes -sha256 \
-subj '/CN=Home Assistant' -extensions EXT -config <( \
printf "[dn]\nCN=Home Assistant\n[req]\ndistinguished_name=dn\n[EXT]\nsubjectAltName=IP:192.168.1.35,DNS:vero.local\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") \
-days 1111 \
-out fullchain.pem -keyout key.pem
$ ls -l
-rw-r--r-- 1 osmc osmc 1115 Jul 30 11:11 fullchain.pem
-rw------- 1 osmc osmc 1704 Jul 30 11:11 key.pem
$ sudo mv ./*.pem /opt/certificates/
$ sudo chown -R www-data:www-data /opt/certificates
Again, the 192.168.1.35
is the (static) IP address of your hub/device, which in my case is Vero V. The subjectAltName=IP:192.168.1.35,DNS:homeassistant.local
might be just the IP, if you don’t have a DNS record for it in your home network. I actually don’t have it, and as a matter of fact, you can probably just drop the entire line altogether, so the command becomes:
$ openssl req -x509 -newkey rsa:2048 -nodes -sha256 \
-subj '/CN=Home Assistant' -extensions EXT -config <( \
printf "[dn]\nCN=Home Assistant\n[req]\ndistinguished_name=dn\n[EXT]\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") \
-days 1111 \
-out fullchain.pem -keyout key.pem
Then to enable HTTPS in NGINX site config:
$ sudo nano /etc/nginx/sites-available/homeassistant
server {
listen 8123 ssl;
ssl_certificate /opt/certificates/fullchain.pem;
ssl_certificate_key /opt/certificates/key.pem;
# ...
}
$ sudo systemctl restart nginx.service
Check the certificate expiration date from some other machine:
$ echo | openssl s_client -connect 192.168.1.35:8123 2>&1 | openssl x509 -noout -dates
notBefore=Jul 30 09:40:32 2024 GMT
notAfter=Aug 15 09:40:32 2027 GMT
Open https://YOUR-VERO-IP:8123/ (note that it’s https://
now, as http://
won’t work anymore). Since it’s a self-signed certificate, web-browsers will give you warnings, and mobile applications will do so too:
Here it also says that the validity period is too long. But who cares, it is you who issued the certificate, and you certainly trust yourself, don’t you. For the purpose of encrypting your local network connections that should be totally fine. One can even say that this whole ordeal with HTTPS in a local home network might be redundant.
If/when at some point later you will re-issue the certificate, for example to renew it, then Home Assistant UI might fail load in browser:
printing this useless error message to the console:
Firefox can't establish a connection to the server at wss://192.168.1.35:8123/api/websocket. core.7weG3TqZ_G4.js:1:65105
Uncaught (in promise) 1
Mobile applications will fail to connect to, but you’ll get a more informative error message there:
which simply means that you need to refresh the page with clearing cache, so clients would get the new certificate.
Zigbee
To communicate with Zigbee devices you need to have a Zigbee radio adapter/module with a coordinator firmware. Many adapters are supposed to be connected via USB, so they are plugged into the hub/server where Home Assistant is running, and preferably not into USB 3.0 port (fortunately, Vero V has both USB 3.0 and USB 2.0 ports) because of the interference. Some adapters (here’s one example) connect via RJ45 port, which might be a better option.
There are several such adaptors/modules available, and I chose SONOFF Zigbee 3.0 USB Dongle Plus-E. I didn’t know how to compare them, I just took the one that is mentioned the most.
You will also need an appropriate software, which will use this adaptor to communicate with your Zigbee devices and organize the network. The most popular options seem to be these two:
- Zigbee Home Automation (ZHA) - probably the easiest one to configure and use. It is also an officially supported one;
- Zigbee2MQTT - an “unofficial” integration, which is not that easy to set-up, especially when Home Assistant is installed in a Docker container, because you’ll need to run some more Docker containers with various required components and make them all talk to each other. On the bright side though, it has a wider range of supported devices/sensors.
ZHA looks like the way to go, but where is the fun in that, if it just works out of the box, so of course I went the Zigbee2MQTT way.
Eclipse Mosquitto
Zigbee coordinator manages Zigbee traffic from/to devices, but it also needs to be able to talk to Home Assistant and the other way around. For that type of communication it uses MQTT protocol, so we will need an MQTT broker.
One such broker is Eclipse Mosquitto. It can be installed as a Docker container too, and the image is here. Setting up a container:
$ sudo docker run -d \
--name mosquitto \
--restart=unless-stopped \
-v /root/mosquitto/config:/mosquitto/config \
-v /root/mosquitto/data:/mosquitto/data \
-v /root/mosquitto/log:/mosquitto/log \
eclipse-mosquitto
Again, the -v
arguments here tell the container which host paths will be mapped to which container paths, so when Mosquitto binary inside container will need to read its “local” /mosquitto/config/mosquitto.conf
file, it will actually read the /root/mosquitto/config/mosquitto.conf
file in the host filesystem.
Speaking about the config, it needs to be created:
$ sudo nano /root/mosquitto/config/mosquitto.conf
persistence true
persistence_location /mosquitto/data
log_dest file /mosquitto/log/mosquitto.log
Now start the container (not sure why it hasn’t started on docker run
, but probably because of the missing config):
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e9bc60ef0f8 ghcr.io/home-assistant/home-assistant:stable "/init" 19 hours ago Up 4 hours homeassistant
$ sudo docker start mosquitto
mosquitto
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5c0503542640 eclipse-mosquitto "/docker-entrypoint.…" 4 minutes ago Up 5 seconds 1883/tcp mosquitto
7e9bc60ef0f8 ghcr.io/home-assistant/home-assistant:stable "/init" 19 hours ago Up 4 hours homeassistant
osmc@vero:~$ mc
Move it to the same network where homeassistant
container is:
$ sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mosquitto
172.17.0.2
$ sudo docker network inspect hub
[
{
"Name": "hub",
"...": "...",
"Containers": {
"7e9bc60ef0f894eec68eaf784b06c0d4605ee5dc52d5733c9f0394de67a3943c": {
"Name": "homeassistant",
"EndpointID": "5a8ddd308055e189ec9a4a37705b150b00aea88e042c2e4d085ec585c35779c8",
"MacAddress": "02:42:ac:12:00:64",
"IPv4Address": "172.18.0.100/16",
"IPv6Address": ""
}
},
"...": {}
}
]
$ sudo docker network disconnect bridge mosquitto
$ sudo docker network connect hub mosquitto
$ sudo docker network inspect hub
[
{
"Name": "hub",
"...": "...",
"Containers": {
"5c0503542640bfc3f8413deb6e1b5c785476783ddbaa385aad4993be28315484": {
"Name": "mosquitto",
"EndpointID": "267141eaccc22d8c3f5e97f38069ca83187f8747b9022079eb03eb7140178df4",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"7e9bc60ef0f894eec68eaf784b06c0d4605ee5dc52d5733c9f0394de67a3943c": {
"Name": "homeassistant",
"EndpointID": "5a8ddd308055e189ec9a4a37705b150b00aea88e042c2e4d085ec585c35779c8",
"MacAddress": "02:42:ac:12:00:64",
"IPv4Address": "172.18.0.100/16",
"IPv6Address": ""
}
},
"...": {}
}
]
$ sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mosquitto
172.18.0.2
…Or you could’ve just provided --network hub
to docker run
on setting up the container.
There is no need to assign a static IP address to it, because homeassistant
container will be addressing it by hostname. And actually, let’s check that it is in fact reachable that way:
$ sudo docker exec -it homeassistant bash
7e9bc60ef0f8:/config# ping -c 2 172.18.0.2
PING 172.18.0.2 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.349 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.327 ms
7e9bc60ef0f8:/config# ping -c 2 mosquitto
PING mosquitto (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.293 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.297 ms
7e9bc60ef0f8:/config# nc mosquitto 1883 < /dev/null
7e9bc60ef0f8:/config# echo $?
1
7e9bc60ef0f8:/config# exit
So it is reachable, however the port 1883
(default MQTT port) isn’t open (as netcat returns non-zero exit code). At first I didn’t understand, why is that, because that port is listed here:
$ sudo docker container ls --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}" -a
CONTAINER ID NAMES PORTS
8941202da971 mosquitto 1883/tcp
7e9bc60ef0f8 homeassistant
At the same time, checking for 8123
port on homeassistant
from mosquitto
container succeeds, even though this port is not listed in the table above:
$ sudo docker exec -it mosquitto sh
/ # nc homeassistant 8123 < /dev/null
/ # echo $?
0
/ # exit
I’m guessing that the ports listed in the table above are just what is specified in the Docker image, which doesn’t necessarily mean that they will be actually open.
Anyway, by default Mosquitto doesn’t seem to be listening on any ports, so you need to explicitly set them in the config:
$ sudo nano /root/mosquitto/config/mosquitto.conf
persistence true
persistence_location /mosquitto/data
log_dest file /mosquitto/log/mosquitto.log
listener 1883 0.0.0.0
listener 9001 0.0.0.0
protocol websockets
and restart the Mosquitto container:
$ sudo docker restart mosquitto
Check the ports again:
$ sudo docker exec -it homeassistant sh
/config # nc mosquitto 1883 < /dev/null
/config # echo $?
0
/config # nc mosquitto 9001 < /dev/null
/config # echo $?
0
/config # exit
Now netcat returns zero for both 1883
and 9001
, so the ports are open.
If you had a question about why the listener
is set to 0.0.0.0
in the config, as you probably thought that this will make the ports exposed from the host, then worry not, you just got confused like I did, and 0.0.0.0
here is this particular container within the hub
network, so it is not the ports of the actual host. You can check for yourself what will happen if you set the listener
to 127.0.0.1
:
$ sudo nano /root/mosquitto/config/mosquitto.conf
# ...
listener 9001 127.0.0.1
protocol websockets
$ sudo docker restart mosquitto
$ sudo docker exec -it homeassistant sh
/config # nc mosquitto 9001 < /dev/null
/config # echo $?
1
/config # exit
As you can see, binding listener
to 127.0.0.1
made that port 9001
to be open only on localhost of the container itself, so now that port is not available for other containers and it isn’t available for the host either (let alone being exposed from the host). And when listener
is bound to 0.0.0.0
, then this port becomes available for other containers (in that Docker network) and for the host too, but it still isn’t exposed from the host (which you can verify by running ss -lntup
). Perfectly splendid, isn’t it. By the way, don’t forget to revert that listener
back to 0.0.0.0
.
Finally, Mosquitto instance should have authentication:
$ sudo docker exec -it mosquitto sh
/ # mosquitto_passwd -c /mosquitto/config/pswds SOME-USERNAME
Password:
Reenter password:
/ # exit
$ sudo nano /root/mosquitto/config/mosquitto.conf
# ...
listener 9001 0.0.0.0
protocol websockets
allow_anonymous false
password_file /mosquitto/config/pswds
$ sudo docker restart mosquitto
Now it is ready to be added to Home Assistant. Open Settings → Devices & Services → Integrations and click on Add Integration. Search for mqtt
:
select MQTT
:
and configure it:
As you can see, since containers are reachable by their hostnames in our network, instead of specifying the mosquitto
’s container IP address we can just use its hostname.
On successful configuration the Mosquitto logs (/root/mosquitto/log/mosquitto.log
) should get something like:
1721766257: New connection from 172.18.0.100:33859 on port 1883.
1721766257: New client connected from 172.18.0.100:33859 as SOME-ID-HERE (p2, c1, k60, u'SOME-USERNAME').
1721766347: Saving in-memory database to /mosquitto/data/mosquitto.db
To test that it actually works, go to MQTT settings in your Home Assistant and subscribe to everything by setting a special #
topic. Then open mosquitto
container shell and publish a message with mosquitto_pub:
$ sudo docker exec -it mosquitto sh
/ # mosquitto_pub -h 127.0.0.1 -t homeassistant/switch/1/on -m "Switch is ON"
Connection error: Connection Refused: not authorised.
Error: The connection was refused.
/ # mosquitto_pub -h 127.0.0.1 -t homeassistant/switch/1/on -m "Switch is ON" -u SOME-USERNAME -P SOME-PASSWORD
/ # exit
You should then see that message in the Home Assistant interface:
Zigbee2MQTT
Now when MQTT broker is running, it’s time to launch Zigbee2MQTT. Create a config:
$ sudo mkdir -p /root/zigbee2mqtt/{data,logs}
$ sudo nano /root/zigbee2mqtt/data/configuration.yaml
# Home Assistant integration (MQTT discovery)
homeassistant: true
# https://zigbee2mqtt.io/guide/configuration/frontend.html
frontend:
port: 8080
# the frontend webpage opens without authentication, but it will be "empty" until you enter
# this authentication token in the browser prompt
auth_token: YOUR-AUTH-TOKEN
# allow new devices to join (don't forget to set to `false` after you add all your devices)
permit_join: true
mqtt:
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL (can use the hostname here too)
server: 'mqtt://mosquitto'
# MQTT server authentication
user: SOME-USERNAME
password: SOME-PASSWORD
serial:
# where Zigbee adapter is connected to
port: /dev/ttyACM0
advanced:
# that way it will generate a random key on the first launch and will put it here
network_key: GENERATE
# debug / info / warning / error
log_level: info
# log levels for certain namespaces to reduce the traffic
log_namespaced_levels:
z2m:mqtt: warning
log_directory: /app/logs
log_file: zigbee2mqtt.log
# rotate log every 10 MB around 3 files
log_rotation: true
log_output:
- console
- file
Configure and run a Docker container:
$ sudo docker run -d \
--name zigbee2mqtt \
--network hub \
--restart=unless-stopped \
--device=/dev/serial/by-id/YOUR-ZIGBEE-ADAPTER:/dev/ttyACM0 \
-v /root/zigbee2mqtt/data:/app/data \
-v /root/zigbee2mqtt/logs:/app/logs \
-e TZ=Europe/Amsterdam \
koenkk/zigbee2mqtt
If you specified incorrect --device
path, or if the device (Zigbee adapter/coordinator) is not connected, then you’ll see errors like these in the log:
$ sudo docker container logs zigbee2mqtt
...
info: z2m: Starting Zigbee2MQTT version 1.39.0 (commit #0326926)
info: z2m: Starting zigbee-herdsman (0.50.1)
error: zh:zstack:znp: Failed to determine if path is valid: 'Error: ENOENT: no such file or directory, lstat '/dev/ttyACM0''
info: zh:zstack:znp: Opening SerialPort with {"path":"/dev/ttyACM0","baudRate":115200,"rtscts":false,"autoOpen":false}
error: z2m: Error while starting zigbee-herdsman
error: z2m: Failed to start zigbee
error: z2m: Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions
error: z2m: Exiting...
error: z2m: Error: Error: No such file or directory, cannot open /dev/ttyACM0
If you are sure that you specified everything correctly and your device is connected, then check that you have /dev/serial/
at all:
$ ls -lah /dev/serial
If you don’t, then you might need to edit /usr/lib/udev/rules.d/60-serial.rules
and apply this fix.
For me that wasn’t the case - I just haven’t connected the adaptor yet. After I did, here’s what dmesg
printed out:
$ sudo dmesg -w
...
usb 1-2: new full-speed USB device number 5 using xhci-hcd
usb 1-2: New USB device found, idVendor=1a86, idProduct=55d4
usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-2: Product: SONOFF Zigbee 3.0 USB Dongle Plus V2
usb 1-2: Manufacturer: ITEAD
usb 1-2: SerialNumber: SERIAL-NUMBER-HERE
cdc_acm 1-2:1.0: ttyACM0: USB ACM device
and I got it in /dev/serial/
:
$ ls -lah /dev/serial/by-id/
drwxr-xr-x 2 root root 60 Aug 5 10:17 .
drwxr-xr-x 4 root root 80 Aug 5 10:17 ..
lrwxrwxrwx 1 root root 13 Aug 5 10:17 usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_SERIAL-NUMBER-HERE-if00 -> ../../ttyACM0
$ ls -lah /dev/serial/by-path/
drwxr-xr-x 2 root root 60 Aug 5 10:17 .
drwxr-xr-x 4 root root 80 Aug 5 10:17 ..
lrwxrwxrwx 1 root root 13 Aug 5 10:17 platform-xhci-hcd.0.auto-usb-0:2:1.0 -> ../../ttyACM0
That usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_SERIAL-NUMBER-HERE-if00
value is what you need to put into --device
mapping on creating/running the container. Since I have already created a container without providing a correct value and I don’t know how to update an already existing container with a correct mapping, I just re-created it:
$ sudo docker stop zigbee2mqtt
$ sudo docker rm zigbee2mqtt
$ sudo docker run -d \
--name zigbee2mqtt \
--network hub \
--restart=unless-stopped \
--device=/dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_SERIAL-NUMBER-HERE-if00:/dev/ttyACM0 \
-v /root/zigbee2mqtt/data:/app/data \
-v /root/zigbee2mqtt/logs:/app/logs \
-e TZ=Europe/Amsterdam \
koenkk/zigbee2mqtt
Successful launch will manifest itself in the log like this:
info: z2m: Starting Zigbee2MQTT version 1.39.0 (commit #0326926)
info: z2m: Starting zigbee-herdsman (0.50.1)
info: zh:zstack:znp: Opening SerialPort with {"path":"/dev/ttyACM0","baudRate":115200,"rtscts":false,"autoOpen":false}
info: zh:zstack:znp: Serialport opened
info: zh:zstack:znp: Writing CC2530/CC2531 skip bootloader payload
info: zh:zstack:znp: Skip bootloader for CC2652/CC1352
…however, after that I got a different error:
error: z2m: Error while starting zigbee-herdsman
error: z2m: Failed to start zigbee
error: z2m: Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions
error: z2m: Exiting...
error: z2m: Error: Failed to connect to the adapter (Error: SRSP - SYS - ping after 6000ms)
at ZStackAdapter.start (/app/node_modules/zigbee-herdsman/src/adapter/z-stack/adapter/zStackAdapter.ts:101:27)
at Controller.start (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:129:29)
at Zigbee.start (/app/lib/zigbee.ts:63:27)
at Controller.start (/app/lib/controller.ts:139:27)
at start (/app/index.js:154:5)
Possible reasons are listed here, and I suspected that in my case it was an obsolete firmware, but then I saw this video, which is exactly about SONOFF Zigbee 3.0 USB Dongle Plus-E, and there I noticed the adapter: ezsp
setting, which I did not have in my config. So I added it:
# ...
serial:
# where Zigbee adapter is connected to
port: /dev/ttyACM0
# if you have SONOFF Zigbee 3.0 USB Dongle Plus-E
adapter: ezsp
And then it finally launched (almost) fine:
info: z2m: Starting Zigbee2MQTT version 1.39.0 (commit #0326926)
info: z2m: Starting zigbee-herdsman (0.50.1)
warning: zh:ezsp: Deprecated driver 'ezsp' currently in use, 'ember' will become the officially supported EmberZNet driver in next release. If using Zigbee2MQTT see https://github.com/Koenkk/zigbee2mqtt/discussions/21462
error: zh:ezsp:ezsp: Failure to init network
info: zh:ezsp:driv: Form network
info: zh:controller: Wrote coordinator backup to '/app/data/coordinator_backup.json'
info: z2m: zigbee-herdsman started (reset)
info: z2m: Coordinator firmware version: '{"meta":{"maintrel":"3 ","majorrel":"6","minorrel":"10","product":8,"revision":"6.10.3.0 build 297"},"type":"EZSP v8"}'
info: z2m: Currently 0 devices are joined:
warning: z2m: `permit_join` set to `true` in configuration.yaml.
warning: z2m: Allowing new devices to join.
warning: z2m: Set `permit_join` to `false` once you joined all devices.
info: z2m: Zigbee: allowing new devices to join.
info: z2m: Connecting to MQTT server at mqtt://mosquitto
info: z2m: Connected to MQTT server
info: z2m: Zigbee2MQTT started!
One of the warnings says that ezsp
is a deprecated driver, and instead it should be ember
. So I tried that:
# ...
serial:
# where Zigbee adapter is connected to
port: /dev/ttyACM0
# if you have SONOFF Zigbee 3.0 USB Dongle Plus-E
adapter: ember
but it failed to launch:
info: z2m: Starting Zigbee2MQTT version 1.39.0 (commit #0326926)
info: z2m: Starting zigbee-herdsman (0.50.1)
info: zh:ember: Using default stack config.
info: zh:ember: ======== Ember Adapter Starting ========
info: zh:ember:ezsp: ======== EZSP starting ========
info: zh:ember:uart:ash: ======== ASH NCP reset ========
info: zh:ember:uart:ash: RTS/CTS config is off, enabling software flow control.
info: zh:ember:uart:ash: Serial port opened
info: zh:ember:uart:ash: ======== ASH starting ========
error: zh:ember:uart:ash: Received ERROR from NCP while connecting, with code=ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT.
error: zh:ember:uart:ash: ASH disconnected | NCP status: ASH_NCP_FATAL_ERROR
error: zh:ember:uart:ash: Error while parsing received frame, status=ASH_NCP_FATAL_ERROR.
info: zh:ember:uart:ash: ======== ASH NCP reset ========
info: zh:ember:uart:ash: ======== ASH starting ========
info: zh:ember:uart:ash: ======== ASH connected ========
info: zh:ember:uart:ash: ======== ASH started ========
info: zh:ember:ezsp: ======== EZSP started ========
error: z2m: Error while starting zigbee-herdsman
error: z2m: Failed to start zigbee
error: z2m: Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions
error: z2m: Exiting...
error: z2m: Error: NCP EZSP protocol version of 8 does not match Host version 13
at EmberAdapter.emberVersion (/app/node_modules/zigbee-herdsman/src/adapter/ember/adapter/emberAdapter.ts:1714:19)
at EmberAdapter.initEzsp (/app/node_modules/zigbee-herdsman/src/adapter/ember/adapter/emberAdapter.ts:893:9)
at EmberAdapter.start (/app/node_modules/zigbee-herdsman/src/adapter/ember/adapter/emberAdapter.ts:2722:24)
at Controller.start (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:129:29)
at Zigbee.start (/app/lib/zigbee.ts:63:27)
at Controller.start (/app/lib/controller.ts:139:27)
at start (/app/index.js:154:5)
From what I see here, to make it work with ember
, apparently I need flash my Zigbee adapter with a different/newer firmware. However, for now I decided not to, and I just reverted the config back to ezsp
, as that one seemed to be already working as it is.
To access the frontend I exposed its port (8080
by default) from the host the same way I did it with Home Assistant container (static IP and reverse proxy):
$ sudo docker network disconnect hub zigbee2mqtt
$ sudo docker network connect --ip 172.18.0.101 hub zigbee2mqtt
$ sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' zigbee2mqtt
172.18.0.101
$ sudo cp /etc/nginx/sites-available/{homeassistant,zigbee2mqtt}
$ sudo nano /etc/nginx/sites-available/zigbee2mqtt
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# again, doesn't have to be exactly 8080
listen 8080 ssl;
ssl_certificate /opt/certificates/fullchain.pem;
ssl_certificate_key /opt/certificates/key.pem;
location / {
proxy_pass http://172.18.0.101:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
$ sudo ln -s /etc/nginx/sites-{available,enabled}/zigbee2mqtt
$ sudo systemctl restart nginx.service
Once I’ve logged-in to my Zigbee2MQTT frontend, I could see that my Zigbee adapter/coordinator revision is 6.10.3.0 build 297
:
but I have no idea how to check if that is the latest available (or how to update it to the latest). I’ve seen several GiHub repositories with some firmwares, but I can’t tell which of them is the official one, and in general distributing firmware binaries via GitHib seems very bizarre to me. Either way, there is no pressure, as it seems to work fine with the current firmware.
Now you can start adding devices to grow your Zigbee network. To add a new device, put it (the device) into pairing mode and wait for it to appear on the Devices page of your Zigbee2MQTT frontend:
Once the device is paired, Zigbee adapter/coordinator hardware will be communicating with it over Zigbee and will be forwarding its data to Home Assistant via MQTT; and the other way around, commands from Home Assistant well be going through MQTT broker to Zigbee2MQTT, which will be transmitting them to devices over Zigbee by coordinator.
Below there will be examples for some of the devices that I have.
Philips Hue Smart Plug
Philips has a whole family of smart home devices, which is called Philips Hue, and out of those I only have a Philips Hue Smart Plug for now:
To put it into pairing mode, plug it into a power socket, wait for the front indicator to turn off, press and hold the button on the side and wait for the indicator to blink with orange for 5 or more times. Shortly after that it should appear in the list of devices in Zigbee2MQTT frontend.
The pairing happens automatically, you don’t need to do anything, just watch the pop-ups. Once the device is paired, here’s how its page will look like (here I have already renamed it):
To easily find it in Home Assistant, rename it from the default name to something meaningful and update the Home Assistant entity ID too:
Then it should appear in Home Assistant list of devices under that exact name:
At this point you can already turn it on and off from Home Assistant and use it in your automation routines.
Sadly, this particular “smart” plug is rather “dumb”, as it only has on/off capabilities with no energy meter/counter:
But that is actually fine, because it costs less than the alternatives with energy meter - 300 NOK versus 400+ NOK - which builds up to a significant difference if you need to buy several of those.
Frient Smart Plug Mini 2
There is another smart plug - Frient Smart Plug Mini 2:
And this one is equiped with an energy meter/counter, so it is “smarter” than the one from Philips, but it is also more expensive (about 500 NOK) and unglier too, so I don’t think I’ll buy more of these.
The pairing should be trivial: when it is not part of any network, it enters the pairing mode automatically. If not, try pressing the button on the side for 5 or more seconds, or/and re-plug it to a power socket.
Once Zigbee2MQTT pairs with it, here are the metrics it will expose:
So it is immediate power consumption, total amount of power already consumed, voltage, frequency and others. As with any other value in Home Assistant, you can get a historical chart of changes, for example here’s one for voltage:
So that’s already quite a lot to play with, as you can create scripts and automations to perform various actions based on any of those metrics.
IKEA Tradfri E27
As it turned out, IKEA bulbs with a remote control, which I have already been using for a couple of years, are also Zigbee-enabled, so they can be connected to a Zigbee network without their remote control:
To put a bulb into pairing mode, turn it off and on for 5 times, and on the 6th time keep it on. It should start doing a certain lighting pattern, meaning that it is now discoverable by a Zigbee coordinator.
Once added, here’s how it looks in Home Assistant:
The controls are great, you can set not only brightness but the color temperature too, plus there are several effects available:
As a very nice surprise, these bulbs also act as Zigbee signal repeaters/extenders, so they allow you to place other devices farther away from the coordinator/hub. And I forgot to mention that smart power plugs (Philips and Frient) also act as network extenders, so that is probably the case for many power-connected devices.
Having smart bulbs (and/or dumb lamps connected to smart plugs), one can create the following basic automation for turning on the (appropriately labeled) lights in one of the rooms on schedule:
It will work with bulbs and plugs, despite them being different types of devices, because they all support switch actions.
Here’s also this automation as code:
alias: Turn ON lights on 2nd floor
description: ""
trigger:
- platform: time
at: "20:12:00"
condition: []
action:
- service: switch.turn_on
metadata: {}
data: {}
target:
area_id: living_room
label_id: lights
mode: single
And it will be trivial to create a similar automation for turning the lights off.
nimly Connect Module
I’ve got a nimly Touch Pro lock with a nimly Connect Module. The module allows you to control the lock remotely, so you can lock/unlock it, change some settings, get information about what method was last used to unlock it (fingerprint, token, pin-code) and which user did it.
The lock itself is great, but I’ve had some problems with the module. It is actually my 3rd module already, as the first two had certain defects (first one was loosing connection and second one was draining the batteries), and nimly people are graciously sending me replacements. The current module that I got has firmware build date 20240625
with version 4.7.79
, and finally everything seems to be good with this one.
Initially I’ve been using the modules with Homely, but I wasn’t entirely satisfied with the functionality exposed in Homely application, so I decided to connect it to Home Assistant instead. The pairing with Zigbee2MQTT went fine, the module got interviewed and marked as supported:
I also got it in Home Assistant, but nothing seemed to actually work. The commands that I tried to send to it (such as lock/unlock) were failing, as there was no reaction on the lock itself, and the logs had errors like:
zh:ezsp:ezsp: Frame changeSourceRouteHandler parsing error: RangeError: Attempt to access memory outside buffer bounds at new NodeError (node:internal/errors:405:5) at boundsError (node:internal/buffer:86:11) at Buffer.readUInt16LE (node:internal/buffer:245:5) at Buffer.readUIntLE (node:internal/buffer:182:17) at Function.deserialize (/app/node_modules/zigbee-herdsman/src/adapter/ezsp/driver/types/basic.ts:19:67) at new EZSPFrameData (/app/node_modules/zigbee-herdsman/src/adapter/ezsp/driver/ezsp.ts:180:54) at /app/node_modules/zigbee-herdsman/src/adapter/ezsp/driver/ezsp.ts:154:23 at Array.every (<anonymous>) at Function.createFrame (/app/node_modules/zigbee-herdsman/src/adapter/ezsp/driver/ezsp.ts:148:15) at Ezsp.onFrameReceived (/app/node_modules/zigbee-herdsman/src/adapter/ezsp/driver/ezsp.ts:439:35)
zh:ezsp:ezsp: Unparsed frame 0xc4. Skipped
z2m: Publish 'set' 'state' to 'nimly Touch Pro' failed: 'Error: ZCL command IEEE-ADDRESS-HERE/11 closuresDoorLock.lockDoor({"pincodevalue":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false}) failed (Timeout - 57813 - 11 - 52 - 257 - 0 after 10000ms)'
Also its sensors were reporting None
in Home Assistant:
Before trying anything else I tried to simply re-pair it (force-remove, re-insert the batteries and add it back again), and this time it got successfully added with everything working (even though there were still the same errors in the logs):
Now that there is a possibility to remotely control such a critical piece of hardware as main door lock, you can quite literally be hacked - just like in the movies. How exciting is that!
Aqara Cube T1 Pro
That little thing is fantastic:
Say you’d like to have a physical button, which would control the lights, so you could just press it instead of reaching to your phone/computer with Home Assistant client application/browser. And then maybe you’d also like to have a dimmer for the lights, and maybe yet another button for controlling a power plug, and so on and so on. Every such Zigbee-capable button/dimmer would cost you some noticable amount of money, plus they would be piling up on your table, so that doesn’t scale very well, does it.
But what if instead of having several such buttons/dimmers you could have just one device that could somehow act as several “buttons”. And Aqara Cube is exactly that: it can trigger at the very least 18 actions (3 for each side: tapping, rotating clockwise and rotating counterclockwise) - but actually more, because there are also actions for pushing, resting, holding, throwing, shaking, flipping 90/180 degrees and more. And all that for the price of less than one physical button.
To state the obvious, you do not need the Aqara hub or anything else to interface with it, as Zigbee2MQTT can pair with it directly just fine. Take off the top side cover/plate and hold the link button for 5 seconds. Once it is paired, don’t put the plate back just yet - I would recommend you to switch the cube to Action mode. That is done by sending the “operation” command to it, either from Zigbee2MQTT device page or from Home Assistant device page. For this to take effect immediately (otherwise it “syncs” once per hour?), press the link button once, and then you can put the plate back.
Here’s a very basic automation that you can make with the cube:
So whenever I double-tap the cube on the table (or wherever it is), while its top side is 1
, all the devices labeled with lights
in the Living room
area will be toggled on/off.
Here it is a code too:
alias: 💡 1 | Toggle lights on 2nd floor
description: ""
trigger:
- platform: device
domain: mqtt
device_id: AQARA-CUBE-DEVICE-ID-HERE
type: action
subtype: tap
condition:
- condition: state
entity_id: sensor.aqara_cube_t1_pro_side
state: "1"
action:
- service: switch.toggle
metadata: {}
data: {}
target:
area_id: living_room
label_id: lights
mode: single
There will be some more automation examples later.
Home Assistant Community Store
To further extend Home Assistant capabilities, you can add Home Assistant Community Store (HACS) to it.
The HACS is supposed to be installed into /path/to/home-assistant/config/custom_components/
, which in case of Docker container would mean creating this folder inside the container, but fortunately we have already mounted/mapped this path on container creation, and it points to /root/homeassistant/
in the host filesystem.
As for the installation itself, yet again it is expected that you will download and run their script, but this time I couldn’t tolerate that, so I just picked out the relevant commads from it. Essentially, it just downloads a ZIP archive and extracts it to /path/to/home-assistant/config/custom_components/hacs/
:
$ cd /tmp
$ wget "https://github.com/hacs/integration/releases/latest/download/hacs.zip"
$ export HOMEASSISTANT_PATH="/root/homeassistant"
$ sudo ls -L1a $HOMEASSISTANT_PATH
automations.yaml
blueprints
.cloud
configuration.yaml
deps
.HA_VERSION
home-assistant.log
home-assistant.log.1
home-assistant.log.fault
home-assistant_v2.db
home-assistant_v2.db-shm
home-assistant_v2.db-wal
scenes.yaml
scripts.yaml
secrets.yaml
.storage
tts
$ sudo cat $HOMEASSISTANT_PATH/.HA_VERSION && echo
2024.7.3
$ sudo mkdir -p "$HOMEASSISTANT_PATH/custom_components/hacs"
$ sudo unzip /tmp/hacs.zip -d $HOMEASSISTANT_PATH/custom_components/hacs
Restart the container:
$ sudo docker restart homeassistant
And then you should get HACS available in the integrations list:
I did not enable experimental features, just in case:
The next step was concerning, as it required to authorize with GitHub. I have no idea why does it need that, but at least it seems to only request “limited access to your public data”, which is probably fine, so I authorized it:
After configuration was done, the HACS entry was added to the side panel:
…although without icon at first, but after reloading the page with clearing cache it did appear. This “reloading with clearing the cache” you will very often see being mentioned in Home Assistant documentation and forum.
With HACS being integrated in your Home Assistant, you can add even more devices and sensors to it.
Dyson HP09
I have a Dyson HP09 purifier/fan/heater:
And I wanted to check if it is possible to get its sensors data and maybe control the device too. Turns out, someone already did that.
To install it, search for “dyson” in HACS. After the installation:
and Home Assistant restart, Dyson became available in the integrations/devices.
To configure it you will need to provide its IP address, which means that you’ll need to assign a static one in your home network DHCP, and also Vero V (or whatever you are using as your Home Assistant hub) and Dyson both need to be in the same network (which is a bummer, because I prefer to keep such devices in a separate isolated network). If it keeps failing to connect to the Dyson, that could be because of the old DHCP lease, so try to power off the Dyson completely, unplug the power cable, wait a bit and power it back on.
In connection methods I chose the Dyson account:
Once the device is added, you can remove your Dyson account credentials from the integration entries and restart Home Assistant. From what I understood, Dyson runs its own MQTT server, which this integration uses to communicate with the device over the local network (no Zigbee involved, obviously), so without the internet, which is great.
At first Home Assistant was showing just some basic controls, like the fan speed, direction and oscillation:
which was quite good already, but still pretty underwhelming, as I was expecting to get all of its marvelous data such as temperature, humidity, air quality metrics and the rest. But then a couple of minutes later I got everything, so it probably just doesn’t arrive all at once:
At this point you should once again think about securing your Home Assistant instance, because a malicious person may be able to remotely switch your fan into the heating mode, and that can be up to 2.5 kWh of energy consumption alone (and with great power comes great electricity bill), not to mention your room being overheated, which might in turn cause some other damage.
Controlling Dyson with Aqara Cube
Here is another example of how great the Aqara Cube is. So I want to toggle Auto/Normal mode of the fan by tapping the cube when the side 6
is up:
Here’s the same as code:
alias: 🪭 6 | Toggle Auto/Normal mode
description: ""
trigger:
- platform: device
domain: mqtt
device_id: AQARA-CUBE-DEVICE-ID-HERE
type: action
subtype: tap
condition:
- condition: state
entity_id: sensor.aqara_cube_t1_pro_side
state: "6"
action:
- if:
- condition: state
entity_id: fan.dyson_hp09
attribute: preset_mode
state: Auto
then:
- service: fan.set_preset_mode
metadata: {}
data:
preset_mode: Normal
target:
device_id: DYSON_DEVICE_ID_HERE
else:
- service: fan.set_preset_mode
metadata: {}
data:
preset_mode: Auto
target:
device_id: DYSON_DEVICE_ID_HERE
mode: single
And it works, it bloody works, what sorcery is this.
I have also created automations for setting certain fan speeds on rotating the cube with the 6
side up, but those are too trivial to be shown here, you can easily create those yourself.
Zaptec
I also have a Zaptec Go charger for my electric car:
along with Zaptec Sense (HAN-port module):
And since Zaptec has a REST API, I was hoping that it would be possible to get some of the charging/power metrics in Home Assistant. And what would you know, someone has already done that too.
This integration is also installed via HACS by searching for “zaptec”, and as usual you will need to restart Home Assistant after the installation. After that it will become available in the list of integrations.
Since neither(?) of Zaptec devices expose a local API, all the REST API HTTP requests are sent to Zaptec server over the internet, so there is no Zigbee involved here either. For the integration to work you need to provide it with your Zaptec Portal credentials:
I created a separate user for this, hoping that it will be a non-owner type of user, but unfortunately no, with the current set of roles you’ll have to grant that user the owner
role too:
After the successful configuration you will get a lot of metrics:
…But unfortunately there is no data from Zaptec Sense. Among other things it provides a total household power consumption, which you can view at your Zaptec Portal:
And it would be very useful to have that data in Home Assistant too.
I’ve submitted a feature request for this, but it isn’t really within the scope of this particular integration, so I will probably have to implement it myself.
Nord Pool electricity prices
In some countries it is possible to get per-hour electricity prices for today an tomorrow, which is a nice metric to have in Home Assistant. Surely enough, someone has already done that too.
Installation procedure is the same, just search for “nordpool” in HACS. For configuration you will need to provide your zone/region (here are the norwegian zones, and you should have yours stated in the electricity invoices that you receive from your power supplier):
But getting plain price values is no fun, we want a chart, and for that there is a yet another HACS repository - ApexCharts Card. Having installed that, you will be able to create a chart like this one:
The code for it:
type: custom:apexcharts-card
graph_span: 46h
header:
title: Цены на электричество по часам
show: true
show_states: true
colorize_states: true
span:
start: day
now:
show: true
label: Сейчас
color: '#0074D9'
experimental:
color_threshold: true
series:
- entity: sensor.nordpool_kwh_oslo_nok_2_10_025
name: Цена
type: column
float_precision: 2
color_threshold:
- value: 0
color: '#2ECC40'
- value: 0.4
color: '#FFDC00'
- value: 0.6
color: '#FF4136'
show:
extremas: true
header_color_threshold: true
name_in_header: false
in_header: raw
data_generator: >
return [...entity.attributes.raw_today,
...entity.attributes.raw_tomorrow].map((item) =>
[Date.parse(item.start), item.value]);
apex_config:
chart:
height: 400px
tooltip:
x:
show: false
And not just that, you can also add some Markdown cards to print out the prices highlights, for example:
The code for it (based on someone else’s code that I found in this thread):
type: vertical-stack
cards:
- type: markdown
alignment: justify
content: >-
{% set priceListToday =
state_attr('sensor.nordpool_kwh_oslo_nok_2_10_025', 'today')[00:23] %} {%
set dateListToday = state_attr('sensor.nordpool_kwh_oslo_nok_2_10_025',
'raw_today')[00:23] %} {% set minPriceToday = min(priceListToday) %} {%
set minIndexToday = priceListToday.index(minPriceToday) %} {% set
minDateTimeStrToday = dateListToday[minIndexToday].start | string %} {%
set minDateTimeToday= strptime(minDateTimeStrToday[0:19], '%Y-%m-%d
%H:%M:%S') %}
Самая низкая цена сегодня: **{{ minPriceToday }}**
NOK/kWh в **{{ minDateTimeToday.hour }}:00**.
{% if state_attr('sensor.nordpool_kwh_oslo_nok_2_10_025','tomorrow') |
length > 1 %} {% set priceListTomorrow =
state_attr('sensor.nordpool_kwh_oslo_nok_2_10_025', 'tomorrow')[00:23] %}
{% set dateListTomorrow =
state_attr('sensor.nordpool_kwh_oslo_nok_2_10_025', 'raw_tomorrow')[00:23]
%} {% set minPriceTomorrow = min(priceListTomorrow) %} {% set
minIndexTomorrow = priceListTomorrow.index(minPriceTomorrow) %} {% set
minDateTimeStrTomorrow = dateListTomorrow[minIndexTomorrow].start | string
%} {% set minDateTimeTomorrow = strptime(minDateTimeStrTomorrow[0:19],
'%Y-%m-%d %H:%M:%S') %}
Самая низкая цена завтра: **{{ minPriceTomorrow
}}** NOK/kWh в **{{ minDateTimeTomorrow.hour }}:00**. {% else %} *На
завтра цен пока нет.* {% endif %}
- type: markdown
alignment: justify
content: >-
{% set iterativesum = namespace(iter=[]) %} {% set lowestiter =
namespace(kr=2) %} {% set timelowest = namespace(hr=2) %} {% set
highestiter = namespace(kr=0) %} {% set timehighest = namespace(hr=0) %}
{% set num_hours = 4 | int %} {% set nordpoolentity =
'sensor.nordpool_kwh_oslo_nok_2_10_025' %} {% set timemapper = { 0:
'00:00', 1 : '01:00', 2 : '02:00', 3 : '03:00', 4 : '04:00', 5 : '05:00',
6 : '06:00', 7 : '07:00', 8 : '08:00', 9 : '09:00', 10 : '10:00', 11 :
'11:00', 12 : '12:00', 13 : '13:00', 14 : '14:00', 15 : '15:00', 16 :
'16:00', 17 : '17:00', 18 : '18:00', 19 : '19:00', 20 : '20:00', 21 :
'21:00', 22 : '22:00', 23 : '23:00', 24 : '00:00', 25 : '01:00', 26 :
'02:00', 27 : '03:00', 28 : '04:00', 29 : '05:00', 30 : '06:00', 31 :
'07:00', 32 : '08:00', 33 : '09:00', 34 : '10:00', 35 : '11:00', 36 :
'12:00', 37 : '13:00', 38 : '14:00', 39 : '15:00', 40 : '16:00', 41 :
'17:00', 42 : '18:00', 43 : '19:00', 44 : '20:00', 45 : '21:00', 46 :
'22:00', 47 : '23:00', 48 : '0:00', } %} {% set prices =
namespace(price=[]) %} {% set prices.price = prices.price +
state_attr(nordpoolentity, 'today') %} {%- if
state_attr(nordpoolentity,'tomorrow') | length == 1 -%} *На завтра цен
пока нет.* {% else %} {% set prices.price = prices.price +
state_attr(nordpoolentity, 'tomorrow') %} {% endif %} {%- for n in
range(prices.price|length -num_hours +1) -%} {%- set tempsum=
namespace(temp=0) -%} {%- for i in range(num_hours) -%} {%- set
tempsum.temp = tempsum.temp + prices.price[n+i] -%} {% endfor -%} {% set
iterativesum.iter = iterativesum.iter + [tempsum.temp] -%} {% endfor -%}
{% for iter in iterativesum.iter -%} {%- if loop.index > now().hour -%}
{%- if iter < lowestiter.kr | float -%} {%- set lowestiter.kr = iter |
float -%} {%- set timelowest.hr = loop.index -1 -%} {%- endif -%} {%- if
iter > highestiter.kr | float -%} {%- set highestiter.kr = iter | float
-%} {%- set timehighest.hr = loop.index -1 -%} {%- endif -%} {%- endif -%}
{% endfor -%}
Самые дешёвые **{{num_hours}}** часа {% if (timelowest.hr <
24) %}сегодня{% else %}будут завтра{% endif %}: с
**{{timemapper[timelowest.hr]}}** (*в среднем по
**{{"%.2f"|format(lowestiter.kr/num_hours)}}** NOK/kWh*).
Самые дорогие **{{num_hours}}** часа {% if (timehighest.hr < 24)
%}сегодня{% else %}будут завтра{% endif %}: с
**{{timemapper[timehighest.hr]}}** (*в среднем по
**{{"%.2f"|format(highestiter.kr/num_hours)}}** NOK/kWh*).
Sorry for the russian parts (if it is of any comfort, the original code was in finnish language) and especially for the ugly formatting, but Home Assistant keeps reformatting the code on saving the card, so there is no point in trying to make it look nice.
Organizing dashboards layout
Eventually you’ll probably want to change the dashboards layout, in particular almost immediately I wanted to have a 2-column layout with the second column being wider. And surprisingly, that isn’t wasn’t possible/trivial to do out of the box.
Until there was a way, one could use the layout-card frontend plugin, which can be installed via HACS. It probably offers more than what I am using it for, but for me the grid layout was enough, so now I can organize my cards like this:
The code for this:
views:
- title: Home
cards:
- ...
- ...
type: custom:grid-layout
layout:
grid-template-columns: 35% auto
grid-template-rows: auto
mediaquery:
'(max-width: 800px)':
grid-template-columns: 100%
So I survived with that for a bit (never figured out how to have more than one mediaquery
), and then Home Assistant added sections, which brought very nice layouting capabilities:
Using sections, here’s how I could arrange the dashboard at full width with 3 columns:
and here’s how it automatically rearranges in 2 columns on narrower screens:
It also rearranges into 1 column on even narrower screens, but that would be a one kilometer long screenshot, so I won’t put it here.
So I switched to sections and deleted the layout-card
plugin.
Backups
Having configured so many things, you should definitely think about backups.
I’ve come up with the following script:
$ mkdir /root/_backups
$ sudo nano /root/_backups/backup.sh
#!/bin/bash
dt="$(date +%Y.%m.%d-%H%M%S)"
backupname="vero-backup_$dt"
workingdir="/root/_backups/$backupname"
mkdir -p $workingdir
# Home Assistant and friends
tar -czf $workingdir/home-assistant.tar.gz \
/root/homeassistant \
/root/mosquitto \
/root/zigbee2mqtt
# NGINX
tar -czf $workingdir/nginx.tar.gz \
/etc/nginx/sites-available/* \
/etc/nginx/mime.types \
/etc/nginx/nginx.conf
# OSMC (given that it makes backups in this folder)
cp /home/osmc/backups/* $workingdir/
# cron
cp /var/spool/cron/crontabs/* $workingdir/
# the script itself
cp /root/_backups/backup.sh $workingdir/
and made it run on schedule:
$ sudo crontab -e
1 1 */20 * * /root/_backups/backup.sh
# or with Healthchecks reporting: https://github.com/retifrav/scraps/blob/master/_linux/installing-healthchecks/index.md
#1 1 */20 * * /root/_backups/backup.sh > /dev/null 2>&1 && /usr/bin/curl -X POST -fsS -m 10 --retry 5 -o /dev/null https://healthchecks.YOUR.DOMAIN/ping/ID/vero-backup
It would also be a good idea to upload/download the backups from the device to some reliable external storage.
Before making a backup, you might want to stop the containers, so files wouldn’t change during the backup procedure:
$ sudo docker stop $(sudo docker ps -aq)
$ sudo /root/_backups/backup.sh
$ sudo docker start $(sudo docker ps -aq)
Updates
In general, the procedure of updating something (that is run in a Docker container) is to get the latest Docker image and re-create the container. So with Home Assistant as an example in my particular network setup (note the --network
and --ip
arguments), the steps would be:
$ echo "Stop all the containers for making a backup"
$ sudo docker stop $(sudo docker ps -aq)
$ sudo /root/_backups/backup.sh
$ echo "Remove the existing Home Assistant container"
$ sudo docker rm homeassistant
$ echo "Get the latest image"
$ sudo docker pull ghcr.io/home-assistant/home-assistant:stable
$ echo "Re-create the container"
$ sudo docker run -d \
--name homeassistant \
--restart=unless-stopped \
--network hub \
--ip 172.18.0.100 \
-e TZ=Europe/Amsterdam \
-v /root/homeassistant:/config \
ghcr.io/home-assistant/home-assistant:stable
$ echo "Start other containers back"
$ echo "Should be able to use this instead: `sudo docker start $(sudo docker ps -aq)`"
$ sudo docker start mosquitto
$ sudo docker start zigbee2mqtt
As you already know, thanks to Home Assistant data being stored in a mapped volume location, it is totally fine to destroy and re-create its container literally whenever, which makes updating Home Assistant a very simple procedure.
Performance monitoring with btop
As a bonus part, I wanted to install btop tool for watching the system CPU load.
There is no(?) pre-built package for btop on Debian, so I had to build it from sources, and for that it needs some tools and dependencies to be installed first, out of which CMake I also had to build from sources, because packaged version 3.18.4
is a bit old, and btop requires at least 3.24
:
$ sudo apt install build-essential libssl-dev ninja-build
$ cd ~/downloads
$ wget https://github.com/Kitware/CMake/releases/download/v3.30.2/cmake-3.30.2.tar.gz
$ tar -xvf ./cmake-3.30.2.tar.gz
$ rm ./cmake-3.30.2.tar.gz
$ mv ./cmake-3.30.2 ~/programs/cmake
$ cd ~/programs/cmake
$ ./bootstrap
$ make -j$(nproc)
$ sudo make install
Then building btop
itself:
$ cd ~/programs
$ git clone https://github.com/aristocratos/btop
$ cd ./btop
$ git checkout v1.3.2
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DBTOP_GPU=0 ..
$ cmake --build .
$ sudo cmake --install .
$ cd ~
$ btop --version
btop version: 1.3.2
To measure the CPU load, I kept it monitoring the system performance for several minutes while playing a 4K movie and running Docker containers with Home Assistant and its components:
And it barely broke any sweat.
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks