🦆

Navigation

🧑‍🦯

Configuring Your Zigbee Services

Quick instructions of how to configure Mosquitto and Zigbee2MQQTT ensuring it to work with this Smart Home setup.

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 dependencies, Mosquitto and Z2M.
I believe you can have them configured in many ways and still be compatible, but to ensure proper integration the configuration below is the duck recommended.
Feel free to play around to see what is working for your specific needs.
No delay - let's get the qwackin' started!

Mosquitto

Not really anything special going on here.

⮞ View Mosquitto code block

{ # 🦆 duck say ⮞ my house - qwack 
  config,
  lib,
  pkgs,
  ...
} : let
in { # 🦆 duck say ⮞ qwack
  services.mosquitto = {
    enable = true;
    listeners = [
      { # 🦆 says ⮞ mqtt:// @ 1883
        acl = [ "pattern readwrite #" ];
        port = 1883;
        omitPasswordAuth = false; # 🦆 says ⮞ safety first!
        users.mqtt.passwordFile = config.house.zigbee.mosquitto.passwordFIle.path;
        settings.allow_anonymous = false; # 🦆 says ⮞ never forget, never forgive right?
#        settings.require_certificate = true; # 🦆 says ⮞ T to the L to the S spells wat? DUCK! 
#        settings.use_identity_as_username = true;
      }   
      { # 🦆 says ⮞ ws:// @ 9001
        acl = [ "pattern readwrite #" ];
        port = 9001;
        settings.protocol = "websockets";
        omitPasswordAuth = false; # 🦆 says ⮞ safety first!
        users.mqtt.passwordFile = config.house.zigbee.mosquitto.passwordFIle.path;
        settings.allow_anonymous = false; # 🦆 says ⮞ never forget, never forgive right?
        #settings.require_certificate = false; # 🦆 says ⮞ T to the L to the S spells wat? DUCK! 
      } 
    ];
  };  

The extra websocket listener is used for communicating with your browser when you are on the dashboard.

⮞ qwack simple huh? - letz move on



Zigbee2MQTT Configuration

Next service in line to get configured is Z2MQTT - and this is a bit more advanced,
I will start by showing the entire code block that I use for Zigbee2MQTT, but if this get's overwhelming for you, that's fine.
Don't worry though. We'll go through each line so you can decide for yourself how you wish to configure it.

⮞ View Zigbee2MQTT code block

{
  config,
  lib,
  pkgs,
  ...
} : let
  # 🦆 says ⮞ define Zigbee devices here yo 
  zigbeeDevices = config.house.zigbee.devices;
  
  # 🦆 says ⮞ Filter devices by rooms
  byRoom = lib.foldlAttrs (acc: id: dev:
    lib.recursiveUpdate acc {
      ${dev.room} = (acc.${dev.room} or []) ++ [ id ];
    }) {} zigbeeDevices;

  # 🦆 says ⮞ dis creates group configuration for Z2M yo
  groupConfig = lib.mapAttrs' (room: ids: {
    name = room;
    value = {
      friendly_name = room;
      devices = map (id: 
        let dev = zigbeeDevices.${id};
        in "${id}/${toString dev.endpoint}"
      ) ids;
    };
  }) byRoom;

  # 🦆 says ⮞ dis creates device configuration for Z2M yo
  deviceConfig = lib.mapAttrs (id: dev: {
    friendly_name = dev.friendly_name;
  }) zigbeeDevices;

in { # 🦆 says ⮞ Z2MQTT configurations
  services.zigbee2mqtt = { # 🦆 says ⮞ dis is server configuration and only needed on one host
    enable = true;
    dataDir = "/var/lib/zigbee";
    settings = {
        experimental.output = "json";
        homeassistant = false; # 🦆 says ⮞ no thnx....
        mqtt = {
          server = "mqtt://localhost:1883";
          user = "mqtt";
          password =  config.house.zigbee.mosquitto.passwordFIle.path; # 🦆 says ⮞ no support for passwordFile?! sneaky duckiie use dis as placeholder lol
          base_topic = "zigbee2mqtt";
        };
        # 🦆 says ⮞ physical port mapping
        serial = { # 🦆 says ⮞ either USB port (/dev/ttyUSB0), network Zigbee adapters (tcp://192.168.1.1:6638) or mDNS adapter (mdns://my-adapter).       
         port = "/dev/" + config.house.zigbee.coordinator.symlink; # 🦆 says ⮞ use symlinked serial port!!!1 
         disable_led = true; # 🦆 says ⮞ save quack on electricity bill yo  
        };
        frontend = { # 🦆 says ⮞ u don't needz diz
          enabled = false;
          host = "0.0.0.0";   
          port = 8099; 
        };
        advanced = { # 🦆 says ⮞ dis is advanced? ='( duck tearz of sadness
          export_state = true;
          export_state_path = "${zigduckDir}/zigbee_devices.json";
          homeassistant_legacy_entity_attributes = false; # 🦆 says ⮞ wat the duck?! told u i don't like ha but i do like to laugh ha ha ha
          legacy_api = false;
          legacy_availability_payload = false;
          log_syslog = { # 🦆 says ⮞ log settings
            app_name = "Zigbee2MQTT";
            eol = "/n";
            host = "localhost";
            localhost = "localhost";
            path = "/dev/log";
            pid = "process.pid"; # 🦆 says ⮞ process id
            port = 123;
            protocol = "tcp4";# 🦆 says ⮞ TCP4pcplife
            type = "5424";
          };
          transmit_power = 9; # 🦆 says ⮞ to avoid brain damage, set low power
          channel = 15; # 🦆 says ⮞ channel 15 optimized for minimal interference from other 2.4Ghz devices, provides good stability  
          last_seen = "ISO_8601_local";
          # 🦆 says ⮞ zigbee encryption key.. quack? - better not expose it yo - letz handle dat down below
            # network_key = [ "..." ]
            pan_id = 60410;
          };
          device_options = { legacy = false; };
          availability = false;
          permit_join = false; # 🦆 says ⮞ allow new devices, not suggested for thin wallets - this can be false and you can still pair - don't see point?
          devices = deviceConfig; # 🦆 says ⮞ inject defined Zigbee D!
          groups = groupConfig // { # 🦆 says ⮞ inject defined Zigbee G, yo!
            all_lights = { # 🦆 says ⮞ + create a group containing all light devices
              friendly_name = "all";
              devices = lib.concatMap (id: 
                let dev = zigbeeDevices.${id};
                in if dev.type == "light" then ["${id}/${toString dev.endpoint}"] else []
              ) (lib.attrNames zigbeeDevices);
            };
          };
        };
      }; 

  # 🦆 says ⮞ let's do some ducktastic decryption magic into yaml files before we boot services up duck duck yo
  systemd.services.zigbee2mqtt = {
    wantedBy = [ "multi-user.target" ];
    after = [ "sops-nix.service" "network.target" ];
    environment.ZIGBEE2MQTT_DATA = config.services.zigbee2mqtt.dataDir;
    preStart = '' 
      mkdir -p ${config.services.zigbee2mqtt.dataDir}    
      # 🦆 says ⮞ our real mosquitto password quack quack
      mosquitto_password=$(cat ${config.house.zigbee.mosquitto.passwordFile}) 
      # 🦆 says ⮞ Injecting password into config...
      sed -i "s|/run/secrets/mosquitto|$mosquitto_password|" ${config.services.zigbee2mqtt.dataDir}/configuration.yaml  
      # 🦆 says ⮞ da real zigbee network key boom boom quack quack yo yo
      TMPFILE="${config.services.zigbee2mqtt.dataDir}/tmp.yaml"
      CFGFILE="${config.services.zigbee2mqtt.dataDir}/configuration.yaml"
      # 🦆 says ⮞ starting awk decryption magic..."
      ${pkgs.gawk}/bin/awk -v keyfile="${config.house.zigbee.networkKeyFile}" '
        /(^|[[:space:]])network_key:/ { found = 1 }

        { lines[NR] = $0 }

        END {
          if (found) {
            for (i = 1; i <= NR; i++) print lines[i]
          } else {
            print lines[1]
            print "  network_key:"
            while ((getline line < keyfile) > 0) {
              print "    " line
            }
            close(keyfile)
            for (i = 2; i <= NR; i++) print lines[i]
          }
        }
      ' "$CFGFILE" > "$TMPFILE"      
      mv "$TMPFILE" "$CFGFILE"
    ''; # 🦆 says ⮞ thnx fo quackin' along!
  };} # ... 🛌🦆💤

So what is actually going on here?
`zigbeeDevices` pulls the user defined Zigbee device map from your configuration (Part 1 in this series)
`byRoom` Groups devices by room.
`groupConfig` builds properly structured group configuration for Z2MQTT. Each group gets:
- friendly_name = room name
- devices = a list of "/" strings

`deviceConfig` Is a simplified device configuration only giving a friendly_name to the device.
In the service definition we use `deviceConfig` & `groupConfig` to create a fully declarative Zigbee2MQTT configuration, automatically generating:
- Device definitions
- Room-based groups
- A special all_lights group containing all devices where type == "light"
Everything under settings is written to configuration.yaml in Zigbee2MQTT’s data directory.

Systemd preStart script
The prestart script does the follow things:
Creates the Zigbee2MQTT data directory if it does not exist.
Reads the MQTT password from the decrypted secret.
Replaces placeholder /run/secrets/mosquitto in configuration.yaml with the real password.
Injects the Zigbee network key into the YAML if it’s missing using the awk script.

says ⮞ duck hopez diz explain how it all workz now!
no fancy pantzy stuffz or complex qwack rly - it just workz!!

I hope this was quick and painless.

This was probably a quite boring read without any real action.
But....

says ⮞ be sure to stick around fwend!
in da next quackyhacky episode duck starts the real enjoyable parts i swear!
🥁 🥁 duck iz showin' how u writez da nix defined automationz!! seee yo on da nexxt page yo QuackHack-McBlindy out - peace

Part 1. The module, the options and defining devices
Part 2. Configure your Mosquitto/Z2MQTT ⮜🦆here u are
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