gopenhab

module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2024 License: Apache-2.0

README

Go Reference Build Coverage Reliability Rating Security Rating Maintainability Rating

gopenHAB

Write your openHAB rules in Go. The power of openHAB rules with the simplicity of Go.

I had some existing code written in Go that I needed to connect to openHAB. My first thought was to make a REST API to make this external system accessible from openHAB, but after fiddling with openHAB DSL rules and trying Jython scripts, I realized the best thing was to actually connect my system to the openHAB event bus and replicate a rule system, all in Go.

In theory, everything you can do in a DSL rule or in Jython should be available.

This is work in progress, but here's an example of what you can already do with it:

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://localhost:8080",
	})

	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(1*time.Minute, openhab.OnConnect()),
	)

	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(10*time.Second, openhab.OnDisconnect()),
	)

	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.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()
}


Unit test your rules

To be able to run some unit tests I created a mock openHAB server, which can trigger events and can keep items in memory. This is work in progress but you can use it to test your rules.

How to test a simple event

Imagine you have a function calculateZoneTemperature that takes an array of values coming from the Context and sends an average temperature to an output item. The context of the function will be as such:


openhab.RuleData{
	Name: "Calculate average",
	Context: zoneContext{
		name:   "test-zone",
		config: ZoneConfiguration{Output: "ZoneTemperature", Sensors: []string{"temperature1", "temperature2"}},
	},
},

Imagine the calculateAverage function simply sends the result to the item in output configuration, from the example the name of the item is ZoneTemperature.

Here's how you can test it with the openHAB mock server:

func TestCalculateZoneTemperature(t *testing.T) {
	const temperatureItem1 = "temperature1"
	const temperatureItem2 = "temperature2"
	const averageTemperatureItem = "ZoneTemperature"

	// 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 all 3 items in the mock server
	server.SetItem(api.Item{
		Name:  averageTemperatureItem,
		Type:  "Number",
		State: "0.0",
	})
	server.SetItem(api.Item{
		Name:  temperatureItem1,
		Type:  "Number",
		State: "10.0",
	})
	server.SetItem(api.Item{
		Name:  temperatureItem2,
		Type:  "Number",
		State: "10.0",
	})

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

	// standard rule to calculate the average
	client.AddRule(
		openhab.RuleData{
			Name: "Calculate average",
			Context: zoneContext{
				name:   "test-zone",
				config: ZoneConfiguration{Output: averageTemperatureItem, Sensors: []string{temperatureItem1, temperatureItem2}},
			},
		},
		calculateZoneTemperature, // this is the code to test
		openhab.OnItemReceivedState(temperatureItem1, nil),
		openhab.OnItemReceivedState(temperatureItem2, nil),
	)

	// testing rule to verify the calculation
	wg := sync.WaitGroup{}
	wg.Add(1)
	client.AddRule(
		openhab.RuleData{
			Name: "Test rule",
		},
		func(client openhab.RuleClient, ruleData openhab.RuleData, e event.Event) {
			// test is finished after receiving this event
			defer wg.Done()

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

	// start the client in the background so we can send events to it
	go func() {
		client.Start()
	}()

	// make sure the client is ready (it surely needs less than that)
	time.Sleep(10 * time.Millisecond)
	// we simulate openhab receiving an event: temperature2 item received a new value of 11 degrees, brrrr!
	server.Event(event.NewItemReceivedState(temperatureItem2, "Number", "11.0"))

	wg.Wait()
	client.Stop()
}

TODO

  • Handle all state types. Handled for now are String, Switch, Number, DateTime.
  • Add triggers for more events. All item events have triggers, and some thing events (but not all)
  • Ability to update rules
  • Handle more events on the openhab test server (typically, things are not supported yet)

Limitations of the mock openHAB server for testing

  • When the server receives a command event, it doesn't follow with any corresponding state event. You need to publish the state events manually if you need them in your tests.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL