├── .drone.yml ├── .gitignore ├── LICENSE ├── README.md ├── channels.go ├── configuration └── configuration.go ├── coordinator ├── coordinator.go └── types.go ├── db └── db.go ├── example └── example.go ├── functions ├── functions.go ├── functions_cluster.go ├── functions_cluster_global.go ├── functions_cluster_local.go ├── functions_cluster_local_level_control.go ├── functions_cluster_local_onoff.go └── functions_generic.go ├── go.mod ├── logger └── logger.go ├── model ├── cluster.go ├── device.go ├── device_incoming_message.go └── endpoint.go └── steward.go /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | test: 3 | image: golang 4 | commands: 5 | - go install 6 | - go test ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | go.sum 2 | .idea/ 3 | db.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yevhen Zadyra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZigBee-Steward 2 | 3 | [![Build Status](https://cloud.drone.io/api/badges/dyrkin/zigbee-steward/status.svg??branch=master)](https://cloud.drone.io/dyrkin/zigbee-steward) 4 | 5 | ## Overview 6 | 7 | Bla bla bla 8 | 9 | ## Example 10 | 11 | In this example, we are going to send **toggle** command to IKEA TRÅDFRI bulb using Xiaomi Aqara Wireless Remote Switch. 12 | To prepare, follow the steps: 13 | 14 | 1. Connect ZigBee stick cc2531 to USB; 15 | 2. Flash it using instruction: https://www.zigbee2mqtt.io/getting_started/flashing_the_cc2531.html; 16 | 3. Run the example; 17 | 4. Pair IKEA TRÅDFRI bulb by repeating 6 ONs and 5 OFFs one by one (ON -> OFF, ON -> OFF, ON -> OFF, ON -> OFF, ON -> OFF, ON); 18 | 5. Pair Xiaomi Aqara Wireless Remote Switch by holding the button for ~10 seconds until blue LEDs start blinking. 19 | 20 | Now you can toggle the bulb using the remote switch. Just click on it. 21 | 22 | 23 | ```go 24 | import ( 25 | "fmt" 26 | "github.com/davecgh/go-spew/spew" 27 | "github.com/dyrkin/zcl-go/cluster" 28 | "github.com/dyrkin/zigbee-steward" 29 | "github.com/dyrkin/zigbee-steward/configuration" 30 | "github.com/dyrkin/zigbee-steward/model" 31 | "sync" 32 | ) 33 | 34 | //simple device database 35 | var devices = map[string]*model.Device{} 36 | 37 | func main() { 38 | 39 | conf := configuration.Default() 40 | conf.PermitJoin = true 41 | 42 | stewie := steward.New(conf) 43 | 44 | eventListener := func() { 45 | for { 46 | select { 47 | case device := <-stewie.Channels().OnDeviceRegistered(): 48 | saveDevice(device) 49 | case device := <-stewie.Channels().OnDeviceUnregistered(): 50 | deleteDevice(device) 51 | case device := <-stewie.Channels().OnDeviceBecameAvailable(): 52 | saveDevice(device) 53 | case deviceIncomingMessage := <-stewie.Channels().OnDeviceIncomingMessage(): 54 | fmt.Printf("Device received incoming message:\n%s", spew.Sdump(deviceIncomingMessage)) 55 | toggleIkeaBulb(stewie, deviceIncomingMessage) 56 | } 57 | } 58 | } 59 | 60 | go eventListener() 61 | stewie.Start() 62 | infiniteWait() 63 | } 64 | 65 | func toggleIkeaBulb(stewie *steward.Steward, message *model.DeviceIncomingMessage) { 66 | if isXiaomiButtonSingleClick(message) { 67 | if ikeaBulb, registered := devices["TRADFRI bulb E27 W opal 1000lm"]; registered { 68 | toggleTarget(stewie, ikeaBulb.NetworkAddress) 69 | } else { 70 | fmt.Println("IKEA bulb is not available") 71 | } 72 | } 73 | } 74 | 75 | func toggleTarget(stewie *steward.Steward, networkAddress string) { 76 | go func() { 77 | stewie.Functions().Cluster().Local().OnOff().Toggle(networkAddress, 0xFF) 78 | }() 79 | } 80 | 81 | func isXiaomiButtonSingleClick(message *model.DeviceIncomingMessage) bool { 82 | command, ok := message.IncomingMessage.Data.Command.(*cluster.ReportAttributesCommand) 83 | 84 | return ok && message.Device.Manufacturer == "LUMI" && 85 | message.Device.Model == "lumi.remote.b186acn01\x00\x00\x00" && 86 | isSingleClick(command) 87 | } 88 | 89 | func isSingleClick(command *cluster.ReportAttributesCommand) bool { 90 | click, ok := command.AttributeReports[0].Attribute.Value.(uint64) 91 | return ok && click == uint64(1) 92 | } 93 | 94 | func saveDevice(device *model.Device) { 95 | fmt.Printf("Registering device:\n%s", spew.Sdump(device)) 96 | devices[device.Model] = device 97 | } 98 | 99 | func deleteDevice(device *model.Device) { 100 | fmt.Printf("Unregistering device:\n%s", spew.Sdump(device)) 101 | delete(devices, device.Model) 102 | } 103 | 104 | func infiniteWait() { 105 | wg := &sync.WaitGroup{} 106 | wg.Add(1) 107 | wg.Wait() 108 | } 109 | ``` 110 | 111 | Full [examples](example/example.go) -------------------------------------------------------------------------------- /channels.go: -------------------------------------------------------------------------------- 1 | package steward 2 | 3 | import "github.com/dyrkin/zigbee-steward/model" 4 | 5 | type Channels struct { 6 | onDeviceRegistered chan *model.Device 7 | onDeviceUnregistered chan *model.Device 8 | onDeviceBecameAvailable chan *model.Device 9 | onDeviceIncomingMessage chan *model.DeviceIncomingMessage 10 | } 11 | 12 | func (c *Channels) OnDeviceRegistered() chan *model.Device { 13 | return c.onDeviceRegistered 14 | } 15 | 16 | func (c *Channels) OnDeviceBecameAvailable() chan *model.Device { 17 | return c.onDeviceBecameAvailable 18 | } 19 | 20 | func (c *Channels) OnDeviceUnregistered() chan *model.Device { 21 | return c.onDeviceUnregistered 22 | } 23 | 24 | func (c *Channels) OnDeviceIncomingMessage() chan *model.DeviceIncomingMessage { 25 | return c.onDeviceIncomingMessage 26 | } 27 | -------------------------------------------------------------------------------- /configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | type Serial struct { 4 | PortName string 5 | BaudRate int 6 | } 7 | 8 | type Configuration struct { 9 | PermitJoin bool 10 | IEEEAddress string 11 | PanId uint16 12 | NetworkKey [16]uint8 13 | Channels []uint8 14 | Led bool 15 | Serial *Serial 16 | } 17 | 18 | func Default() *Configuration { 19 | return &Configuration{ 20 | PermitJoin: false, 21 | IEEEAddress: "0x7a2d6265656e6574", 22 | PanId: 0x1234, 23 | NetworkKey: [16]uint8{4, 3, 2, 1, 9, 8, 7, 6, 255, 254, 253, 252, 50, 49, 48, 47}, 24 | Channels: []uint8{11, 12}, 25 | Led: false, 26 | Serial: &Serial{ 27 | PortName: "/dev/tty.usbmodem14101", 28 | BaudRate: 115200, 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /coordinator/coordinator.go: -------------------------------------------------------------------------------- 1 | package coordinator 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dyrkin/zcl-go/frame" 6 | "github.com/tv42/topic" 7 | "reflect" 8 | "sync" 9 | "time" 10 | 11 | "github.com/davecgh/go-spew/spew" 12 | "github.com/dyrkin/unp-go" 13 | "github.com/dyrkin/zigbee-steward/configuration" 14 | "github.com/dyrkin/zigbee-steward/logger" 15 | "github.com/dyrkin/znp-go" 16 | "go.bug.st/serial.v1" 17 | ) 18 | 19 | var log = logger.MustGetLogger("coordinator") 20 | 21 | var nextTransactionId = frame.MakeDefaultTransactionIdProvider() 22 | 23 | const defaultTimeout = 10 * time.Second 24 | 25 | type Network struct { 26 | Address string 27 | } 28 | 29 | type MessageChannels struct { 30 | onError chan error 31 | onDeviceAnnounce chan *znp.ZdoEndDeviceAnnceInd 32 | onDeviceLeave chan *znp.ZdoLeaveInd 33 | onDeviceTc chan *znp.ZdoTcDevInd 34 | onIncomingMessage chan *znp.AfIncomingMessage 35 | } 36 | 37 | type Coordinator struct { 38 | config *configuration.Configuration 39 | started bool 40 | networkProcessor *znp.Znp 41 | messageChannels *MessageChannels 42 | network *Network 43 | broadcast *topic.Topic 44 | } 45 | 46 | func (c *Coordinator) OnIncomingMessage() chan *znp.AfIncomingMessage { 47 | return c.messageChannels.onIncomingMessage 48 | } 49 | 50 | func (c *Coordinator) OnDeviceTc() chan *znp.ZdoTcDevInd { 51 | return c.messageChannels.onDeviceTc 52 | } 53 | 54 | func (c *Coordinator) OnDeviceLeave() chan *znp.ZdoLeaveInd { 55 | return c.messageChannels.onDeviceLeave 56 | } 57 | 58 | func (c *Coordinator) OnDeviceAnnounce() chan *znp.ZdoEndDeviceAnnceInd { 59 | return c.messageChannels.onDeviceAnnounce 60 | } 61 | 62 | func (c *Coordinator) OnError() chan error { 63 | return c.messageChannels.onError 64 | } 65 | 66 | func (c *Coordinator) Network() *Network { 67 | return c.network 68 | } 69 | 70 | func New(config *configuration.Configuration) *Coordinator { 71 | messageChannels := &MessageChannels{ 72 | onError: make(chan error, 100), 73 | onDeviceAnnounce: make(chan *znp.ZdoEndDeviceAnnceInd, 100), 74 | onDeviceLeave: make(chan *znp.ZdoLeaveInd, 100), 75 | onDeviceTc: make(chan *znp.ZdoTcDevInd, 100), 76 | onIncomingMessage: make(chan *znp.AfIncomingMessage, 100), 77 | } 78 | return &Coordinator{ 79 | config: config, 80 | messageChannels: messageChannels, 81 | network: &Network{}, 82 | broadcast: topic.New(), 83 | } 84 | } 85 | 86 | func (c *Coordinator) Start() error { 87 | log.Info("Starting coordinator...") 88 | port, err := openPort(c.config) 89 | if err != nil { 90 | return err 91 | } 92 | networkProtocol := unp.New(1, port) 93 | c.networkProcessor = znp.New(networkProtocol) 94 | c.mapMessageChannels() 95 | c.networkProcessor.Start() 96 | configure(c) 97 | subscribe(c) 98 | startup(c) 99 | enrichNetworkDetails(c) 100 | switchLed(c) 101 | registerEndpoints(c) 102 | permitJoin(c) 103 | c.started = true 104 | log.Info("Coordinator started") 105 | return nil 106 | } 107 | 108 | func (c *Coordinator) Reset() { 109 | reset := func() error { 110 | return c.networkProcessor.SysResetReq(1) 111 | } 112 | 113 | _, err := c.syncCallRetryable(reset, SysResetIndType, 15*time.Second, 5) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | } 118 | 119 | func (c *Coordinator) ActiveEndpoints(nwkAddress string) (*znp.ZdoActiveEpRsp, error) { 120 | np := c.networkProcessor 121 | activeEpReq := func() error { 122 | status, err := np.ZdoActiveEpReq(nwkAddress, nwkAddress) 123 | if err == nil && status.Status != znp.StatusSuccess { 124 | return fmt.Errorf("unable to request active endpoints. Status: [%s]", status.Status) 125 | } 126 | return err 127 | } 128 | 129 | response, err := c.syncCallRetryable(activeEpReq, ZdoActiveEpRspType, defaultTimeout, 3) 130 | if err == nil { 131 | return response.(*znp.ZdoActiveEpRsp), nil 132 | } 133 | return nil, err 134 | } 135 | 136 | func (c *Coordinator) NodeDescription(nwkAddress string) (*znp.ZdoNodeDescRsp, error) { 137 | np := c.networkProcessor 138 | activeEpReq := func() error { 139 | status, err := np.ZdoNodeDescReq(nwkAddress, nwkAddress) 140 | if err == nil && status.Status != znp.StatusSuccess { 141 | return fmt.Errorf("unable to request node description. Status: [%s]", status.Status) 142 | } 143 | return err 144 | } 145 | 146 | response, err := c.syncCallRetryable(activeEpReq, ZdoNodeDescRspType, defaultTimeout, 3) 147 | if err == nil { 148 | return response.(*znp.ZdoNodeDescRsp), nil 149 | } 150 | return nil, err 151 | } 152 | 153 | func (c *Coordinator) SimpleDescription(nwkAddress string, endpoint uint8) (*znp.ZdoSimpleDescRsp, error) { 154 | np := c.networkProcessor 155 | activeEpReq := func() error { 156 | status, err := np.ZdoSimpleDescReq(nwkAddress, nwkAddress, endpoint) 157 | if err == nil && status.Status != znp.StatusSuccess { 158 | return fmt.Errorf("unable to request simple description. Status: [%s]", status.Status) 159 | } 160 | return err 161 | } 162 | 163 | response, err := c.syncCallRetryable(activeEpReq, ZdoSimpleDescRspType, defaultTimeout, 3) 164 | if err == nil { 165 | return response.(*znp.ZdoSimpleDescRsp), nil 166 | } 167 | return nil, err 168 | } 169 | 170 | func (c *Coordinator) Bind(dstAddr string, srcAddress string, srcEndpoint uint8, clusterId uint16, 171 | dstAddrMode znp.AddrMode, dstAddress string, dstEndpoint uint8) (*znp.ZdoBindRsp, error) { 172 | np := c.networkProcessor 173 | bindReqReq := func() error { 174 | status, err := np.ZdoBindReq(dstAddr, srcAddress, srcEndpoint, clusterId, dstAddrMode, dstAddress, dstEndpoint) 175 | if err == nil && status.Status != znp.StatusSuccess { 176 | return fmt.Errorf("unable to bind. Status: [%s]", status.Status) 177 | } 178 | return err 179 | } 180 | 181 | response, err := c.syncCallRetryable(bindReqReq, ZdoBindRspType, defaultTimeout, 3) 182 | if err == nil { 183 | return response.(*znp.ZdoBindRsp), nil 184 | } 185 | return nil, err 186 | } 187 | 188 | func (c *Coordinator) Unbind(dstAddr string, srcAddress string, srcEndpoint uint8, clusterId uint16, 189 | dstAddrMode znp.AddrMode, dstAddress string, dstEndpoint uint8) (*znp.ZdoUnbindRsp, error) { 190 | np := c.networkProcessor 191 | bindReqReq := func() error { 192 | status, err := np.ZdoUnbindReq(dstAddr, srcAddress, srcEndpoint, clusterId, dstAddrMode, dstAddress, dstEndpoint) 193 | if err == nil && status.Status != znp.StatusSuccess { 194 | return fmt.Errorf("unable to unbind. Status: [%s]", status.Status) 195 | } 196 | return err 197 | } 198 | 199 | response, err := c.syncCallRetryable(bindReqReq, ZdoUnbindRspType, defaultTimeout, 3) 200 | if err == nil { 201 | return response.(*znp.ZdoUnbindRsp), nil 202 | } 203 | return nil, err 204 | } 205 | 206 | func (c *Coordinator) DataRequest(dstAddr string, dstEndpoint uint8, srcEndpoint uint8, clusterId uint16, options *znp.AfDataRequestOptions, radius uint8, data []uint8) (*znp.AfIncomingMessage, error) { 207 | np := c.networkProcessor 208 | dataRequest := func(networkAddress string, transactionId uint8) error { 209 | status, err := np.AfDataRequest(networkAddress, dstEndpoint, srcEndpoint, clusterId, transactionId, options, radius, data) 210 | if err == nil && status.Status != znp.StatusSuccess { 211 | return fmt.Errorf("unable to unbind. Status: [%s]", status.Status) 212 | } 213 | return err 214 | } 215 | 216 | return c.syncDataRequestRetryable(dataRequest, dstAddr, nextTransactionId(), defaultTimeout, 3) 217 | } 218 | 219 | func (c *Coordinator) syncCall(call func() error, expectedType reflect.Type, timeout time.Duration) (interface{}, error) { 220 | receiver := make(chan interface{}) 221 | responseChannel := make(chan interface{}, 1) 222 | errorChannel := make(chan error, 1) 223 | deadline := time.NewTimer(timeout) 224 | var wg sync.WaitGroup 225 | wg.Add(1) 226 | go func() { 227 | c.broadcast.Register(receiver) 228 | wg.Done() 229 | for { 230 | select { 231 | case response := <-receiver: 232 | if reflect.TypeOf(response) == expectedType { 233 | deadline.Stop() 234 | responseChannel <- response 235 | return 236 | } 237 | case _ = <-deadline.C: 238 | if !deadline.Stop() { 239 | errorChannel <- fmt.Errorf("timeout. didn't receive response of type: %s", expectedType) 240 | } 241 | return 242 | } 243 | } 244 | }() 245 | wg.Wait() 246 | err := call() 247 | if err != nil { 248 | deadline.Stop() 249 | c.broadcast.Unregister(receiver) 250 | return nil, err 251 | } 252 | select { 253 | case err = <-errorChannel: 254 | c.broadcast.Unregister(receiver) 255 | return nil, err 256 | case response := <-responseChannel: 257 | c.broadcast.Unregister(receiver) 258 | return response, nil 259 | } 260 | } 261 | 262 | func (c *Coordinator) syncCallRetryable(call func() error, expectedType reflect.Type, timeout time.Duration, retries int) (interface{}, error) { 263 | response, err := c.syncCall(call, expectedType, timeout) 264 | switch { 265 | case err != nil && retries > 0: 266 | log.Errorf("%s. Retries: %d", err, retries) 267 | return c.syncCallRetryable(call, expectedType, timeout, retries-1) 268 | case err != nil && retries == 0: 269 | log.Errorf("failure: %s", err) 270 | return nil, err 271 | } 272 | return response, nil 273 | } 274 | 275 | func (c *Coordinator) syncDataRequestRetryable(request func(string, uint8) error, nwkAddress string, transactionId uint8, timeout time.Duration, retries int) (*znp.AfIncomingMessage, error) { 276 | incomingMessage, err := c.syncDataRequest(request, nwkAddress, transactionId, timeout) 277 | switch { 278 | case err != nil && retries > 0: 279 | log.Errorf("%s. Retries: %d", err, retries) 280 | return c.syncDataRequestRetryable(request, nwkAddress, transactionId, timeout, retries-1) 281 | case err != nil && retries == 0: 282 | log.Errorf("failure: %s", err) 283 | return nil, err 284 | } 285 | return incomingMessage, nil 286 | } 287 | 288 | func (c *Coordinator) syncDataRequest(request func(string, uint8) error, nwkAddress string, transactionId uint8, timeout time.Duration) (*znp.AfIncomingMessage, error) { 289 | messageReceiver := make(chan interface{}) 290 | 291 | responseChannel := make(chan *znp.AfIncomingMessage, 1) 292 | errorChannel := make(chan error, 1) 293 | 294 | incomingMessageListener := func() { 295 | deadline := time.NewTimer(timeout) 296 | for { 297 | select { 298 | case response := <-messageReceiver: 299 | if incomingMessage, ok := response.(*znp.AfIncomingMessage); ok { 300 | frm := frame.Decode(incomingMessage.Data) 301 | if (frm.TransactionSequenceNumber == transactionId) && 302 | (incomingMessage.SrcAddr == nwkAddress) { 303 | deadline.Stop() 304 | responseChannel <- incomingMessage 305 | return 306 | } 307 | } 308 | case _ = <-deadline.C: 309 | if !deadline.Stop() { 310 | errorChannel <- fmt.Errorf("timeout. didn't receive response for transcation: %d", transactionId) 311 | } 312 | return 313 | } 314 | } 315 | } 316 | 317 | var wg sync.WaitGroup 318 | wg.Add(1) 319 | 320 | confirmListener := func() { 321 | deadline := time.NewTimer(timeout) 322 | c.broadcast.Register(messageReceiver) 323 | wg.Done() 324 | for { 325 | select { 326 | case response := <-messageReceiver: 327 | if dataConfirm, ok := response.(*znp.AfDataConfirm); ok { 328 | if dataConfirm.TransID == transactionId { 329 | deadline.Stop() 330 | switch dataConfirm.Status { 331 | case znp.StatusSuccess: 332 | go incomingMessageListener() 333 | default: 334 | errorChannel <- fmt.Errorf("invalid transcation status: [%s]", dataConfirm.Status) 335 | } 336 | return 337 | } 338 | } 339 | case _ = <-deadline.C: 340 | if !deadline.Stop() { 341 | errorChannel <- fmt.Errorf("timeout. didn't receive confiramtion for transcation: %d", transactionId) 342 | } 343 | return 344 | } 345 | } 346 | } 347 | go confirmListener() 348 | wg.Wait() 349 | err := request(nwkAddress, transactionId) 350 | 351 | if err != nil { 352 | c.broadcast.Unregister(messageReceiver) 353 | return nil, fmt.Errorf("unable to send data request: %s", err) 354 | } 355 | 356 | select { 357 | case err = <-errorChannel: 358 | c.broadcast.Unregister(messageReceiver) 359 | return nil, err 360 | case zclIncomingMessage := <-responseChannel: 361 | c.broadcast.Unregister(messageReceiver) 362 | return zclIncomingMessage, nil 363 | } 364 | } 365 | 366 | func (c *Coordinator) mapMessageChannels() { 367 | go func() { 368 | for { 369 | select { 370 | case err := <-c.networkProcessor.Errors(): 371 | c.messageChannels.onError <- err 372 | case incoming := <-c.networkProcessor.AsyncInbound(): 373 | debugIncoming := func(format string) { 374 | log.Debugf(format, func() string { return spew.Sdump(incoming) }) 375 | } 376 | c.broadcast.Broadcast <- incoming 377 | switch message := incoming.(type) { 378 | case *znp.ZdoEndDeviceAnnceInd: 379 | debugIncoming("Device announce:\n%s") 380 | c.messageChannels.onDeviceAnnounce <- message 381 | case *znp.ZdoLeaveInd: 382 | debugIncoming("Device leave:\n%s") 383 | c.messageChannels.onDeviceLeave <- message 384 | case *znp.ZdoTcDevInd: 385 | debugIncoming("Device TC:\n%s") 386 | c.messageChannels.onDeviceTc <- message 387 | case *znp.AfIncomingMessage: 388 | debugIncoming("Incoming message:\n%s") 389 | c.messageChannels.onIncomingMessage <- message 390 | } 391 | } 392 | } 393 | }() 394 | } 395 | 396 | func configure(coordinator *Coordinator) { 397 | coordinator.Reset() 398 | np := coordinator.networkProcessor 399 | 400 | mandatorySetting := func(call func() error) { 401 | err := call() 402 | if err != nil { 403 | log.Fatal(err) 404 | } 405 | } 406 | 407 | t := time.Now() 408 | np.SysSetTime(0, uint8(t.Hour()), uint8(t.Minute()), uint8(t.Second()), 409 | uint8(t.Month()), uint8(t.Day()), uint16(t.Year())) 410 | 411 | mandatorySetting(func() error { 412 | _, err := np.UtilSetPreCfgKey(coordinator.config.NetworkKey) 413 | return err 414 | }) 415 | //logical type 416 | mandatorySetting(func() error { 417 | _, err := np.SapiZbWriteConfiguration(0x87, []uint8{0}) 418 | return err 419 | }) 420 | mandatorySetting(func() error { 421 | _, err := np.UtilSetPanId(coordinator.config.PanId) 422 | return err 423 | }) 424 | //zdo direc cb 425 | mandatorySetting(func() error { 426 | _, err := np.SapiZbWriteConfiguration(0x8F, []uint8{1}) 427 | return err 428 | }) 429 | //enable security 430 | mandatorySetting(func() error { 431 | _, err := np.SapiZbWriteConfiguration(0x64, []uint8{1}) 432 | return err 433 | }) 434 | mandatorySetting(func() error { 435 | _, err := np.SysSetExtAddr(coordinator.config.IEEEAddress) 436 | return err 437 | }) 438 | 439 | channels := &znp.Channels{} 440 | for _, v := range coordinator.config.Channels { 441 | switch v { 442 | case 11: 443 | channels.Channel11 = 1 444 | case 12: 445 | channels.Channel12 = 1 446 | case 13: 447 | channels.Channel13 = 1 448 | case 14: 449 | channels.Channel14 = 1 450 | case 15: 451 | channels.Channel15 = 1 452 | case 16: 453 | channels.Channel16 = 1 454 | case 17: 455 | channels.Channel17 = 1 456 | case 18: 457 | channels.Channel18 = 1 458 | case 19: 459 | channels.Channel19 = 1 460 | case 20: 461 | channels.Channel20 = 1 462 | case 21: 463 | channels.Channel21 = 1 464 | case 22: 465 | channels.Channel22 = 1 466 | case 23: 467 | channels.Channel23 = 1 468 | case 24: 469 | channels.Channel24 = 1 470 | case 25: 471 | channels.Channel25 = 1 472 | case 26: 473 | channels.Channel26 = 1 474 | } 475 | } 476 | 477 | mandatorySetting(func() error { 478 | _, err := coordinator.networkProcessor.UtilSetChannels(channels) 479 | return err 480 | }) 481 | coordinator.Reset() 482 | } 483 | 484 | func subscribe(coordinator *Coordinator) { 485 | np := coordinator.networkProcessor 486 | _, err := np.UtilCallbackSubCmd(znp.SubsystemIdAllSubsystems, znp.ActionEnable) 487 | 488 | if err != nil { 489 | log.Fatal(err) 490 | } 491 | } 492 | 493 | func registerEndpoints(coordinator *Coordinator) { 494 | np := coordinator.networkProcessor 495 | np.AfRegister(0x01, 0x0104, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 496 | 497 | np.AfRegister(0x02, 0x0101, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 498 | 499 | np.AfRegister(0x03, 0x0105, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 500 | 501 | np.AfRegister(0x04, 0x0107, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 502 | 503 | np.AfRegister(0x05, 0x0108, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 504 | 505 | np.AfRegister(0x06, 0x0109, 0x0005, 0x1, znp.LatencyNoLatency, []uint16{}, []uint16{}) 506 | } 507 | 508 | func enrichNetworkDetails(coordinator *Coordinator) { 509 | deviceInfo, err := coordinator.networkProcessor.UtilGetDeviceInfo() 510 | if err != nil { 511 | log.Fatal(err) 512 | } 513 | coordinator.network.Address = deviceInfo.ShortAddr 514 | } 515 | 516 | func permitJoin(coordinator *Coordinator) { 517 | var timeout uint8 = 0x00 518 | if coordinator.config.PermitJoin { 519 | timeout = 0xFF 520 | } 521 | coordinator.networkProcessor.SapiZbPermitJoiningRequest(coordinator.network.Address, timeout) 522 | } 523 | 524 | func switchLed(coordinator *Coordinator) { 525 | mode := znp.ModeOFF 526 | if coordinator.config.Led { 527 | mode = znp.ModeON 528 | } 529 | log.Debugf("Led mode [%s]", mode) 530 | coordinator.networkProcessor.UtilLedControl(1, mode) 531 | } 532 | 533 | func startup(coordinator *Coordinator) { 534 | _, err := coordinator.networkProcessor.SapiZbStartRequest() 535 | if err != nil { 536 | log.Fatal(err) 537 | } 538 | } 539 | 540 | func openPort(config *configuration.Configuration) (port serial.Port, err error) { 541 | log.Debugf("Opening port [%s] at rate [%d]", config.Serial.PortName, config.Serial.BaudRate) 542 | mode := &serial.Mode{BaudRate: config.Serial.BaudRate} 543 | port, err = serial.Open(config.Serial.PortName, mode) 544 | if err == nil { 545 | log.Debugf("Port [%s] is opened", config.Serial.PortName) 546 | err = port.SetRTS(true) 547 | } 548 | return 549 | } 550 | -------------------------------------------------------------------------------- /coordinator/types.go: -------------------------------------------------------------------------------- 1 | package coordinator 2 | 3 | import ( 4 | "github.com/dyrkin/znp-go" 5 | "reflect" 6 | ) 7 | 8 | var SysResetIndType = reflect.TypeOf(&znp.SysResetInd{}) 9 | var ZdoActiveEpRspType = reflect.TypeOf(&znp.ZdoActiveEpRsp{}) 10 | var ZdoSimpleDescRspType = reflect.TypeOf(&znp.ZdoSimpleDescRsp{}) 11 | var ZdoNodeDescRspType = reflect.TypeOf(&znp.ZdoNodeDescRsp{}) 12 | var ZdoBindRspType = reflect.TypeOf(&znp.ZdoBindRsp{}) 13 | var ZdoUnbindRspType = reflect.TypeOf(&znp.ZdoUnbindRsp{}) 14 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/dyrkin/zigbee-steward/logger" 7 | "github.com/dyrkin/zigbee-steward/model" 8 | "github.com/natefinch/atomic" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | ) 14 | 15 | var log = logger.MustGetLogger("db") 16 | 17 | type Devices map[string]*model.Device 18 | 19 | type tables struct { 20 | Devices Devices 21 | } 22 | 23 | type Db struct { 24 | tables *tables 25 | location string 26 | } 27 | 28 | var database *Db 29 | var rw *sync.RWMutex 30 | 31 | func (db *Db) Tables() *tables { 32 | return db.tables 33 | } 34 | 35 | func (devices Devices) Add(device *model.Device) { 36 | update(func() { 37 | database.tables.Devices[device.IEEEAddress] = device 38 | }) 39 | } 40 | 41 | func (devices Devices) Get(ieeeAddress string) (*model.Device, bool) { 42 | rw.RLock() 43 | defer rw.RUnlock() 44 | device, ok := database.tables.Devices[ieeeAddress] 45 | return device, ok 46 | } 47 | 48 | func (devices Devices) GetByNetworkAddress(networkAddress string) (*model.Device, bool) { 49 | rw.RLock() 50 | defer rw.RUnlock() 51 | for _, d := range devices { 52 | if d.NetworkAddress == networkAddress { 53 | return d, true 54 | } 55 | } 56 | return nil, false 57 | } 58 | 59 | func (devices Devices) Remove(ieeeAddress string) { 60 | update(func() { 61 | delete(database.tables.Devices, ieeeAddress) 62 | }) 63 | } 64 | 65 | func (devices Devices) Exists(ieeeAddress string) bool { 66 | rw.RLock() 67 | defer rw.RUnlock() 68 | _, ok := database.tables.Devices[ieeeAddress] 69 | return ok 70 | } 71 | 72 | func Database() *Db { 73 | return database 74 | } 75 | 76 | func init() { 77 | rw = &sync.RWMutex{} 78 | dbPath, err := filepath.Abs("db.json") 79 | if err != nil { 80 | log.Fatalf("Can't load database. %s", err) 81 | } 82 | database = &Db{ 83 | tables: &tables{ 84 | Devices: map[string]*model.Device{}, 85 | }, 86 | location: dbPath, 87 | } 88 | if !exists() { 89 | write() 90 | } 91 | read() 92 | } 93 | 94 | func update(updateFn func()) { 95 | rw.Lock() 96 | defer rw.Unlock() 97 | updateFn() 98 | write() 99 | } 100 | 101 | func read() { 102 | data, err := ioutil.ReadFile(database.location) 103 | if err != nil { 104 | log.Fatalf("Can't read database. %s", err) 105 | } 106 | if err = json.Unmarshal(data, &(database.tables)); err != nil { 107 | log.Fatalf("Can't unmarshal database. %s", err) 108 | } 109 | } 110 | 111 | func write() { 112 | data, err := json.MarshalIndent(database.tables, "", " ") 113 | if err != nil { 114 | log.Fatalf("Can't marshal database. %s", err) 115 | } 116 | if err = atomic.WriteFile(database.location, bytes.NewBuffer(data)); err != nil { 117 | log.Fatalf("Can't write database. %s", err) 118 | } 119 | return 120 | } 121 | 122 | func exists() bool { 123 | _, err := os.Stat(database.location) 124 | return !os.IsNotExist(err) 125 | } 126 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/davecgh/go-spew/spew" 6 | "github.com/dyrkin/zcl-go/cluster" 7 | "github.com/dyrkin/zigbee-steward" 8 | "github.com/dyrkin/zigbee-steward/configuration" 9 | "github.com/dyrkin/zigbee-steward/model" 10 | "sync" 11 | ) 12 | 13 | //simple device database 14 | var devices = map[string]*model.Device{} 15 | 16 | func main() { 17 | 18 | conf := configuration.Default() 19 | conf.PermitJoin = true 20 | 21 | stewie := steward.New(conf) 22 | 23 | eventListener := func() { 24 | for { 25 | select { 26 | case device := <-stewie.Channels().OnDeviceRegistered(): 27 | saveDevice(device) 28 | case device := <-stewie.Channels().OnDeviceUnregistered(): 29 | deleteDevice(device) 30 | case device := <-stewie.Channels().OnDeviceBecameAvailable(): 31 | saveDevice(device) 32 | case deviceIncomingMessage := <-stewie.Channels().OnDeviceIncomingMessage(): 33 | fmt.Printf("Device received incoming message:\n%s", spew.Sdump(deviceIncomingMessage)) 34 | toggleIkeaBulb(stewie, deviceIncomingMessage) 35 | } 36 | } 37 | } 38 | 39 | go eventListener() 40 | stewie.Start() 41 | infiniteWait() 42 | } 43 | 44 | func toggleIkeaBulb(stewie *steward.Steward, message *model.DeviceIncomingMessage) { 45 | if isXiaomiButtonSingleClick(message) { 46 | if ikeaBulb, registered := devices["TRADFRI bulb E27 W opal 1000lm"]; registered { 47 | toggleTarget(stewie, ikeaBulb.NetworkAddress) 48 | } else { 49 | fmt.Println("IKEA bulb is not available") 50 | } 51 | } 52 | } 53 | 54 | func toggleTarget(stewie *steward.Steward, networkAddress string) { 55 | go func() { 56 | stewie.Functions().Cluster().Local().OnOff().Toggle(networkAddress, 0xFF) 57 | }() 58 | } 59 | 60 | func isXiaomiButtonSingleClick(message *model.DeviceIncomingMessage) bool { 61 | command, ok := message.IncomingMessage.Data.Command.(*cluster.ReportAttributesCommand) 62 | 63 | return ok && message.Device.Manufacturer == "LUMI" && 64 | message.Device.Model == "lumi.remote.b186acn01\x00\x00\x00" && 65 | isSingleClick(command) 66 | } 67 | 68 | func isSingleClick(command *cluster.ReportAttributesCommand) bool { 69 | click, ok := command.AttributeReports[0].Attribute.Value.(uint64) 70 | return ok && click == uint64(1) 71 | } 72 | 73 | func saveDevice(device *model.Device) { 74 | fmt.Printf("Registering device:\n%s", spew.Sdump(device)) 75 | devices[device.Model] = device 76 | } 77 | 78 | func deleteDevice(device *model.Device) { 79 | fmt.Printf("Unregistering device:\n%s", spew.Sdump(device)) 80 | delete(devices, device.Model) 81 | } 82 | 83 | func infiniteWait() { 84 | wg := &sync.WaitGroup{} 85 | wg.Add(1) 86 | wg.Wait() 87 | } 88 | 89 | //TODO Remove this 90 | func subscribeForLevelControlEvents(stewie *steward.Steward, device *model.Device) { 91 | if device.Manufacturer == "IKEA of Sweden" && device.Model == "TRADFRI wireless dimmer" { 92 | go func() { 93 | rsp, err := stewie.Functions().Generic().Bind(device.NetworkAddress, device.IEEEAddress, 1, 94 | uint16(cluster.LevelControl), stewie.Configuration().IEEEAddress, 1) 95 | fmt.Printf("Bind result: [%v] [%s]", rsp, err) 96 | }() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /functions/functions.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/dyrkin/zcl-go" 5 | "github.com/dyrkin/zigbee-steward/coordinator" 6 | "github.com/dyrkin/zigbee-steward/logger" 7 | ) 8 | 9 | var log = logger.MustGetLogger("functions") 10 | 11 | type Functions struct { 12 | generic *GenericFunctions 13 | cluster *ClusterFunctions 14 | } 15 | 16 | func New(coordinator *coordinator.Coordinator, zcl *zcl.Zcl) *Functions { 17 | return &Functions{ 18 | generic: &GenericFunctions{coordinator: coordinator}, 19 | cluster: &ClusterFunctions{ 20 | global: &GlobalClusterFunctions{ 21 | coordinator: coordinator, 22 | zcl: zcl, 23 | }, 24 | local: NewLocalClusterFunctions(coordinator, zcl), 25 | }, 26 | } 27 | } 28 | 29 | func (f *Functions) Generic() *GenericFunctions { 30 | return f.generic 31 | } 32 | 33 | func (f *Functions) Cluster() *ClusterFunctions { 34 | return f.cluster 35 | } 36 | -------------------------------------------------------------------------------- /functions/functions_cluster.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | type ClusterFunctions struct { 4 | global *GlobalClusterFunctions 5 | local *LocalClusterFunctions 6 | } 7 | 8 | func (f *ClusterFunctions) Global() *GlobalClusterFunctions { 9 | return f.global 10 | } 11 | 12 | func (f *ClusterFunctions) Local() *LocalClusterFunctions { 13 | return f.local 14 | } 15 | -------------------------------------------------------------------------------- /functions/functions_cluster_global.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/davecgh/go-spew/spew" 5 | "github.com/dyrkin/bin" 6 | "github.com/dyrkin/zcl-go" 7 | "github.com/dyrkin/zcl-go/cluster" 8 | "github.com/dyrkin/zcl-go/frame" 9 | "github.com/dyrkin/zigbee-steward/coordinator" 10 | "github.com/dyrkin/znp-go" 11 | ) 12 | 13 | type GlobalClusterFunctions struct { 14 | coordinator *coordinator.Coordinator 15 | zcl *zcl.Zcl 16 | } 17 | 18 | func (f *GlobalClusterFunctions) ReadAttributes(nwkAddress string, clusterId cluster.ClusterId, attributeIds []uint16) (*cluster.ReadAttributesResponse, error) { 19 | response, err := f.globalCommand(nwkAddress, clusterId, 0x00, &cluster.ReadAttributesCommand{attributeIds}) 20 | 21 | if err == nil { 22 | return response.(*cluster.ReadAttributesResponse), nil 23 | } 24 | return nil, err 25 | } 26 | 27 | func (f *GlobalClusterFunctions) WriteAttributes(nwkAddress string, clusterId cluster.ClusterId, writeAttributeRecords []*cluster.WriteAttributeRecord) (*cluster.WriteAttributesResponse, error) { 28 | response, err := f.globalCommand(nwkAddress, clusterId, 0x02, &cluster.WriteAttributesCommand{writeAttributeRecords}) 29 | 30 | if err == nil { 31 | return response.(*cluster.WriteAttributesResponse), nil 32 | } 33 | return nil, err 34 | } 35 | 36 | func (f *GlobalClusterFunctions) globalCommand(nwkAddress string, clusterId cluster.ClusterId, commandId uint8, command interface{}) (interface{}, error) { 37 | options := &znp.AfDataRequestOptions{} 38 | frm, err := frame.New(). 39 | DisableDefaultResponse(true). 40 | FrameType(frame.FrameTypeGlobal). 41 | Direction(frame.DirectionClientServer). 42 | CommandId(commandId). 43 | Command(command). 44 | Build() 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | response, err := f.coordinator.DataRequest(nwkAddress, 255, 1, uint16(clusterId), options, 15, bin.Encode(frm)) 51 | if err == nil { 52 | zclIncomingMessage, err := f.zcl.ToZclIncomingMessage(response) 53 | if err == nil { 54 | return zclIncomingMessage.Data.Command, nil 55 | } else { 56 | log.Errorf("Unsupported data response message:\n%s\n", func() string { return spew.Sdump(response) }) 57 | } 58 | 59 | } 60 | return nil, err 61 | } 62 | -------------------------------------------------------------------------------- /functions/functions_cluster_local.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/davecgh/go-spew/spew" 6 | "github.com/dyrkin/bin" 7 | "github.com/dyrkin/zcl-go" 8 | "github.com/dyrkin/zcl-go/cluster" 9 | "github.com/dyrkin/zcl-go/frame" 10 | "github.com/dyrkin/zigbee-steward/coordinator" 11 | "github.com/dyrkin/znp-go" 12 | ) 13 | 14 | type LocalClusterFunctions struct { 15 | onOff *OnOff 16 | levelControl *LevelControl 17 | } 18 | 19 | type LocalCluster struct { 20 | clusterId cluster.ClusterId 21 | coordinator *coordinator.Coordinator 22 | zcl *zcl.Zcl 23 | } 24 | 25 | func NewLocalClusterFunctions(coordinator *coordinator.Coordinator, zcl *zcl.Zcl) *LocalClusterFunctions { 26 | return &LocalClusterFunctions{ 27 | onOff: &OnOff{ 28 | LocalCluster: &LocalCluster{ 29 | clusterId: cluster.OnOff, 30 | coordinator: coordinator, 31 | zcl: zcl, 32 | }, 33 | }, 34 | levelControl: &LevelControl{ 35 | LocalCluster: &LocalCluster{ 36 | clusterId: cluster.LevelControl, 37 | coordinator: coordinator, 38 | zcl: zcl, 39 | }, 40 | }, 41 | } 42 | } 43 | 44 | func (f *LocalClusterFunctions) OnOff() *OnOff { 45 | return f.onOff 46 | } 47 | 48 | func (f *LocalClusterFunctions) LevelControl() *LevelControl { 49 | return f.levelControl 50 | } 51 | 52 | func (f *LocalCluster) localCommand(nwkAddress string, endpoint uint8, commandId uint8, command interface{}) error { 53 | options := &znp.AfDataRequestOptions{} 54 | frm, err := frame.New(). 55 | DisableDefaultResponse(false). 56 | FrameType(frame.FrameTypeLocal). 57 | Direction(frame.DirectionClientServer). 58 | CommandId(commandId). 59 | Command(command). 60 | Build() 61 | 62 | if err != nil { 63 | return err 64 | } 65 | 66 | response, err := f.coordinator.DataRequest(nwkAddress, endpoint, 1, uint16(f.clusterId), options, 15, bin.Encode(frm)) 67 | if err == nil { 68 | zclIncomingMessage, err := f.zcl.ToZclIncomingMessage(response) 69 | if err == nil { 70 | zclCommand := zclIncomingMessage.Data.Command.(*cluster.DefaultResponseCommand) 71 | if zclCommand.Status != cluster.ZclStatusSuccess { 72 | return fmt.Errorf("unable to run command [%d] on cluster [%d]. Status: [%d]", commandId, f.clusterId, zclCommand.Status) 73 | } 74 | return nil 75 | } else { 76 | log.Errorf("Unsupported data response message:\n%s\n", func() string { return spew.Sdump(response) }) 77 | } 78 | 79 | } 80 | return err 81 | } 82 | -------------------------------------------------------------------------------- /functions/functions_cluster_local_level_control.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/dyrkin/zcl-go/cluster" 5 | ) 6 | 7 | type LevelControl struct { 8 | *LocalCluster 9 | } 10 | 11 | func (f *LevelControl) MoveToLevel(nwkAddress string, endpoint uint8, level uint8, transitionTime uint16) error { 12 | return f.localCommand(nwkAddress, endpoint, 0x00, &cluster.MoveToLevelCommand{level, transitionTime}) 13 | } 14 | 15 | func (f *LevelControl) Move(nwkAddress string, endpoint uint8, moveMode uint8, rate uint8) error { 16 | return f.localCommand(nwkAddress, endpoint, 0x01, &cluster.MoveCommand{moveMode, rate}) 17 | } 18 | 19 | func (f *LevelControl) Step(nwkAddress string, endpoint uint8, stepMode uint8, stepSize uint8, transitionTime uint16) error { 20 | return f.localCommand(nwkAddress, endpoint, 0x02, &cluster.StepCommand{stepMode, stepSize, transitionTime}) 21 | } 22 | 23 | func (f *LevelControl) Stop(nwkAddress string, endpoint uint8) error { 24 | return f.localCommand(nwkAddress, endpoint, 0x03, &cluster.StopCommand{}) 25 | } 26 | 27 | func (f *LevelControl) MoveToLevelOnOff(nwkAddress string, endpoint uint8, level uint8, transitionTime uint16) error { 28 | return f.localCommand(nwkAddress, endpoint, 0x04, &cluster.MoveToLevelOnOffCommand{level, transitionTime}) 29 | } 30 | 31 | func (f *LevelControl) MoveOnOff(nwkAddress string, endpoint uint8, moveMode uint8, rate uint8) error { 32 | return f.localCommand(nwkAddress, endpoint, 0x05, &cluster.MoveOnOffCommand{moveMode, rate}) 33 | } 34 | 35 | func (f *LevelControl) StepOnOff(nwkAddress string, endpoint uint8, stepMode uint8, stepSize uint8, transitionTime uint16) error { 36 | return f.localCommand(nwkAddress, endpoint, 0x06, &cluster.StepOnOffCommand{stepMode, stepSize, transitionTime}) 37 | } 38 | 39 | func (f *LevelControl) StopOnOff(nwkAddress string, endpoint uint8) error { 40 | return f.localCommand(nwkAddress, endpoint, 0x07, &cluster.StopOnOffCommand{}) 41 | } 42 | -------------------------------------------------------------------------------- /functions/functions_cluster_local_onoff.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/dyrkin/zcl-go/cluster" 5 | ) 6 | 7 | type OnOff struct { 8 | *LocalCluster 9 | } 10 | 11 | func (f *OnOff) Off(nwkAddress string, endpoint uint8) error { 12 | return f.localCommand(nwkAddress, endpoint, 0x00, &cluster.OffCommand{}) 13 | } 14 | 15 | func (f *OnOff) On(nwkAddress string, endpoint uint8) error { 16 | return f.localCommand(nwkAddress, endpoint, 0x01, &cluster.OnCommand{}) 17 | } 18 | 19 | func (f *OnOff) Toggle(nwkAddress string, endpoint uint8) error { 20 | return f.localCommand(nwkAddress, endpoint, 0x02, &cluster.ToggleCommand{}) 21 | } 22 | 23 | func (f *OnOff) OffWithEffect(nwkAddress string, endpoint uint8, effectId uint8, effectVariant uint8) error { 24 | return f.localCommand(nwkAddress, endpoint, 0x40, &cluster.OffWithEffectCommand{effectId, effectVariant}) 25 | } 26 | 27 | func (f *OnOff) OnWithRecallGlobalScene(nwkAddress string, endpoint uint8, effectId uint8, effectVariant uint8) error { 28 | return f.localCommand(nwkAddress, endpoint, 0x41, &cluster.OnWithRecallGlobalSceneCommand{}) 29 | } 30 | 31 | func (f *OnOff) OnWithTimedOff(nwkAddress string, endpoint uint8, onOffControl uint8, onTime uint16, offWaitTime uint16) error { 32 | return f.localCommand(nwkAddress, endpoint, 0x42, &cluster.OnWithTimedOffCommand{onOffControl, onTime, offWaitTime}) 33 | } 34 | -------------------------------------------------------------------------------- /functions/functions_generic.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/dyrkin/zigbee-steward/coordinator" 5 | "github.com/dyrkin/znp-go" 6 | ) 7 | 8 | type GenericFunctions struct { 9 | coordinator *coordinator.Coordinator 10 | } 11 | 12 | func (f *GenericFunctions) Bind(sourceAddress string, sourceIeeeAddress string, sourceEndpoint uint8, clusterId uint16, destinationIeeeAddress string, destinationEndpoint uint8) (*znp.ZdoBindRsp, error) { 13 | return f.coordinator.Bind(sourceAddress, sourceIeeeAddress, sourceEndpoint, clusterId, znp.AddrModeAddr64Bit, destinationIeeeAddress, destinationEndpoint) 14 | } 15 | 16 | func (f *GenericFunctions) Unbind(sourceAddress string, sourceIeeeAddress string, sourceEndpoint uint8, clusterId uint16, destinationIeeeAddress string, destinationEndpoint uint8) (*znp.ZdoUnbindRsp, error) { 17 | return f.coordinator.Unbind(sourceAddress, sourceIeeeAddress, sourceEndpoint, clusterId, znp.AddrModeAddr64Bit, destinationIeeeAddress, destinationEndpoint) 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dyrkin/zigbee-steward 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 5 | github.com/dyrkin/bin v0.0.0-20190204210718-06bd23f8c0ce 6 | github.com/dyrkin/unp-go v1.0.2 7 | github.com/dyrkin/zcl-go v0.0.0-20190327145041-12e9da09dc07 8 | github.com/dyrkin/znp-go v0.0.0-20190319130731-f2cccabe8c69 9 | github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc 10 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 11 | github.com/tv42/topic v0.0.0-20130729201830-aa72cbe81b48 12 | go.bug.st/serial.v1 v0.0.0-20180827123349-5f7892a7bb45 13 | ) 14 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | "os" 6 | ) 7 | 8 | type Logger struct { 9 | *logging.Logger 10 | } 11 | 12 | func MustGetLogger(module string) *Logger { 13 | log := logging.MustGetLogger(module) 14 | format := logging.MustStringFormatter( 15 | `%{color}%{time:15:04:05.000} %{level:.5s} %{module} %{color:reset} %{message}`, 16 | ) 17 | 18 | backend := logging.NewLogBackend(os.Stdout, "", 0) 19 | backendFormatter := logging.NewBackendFormatter(backend, format) 20 | backendLeveled := logging.AddModuleLevel(backendFormatter) 21 | backendLeveled.SetLevel(logging.DEBUG, "") 22 | 23 | logging.SetBackend(backendLeveled) 24 | return &Logger{log} 25 | } 26 | 27 | func (log *Logger) Debugf(format string, args ...interface{}) { 28 | if log.IsEnabledFor(logging.DEBUG) { 29 | renderLazyArgs(args...) 30 | log.Logger.Debugf(format, args...) 31 | } 32 | } 33 | 34 | func (log *Logger) Debug(args ...interface{}) { 35 | if log.IsEnabledFor(logging.DEBUG) { 36 | renderLazyArgs(args...) 37 | log.Logger.Debug(args...) 38 | } 39 | } 40 | 41 | func (log *Logger) Infof(format string, args ...interface{}) { 42 | if log.IsEnabledFor(logging.INFO) { 43 | renderLazyArgs(args...) 44 | log.Logger.Infof(format, args...) 45 | } 46 | } 47 | 48 | func (log *Logger) Info(args ...interface{}) { 49 | if log.IsEnabledFor(logging.INFO) { 50 | renderLazyArgs(args...) 51 | log.Logger.Info(args...) 52 | } 53 | } 54 | 55 | func (log *Logger) Errorf(format string, args ...interface{}) { 56 | if log.IsEnabledFor(logging.ERROR) { 57 | renderLazyArgs(args...) 58 | log.Logger.Errorf(format, args...) 59 | } 60 | } 61 | 62 | func (log *Logger) Error(args ...interface{}) { 63 | if log.IsEnabledFor(logging.ERROR) { 64 | renderLazyArgs(args...) 65 | log.Logger.Error(args...) 66 | } 67 | } 68 | 69 | func renderLazyArgs(args ...interface{}) { 70 | for i := range args { 71 | if fn, ok := args[i].(func() string); ok { 72 | args[i] = fn() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /model/cluster.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Cluster struct { 4 | Id uint16 5 | Name string 6 | Supported bool 7 | } 8 | -------------------------------------------------------------------------------- /model/device.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/dyrkin/znp-go" 4 | 5 | type PowerSource uint8 6 | 7 | const ( 8 | Unknown PowerSource = iota 9 | MainsSinglePhase 10 | Mains2Phase 11 | Battery 12 | DCSource 13 | EmergencyMainsConstantlyPowered 14 | EmergencyMainsAndTransfer 15 | ) 16 | 17 | type Device struct { 18 | Manufacturer string 19 | ManufacturerId uint16 20 | Model string 21 | LogicalType znp.LogicalType 22 | MainPowered bool 23 | PowerSource PowerSource 24 | NetworkAddress string 25 | IEEEAddress string 26 | Endpoints []*Endpoint 27 | } 28 | 29 | func (d *Device) SupportedInClusters() []*Cluster { 30 | return d.supportedClusters(func(e *Endpoint) []*Cluster { 31 | return e.InClusterList 32 | }) 33 | } 34 | 35 | func (d *Device) SupportedOutClusters() []*Cluster { 36 | return d.supportedClusters(func(e *Endpoint) []*Cluster { 37 | return e.OutClusterList 38 | }) 39 | } 40 | 41 | func (d *Device) supportedClusters(clusterListExtractor func(e *Endpoint) []*Cluster) []*Cluster { 42 | var clusters []*Cluster 43 | for _, e := range d.Endpoints { 44 | for _, c := range clusterListExtractor(e) { 45 | if c.Supported { 46 | clusters = append(clusters, c) 47 | } 48 | } 49 | 50 | } 51 | return clusters 52 | } 53 | 54 | var powerSourceStrings = map[PowerSource]string{ 55 | Unknown: "Unknown", 56 | MainsSinglePhase: "MainsSinglePhase", 57 | Mains2Phase: "Mains2Phase", 58 | Battery: "Battery", 59 | DCSource: "DCSource", 60 | EmergencyMainsConstantlyPowered: "EmergencyMainsConstantlyPowered", 61 | EmergencyMainsAndTransfer: "EmergencyMainsAndTransfer", 62 | } 63 | 64 | func (ps PowerSource) String() string { 65 | return powerSourceStrings[ps] 66 | } 67 | -------------------------------------------------------------------------------- /model/device_incoming_message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/dyrkin/zcl-go" 4 | 5 | type DeviceIncomingMessage struct { 6 | Device *Device 7 | IncomingMessage *zcl.ZclIncomingMessage 8 | } 9 | -------------------------------------------------------------------------------- /model/endpoint.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Endpoint struct { 4 | Id uint8 5 | ProfileId uint16 6 | DeviceId uint16 7 | DeviceVersion uint8 8 | InClusterList []*Cluster 9 | OutClusterList []*Cluster 10 | } 11 | -------------------------------------------------------------------------------- /steward.go: -------------------------------------------------------------------------------- 1 | package steward 2 | 3 | import ( 4 | "github.com/davecgh/go-spew/spew" 5 | "github.com/dyrkin/zcl-go" 6 | "github.com/dyrkin/zcl-go/cluster" 7 | "github.com/dyrkin/zigbee-steward/configuration" 8 | "github.com/dyrkin/zigbee-steward/coordinator" 9 | "github.com/dyrkin/zigbee-steward/db" 10 | "github.com/dyrkin/zigbee-steward/functions" 11 | "github.com/dyrkin/zigbee-steward/logger" 12 | "github.com/dyrkin/zigbee-steward/model" 13 | "github.com/dyrkin/znp-go" 14 | ) 15 | 16 | var log = logger.MustGetLogger("steward") 17 | 18 | type Steward struct { 19 | configuration *configuration.Configuration 20 | coordinator *coordinator.Coordinator 21 | registrationQueue chan *znp.ZdoEndDeviceAnnceInd 22 | zcl *zcl.Zcl 23 | channels *Channels 24 | functions *functions.Functions 25 | } 26 | 27 | func New(configuration *configuration.Configuration) *Steward { 28 | coordinator := coordinator.New(configuration) 29 | zcl := zcl.New() 30 | steward := &Steward{ 31 | configuration: configuration, 32 | coordinator: coordinator, 33 | registrationQueue: make(chan *znp.ZdoEndDeviceAnnceInd), 34 | zcl: zcl, 35 | channels: &Channels{ 36 | onDeviceRegistered: make(chan *model.Device, 10), 37 | onDeviceBecameAvailable: make(chan *model.Device, 10), 38 | onDeviceUnregistered: make(chan *model.Device, 10), 39 | onDeviceIncomingMessage: make(chan *model.DeviceIncomingMessage, 100), 40 | }, 41 | } 42 | steward.functions = functions.New(coordinator, zcl) 43 | return steward 44 | } 45 | 46 | func (s *Steward) Start() { 47 | go s.enableRegistrationQueue() 48 | go s.enableListeners() 49 | err := s.coordinator.Start() 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | func (s *Steward) Channels() *Channels { 56 | return s.channels 57 | } 58 | 59 | func (s *Steward) Functions() *functions.Functions { 60 | return s.functions 61 | } 62 | 63 | func (s *Steward) Network() *coordinator.Network { 64 | return s.coordinator.Network() 65 | } 66 | 67 | func (s *Steward) Configuration() *configuration.Configuration { 68 | return s.configuration 69 | } 70 | 71 | func (s *Steward) enableListeners() { 72 | for { 73 | select { 74 | case err := <-s.coordinator.OnError(): 75 | log.Errorf("Received error: %s", err) 76 | case announcedDevice := <-s.coordinator.OnDeviceAnnounce(): 77 | s.registrationQueue <- announcedDevice 78 | case deviceLeave := <-s.coordinator.OnDeviceLeave(): 79 | s.unregisterDevice(deviceLeave) 80 | case _ = <-s.coordinator.OnDeviceTc(): 81 | case incomingMessage := <-s.coordinator.OnIncomingMessage(): 82 | s.processIncomingMessage(incomingMessage) 83 | } 84 | } 85 | } 86 | 87 | func (s *Steward) enableRegistrationQueue() { 88 | for announcedDevice := range s.registrationQueue { 89 | s.registerDevice(announcedDevice) 90 | } 91 | } 92 | 93 | func (s *Steward) registerDevice(announcedDevice *znp.ZdoEndDeviceAnnceInd) { 94 | ieeeAddress := announcedDevice.IEEEAddr 95 | log.Infof("Registering device [%s]", ieeeAddress) 96 | if device, ok := db.Database().Tables().Devices.Get(ieeeAddress); ok { 97 | log.Debugf("Device [%s] already exists in DB. Updating network address", ieeeAddress) 98 | device.NetworkAddress = announcedDevice.NwkAddr 99 | db.Database().Tables().Devices.Add(device) 100 | select { 101 | case s.channels.onDeviceBecameAvailable <- device: 102 | default: 103 | log.Errorf("onDeviceBecameAvailable channel has no capacity. Maybe channel has no subscribers") 104 | } 105 | return 106 | } 107 | 108 | device := &model.Device{Endpoints: []*model.Endpoint{}} 109 | device.IEEEAddress = ieeeAddress 110 | nwkAddress := announcedDevice.NwkAddr 111 | device.NetworkAddress = nwkAddress 112 | if announcedDevice.Capabilities.MainPowered > 0 { 113 | device.MainPowered = true 114 | } 115 | 116 | deviceDetails, err := s.Functions().Cluster().Global().ReadAttributes(nwkAddress, cluster.Basic, []uint16{0x0004, 0x0005, 0x0007}) 117 | if err != nil { 118 | log.Errorf("Unable to register device: %s", err) 119 | return 120 | } 121 | if manufacturer, ok := deviceDetails.ReadAttributeStatuses[0].Attribute.Value.(string); ok { 122 | device.Manufacturer = manufacturer 123 | } 124 | if modelId, ok := deviceDetails.ReadAttributeStatuses[1].Attribute.Value.(string); ok { 125 | device.Model = modelId 126 | } 127 | if powerSource, ok := deviceDetails.ReadAttributeStatuses[2].Attribute.Value.(uint64); ok { 128 | device.PowerSource = model.PowerSource(powerSource) 129 | } 130 | 131 | log.Debugf("Request node description: [%s]", ieeeAddress) 132 | nodeDescription, err := s.coordinator.NodeDescription(nwkAddress) 133 | if err != nil { 134 | log.Errorf("Unable to register device: %s", err) 135 | return 136 | } 137 | 138 | device.LogicalType = nodeDescription.LogicalType 139 | device.ManufacturerId = nodeDescription.ManufacturerCode 140 | 141 | log.Debugf("Request active endpoints: [%s]", ieeeAddress) 142 | activeEndpoints, err := s.coordinator.ActiveEndpoints(nwkAddress) 143 | if err != nil { 144 | log.Errorf("Unable to register device: %s", err) 145 | return 146 | } 147 | 148 | for _, ep := range activeEndpoints.ActiveEPList { 149 | log.Debugf("Request endpoint description: [%s], ep: [%d]", ieeeAddress, ep) 150 | simpleDescription, err := s.coordinator.SimpleDescription(nwkAddress, ep) 151 | if err != nil { 152 | log.Errorf("Unable to receive endpoint data: %d. Reason: %s", ep, err) 153 | continue 154 | } 155 | endpoint := s.createEndpoint(simpleDescription) 156 | device.Endpoints = append(device.Endpoints, endpoint) 157 | } 158 | 159 | db.Database().Tables().Devices.Add(device) 160 | select { 161 | case s.channels.onDeviceRegistered <- device: 162 | default: 163 | log.Errorf("onDeviceRegistered channel has no capacity. Maybe channel has no subscribers") 164 | } 165 | 166 | log.Infof("Registered new device [%s]. Manufacturer: [%s], Model: [%s], Logical type: [%s]", 167 | ieeeAddress, device.Manufacturer, device.Model, device.LogicalType) 168 | log.Debugf("Registered new device:\n%s", func() string { return spew.Sdump(device) }) 169 | } 170 | 171 | func (s *Steward) createEndpoint(simpleDescription *znp.ZdoSimpleDescRsp) *model.Endpoint { 172 | return &model.Endpoint{ 173 | Id: simpleDescription.Endpoint, 174 | ProfileId: simpleDescription.ProfileID, 175 | DeviceId: simpleDescription.DeviceID, 176 | DeviceVersion: simpleDescription.DeviceVersion, 177 | InClusterList: s.createClusters(simpleDescription.InClusterList), 178 | OutClusterList: s.createClusters(simpleDescription.OutClusterList), 179 | } 180 | } 181 | 182 | func (s *Steward) createClusters(clusterIds []uint16) []*model.Cluster { 183 | var clusters []*model.Cluster 184 | for _, clusterId := range clusterIds { 185 | cl := &model.Cluster{Id: clusterId} 186 | if c, ok := s.zcl.ClusterLibrary().Clusters()[cluster.ClusterId(clusterId)]; ok { 187 | cl.Supported = true 188 | cl.Name = c.Name 189 | } 190 | clusters = append(clusters, cl) 191 | } 192 | return clusters 193 | } 194 | 195 | func (s *Steward) processIncomingMessage(incomingMessage *znp.AfIncomingMessage) { 196 | zclIncomingMessage, err := s.zcl.ToZclIncomingMessage(incomingMessage) 197 | if err == nil { 198 | log.Debugf("Foundation Frame Payload\n%s\n", func() string { return spew.Sdump(zclIncomingMessage) }) 199 | if device, ok := db.Database().Tables().Devices.GetByNetworkAddress(incomingMessage.SrcAddr); ok { 200 | deviceIncomingMessage := &model.DeviceIncomingMessage{ 201 | Device: device, 202 | IncomingMessage: zclIncomingMessage, 203 | } 204 | select { 205 | case s.channels.onDeviceIncomingMessage <- deviceIncomingMessage: 206 | default: 207 | log.Errorf("onDeviceIncomingMessage channel has no capacity. Maybe channel has no subscribers") 208 | } 209 | } else { 210 | log.Errorf("Received message from unknown device [%s]", incomingMessage.SrcAddr) 211 | } 212 | } else { 213 | log.Errorf("Unsupported incoming message:\n%s\n", func() string { return spew.Sdump(incomingMessage) }) 214 | } 215 | } 216 | 217 | func (s *Steward) unregisterDevice(deviceLeave *znp.ZdoLeaveInd) { 218 | ieeeAddress := deviceLeave.ExtAddr 219 | if device, ok := db.Database().Tables().Devices.Get(ieeeAddress); ok { 220 | log.Infof("Unregistering device: [%s]", ieeeAddress) 221 | db.Database().Tables().Devices.Remove(ieeeAddress) 222 | select { 223 | case s.channels.onDeviceUnregistered <- device: 224 | default: 225 | log.Errorf("onDeviceUnregistered channel has no capacity. Maybe channel has no subscribers") 226 | } 227 | 228 | log.Infof("Unregistered device [%s]. Manufacturer: [%s], Model: [%s], Logical type: [%s]", 229 | ieeeAddress, device.Manufacturer, device.Model, device.LogicalType) 230 | } 231 | } 232 | --------------------------------------------------------------------------------