🦆

Navigation

🧑‍🦯

Defining Your Home

The module I am configuring today is: modules/house.nix

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";
    };
   

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;
      };
    };
    
  };}   

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:

⮞ View Zigbee Netwoork Key in json

{
	"zigbee_network_key": [
                83,
                174,
                42,
                219,
                156,
                77,
                201,
                23,
                189,
                210,
                101,
                54,
                147,
                225,
                18,
                92
	]
}

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
      };  

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

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

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

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

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

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!

Part 1. The module, the options and defining devices ⮜🦆here u are
Part 2. Configure your Mosquitto/Z2MQTT
Part 3. Nix Configured Automations
Part 4. Writing a Server Service - In Rust
Part 5. Writing a Client - With Voice Commands
Part 6. The Auto-Generated Dashboard



View source code on GitHub

Comments on this blog post