Write your rules in Go

Hi!

I’ve been using openHAB for many years and during all this time I had the chance to try different ways of writing my rules. I ended up with a hybrid system with rules in DSL, in Jython, and another legacy system that I connected directly via the REST API.

Then I thought I could actually centralise everything around the REST API. Even better, that would allow me to upgrade all my rules to openHAB 3 easily.

gopenhab was born: it’s a system where I can write all my rules in Go.

It’s been running fine for many months, and I was thinking about writing some documentation so people can use what I’ve done.

But first, before I start tidying things up, I’d like to know if people would be interested? otherwise I’ll just keep it for me, no need to spend time writing how-to and things :smile:

The idea is very much like HABapp, but in Go.

Here’s a fully working demo of the events you can subscribe to:

package main

import (
	"log"
	"time"

	"github.com/creativeprojects/gopenhab/event"
	"github.com/creativeprojects/gopenhab/openhab"
)

func main() {
	openhab.SetDebugLog(log.Default())
	client := openhab.NewClient(openhab.Config{
		URL: "http://openhab.lan:8080",
	})

	client.AddRule(
		openhab.RuleData{Name: "Started"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Printf("SYSTEM EVENT: client started")
		},
		openhab.OnStart(),
	)

	client.AddRule(
		openhab.RuleData{Name: "Stopped"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Printf("SYSTEM EVENT: client stopped")
			time.Sleep(time.Second)
		},
		openhab.OnStop(),
	)

	client.AddRule(
		openhab.RuleData{Name: "Connected to openHAB events"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Printf("SYSTEM EVENT: client connected")
		},
		openhab.Debounce(openhab.OnConnect(), 1*time.Minute),
	)

	client.AddRule(
		openhab.RuleData{Name: "Disconnected from openHAB events"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Print("SYSTEM EVENT: client disconnected")
		},
		openhab.Debounce(openhab.OnDisconnect(), 10*time.Second),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving item command"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			if ev, ok := e.(event.ItemReceivedCommand); ok {
				log.Printf("COMMAND EVENT: Back_Garden_Lighting_Switch received command %+v", ev.Command)
			}
		},
		openhab.OnItemReceivedCommand("Back_Garden_Lighting_Switch", nil),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving ON command"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Print("COMMAND EVENT: Back_Garden_Lighting_Switch switched ON")
		},
		openhab.OnItemReceivedCommand("Back_Garden_Lighting_Switch", openhab.SwitchON),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving OFF command"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Print("COMMAND EVENT: Back_Garden_Lighting_Switch switched OFF")
		},
		openhab.OnItemReceivedCommand("Back_Garden_Lighting_Switch", openhab.SwitchOFF),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving updated state"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			if ev, ok := e.(event.ItemReceivedState); ok {
				log.Printf("STATE EVENT: Back_Garden_Lighting_Switch received state %+v", ev.State)
			}
		},
		openhab.OnItemReceivedState("Back_Garden_Lighting_Switch", nil),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving ON state"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Printf("STATE EVENT: Back_Garden_Lighting_Switch state is now ON")
		},
		openhab.OnItemReceivedState("Back_Garden_Lighting_Switch", openhab.SwitchON),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving OFF state"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			log.Printf("STATE EVENT: Back_Garden_Lighting_Switch state is now OFF")
		},
		openhab.OnItemReceivedState("Back_Garden_Lighting_Switch", openhab.SwitchOFF),
	)

	client.AddRule(
		openhab.RuleData{Name: "Receiving state changed"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			if ev, ok := e.(event.ItemStateChanged); ok {
				log.Printf("STATE CHANGED EVENT: Back_Garden_Lighting_Switch changed to state %+v", ev.NewState)
			}
		},
		openhab.OnItemStateChanged("Back_Garden_Lighting_Switch"),
	)

	client.AddRule(
		openhab.RuleData{
			Name: "Test rule",
		},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			item, err := client.Items().GetItem("Back_Garden_Lighting_Switch")
			if err != nil {
				log.Print(err)
			}

			_, err = item.SendCommandWait(openhab.SwitchON, 2*time.Second)
			if err != nil {
				log.Printf("sending command: %s", err)
			}
			time.Sleep(4 * time.Second)

			_, err = item.SendCommandWait(openhab.SwitchOFF, 2*time.Second)
			if err != nil {
				log.Printf("sending command: %s", err)
			}
		},
		openhab.OnTimeCron("*/10 * * ? * *"),
	)

	client.Start()
}

and there’s a godoc if you’re interested :+1:

7 Likes

There’s another thing I forgot to mention: as much as I like openHAB, it is very difficult (sometimes impossible) to run unit-tests on your rules. And that bothered me for a long time.

For that matter, I created a mock openHAB server that you can use in your unit tests.

The mock server is only keeping items in memory, and is able to receive and publish events through the REST API. That’s all we need for our unit-tests.

I have a complete example in the project documentation, but here’s what it looks like:

func TestRule(t *testing.T) {
	// Create the openHAB mock server that will publish events from changes coming from the API calls
	server := openhabtest.NewServer(openhabtest.Config{Log: t, SendEventsFromAPI: true})
	defer server.Close()

	// setup the items in the mock server
	server.SetItem(api.Item{
		Name:  "UpstairsTemperature",
		Type:  "Number",
		State: "0.0",
	})
	server.SetItem(api.Item{
		Name: "Sensor1",
		Type:  "Number",
		State: "10.0",
	})
	server.SetItem(api.Item{
		Name: "Sensor2",
		Type:  "Number",
		State: "10.0",
	})

	// create a client that connects to our mock server
	client := openhab.NewClient(openhab.Config{URL: server.URL()})

	// describe your rules here...
	

	// testing rule to verify the calculation
	client.AddRule(
		openhab.RuleData{Name: "Test rule"},
		func(client *openhab.Client, ruleData openhab.RuleData, e event.Event) {
			// stop the client after receiving this event
			defer client.Stop()

			ev, ok := e.(event.ItemReceivedCommand)
			if !ok {
				t.Fatalf("expected event to be of type ItemReceivedCommand")
			}
			assert.Equal(t, expectedValue, ev.Command)
		},
		openhab.OnItemReceivedCommand("UpstairsTemperature", nil),
	)

	// start the client in the background so we can send events to it
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		client.Start()
		wg.Done()
	}()

	// we simulate openhab receiving and publishing an event:
	// sensor2 item received a new value of 11 degrees, brrrr!
	server.Event(event.NewItemReceivedState("Sensor2", "Number", "11.0"))

	wg.Wait()
}

Being able to test all your rules is a must :+1: