OpenZWave is a C++ library; while I can work in C++, for web servers and the like I prefer a less detailed abstraction. Fortunately for me, an Australian firm called Ninja Blocks put together an excellent Go-language wrapper available on their github repo, which was exactly the tool for the job.
Here, I document what I liked about go-openzwave and my experience getting it working with Belphanior.
The go-openzwave library
The go-openzwave library is a wrapper around the OpenZWave C++ library (version 1.4) that provides a Go language abstraction for interacting with the objects the library manages. It's released under the MIT license, so it's open to modify and use as needed (provided, of course, you keep the license intact).
Installation
Installation was a little tricky, but nothing serious. As noted in the Code Generation section of the library's README, the code assumes its OpenZWave dependency is already built before the scripts run by go get -d will function. The process I used was
- git clone https://github.com/ninjasphere/go-openzwave.git localdir
- cd localdir
- git submodule init
- git submodule update
- make deps
- mkdir -p $GOPATH/src/github.com/ninjasphere
- cd ..;mv localdir $GOPATH/src/github.com/ninjasphere/go-openzwave
- go get -d github.com/ninjasphere/go-openzwave
Usage
Once I got the library installed, I hunted around a bit for another client of the library to get a better sense of how to interface with it. I was able to find pepper-openzwave, which does something quite similar to Belphanior (in terms of managing abstractions in another system and how they map to Z-Wave devices and behaviors). It served as a valuable example for setting up use of go-openzwave.
Configuration and the main thread
The first stage of a go-openzwave client is setting up the configuration and launching the main thread. Like many Go libraries, go-openzwave operates by way of a continuously-running main thread and a family of callbacks. The main thread handles the care-and-feeding of the OpenZWave library.
Snippets of the main thread for my Z-Wave servant are below (note: all code appearing on my blog is licensed under Creative Commons Attribution 3.0 license).
import ( "flag" "github.com/ninjasphere/go-openzwave" ) func main() { var configPath = flag.String("zwaveConfigPath", "~/.zwaveConfig", "Path to directory for OpenZWave configuration files") var userPath = flag.String("zwaveUserPath", "~/.zwaveConfig", "Path to user-specific directory for OpenZwave configuration files") var devicePath = flag.String("device", "/dev/serial/by-id/usb-0658_0200-if00", "Path to OpenZWave transceiver device.") flag.Parse() config := openzwave.BuildAPI(*configPath, *userPath, "") config.SetEventsCallback(myEventHandler) config.setNotification(myNotificationHandler) config.SetDeviceFactory(myDeviceFactory) config.SetDeviceName(*devicePath) config.AddBoolOption("logging", false) go func() { os.Exit(config.Run()) }() b.servant.Run() }
Some things of note:
- The configPath and userPath are paths to configuration data for the OpenZWave library. The configuration files include descriptions of devices and XML files to fine-tune configs for the library. The main config path is part of the OpenZWave project and can be found in the submodule in go-openzwave at github.com/ninjasphere/go-openzwave/openzwave/config. In practice, I copied that directory to ~/.zwaveConfig and set both config paths to that location.
- The OpenZWave library does very chatty logging; the "logging" boolean option controls this. The chatty logging is extremely helpful for confirming that the low-level protocol is working as expected, but is definitely hard to follow in normal use. In addition to setting a default in the code, I edited config/options.xml to disable logging by modifying the <Option name="logging"/> tag.
- I specify an event handler and a device factory callback in the configs, which is the primary way go-openzwave interacts with my code.
- We "exit" main by dropping into an eternal Run loop. This is a standard pattern in Go (compare the net/http ListenAndServe function). If you find yourself needing more than one such run loop, you can spawn the additional ones in parallel as goroutines:
go func() { os.Exit(config.Run()) }()
myOtherEventHandler.Run()
... noting that a well-behaved Go program should cleanly terminate the go-openzwave main loop by calling api.Shutdown(exit int) before exiting the program (where does one get an "api" instance? Read on).
Event Callback
As things happen on the Z-Wave network, the library notifies our code by firing our callbacks. One such callback is the Event callback:
func myEventHandler(api opezwave.API, evt openzwave.Event) {
. . .
}
The Event type describes events that occur on the Z-Wave network. Its single method, GetNode() Node, lets us retrieve information on what node was impacted by the event. It doesn't really publish any further information, and to be honest, I haven't had use for it in my code; if you don't set one, the default handler just logs that an event occurred and the type of event (such as *openzwave.NodeAvailable).
Notification Callback
Possibly a bit more useful than the Event handler, the notification callback is fired when something happens on the network that the controller should be notified about.
func myNotificationHandler(api openzwave.API, notification api.Notification) {
. . .
}
The Notification type carries more information than the Event type, including the Node the notification is associated with and the notification type. Notification types are listed in NT/NT.go, but aren't in the source repository; they're auto-generated by the GenerateNT.sh script when make deps is run on go-openzwave. These include such messages as "awake nodes queried," "driver removed," and "scene event." Useful to monitor for keeping an eye on the behavior of the network, but for controlling devices, the device factory callback is what I've used.
Device Factory Callback
The Device Factory callback serves as the glue between the Z-Wave representation of devices on the network and the application-layer glue that your app uses to control those devices. It's a very clever bit of work that really shows off Go's strengths in terms of behavior-based typing. The callback's signature is
func myDeviceFactory(api openzwave.API, node openzwave.Node) openzwave.Device {
. . .
}
Notice the return value for this function is a Device type, which is an interface providing four simple callback methods: NodeAdded(), NodeChanged(), NodeRemoved(), and ValueChanged(openzwave.Value). The library calls those callbacks on whatever you return from the device factory when the relevant events occur on the device. By bundling up the API and Node in whatever structure you care to and making sure that structure has the four required methods, you can synchronize behaviors between the Z-Wave devices on your network and other parts of your application.
My device factory looks a bit like this:
func (b *Bridge) deviceFactory(api openzwave.API, node openzwave.Node) openzwave.Device { logger.Print("Creating a new device.") logger.Printf("%d : %d\n", node.GetHomeId(), node.GetId()) data := node.GetProductId() desc := node.GetProductDescription() pType := desc.ProductType manufacturer := data.ManufacturerId pId := data.ProductId logger.Printf("Node name: %s\n", node.GetNodeName()) logger.Printf("Product details:\n manufacturer: %s\n product: %s\n type: %s\n", desc.ManufacturerName, desc.ProductName, pType) logger.Printf(" Manufacturer ID: %s\n Product ID: %s\n", manufacturer, pId) if pType == "0x5044" { return MakeDimmer(node) } else { return MakeUnknownDevice(node) } }
After logging some information about the node, it checks the type of device and if it matches the known type for dimmer modules, we make a dimmer (Dimmer is a simple struct type I have for managing lamp dimmer that just retains the Node for future reference). Otherwise, we just make an "unknown" device that logs information about activity on it but otherwise does nothing. In my app, the NodeAdded() handler for dimmers sets up the necessary infrastructure to create a REST endpoint that can accept "/on" and "/off" POST requests to turn the lamp on and off.
Controlling Devices By Changing Values
Now we know how to get information on devices, but how do we control devices? The key is the Value type, which can be retrieved from a Node. if the value is modified, the OpenZWave library will send the necessary commands to reflect that value change on the network. Simple!
Here's a bit of the handler logic for my dimmer's REST endpoint:
import ( "github.com/ninjasphere/go-openzwave" "sync" ) type Dimmer struct { node openzwave.Node lock sync.Mutex } func (Dimmer *d) TurnOn() { d.lock.Lock() defer d.lock.Unlock() d.node.GetValue(38, 1, 0).SetString("99") }
There's a bit of magic in there; the arguments to GetValue are the command class ID, instance ID, and index for the value you want to modify. Command class ID 38 is 0x26, the multilevel switch command class (Z-Wave publications). To determine the instance ID and value index, I added logging to the ValueChanged handler for the Dimmer type and then toggled the light on and off a couple of times by using the Z-Wave lamp module's button directly (you can also glean some values from directly inspecting the C++ library code).
So there you have it! Your Go app can now use OpenZWave to listen to and talk to devices on your Z-Wave network.
So there you have it! Your Go app can now use OpenZWave to listen to and talk to devices on your Z-Wave network.
Next Steps
In the short run, I'm happy with what I've built, but there's room for improvement. For one thing, the go-openzwave wrapper doesn't expose any of the functionality for adding new devices to the network, which is supported by the OpenZWave control panel (see my past experiments with that). I'm considering forking the library and adding those capabilities, though in the short run I have to add new devices so infrequently that I just boot up the control panel when I need to do that.
Overall, I'm going to keep the design of this library in mind; I think it's a good pattern for bridging between two architectures in Go.
No comments:
Post a Comment