${cmdHelpers}
# set -euo pipefail
# 🦆 says ⮞ create case insensitive map of device friendly_name
declare -A device_map=( ${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "['${lib.toLower k}']='${v}'") normalizedDeviceMap)} )
available_devices=( ${toString deviceList} )
DOTFILES="$flake"
STATE_DIR="${zigduckDir}"
DEVICE="$device"
STATE="$state"
SCENE="$scene"
BRIGHTNESS="$brightness"
COLOR="$color"
TEMP="$temperature"
MQTT_BROKER="${mqttHostIp}"
PWFILE="$passwordfile"
MQTT_USER="$user"
MQTT_PASSWORD=$(cat "$PWFILE")
ROOM="$device"
touch "$STATE_DIR/voice-debug.log"
if [[ "$cheapMode" == "true" ]]; then
reset_room_timer "$ROOM"
dt_info "Restarting room timer for $ROOM"
exit 0
fi
if [[ -n "$SCENE" && -z "$DEVICE" ]]; then
scene "$SCENE"
exit 0
fi
if [[ "$pair" == "true" ]]; then
echo "🦆 Activating Zigbee2MQTT pairing mode for 120 seconds..."
mqtt_pub -t "zigbee2mqtt/bridge/request/permit_join" -m '{"value": true, "time": 120}'
dt_info "📡 Searching for new Zigbee devices... Put your device in pairing mode now!"
dt_info "⏰ Pairing mode! 120 sec... (Ctrl+C to stop early)"
cleanup() {
dt_debug "Disabling pairing mode..."
mqtt_pub -t "zigbee2mqtt/bridge/request/permit_join" -m '{"value": false}'
exit 0
}
trap cleanup INT TERM EXIT
${pkgs.mosquitto}/bin/mosquitto_sub -h "$MQTT_BROKER" -u "$MQTT_USER" -P "$MQTT_PASSWORD" \
-t "zigbee2mqtt/bridge/event" -t "zigbee2mqtt/bridge/log" | while IFS= read -r line; do
dt_debug "Received: $line"
if echo "$line" | jq -e '.type == "device_joined"' > /dev/null 2>&1; then
device_data=$(echo "$line" | jq -r '.data')
friendly_name=$(echo "$device_data" | jq -r '.friendly_name')
ieee_address=$(echo "$device_data" | jq -r '.ieee_address')
echo "✅ New device joined: $friendly_name ($ieee_address)"
fi
if echo "$line" | jq -e '.type == "device_interview"' > /dev/null; then
interview_data=$(echo "$line" | jq -r '.data')
status=$(echo "$interview_data" | jq -r '.status')
ieee_address=$(echo "$interview_data" | jq -r '.ieee_address')
if [[ "$status" == "successful" ]]; then
model=$(echo "$interview_data" | jq -r '.definition.model // "unknown"')
vendor=$(echo "$interview_data" | jq -r '.definition.vendor // "unknown"')
description=$(echo "$interview_data" | jq -r '.definition.description // "unknown"')
dt_info "🎉 Device interview successful!"
dt_info "Model: $model"
dt_info "Vendor: $vendor"
dt_info "Description: $description"
dt_info "IEEE: $ieee_address"
cat << EOF
🦆 says ⮞ To add this device to your Nix configuration, add to `house.zigbee.devices`:
''${ieee_address} = {
friendly_name = "$friendly_name";
room = "unknown"; # ⮜ 🦆 says ⮞ Set the room name
type = "unknown"; # ⮜ 🦆 says ⮞ Set device type (light, dimmer, sensor, motion, outlet, remote, pusher, blind)
endpoint = 11; # ⮜ 🦆 says ⮞ Set endpoint if needed
icon = "mdi:toggle-switch"; # ⮜ 🦆 says ⮞ Set icon for frontend
batteryType = "CR2450"; }; # ⮜ 🦆 says ⮞ Optional option if device has a battery. (AAA, CR1, CR2032, CR2450)
};
EOF
elif [[ "$status" == "failed" ]]; then
dt_warning "❌ Device interview failed for $ieee_address"
fi
fi
if echo "$line" | jq -e '.message != null' > /dev/null 2>&1; then
message=$(echo "$line" | jq -r '.message')
level=$(echo "$line" | jq -r '.level // "info"')
if [[ "$level" == "info" ]]; then
dt_info "Bridge: $message"
elif [[ "$level" == "warning" ]]; then
dt_warning "Bridge: $message"
elif [[ "$level" == "error" ]]; then
dt_error "Bridge: $message"
else
dt_debug "Bridge: $message"
fi
fi
done
cleanup
fi
if [[ "$DEVICE" == "ALL_LIGHTS" ]]; then
if [[ "$STATE" == "ON" ]]; then
scene max
if_voice_say "Jag maxade alla lampor brorsan."
elif [[ "$STATE" == "OFF" ]]; then
scene dark
if_voice_say "Nu blev det mörkt!"
else
echo "$(date) - ❌ Unknown state for all_lights: $STATE" >> "$STATE_DIR/voice-debug.log"
say_duck "❌ Unknown state for all_lights: $STATE"
exit 1
fi
exit 0
fi
control_device() {
local dev="$1"
local state="$2"
local brightness="$3"
local color_input="$4"
local hex_code=""
if [[ "$dev" == "Smoke Alarm Kitchen" ]]; then
dt_info "$dev is a sensor, exiting"
return 0
fi
if [[ -n "$color_input" ]]; then
if [[ "$color_input" =~ ^#[0-9a-fA-F]{6}$ ]]; then
hex_code="$color_input"
else
hex_code=$(color2hex "$color_input") || {
echo "$(date) - ❌ Unknown color: $color_input" >> "$STATE_DIR/voice-debug.log"
say_duck "fuck ❌ Invalid color: $color_input"
exit 1
}
fi
fi
if [[ -n "$SCENE" ]]; then
scene $SCENE
say_duck "Activated scene $SCENE"
fi
if [[ "$state" == "off" ]]; then
mqtt_pub -t "zigbee2mqtt/$dev/set" -m '{"state":"OFF"}'
say_duck "Turned off $dev"
if_voice_say "Stängde av $dev"
else
# 🦆 says ⮞ Validate brightness value
if [[ -n "$brightness" ]]; then
if ! [[ "$brightness" =~ ^[0-9]+$ ]] || [ "$brightness" -lt 1 ] || [ "$brightness" -gt 100 ]; then
echo "Unknown brightness: $brightness" >> "$STATE_DIR/voice-debug.log"
say_duck "Ogiltig ljusstyrka: $brightness%. Ange 1-100."
exit 1
fi
brightness=$((brightness * 254 / 100))
fi
local payload='{"state":"ON"'
[[ -n "$brightness" ]] && payload+=", \"brightness\":$brightness"
[[ -n "$hex_code" ]] && payload+=", \"color\":{\"hex\":\"$hex_code\"}"
payload+="}"
mqtt_pub -t "zigbee2mqtt/$dev/set" -m "$payload"
say_duck "Set $dev: $payload"
if_voice_say "Klart kompis"
fi
}
if [[ -n "$DEVICE" ]]; then
input_lower=$(echo "$DEVICE" | tr '[:upper:]' '[:lower:]')
exact_name="''${device_map["''$input_lower"]:-}"
if [[ -n "$exact_name" ]]; then
control_device "$exact_name" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
else
for dev in "''${!device_map[@]}"; do
if [[ "$dev" == *"$input_lower"* ]]; then
exact_name="''${device_map[$dev]}"
break
fi
done
if [[ -n "$exact_name" ]]; then
control_device "$exact_name" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
fi
group_topics=($(jq -r '.groups | keys[]' "$STATE_DIR/zigbee_devices.json"))
for group in "''${group_topics[@]}"; do
if [[ "$(echo "$group" | tr '[:upper:]' '[:lower:]')" == *"$input_lower"* ]]; then
control_group "$group" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
fi
done
AREA="$DEVICE"
say_duck "⚠️ Device '$DEVICE' not found, trying as area '$AREA'"
echo "$(date) - ⚠️ Device $DEVICE not found as area" >> "$STATE_DIR/voice-debug.log"
fi
fi
control_room() {
local room="$1"
if [[ -z "$room" ]]; then
echo "Usage: control_room "
return 1
fi
readarray -t devices < <(nix eval $DOTFILES#nixosConfigurations.desktop.config.house --json \
| jq -r --arg room "$room" '
.zigbee.devices
| to_entries
| map(select(.value.room == $room and .value.type == "light"))
| map(.value.friendly_name)
| .[]')
for light_id in "''${devices[@]}"; do
local hex_code=""
if [[ -n "$COLOR" ]]; then
hex_code=$(color2hex "$COLOR") || {
echo "$(date) - ❌ Unknown color: $COLOR" >> "$STATE_DIR/voice-debug.log"
say_duck "❌ Invalid color: $COLOR"
continue
}
fi
local payload='{"state":"ON"'
[[ -n "$BRIGHTNESS" ]] && payload+=", \"brightness\":$BRIGHTNESS"
[[ -n "$hex_code" ]] && payload+=", \"color\":{\"hex\":\"$hex_code\"}"
payload+="}"
if [[ "$light_id" == *"Smoke"* || "$light_id" == *"Sensor"* || "$light_id" == *"Alarm"* ]]; then
echo "Skipping invalid light device: $light_id" >> "$STATE_DIR/voice-debug.log"
continue
fi
mqtt_pub -t "zigbee2mqtt/$light_id/set" -m "$payload"
say_duck "$light_id $payload"
done
}
if [[ -n "$AREA" ]]; then
normalized_area=$(echo "$AREA" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
control_room $AREA
fi
'';
code = ''
declare -A device_map=( ${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "['${lib.toLower k}']='${v}'") normalizedDeviceMap)} )
available_devices=( ${toString deviceList} )
DOTFILES="$flake"
STATE_DIR="${zigduckDir}"
DEVICE="$device"
STATE="$state"
SCENE="$scene"
BRIGHTNESS="$brightness"
COLOR="$color"
TEMP="$temperature"
MQTT_BROKER="${mqttHostIp}"
PWFILE="$passwordfile"
MQTT_USER="$user"
MQTT_PASSWORD=$(cat "$PWFILE")
ROOM="$device"
touch "$STATE_DIR/voice-debug.log"
if [[ "$cheapMode" == "true" ]]; then
reset_room_timer "$ROOM"
dt_info "Restarting room timer for $ROOM"
exit 0
fi
if [[ -n "$SCENE" && -z "$DEVICE" ]]; then
scene "$SCENE"
exit 0
fi
if [[ "$pair" == "true" ]]; then
echo "🦆 Activating Zigbee2MQTT pairing mode for 120 seconds..."
mqtt_pub -t "zigbee2mqtt/bridge/request/permit_join" -m '{"value": true, "time": 120}'
dt_info "📡 Searching for new Zigbee devices... Put your device in pairing mode now!"
dt_info "⏰ Pairing mode! 120 sec... (Ctrl+C to stop early)"
cleanup() {
dt_debug "Disabling pairing mode..."
mqtt_pub -t "zigbee2mqtt/bridge/request/permit_join" -m '{"value": false}'
exit 0
}
trap cleanup INT TERM EXIT
${pkgs.mosquitto}/bin/mosquitto_sub -h "$MQTT_BROKER" -u "$MQTT_USER" -P "$MQTT_PASSWORD" \
-t "zigbee2mqtt/bridge/event" -t "zigbee2mqtt/bridge/log" | while IFS= read -r line; do
dt_debug "Received: $line"
if echo "$line" | jq -e '.type == "device_joined"' > /dev/null 2>&1; then
device_data=$(echo "$line" | jq -r '.data')
friendly_name=$(echo "$device_data" | jq -r '.friendly_name')
ieee_address=$(echo "$device_data" | jq -r '.ieee_address')
echo "✅ New device joined: $friendly_name ($ieee_address)"
fi
if echo "$line" | jq -e '.type == "device_interview"' > /dev/null; then
interview_data=$(echo "$line" | jq -r '.data')
status=$(echo "$interview_data" | jq -r '.status')
ieee_address=$(echo "$interview_data" | jq -r '.ieee_address')
if [[ "$status" == "successful" ]]; then
model=$(echo "$interview_data" | jq -r '.definition.model // "unknown"')
vendor=$(echo "$interview_data" | jq -r '.definition.vendor // "unknown"')
description=$(echo "$interview_data" | jq -r '.definition.description // "unknown"')
dt_info "🎉 Device interview successful!"
dt_info "Model: $model"
dt_info "Vendor: $vendor"
dt_info "Description: $description"
dt_info "IEEE: $ieee_address"
cat << EOF
🦆 says ⮞ To add this device to your Nix configuration, add to `house.zigbee.devices`:
''${ieee_address} = {
friendly_name = "$friendly_name";
room = "unknown"; # ⮜ 🦆 says ⮞ Set the room name
type = "unknown"; # ⮜ 🦆 says ⮞ Set device type (light, dimmer, sensor, motion, outlet, remote, pusher, blind)
endpoint = 11; # ⮜ 🦆 says ⮞ Set endpoint if needed
icon = "mdi:toggle-switch"; # ⮜ 🦆 says ⮞ Set icon for frontend
batteryType = "CR2450"; }; # ⮜ 🦆 says ⮞ Optional option if device has a battery. (AAA, CR1, CR2032, CR2450)
};
EOF
elif [[ "$status" == "failed" ]]; then
dt_warning "❌ Device interview failed for $ieee_address"
fi
fi
if echo "$line" | jq -e '.message != null' > /dev/null 2>&1; then
message=$(echo "$line" | jq -r '.message')
level=$(echo "$line" | jq -r '.level // "info"')
if [[ "$level" == "info" ]]; then
dt_info "Bridge: $message"
elif [[ "$level" == "warning" ]]; then
dt_warning "Bridge: $message"
elif [[ "$level" == "error" ]]; then
dt_error "Bridge: $message"
else
dt_debug "Bridge: $message"
fi
fi
done
cleanup
fi
if [[ "$DEVICE" == "ALL_LIGHTS" ]]; then
if [[ "$STATE" == "ON" ]]; then
scene max
if_voice_say "Jag maxade alla lampor brorsan."
elif [[ "$STATE" == "OFF" ]]; then
scene dark
if_voice_say "Nu blev det mörkt!"
else
echo "$(date) - ❌ Unknown state for all_lights: $STATE" >> "$STATE_DIR/voice-debug.log"
say_duck "❌ Unknown state for all_lights: $STATE"
exit 1
fi
exit 0
fi
control_device() {
local dev="$1"
local state="$2"
local brightness="$3"
local color_input="$4"
local hex_code=""
if [[ "$dev" == "Smoke Alarm Kitchen" ]]; then
dt_info "$dev is a sensor, exiting"
return 0
fi
if [[ -n "$color_input" ]]; then
if [[ "$color_input" =~ ^#[0-9a-fA-F]{6}$ ]]; then
hex_code="$color_input"
else
hex_code=$(color2hex "$color_input") || {
echo "$(date) - ❌ Unknown color: $color_input" >> "$STATE_DIR/voice-debug.log"
say_duck "fuck ❌ Invalid color: $color_input"
exit 1
}
fi
fi
if [[ -n "$SCENE" ]]; then
scene $SCENE
say_duck "Activated scene $SCENE"
fi
if [[ "$state" == "off" ]]; then
mqtt_pub -t "zigbee2mqtt/$dev/set" -m '{"state":"OFF"}'
say_duck "Turned off $dev"
if_voice_say "Stängde av $dev"
else
if [[ -n "$brightness" ]]; then
if ! [[ "$brightness" =~ ^[0-9]+$ ]] || [ "$brightness" -lt 1 ] || [ "$brightness" -gt 100 ]; then
echo "Unknown brightness: $brightness" >> "$STATE_DIR/voice-debug.log"
say_duck "Ogiltig ljusstyrka: $brightness%. Ange 1-100."
exit 1
fi
brightness=$((brightness * 254 / 100))
fi
local payload='{"state":"ON"'
[[ -n "$brightness" ]] && payload+=", \"brightness\":$brightness"
[[ -n "$hex_code" ]] && payload+=", \"color\":{\"hex\":\"$hex_code\"}"
payload+="}"
mqtt_pub -t "zigbee2mqtt/$dev/set" -m "$payload"
say_duck "Set $dev: $payload"
if_voice_say "Klart kompis"
fi
}
if [[ -n "$DEVICE" ]]; then
input_lower=$(echo "$DEVICE" | tr '[:upper:]' '[:lower:]')
exact_name="''${device_map["''$input_lower"]:-}"
if [[ -n "$exact_name" ]]; then
control_device "$exact_name" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
else
for dev in "''${!device_map[@]}"; do
if [[ "$dev" == *"$input_lower"* ]]; then
exact_name="''${device_map[$dev]}"
break
fi
done
if [[ -n "$exact_name" ]]; then
control_device "$exact_name" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
fi
group_topics=($(jq -r '.groups | keys[]' "$STATE_DIR/zigbee_devices.json"))
for group in "''${group_topics[@]}"; do
if [[ "$(echo "$group" | tr '[:upper:]' '[:lower:]')" == *"$input_lower"* ]]; then
control_group "$group" "$STATE" "$BRIGHTNESS" "$COLOR"
exit 0
fi
done
AREA="$DEVICE"
say_duck "⚠️ Device '$DEVICE' not found, trying as area '$AREA'"
echo "$(date) - ⚠️ Device $DEVICE not found as area" >> "$STATE_DIR/voice-debug.log"
fi
fi
control_room() {
local room="$1"
if [[ -z "$room" ]]; then
echo "Usage: control_room "
return 1
fi
readarray -t devices < <(nix eval $DOTFILES#nixosConfigurations.desktop.config.house --json \
| jq -r --arg room "$room" '
.zigbee.devices
| to_entries
| map(select(.value.room == $room and .value.type == "light"))
| map(.value.friendly_name)
| .[]')
for light_id in "''${devices[@]}"; do
local hex_code=""
if [[ -n "$COLOR" ]]; then
hex_code=$(color2hex "$COLOR") || {
echo "$(date) - ❌ Unknown color: $COLOR" >> "$STATE_DIR/voice-debug.log"
say_duck "❌ Invalid color: $COLOR"
continue
}
fi
local payload='{"state":"ON"'
[[ -n "$BRIGHTNESS" ]] && payload+=", \"brightness\":$BRIGHTNESS"
[[ -n "$hex_code" ]] && payload+=", \"color\":{\"hex\":\"$hex_code\"}"
payload+="}"
if [[ "$light_id" == *"Smoke"* || "$light_id" == *"Sensor"* || "$light_id" == *"Alarm"* ]]; then
echo "Skipping invalid light device: $light_id" >> "$STATE_DIR/voice-debug.log"
continue
fi
mqtt_pub -t "zigbee2mqtt/$light_id/set" -m "$payload"
say_duck "$light_id $payload"
done
}
if [[ -n "$AREA" ]]; then
normalized_area=$(echo "$AREA" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
control_room $AREA
fi
'';
Comments on this blog post