says ⮞ I use 20x magnification when I code and debug. I use emoji to simplify logs for myself. If you can't handle my code style you can disable most of it on this website by toggling the button in the navbar. Shall duck continue?
Today we are defining our Smart Home devices, scenes, and configure other options that helps setup our home.
I will try to keep this as short as possible as there is not much to say about defining devices - and to reduce the risk of this becoming a massive series.
Rooms
Let's start with the basic building blocks - defining the rooms in your home.
This creates logical groups for your devices and enables room-specific automations.
⮞ View room code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 duck say ⮞ qwack
house = {
rooms = {
bedroom.icon = "mdi:bedroom";
hallway.icon = "mdi:hallway";
kitchen.icon = "mdi:food-fork-drink";
livingroom.icon = "mdi:sofa";
wc.icon = "mdi:toilet";
other.icon = "mdi:misc";
};
{
config,
lib,
pkgs,
...
} : let
in {
house = {
rooms = {
bedroom.icon = "mdi:bedroom";
hallway.icon = "mdi:hallway";
kitchen.icon = "mdi:food-fork-drink";
livingroom.icon = "mdi:sofa";
wc.icon = "mdi:toilet";
other.icon = "mdi:misc";
};
Each room gets a fancy icon! Makes the dashboard much cleaner and more enjoyable to use.
Using consistent room naming makes automations much easier to manage and understand later.
⮞ qwack simple huh? - letz move on
Quick Quack Mosquitto Stuff
To simplify I enter basic Mosquitto information into the house module:
⮞ View Zigbee/Mosquitto settings code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 says ⮞ qwack
house = {
# 🦆 ⮞ ZIGBEE ⮜ 🐝
zigbee = {
# 🦆 says ⮞ safety first! not dat good 2 expose
networkKeyFile = config.sops.secrets.z2m_network_key.path;
mosquitto = {
username = "mqtt";
passwordFile = config.sops.secrets.mosquitto.path;
};
};
};}
{
config,
lib,
pkgs,
...
} : let
in {
house = {
zigbee = {
networkKeyFile = config.sops.secrets.z2m_network_key.path;
mosquitto = {
username = "mqtt";
passwordFile = config.sops.secrets.mosquitto.path;
};
};
};}
A Zigbee network key is a shared secret key used to encrypt and protect all communication within a Zigbee network.
Think of it as the master password for the entire network. It is generated by the coordinator and is used to:
1. Securely allow new devices to join the network. 2. Encrypt and decrypt all messages sent between devices.
For security reasons I do not expose this key.
Technically, you can skip configuring the key, but since we are building a fully reproducable home automation system here, we choose to define and encrypt it.
I encrypt it using sops. To do this I save the key as a json file in the following format:
Coordinator Configuration - Symlinking the Serial Port
Zigbee (IEEE 802.15.4) provides the wireless mesh network that connects all our devices,
You need a coordinator to be able to communicate with your Zigbee devices.
Since we are writing a reproducible declarative configuration, it should not matter which USB port the coordinator is connected to.
That is why we are symlinking the serial port to a specific address.
In the example below I will be symlinking my Sonoff Zigbee 3.0 USB Dongle Plus from "/dev/ttyUSB0" to "/dev/zigduck"
⮞ View coordinator code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 duck say ⮞ qwack
house = {
# 🦆 ⮞ ZIGBEE ⮜ 🐝
zigbee = {
# 🦆 ⮞ COORDINATOR ⮜
coordinator = {
vendorId = "10c4"; # 🦆 duck say ⮞ use `lsusb`
productId = "ea60"; # 🦆 duck say ⮞ or `udevadm info -q property -n /dev/ttyUSB0`
symlink = "zigduck"; # 🦆 duck say ⮞ now you can use as /dev/zigduck
};
{
config,
lib,
pkgs,
...
} : let
in {
house = {
zigbee = {
coordinator = {
vendorId = "10c4";
productId = "ea60";
symlink = "zigduck";
};
Cool huh? But you are thinking "What if i don't know the vendorId or productId?"
No worries, I will show you two approaches to finding them.
Make sure your Zigbee coordinator is connected to the serial port on the host that you are running the commands from.
⮞ View `lsusb` output
QuackHack-McBlindy in 🌐 homie in 🦆🏠 HOME
21:54:54 ❯ lsusb
Bus 001 Device 004: ID 05e3:0608
Bus 001 Device 003: ID 08bb:2902
Bus 001 Device 001: ID 1d6b:0002
# ⮟ 🦆 says ⮞ diz iz da vendorId
Bus 001 Device 002: ID 10c4:ea60
# ⮝ 🦆 says ⮞ diz iz da productId
Bus 002 Device 001: ID 1d6b:0003
QuackHack-McBlindy in 🌐 homie in 🦆🏠 HOME
21:54:54 ❯ lsusb
Bus 001 Device 004: ID 05e3:0608
Bus 001 Device 003: ID 08bb:2902
Bus 001 Device 001: ID 1d6b:0002
Bus 001 Device 002: ID 10c4:ea60
Bus 002 Device 001: ID 1d6b:0003
As you can see in the `lsusb` output the first four characters in the ID is the vendorId, and the four last characters the productId.
If you are looking for another solution, using the `udevadm info` command is also a simple way of getting the ID's.
⮞ View `udevadm info -q property -n /dev/ttyUSB0` output
QuackHack-McBlindy in 🌐 homie in 🦆🏠 HOME
✦ 22:12:53 ❯ udevadm info -q property -n /dev/ttyUSB0
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/ttyUSB0/tty/ttyUSB0
DEVNAME=/dev/ttyUSB0
MAJOR=188
MINOR=0
SUBSYSTEM=tty
USEC_INITIALIZED=6546849
PATH=/nix/store/gmydihdyaskbwkqwkn5w8yjh9nzjz56p-udev-path/bin:/nix/store/gmydi>
ID_BUS=usb
ID_MODEL=Sonoff_Zigbee_3.0_USB_Dongle_Plus # 🦆 duck say ⮞ obviously correct port
ID_MODEL_ENC=Sonoff\x20Zigbee\x203.0\x20USB\x20Dongle\x20Plus
ID_MODEL_ID=ea60
ID_SERIAL=Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001
ID_SERIAL_SHORT=0001
ID_VENDOR=Silicon_Labs
ID_VENDOR_ENC=Silicon\x20Labs
ID_VENDOR_ID=10c4 # 🦆 duck say ⮞ here is da vendorId
ID_REVISION=0100
ID_TYPE=generic
ID_USB_MODEL=Sonoff_Zigbee_3.0_USB_Dongle_Plus
ID_USB_MODEL_ENC=Sonoff\x20Zigbee\x203.0\x20USB\x20Dongle\x20Plus
ID_USB_MODEL_ID=ea60 # 🦆 duck say ⮞ here is da productId
ID_USB_SERIAL=Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001
ID_USB_SERIAL_SHORT=0001
QuackHack-McBlindy in 🌐 homie in 🦆🏠 HOME
✦ 22:12:53 ❯ udevadm info -q property -n /dev/ttyUSB0
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/ttyUSB0/tty/ttyUSB0
DEVNAME=/dev/ttyUSB0
MAJOR=188
MINOR=0
SUBSYSTEM=tty
USEC_INITIALIZED=6546849
PATH=/nix/store/gmydihdyaskbwkqwkn5w8yjh9nzjz56p-udev-path/bin:/nix/store/gmydi>
ID_BUS=usb
ID_MODEL=Sonoff_Zigbee_3.0_USB_Dongle_Plus
ID_MODEL_ENC=Sonoff\x20Zigbee\x203.0\x20USB\x20Dongle\x20Plus
ID_MODEL_ID=ea60
ID_SERIAL=Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001
ID_SERIAL_SHORT=0001
ID_VENDOR=Silicon_Labs
ID_VENDOR_ENC=Silicon\x20Labs
ID_VENDOR_ID=10c4
ID_REVISION=0100
ID_TYPE=generic
ID_USB_MODEL=Sonoff_Zigbee_3.0_USB_Dongle_Plus
ID_USB_MODEL_ENC=Sonoff\x20Zigbee\x203.0\x20USB\x20Dongle\x20Plus
ID_USB_MODEL_ID=ea60
ID_USB_SERIAL=Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001
ID_USB_SERIAL_SHORT=0001
says ⮞ woow! u can now reference da stick as `/dev/zigduck` in ur z2m configuration! i will talk about dat in teh next post - promise!
Devices - Lights, Sensors, Dimmers etc
I hope you have a bunch of devices ready! Here's where we define every Zigbee device in the home.
Each device is defined by their unique identifier, the devices IEEE address.
⮞ View device code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 duck say ⮞ qwack
house = {
# 🦆 ⮞ ZIGBEE ⮜ 🐝
zigbee = {
# 🦆 ⮞ DEVICES ⮜
devices = {
# 🦆 says ⮞ Kitchen
"0x0017880103ca6e95" = { # 🦆 says ⮞ 64bit IEEE address (this is the unique device ID)
friendly_name = "Dimmer Switch Kök"; # 🦆 says ⮞ simple human readable friendly name
room = "kitchen"; # 🦆 says ⮞ bind to group
type = "dimmer"; # 🦆 says ⮞ set da device type (lib.types.enum [ "light" "dimmer" "sensor" "motion" "outlet" "remote" "pusher" "blind" ];)
icon = "mdi-toggle-switch"; # 🦆 says ⮞ for da frontend yo
endpoint = 1; # 🦆 says ⮞ endpoint to call the device on
batteryType = "CR2450"; # 🦆 says ⮞ optional yo
}; # 🦆 says ⮞ see dat batteryType? duck never surprised by dead devices! sneaky track battery sweet QWACK!
"0x0017880102f0848a" = {
friendly_name = "Spotlight kök 1";
room = "kitchen";
type = "light";
supports_color = true; # 🦆 says ⮞ default to false
icon = "mdi-spotlight";
endpoint = 11;
};
# 🦆 says ⮞ next device here
{
config,
lib,
pkgs,
...
} : let
in {
house = {
zigbee = {
devices = {
"0x0017880103ca6e95" = {
friendly_name = "Dimmer Switch Kök";
room = "kitchen";
type = "dimmer";
icon = "mdi-toggle-switch";
endpoint = 1;
batteryType = "CR2450"; # 🦆 says ⮞ optional yo
};
"0x0017880102f0848a" = {
friendly_name = "Spotlight kök 1";
room = "kitchen";
type = "light";
supports_color = true;
icon = "mdi-spotlight";
endpoint = 11;
};
Keep adding the rest of your devices - if you are anything like me - the list will be long and take a while.
We're moving on to:
Smart Validation: The system automatically validates your configuration -
checking room existence, device names, scene consistency, and catching duplicates before
you even deploy.
Scenes - Setting the Mood
Scenes allow you to configure multiple lights with specific colors, brightness, and transitions.
Perfect for different activities and moods.
⮞ View scenes code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 duck say ⮞ qwack
house = {
# 🦆 ⮞ ZIGBEE ⮜ 🐝
zigbee = {
# 🦆 ⮞ SCENES ⮜
scenes = {
# 🦆 says ⮞ Scene name
"Duck Scene" = {
# 🦆 says ⮞ Device friendly_name
"Light1" = {
state = "ON"; # 🦆 says ⮞ Device state
brightness = 200; # 🦆 says ⮞ brightness (0-255)
color = { hex = "#00FF00"; }; # 🦆 says ⮞ only if support_color is true for the device
transition = 10; # 🦆 says ⮞ state change transition time in seconds
};
"Light2" = {
state = "ON";
brightness = 200;
color = { hex = "#00FF00"; };
};
};
# 🦆 says ⮞ next scene
{
config,
lib,
pkgs,
...
} : let
in {
house = {
zigbee = {
scenes = {
"Duck Scene" = {
"Light1" = {
state = "ON";
brightness = 200;
color = { hex = "#00FF00"; };
transition = 10;
};
"Light2" = {
state = "ON";
brightness = 200;
color = { hex = "#00FF00"; };
};
};
Keep typing up and defining all your favourite scenes.
It might be a bit of a drag if you have a lot of devices/scenes,
but it's totally worth it because this time - You know it will be your last time!
This completes our zigbee configuration for now.
We have our hosts defined in Nix, we have our lightbulbs, our scenes, our sensors and other Zigbee devices.
You really thought I was going to let you go without defining our TV's?! - Since I can't see I rarely watch myself - but I hear the TV is quite popular.
Let's get to it.
Bonus - TV Devices & Channel Definitions
Who needs cable when you have Nix? Let's define our TV devices with the same declarative approach.
This gives us unified control over our Android TV devices using Android Debug Bridge, complete with per-device channel definitions and automated TV guides.
⮞ View TV code block
{ # 🦆 duck say ⮞ my house - qwack
config,
lib,
pkgs,
...
} : let
in { # 🦆 duck say ⮞ qwack
house = {
# 🦆 ⮞ TV ⮜
tv = {
shield = { # 🦆 says ⮞ Device name
enable = true;
room = "livingroom";
ip = "192.168.1.223";
apps = { # 🦆 says ⮞ define applications
telenor = "se.telenor.stream/.MainActivity";
tv4 = "se.tv4.tv4playtab/se.tv4.tv4play.ui.mobile.main.BottomNavigationActivity";
}; # 🦆 says ⮞ per-device channel definitions
channels = {
"1" = {
name = "SVT1";
id = 1; # 🦆 says ⮞ adb channel ID
# 🦆 says ⮞ OR
# stream_url = "https://url.com/"; # 🦆 recommendz ⮞ if u can, use diz arrr! 🏴☠️🦆
cmd = "open_telenor && wait 5 && start_channel_1";
# 🦆 says ⮞ automagi generated tv-guide web & EPG
icon = ./themes/icons/tv/1.png;
scrape_url = "https://tv-tabla.se/tabla/svt1/";
}; # 🦆 says ⮞ next channel
"2" = {
id = 2;
name = "SVT2";
cmd = "open_telenor && wait 5 && start_channel_2";
icon = ./themes/icons/tv/2.png;
scrape_url = "https://tv-tabla.se/tabla/svt2/";
};
};
};
# 🦆 says ⮞ next TV device
{
config,
lib,
pkgs,
...
} : let
in {
house = {
tv = {
shield = {
enable = true;
room = "livingroom";
ip = "192.168.1.223";
apps = {
telenor = "se.telenor.stream/.MainActivity";
tv4 = "se.tv4.tv4playtab/se.tv4.tv4play.ui.mobile.main.BottomNavigationActivity";
};
channels = {
"1" = {
name = "SVT1";
id = 1;
# stream_url = "https://url.com/";
cmd = "open_telenor && wait 5 && start_channel_1";
icon = ./themes/icons/tv/1.png;
scrape_url = "https://tv-tabla.se/tabla/svt1/";
};
"2" = {
id = 2;
name = "SVT2";
cmd = "open_telenor && wait 5 && start_channel_2";
icon = ./themes/icons/tv/2.png;
scrape_url = "https://tv-tabla.se/tabla/svt2/";
};
};
};
This was fun huh?
And this is just the beginning - trust me!
says ⮞ be sure to stick around!
in da next qwack attack duck shows how to configure MQTT & Z2M!
and duck show's how to finally write home automations in Nix yaay!
Comments on this blog post