019-golights.txt (5665B)
1 Source: [1]src.adamsgaard.dk/golights. 2 3 golights is a small [2]Go service that handles motion sensors, 4 wall switches, and lights via [3]Zigbee2MQTT. It runs as a single 5 container alongside the broker and the Zigbee2MQTT bridge. 6 7 ## Motivation 8 9 Zigbee2MQTT supports direct device-to-group bindings, where a motion 10 sensor turns on a light group without any external service. That 11 works, but it has limits: 12 13 - Brightness and color temperature are fixed to whatever was 14 last set on the group. 15 - There is no built-in way to skip a turn-on when the room is 16 already bright. 17 - If someone changes a light by hand, the binding does not know, 18 and the next motion event overrides it. 19 20 I wanted a setup that follows daylight through the day, suppresses 21 turn-ons above an illuminance threshold, and steps out of the way 22 when a wall switch or app is used. Existing tools like Home Assistant 23 or Node-RED can do this, but at a much larger footprint than the 24 job warrants. golights is the minimal piece that sits between 25 Zigbee2MQTT and the lights. 26 27 ## What it does 28 29 The service subscribes to one Zigbee2MQTT base topic, reacts to 30 configured motion sensors and switches, and publishes commands for 31 configured groups or individual lights: 32 33 - Daylight schedule: brightness and color temperature follow 34 time-of-day bands. 35 - Illuminance cutoff: motion does not trigger a turn-on if the 36 room is already above a configured lux threshold. 37 - Service-owned off timers: lights turned on by the service are 38 turned off after a per-sensor timeout. Lights turned on by hand 39 are left alone. 40 - Manual override cancellation: a state change that does not match 41 what the service last published cancels ownership of that target. 42 - Per-target command encoding: some lights handle state, brightness, 43 and color temperature in one payload, others need them split 44 with a small delay. 45 46 ## Configuration 47 48 Settings live in a single settings.json file. The daylight schedule 49 is a list of bands keyed by local clock time: 50 51 "daylight_schedule": [ 52 { "from": "06:00", "to": "09:00", "brightness": 190, "color_temp": 330, "transition_seconds": 1 }, 53 { "from": "09:00", "to": "17:00", "brightness": 230, "color_temp": 250, "transition_seconds": 1 }, 54 { "from": "17:00", "to": "22:30", "brightness": 130, "color_temp": 420, "transition_seconds": 1 }, 55 { "from": "22:30", "to": "06:00", "brightness": 40, "color_temp": 480, "transition_seconds": 2 } 56 ] 57 58 Bands wrap across midnight when from is greater than to. A motion 59 event on a sensor with follow_daylight: true picks up the brightness, 60 color temperature, and transition from the current band. 61 62 Motion sensors are configured with their target groups, an off 63 timer, and an optional illuminance cutoff: 64 65 "motion_sensors": { 66 "fm_koekken_sensor": { 67 "targets": ["fm_house_lights"], 68 "timeout_seconds": 180, 69 "follow_daylight": true, 70 "illuminance_cutoff": 25 71 } 72 } 73 74 The cutoff is checked only when no service-owned light in the target 75 set is currently on. Once the service has turned a light on, raising 76 the room illuminance above the cutoff does not turn it off again. 77 The off timer does that. 78 79 ## Ownership and manual overrides 80 81 Each target keeps a set of owners: sensors or switches that are 82 currently responsible for keeping it on. A motion event adds the 83 sensor as an owner and resets the sensor's off timer. The off timer 84 fires per sensor, removes that sensor as an owner, and turns the 85 target off only if no other owners remain. 86 87 State messages from Zigbee2MQTT are matched against the last command 88 the service published to that target. If the brightness, color 89 temperature, or state differs, the service treats the change as a 90 manual override, drops all owners for that target, and stops its 91 off timers. The next motion event starts ownership again from 92 scratch. 93 94 This is the part that direct Zigbee2MQTT bindings cannot do, and 95 it is the reason the service exists. 96 97 ## Layout 98 99 The code is split into four small packages: 100 101 - config: loads and validates settings.json. 102 - daylight: matches a time against the schedule bands. 103 - mqtt: a thin wrapper around [4]paho.mqtt.golang with reconnect 104 handling. 105 - automation: the service itself: message dispatch, ownership, 106 timers, command encoding. 107 108 The daylight and config packages have no MQTT dependencies and are 109 unit-tested directly. The automation tests inject a fake clock and 110 a fake AfterFunc, so timer-driven behaviour can be verified 111 deterministically. 112 113 ## Deployment 114 115 The repository ships a Dockerfile and a docker-compose.yml. To run 116 it next to an existing Zigbee2MQTT setup: 117 118 git clone git://src.adamsgaard.dk/golights 119 cd golights 120 cp .env.example .env 121 cp settings.example.json settings.json 122 $EDITOR .env settings.json 123 docker compose up --build -d 124 125 To run without Docker, build and start the binary directly. Go 126 1.24 or newer is required: 127 128 go build -o golights ./cmd/golights 129 ./golights 130 131 The binary reads .env and settings.json from the current working 132 directory. Set SETTINGS_PATH to point at a settings file in another 133 location. MQTT credentials can be supplied either through .env or 134 through the process environment; existing environment variables 135 take precedence. 136 137 Before relying on the service, disable any direct Zigbee2MQTT 138 sensor-to-group bindings for the same devices. Otherwise the binding 139 races the service and turns lights on before the illuminance cutoff 140 and ownership rules apply. 141 142 Patches and bug reports by [5]email. 143 144 References: 145 146 [1] https://src.adamsgaard.dk/golights 147 [2] https://go.dev/ 148 [3] https://www.zigbee2mqtt.io/ 149 [4] https://github.com/eclipse/paho.mqtt.golang 150 [5] mailto:anders@adamsgaard.dk