cleanup, comments, tests

This commit is contained in:
Javier Peletier
2020-12-21 18:35:58 +01:00
parent e1984ddf62
commit 73bb3cae89
16 changed files with 7517 additions and 657 deletions

View File

@@ -1,151 +0,0 @@
package bimap
// Package bimap provides a threadsafe bidirectional map
// original implementation by @vishalkuo: https://github.com/vishalkuo/bimap/
import "sync"
// BiMap is a bi-directional hashmap that is thread safe and supports immutability
type BiMap struct {
s sync.RWMutex
immutable bool
forward map[interface{}]interface{}
inverse map[interface{}]interface{}
}
// New returns a an empty, mutable, biMap
func New(content map[interface{}]interface{}) *BiMap {
b := &BiMap{forward: make(map[interface{}]interface{}), inverse: make(map[interface{}]interface{}), immutable: false}
if content != nil {
for k, v := range content {
b.Insert(k, v)
}
}
return b
}
// Insert puts a key and value into the BiMap, provided its mutable. Also creates the reverse mapping from value to key.
func (b *BiMap) Insert(k interface{}, v interface{}) {
b.s.RLock()
if b.immutable {
panic("Cannot modify immutable map")
}
b.s.RUnlock()
b.s.Lock()
defer b.s.Unlock()
b.forward[k] = v
b.inverse[v] = k
}
// Exists checks whether or not a key exists in the BiMap
func (b *BiMap) Exists(k interface{}) bool {
b.s.RLock()
defer b.s.RUnlock()
_, ok := b.forward[k]
return ok
}
// ExistsInverse checks whether or not a value exists in the BiMap
func (b *BiMap) ExistsInverse(k interface{}) bool {
b.s.RLock()
defer b.s.RUnlock()
_, ok := b.inverse[k]
return ok
}
// Get returns the value for a given key in the BiMap and whether or not the element was present.
func (b *BiMap) Get(k interface{}) (interface{}, bool) {
if !b.Exists(k) {
return "", false
}
b.s.RLock()
defer b.s.RUnlock()
return b.forward[k], true
}
// GetInverse returns the key for a given value in the BiMap and whether or not the element was present.
func (b *BiMap) GetInverse(v interface{}) (interface{}, bool) {
if !b.ExistsInverse(v) {
return "", false
}
b.s.RLock()
defer b.s.RUnlock()
return b.inverse[v], true
}
// Delete removes a key-value pair from the BiMap for a given key. Returns if the key doesn't exist
func (b *BiMap) Delete(k interface{}) {
b.s.RLock()
if b.immutable {
panic("Cannot modify immutable map")
}
b.s.RUnlock()
if !b.Exists(k) {
return
}
val, _ := b.Get(k)
b.s.Lock()
defer b.s.Unlock()
delete(b.forward, k)
delete(b.inverse, val)
}
// DeleteInverse emoves a key-value pair from the BiMap for a given value. Returns if the value doesn't exist
func (b *BiMap) DeleteInverse(v interface{}) {
b.s.RLock()
if b.immutable {
panic("Cannot modify immutable map")
}
b.s.RUnlock()
if !b.ExistsInverse(v) {
return
}
key, _ := b.GetInverse(v)
b.s.Lock()
defer b.s.Unlock()
delete(b.inverse, v)
delete(b.forward, key)
}
// Size returns the number of elements in the bimap
func (b *BiMap) Size() int {
b.s.RLock()
defer b.s.RUnlock()
return len(b.forward)
}
// MakeImmutable freezes the BiMap preventing any further write actions from taking place
func (b *BiMap) MakeImmutable() {
b.s.Lock()
defer b.s.Unlock()
b.immutable = true
}
// GetInverseMap returns a regular go map mapping from the BiMap's values to its keys
func (b *BiMap) GetInverseMap() map[interface{}]interface{} {
return b.inverse
}
// GetForwardMap returns a regular go map mapping from the BiMap's keys to its values
func (b *BiMap) GetForwardMap() map[interface{}]interface{} {
return b.forward
}
// Lock manually locks the BiMap's mutex
func (b *BiMap) Lock() {
b.s.Lock()
}
// Unlock manually unlocks the BiMap's mutex
func (b *BiMap) Unlock() {
b.s.Unlock()
}

View File

@@ -1,332 +0,0 @@
package bimap
// original implementation by @vishalkuo: https://github.com/vishalkuo/bimap/
import (
"reflect"
"runtime/debug"
"testing"
"github.com/epiclabs-io/ut"
)
const key = "key"
const value = "value"
// isEmpty gets whether the specified object is considered empty or not.
func isEmpty(object interface{}) bool {
// get nil case out of the way
if object == nil {
return true
}
objValue := reflect.ValueOf(object)
switch objValue.Kind() {
// collection types are empty when they have no element
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
// pointers are empty if nil or if the value they point to is empty
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
// for all other types, compare against the zero value
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
}
}
// didPanic returns true if the function passed to it panics. Otherwise, it returns false.
func didPanic(f func()) (bool, interface{}, string) {
didPanic := false
var message interface{}
var stack string
func() {
defer func() {
if message = recover(); message != nil {
didPanic = true
stack = string(debug.Stack())
}
}()
// call the target function
f()
}()
return didPanic, message, stack
}
func TestNew(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
expected := &BiMap{forward: make(map[interface{}]interface{}), inverse: make(map[interface{}]interface{})}
t.Equals(expected, actual)
}
func TestBiMap_Insert(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
actual.Insert(key, value)
fwdExpected := make(map[interface{}]interface{})
invExpected := make(map[interface{}]interface{})
fwdExpected[key] = value
invExpected[value] = key
expected := &BiMap{forward: fwdExpected, inverse: invExpected}
t.Equals(expected, actual)
}
func TestBiMap_Exists(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
actual.Insert(key, value)
t.Assert(!actual.Exists("ARBITARY_KEY"), "Key should not exist")
t.Assert(actual.Exists(key), "Inserted key should exist")
}
func TestBiMap_InverseExists(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
actual.Insert(key, value)
t.Assert(!actual.ExistsInverse("ARBITARY_VALUE"), "Value should not exist")
t.Assert(actual.ExistsInverse(value), "Inserted value should exist")
}
func TestBiMap_Get(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
actual.Insert(key, value)
actualVal, ok := actual.Get(key)
t.Assert(ok, "It should return true")
t.Equals(value, actualVal)
actualVal, ok = actual.Get(value)
t.Assert(!ok, "It should return false")
t.Assert(isEmpty(actualVal), "Actual val should be empty")
}
func TestBiMap_GetInverse(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
actual.Insert(key, value)
actualKey, ok := actual.GetInverse(value)
t.Assert(ok, "It should return true")
t.Equals(key, actualKey)
actualKey, ok = actual.Get(value)
t.Assert(!ok, "It should return false")
t.Assert(isEmpty(actualKey), "Actual key should be empty")
}
func TestBiMap_Size(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
t.Equals(0, actual.Size())
actual.Insert(key, value)
t.Equals(1, actual.Size())
}
func TestBiMap_Delete(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "DummyKey"
dummyVal := "DummyVal"
actual.Insert(key, value)
actual.Insert(dummyKey, dummyVal)
t.Equals(2, actual.Size())
actual.Delete(dummyKey)
fwdExpected := make(map[interface{}]interface{})
invExpected := make(map[interface{}]interface{})
fwdExpected[key] = value
invExpected[value] = key
expected := &BiMap{forward: fwdExpected, inverse: invExpected}
t.Equals(1, actual.Size())
t.Equals(expected, actual)
actual.Delete(dummyKey)
t.Equals(1, actual.Size())
t.Equals(expected, actual)
}
func TestBiMap_InverseDelete(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "DummyKey"
dummyVal := "DummyVal"
actual.Insert(key, value)
actual.Insert(dummyKey, dummyVal)
t.Equals(2, actual.Size())
actual.DeleteInverse(dummyVal)
fwdExpected := make(map[interface{}]interface{})
invExpected := make(map[interface{}]interface{})
fwdExpected[key] = value
invExpected[value] = key
expected := &BiMap{forward: fwdExpected, inverse: invExpected}
t.Equals(1, actual.Size())
t.Equals(expected, actual)
actual.DeleteInverse(dummyVal)
t.Equals(1, actual.Size())
t.Equals(expected, actual)
}
func TestBiMap_WithVaryingType(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "Dummy key"
dummyVal := 3
actual.Insert(dummyKey, dummyVal)
res, _ := actual.Get(dummyKey)
resVal, _ := actual.GetInverse(dummyVal)
t.Equals(dummyVal, res)
t.Equals(dummyKey, resVal)
}
func TestBiMap_MakeImmutable(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "Dummy key"
dummyVal := 3
actual.Insert(dummyKey, dummyVal)
actual.MakeImmutable()
panicked, _, _ := didPanic(func() {
actual.Delete(dummyKey)
})
t.Assert(panicked, "It should panic on a mutation operation")
val, _ := actual.Get(dummyKey)
t.Equals(dummyVal, val)
panicked, _, _ = didPanic(func() {
actual.DeleteInverse(dummyVal)
})
t.Assert(panicked, "It should panic on a mutation operation")
key, _ := actual.GetInverse(dummyVal)
t.Equals(dummyKey, key)
size := actual.Size()
t.Equals(1, size)
panicked, _, _ = didPanic(func() {
actual.Insert("New", 1)
})
t.Assert(panicked, "It should panic on a mutation operation")
size = actual.Size()
t.Equals(1, size)
}
func TestBiMap_GetForwardMap(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "Dummy key"
dummyVal := 42
forwardMap := make(map[interface{}]interface{})
forwardMap[dummyKey] = dummyVal
actual.Insert(dummyKey, dummyVal)
actualForwardMap := actual.GetForwardMap()
eq := reflect.DeepEqual(actualForwardMap, forwardMap)
t.Assert(eq, "Forward maps should be equal")
}
func TestBiMap_GetInverseMap(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
actual := New(nil)
dummyKey := "Dummy key"
dummyVal := 42
inverseMap := make(map[interface{}]interface{})
inverseMap[dummyVal] = dummyKey
actual.Insert(dummyKey, dummyVal)
actualInverseMap := actual.GetInverseMap()
eq := reflect.DeepEqual(actualInverseMap, inverseMap)
t.Assert(eq, "Inverse maps should be equal")
}
func TestBiMap_Initialize(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
content := map[interface{}]interface{}{
"a": 1,
"b": 2,
"c": 3,
}
m := New(content)
v, present := m.Get("a")
t.Equals(1, v)
t.Equals(true, present)
k, present := m.GetInverse(3)
t.Equals("c", k)
t.Equals(true, present)
}

View File

@@ -3,43 +3,46 @@ package kn
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"koolnova2mqtt/modbus"
"koolnova2mqtt/watcher" "koolnova2mqtt/watcher"
"log" "log"
"strconv" "strconv"
) )
// MqttClient defines the expected MQTT client pub sub interface
type MqttClient interface { type MqttClient interface {
Publish(topic string, qos byte, retained bool, payload string) error Publish(topic string, qos byte, retained bool, payload string) error
Subscribe(topic string, callback func(message string)) error Subscribe(topic string, callback func(message string)) error
} }
// Config defines de Modbus<>MQTT bridge configuration
type Config struct { type Config struct {
ModuleName string ModuleName string // name of the module the modbus interface is connected to
SlaveID byte SlaveID byte // SlaveID of the module in the bus
Mqtt MqttClient TopicPrefix string // MQTT topic prefix to publish information
TopicPrefix string HassPrefix string // Home Assistant sensor discovery prefix
HassPrefix string Mqtt MqttClient // MQTT client
Modbus modbus.Modbus Modbus watcher.Modbus // Modbus client
} }
// Bridge bridges Modbus and MQTT protocols
type Bridge struct { type Bridge struct {
Config Config // embedded configuration
zw *watcher.Watcher zw *watcher.Watcher // watcher to detect register changes in zones
sysw *watcher.Watcher sysw *watcher.Watcher // watcher to detect register changes in system registers
refresh func() zones []*Zone // List of present zones in this module
zones []*Zone sys *SysDriver
} }
// getActiveZones returns the list of active zones in this module
func getActiveZones(w Watcher) ([]*Zone, error) { func getActiveZones(w Watcher) ([]*Zone, error) {
var zones []*Zone var zones []*Zone
for n := 0; n < NUM_ZONES; n++ { for n := 0; n < NUM_ZONES; n++ {
zone := NewZone(&ZoneConfig{ zone := newZone(&ZoneConfig{
ZoneNumber: n + 1, ZoneNumber: n + 1,
Watcher: w, Watcher: w,
}) })
isPresent := zone.IsPresent() isPresent := zone.isPresent()
if isPresent { if isPresent {
zones = append(zones, zone) zones = append(zones, zone)
} }
@@ -47,6 +50,7 @@ func getActiveZones(w Watcher) ([]*Zone, error) {
return zones, nil return zones, nil
} }
// NewBridge returns a new Modbus<>MQTT bridge
func NewBridge(config *Config) *Bridge { func NewBridge(config *Config) *Bridge {
b := &Bridge{ b := &Bridge{
Config: *config, Config: *config,
@@ -54,8 +58,11 @@ func NewBridge(config *Config) *Bridge {
return b return b
} }
// Start starts the bridge, publishes the configuration to Home Assistant topics and publishes
// the current state. Call Start() every time MQTT gets disconnected.
func (b *Bridge) Start() error { func (b *Bridge) Start() error {
// Define a watcher to watch the zone registers
zw := watcher.New(&watcher.Config{ zw := watcher.New(&watcher.Config{
Address: FIRST_ZONE_REGISTER, Address: FIRST_ZONE_REGISTER,
Quantity: TOTAL_ZONE_REGISTERS, Quantity: TOTAL_ZONE_REGISTERS,
@@ -63,17 +70,20 @@ func (b *Bridge) Start() error {
Modbus: b.Modbus, Modbus: b.Modbus,
}) })
// Define a watcher to watch the system registers
sysw := watcher.New(&watcher.Config{ sysw := watcher.New(&watcher.Config{
Address: FIRST_SYS_REGISTER, Address: FIRST_SYS_REGISTER,
Quantity: TOTAL_SYS_REGISTERS, Quantity: TOTAL_SYS_REGISTERS,
SlaveID: b.SlaveID, SlaveID: b.SlaveID,
Modbus: b.Modbus, Modbus: b.Modbus,
}) })
b.zw = zw b.zw = zw
b.sysw = sysw b.sysw = sysw
sys := NewSys(&SysConfig{ sys := NewSys(&SysConfig{
Watcher: b.sysw, Watcher: b.sysw,
}) })
b.sys = sys
log.Printf("Starting bridge for %s\n", b.ModuleName) log.Printf("Starting bridge for %s\n", b.ModuleName)
err := b.poll() err := b.poll()
@@ -81,49 +91,18 @@ func (b *Bridge) Start() error {
return err return err
} }
// Get Active zones
zones, err := getActiveZones(b.zw) zones, err := getActiveZones(b.zw)
b.zones = zones b.zones = zones
log.Printf("%d zones are present in %s\n", len(zones), b.ModuleName) log.Printf("%d zones are present in %s\n", len(zones), b.ModuleName)
// Downsize watched range to the required number of registers
b.zw.Resize(len(zones) * REG_PER_ZONE) b.zw.Resize(len(zones) * REG_PER_ZONE)
getHVACMode := func() string {
if !sys.GetSystemEnabled() {
return HVAC_MODE_OFF
}
switch sys.GetSystemKNMode() {
case MODE_AIR_COOLING, MODE_UNDERFLOOR_AIR_COOLING:
return HVAC_MODE_COOL
case MODE_AIR_HEATING, MODE_UNDERFLOOR_HEATING, MODE_UNDERFLOOR_AIR_HEATING:
return HVAC_MODE_HEAT
}
return "unknown"
}
getHoldMode := func() string {
switch sys.GetSystemKNMode() {
case MODE_AIR_COOLING, MODE_AIR_HEATING:
return HOLD_MODE_FAN_ONLY
case MODE_UNDERFLOOR_HEATING:
return HOLD_MODE_UNDERFLOOR_ONLY
case MODE_UNDERFLOOR_AIR_COOLING, MODE_UNDERFLOOR_AIR_HEATING:
return HOLD_MODE_UNDERFLOOR_AND_FAN
}
return "unknown"
}
publishHvacMode := func() {
for _, zone := range zones {
if zone.IsOn() {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
mode := getHVACMode()
b.Mqtt.Publish(hvacModeTopic, 0, true, mode)
}
}
}
holdModeTopic := b.getSysTopic("holdMode") holdModeTopic := b.getSysTopic("holdMode")
holdModeSetTopic := holdModeTopic + "/set" holdModeSetTopic := holdModeTopic + "/set"
// configure publishing when modbus registers change
for _, zone := range zones { for _, zone := range zones {
zone := zone zone := zone
currentTempTopic := b.getZoneTopic(zone.ZoneNumber, "currentTemp") currentTempTopic := b.getZoneTopic(zone.ZoneNumber, "currentTemp")
@@ -134,36 +113,45 @@ func (b *Bridge) Start() error {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode") hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
hvacModeSetTopic := hvacModeTopic + "/set" hvacModeSetTopic := hvacModeTopic + "/set"
// In HA there is three HVAC modes: "cool", "heat" and "off". Therefore,
// publish "OFF" if we detect the REG_ENABLED change is off
// Otherwise, publish "heat" or "cool" depending on REG_MODE
zone.OnEnabledChange = func() { zone.OnEnabledChange = func() {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode") hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
var mode string var mode string
if zone.IsOn() { if zone.isOn() {
mode = getHVACMode() mode = sys.HVACMode()
} else { } else {
mode = HVAC_MODE_OFF mode = HVAC_MODE_OFF
} }
b.Mqtt.Publish(hvacModeTopic, 0, true, mode) b.Mqtt.Publish(hvacModeTopic, 0, true, mode)
} }
// if the current temperature changes, forward value to the
// correspondig MQTT topic
zone.OnCurrentTempChange = func(currentTemp float32) { zone.OnCurrentTempChange = func(currentTemp float32) {
b.Mqtt.Publish(currentTempTopic, 0, true, fmt.Sprintf("%g", currentTemp)) b.Mqtt.Publish(currentTempTopic, 0, true, fmt.Sprintf("%g", currentTemp))
} }
// if the target temperature changes, publish it to MQTT
// this is fired when the target is set over MQTT or via a thermostat
zone.OnTargetTempChange = func(targetTemp float32) { zone.OnTargetTempChange = func(targetTemp float32) {
b.Mqtt.Publish(targetTempTopic, 0, true, fmt.Sprintf("%g", targetTemp)) b.Mqtt.Publish(targetTempTopic, 0, true, fmt.Sprintf("%g", targetTemp))
} }
// Publish changes to the fan mode
zone.OnFanModeChange = func(fanMode FanMode) { zone.OnFanModeChange = func(fanMode FanMode) {
b.Mqtt.Publish(fanModeTopic, 0, true, FanMode2Str(fanMode)) b.Mqtt.Publish(fanModeTopic, 0, true, FanMode2Str(fanMode))
} }
zone.OnKnModeChange = func(knMode KnMode) {
}
// Subscribe to target temperature set topic in MQTT
err = b.Mqtt.Subscribe(targetTempSetTopic, func(message string) { err = b.Mqtt.Subscribe(targetTempSetTopic, func(message string) {
targetTemp, err := strconv.ParseFloat(message, 32) targetTemp, err := strconv.ParseFloat(message, 32)
if err != nil { if err != nil {
log.Printf("Error parsing targetTemperature in topic %s: %s", targetTempSetTopic, err) log.Printf("Error parsing targetTemperature in topic %s: %s", targetTempSetTopic, err)
return return
} }
err = zone.SetTargetTemperature(float32(targetTemp)) err = zone.setTargetTemperature(float32(targetTemp))
if err != nil { if err != nil {
log.Printf("Cannot set target temperature to %g in zone %d", targetTemp, zone.ZoneNumber) log.Printf("Cannot set target temperature to %g in zone %d", targetTemp, zone.ZoneNumber)
} }
@@ -172,12 +160,13 @@ func (b *Bridge) Start() error {
return err return err
} }
// Subscribe to fan mode set topic in MQTT
err = b.Mqtt.Subscribe(fanModeSetTopic, func(message string) { err = b.Mqtt.Subscribe(fanModeSetTopic, func(message string) {
fm, err := Str2FanMode(message) fm, err := Str2FanMode(message)
if err != nil { if err != nil {
log.Printf("Unknown fan mode %q in message to zone %d", message, zone.ZoneNumber) log.Printf("Unknown fan mode %q in message to zone %d", message, zone.ZoneNumber)
} }
err = zone.SetFanMode(fm) err = zone.setFanMode(fm)
if err != nil { if err != nil {
log.Printf("Cannot set fan mode to %s in zone %d", message, zone.ZoneNumber) log.Printf("Cannot set fan mode to %s in zone %d", message, zone.ZoneNumber)
} }
@@ -186,21 +175,24 @@ func (b *Bridge) Start() error {
return err return err
} }
// Subscribe to HVAC Mode set topic in MQTT
err = b.Mqtt.Subscribe(hvacModeSetTopic, func(message string) { err = b.Mqtt.Subscribe(hvacModeSetTopic, func(message string) {
// If user sets the Home Assistant HVAC mode to off, turn off this zone
if message == HVAC_MODE_OFF { if message == HVAC_MODE_OFF {
err := zone.SetOn(false) err := zone.setOn(false) // turn zone off (REG_ENABLED)
if err != nil { if err != nil {
log.Printf("Cannot set zone %d to off", zone.ZoneNumber) log.Printf("Cannot set zone %d to off", zone.ZoneNumber)
} }
return return
} }
// Translate HA HVAC mode to Koolnova's
knMode := sys.GetSystemKNMode() knMode := sys.GetSystemKNMode()
knMode = ApplyHvacMode(knMode, message) knMode = ApplyHvacMode(knMode, message)
err = sys.SetSystemKNMode(knMode) err = sys.SetSystemKNMode(knMode)
if err != nil { if err != nil {
log.Printf("Cannot set knmode mode to %x in zone %d", knMode, zone.ZoneNumber) log.Printf("Cannot set knmode mode to %x in zone %d", knMode, zone.ZoneNumber)
} }
err := zone.SetOn(true) err := zone.setOn(true)
if err != nil { if err != nil {
log.Printf("Cannot set zone %d to on", zone.ZoneNumber) log.Printf("Cannot set zone %d to on", zone.ZoneNumber)
return return
@@ -210,7 +202,9 @@ func (b *Bridge) Start() error {
return err return err
} }
// Subscribe to changes in hold mode:
err = b.Mqtt.Subscribe(holdModeSetTopic, func(message string) { err = b.Mqtt.Subscribe(holdModeSetTopic, func(message string) {
// Translate HA's hold mode to Koolnova's
knMode := sys.GetSystemKNMode() knMode := sys.GetSystemKNMode()
knMode = ApplyHoldMode(knMode, message) knMode = ApplyHoldMode(knMode, message)
err := sys.SetSystemKNMode(knMode) err := sys.SetSystemKNMode(knMode)
@@ -222,8 +216,9 @@ func (b *Bridge) Start() error {
return err return err
} }
// Define a Home Assistant thermostat
name := fmt.Sprintf("%s_zone%d", b.ModuleName, zone.ZoneNumber) name := fmt.Sprintf("%s_zone%d", b.ModuleName, zone.ZoneNumber)
config := map[string]interface{}{ b.publishComponent(HA_COMPONENT_CLIMATE, fmt.Sprintf("zone%d", zone.ZoneNumber), map[string]interface{}{
"name": name, "name": name,
"current_temperature_topic": currentTempTopic, "current_temperature_topic": currentTempTopic,
"precision": 0.1, "precision": 0.1,
@@ -243,62 +238,74 @@ func (b *Bridge) Start() error {
"hold_modes": []string{HOLD_MODE_UNDERFLOOR_ONLY, HOLD_MODE_FAN_ONLY, HOLD_MODE_UNDERFLOOR_AND_FAN}, "hold_modes": []string{HOLD_MODE_UNDERFLOOR_ONLY, HOLD_MODE_FAN_ONLY, HOLD_MODE_UNDERFLOOR_AND_FAN},
"hold_state_topic": holdModeTopic, "hold_state_topic": holdModeTopic,
"hold_command_topic": holdModeSetTopic, "hold_command_topic": holdModeSetTopic,
} })
configJSON, _ := json.Marshal(config) // Define a current temperature sensor:
// <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
b.Mqtt.Publish(fmt.Sprintf("%s/climate/%s/zone%d/config", b.HassPrefix, b.ModuleName, zone.ZoneNumber), 0, true, string(configJSON))
// temperature sensor configuration:
name = fmt.Sprintf("%s_zone%d_temp", b.ModuleName, zone.ZoneNumber) name = fmt.Sprintf("%s_zone%d_temp", b.ModuleName, zone.ZoneNumber)
config = map[string]interface{}{ b.publishComponent(HA_COMPONENT_SENSOR, fmt.Sprintf("zone%d_temp", zone.ZoneNumber), map[string]interface{}{
"name": name, "name": name,
"device_class": "temperature", "device_class": "temperature",
"state_topic": currentTempTopic, "state_topic": currentTempTopic,
"unit_of_measurement": "ºC", "unit_of_measurement": "ºC",
"unique_id": name, "unique_id": name,
} })
configJSON, _ = json.Marshal(config) // Define a target temperature sensor:
b.Mqtt.Publish(fmt.Sprintf("%s/sensor/%s/zone%d_temp/config", b.HassPrefix, b.ModuleName, zone.ZoneNumber), 0, true, string(configJSON)) name = fmt.Sprintf("%s_zone%d_target_temp", b.ModuleName, zone.ZoneNumber)
b.publishComponent(HA_COMPONENT_SENSOR, fmt.Sprintf("zone%d_target_temp", zone.ZoneNumber), map[string]interface{}{
"name": name,
"device_class": "temperature",
"state_topic": targetTempTopic,
"unit_of_measurement": "ºC",
"unique_id": name,
})
} }
// Publish changes to system registers:
sys.OnACAirflowChange = func(ac ACMachine) { sys.OnACAirflowChange = func(ac ACMachine) {
airflow := sys.GetAirflow(ac) airflow := sys.GetAirflow(ac)
b.Mqtt.Publish(b.getACTopic(ac, "airflow"), 0, true, strconv.Itoa(airflow)) b.Mqtt.Publish(b.getACTopic(ac, "airflow"), 0, true, strconv.Itoa(airflow))
} }
sys.OnACTargetTempChange = func(ac ACMachine) { sys.OnACTargetTempChange = func(ac ACMachine) {
targetTemp := sys.GetMachineTargetTemp(ac) targetTemp := sys.GetMachineTargetTemp(ac)
b.Mqtt.Publish(b.getACTopic(ac, "targetTemp"), 0, true, fmt.Sprintf("%g", targetTemp)) b.Mqtt.Publish(b.getACTopic(ac, "targetTemp"), 0, true, fmt.Sprintf("%g", targetTemp))
} }
sys.OnACTargetFanModeChange = func(ac ACMachine) { sys.OnACTargetFanModeChange = func(ac ACMachine) {
targetAirflow := sys.GetTargetFanMode(ac) targetAirflow := sys.GetTargetFanMode(ac)
b.Mqtt.Publish(b.getACTopic(ac, "fanMode"), 0, true, FanMode2Str(targetAirflow)) b.Mqtt.Publish(b.getACTopic(ac, "fanMode"), 0, true, FanMode2Str(targetAirflow))
} }
sys.OnEfficiencyChange = func() { sys.OnEfficiencyChange = func() {
efficiency := sys.GetEfficiency() efficiency := sys.GetEfficiency()
b.Mqtt.Publish(b.getSysTopic("efficiency"), 0, true, strconv.Itoa(efficiency)) b.Mqtt.Publish(b.getSysTopic("efficiency"), 0, true, strconv.Itoa(efficiency))
} }
sys.OnSystemEnabledChange = func() { sys.OnSystemEnabledChange = func() {
enabled := sys.GetSystemEnabled() enabled := sys.GetSystemEnabled()
b.Mqtt.Publish(b.getSysTopic("enabled"), 0, true, fmt.Sprintf("%t", enabled)) b.Mqtt.Publish(b.getSysTopic("enabled"), 0, true, fmt.Sprintf("%t", enabled))
publishHvacMode() b.publishHvacMode()
}
sys.OnKnModeChange = func() {
publishHvacMode()
b.Mqtt.Publish(holdModeTopic, 0, true, getHoldMode())
} }
sys.OnKnModeChange = func() {
b.publishHvacMode()
b.Mqtt.Publish(holdModeTopic, 0, true, sys.HoldMode())
}
// Trigger a callback on all registers so the MQTT broker is updated on connect:
b.zw.TriggerCallbacks() b.zw.TriggerCallbacks()
b.sysw.TriggerCallbacks() b.sysw.TriggerCallbacks()
// Publish one-off static information
b.Mqtt.Publish(b.getSysTopic("serialBaud"), 0, true, strconv.Itoa(sys.GetBaudRate())) b.Mqtt.Publish(b.getSysTopic("serialBaud"), 0, true, strconv.Itoa(sys.GetBaudRate()))
b.Mqtt.Publish(b.getSysTopic("serialParity"), 0, true, sys.GetParity()) b.Mqtt.Publish(b.getSysTopic("serialParity"), 0, true, sys.GetParity())
b.Mqtt.Publish(b.getSysTopic("slaveId"), 0, true, strconv.Itoa(sys.GetSlaveID())) b.Mqtt.Publish(b.getSysTopic("slaveId"), 0, true, strconv.Itoa(sys.GetSlaveID()))
return nil return nil
} }
// poll polls modbus for changes
func (b *Bridge) poll() error { func (b *Bridge) poll() error {
err := b.zw.Poll() err := b.zw.Poll()
if err != nil { if err != nil {
@@ -314,13 +321,15 @@ func (b *Bridge) poll() error {
return nil return nil
} }
// Tick must be invoked periodically to refesh registers from modbus
// it also samples temperature and calculates a moving average of read temperatures
func (b *Bridge) Tick() error { func (b *Bridge) Tick() error {
err := b.poll() err := b.poll()
if err != nil { if err != nil {
return err return err
} }
for _, z := range b.zones { for _, z := range b.zones {
z.SampleTemperature() z.sampleTemperature()
} }
return nil return nil
} }
@@ -336,3 +345,19 @@ func (b *Bridge) getSysTopic(subtopic string) string {
func (b *Bridge) getACTopic(ac ACMachine, subtopic string) string { func (b *Bridge) getACTopic(ac ACMachine, subtopic string) string {
return b.getSysTopic(fmt.Sprintf("ac%d/%s", ac, subtopic)) return b.getSysTopic(fmt.Sprintf("ac%d/%s", ac, subtopic))
} }
func (b *Bridge) publishHvacMode() {
for _, zone := range b.zones {
if zone.isOn() {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
mode := b.sys.HVACMode()
b.Mqtt.Publish(hvacModeTopic, 0, true, mode)
}
}
}
// publishComponent publishes a Home Assistant component configuration for autodiscovery
func (b *Bridge) publishComponent(component, ObjectID string, config map[string]interface{}) {
configJSON, _ := json.Marshal(config)
b.Mqtt.Publish(fmt.Sprintf("%s/%s/%s/%s/config", b.HassPrefix, component, b.ModuleName, ObjectID), 0, true, string(configJSON))
}

193
kn/bridge_test.go Normal file
View File

@@ -0,0 +1,193 @@
package kn_test
import (
"encoding/json"
"fmt"
"koolnova2mqtt/kn"
"koolnova2mqtt/modbus"
"sort"
"strconv"
"strings"
"testing"
"github.com/epiclabs-io/ut"
)
type Message struct {
Topic string
Payload interface{}
}
type MqttClientMock struct {
subscriptions map[string]func(message string)
messages []Message
}
func NewMqttClientMock() *MqttClientMock {
return &MqttClientMock{
subscriptions: make(map[string]func(string)),
}
}
func (m *MqttClientMock) Publish(topic string, qos byte, retained bool, payload string) error {
var jsonObject map[string]interface{}
var p interface{}
err := json.Unmarshal([]byte(payload), &jsonObject)
if err == nil {
p = jsonObject
} else {
p = payload
}
m.messages = append(m.messages, Message{
Topic: topic,
Payload: p,
})
return nil
}
func (m *MqttClientMock) simulateMessage(topic string, payload string) {
callback := m.subscriptions[topic]
if callback != nil {
callback(payload)
}
}
func (m *MqttClientMock) LastMessage() *Message {
if len(m.messages) == 0 {
return nil
}
return &m.messages[len(m.messages)-1]
}
func (m *MqttClientMock) Clear() {
m.messages = nil
}
func (m *MqttClientMock) Subscribe(topic string, callback func(message string)) error {
m.subscriptions[topic] = callback
return nil
}
func getKeys(funcMap map[string]func(string)) []string {
keys := make([]string, 0, len(funcMap))
for k := range funcMap {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return strings.Compare(keys[i], keys[j]) < 0
})
return keys
}
type DiffValue struct {
Address uint16
Old uint16
New uint16
}
type TestMessage struct {
ID int
Topic string
Payload string
Diffs []DiffValue
}
func diffState(old, new []uint16) []DiffValue {
if len(old) != len(new) {
panic("not the same length")
}
var diffs []DiffValue
for n := 0; n < len(old); n++ {
if old[n] != new[n] {
diffs = append(diffs, DiffValue{
Old: old[n],
New: new[n],
Address: uint16(n + 1),
})
}
}
return diffs
}
func TestBridge(tx *testing.T) {
t := ut.BeginTest(tx, false)
defer t.FinishTest()
var err error
mqttClient := NewMqttClientMock()
modbusClient := modbus.NewMock()
b := kn.NewBridge(&kn.Config{
ModuleName: "TestModule",
SlaveID: 49,
TopicPrefix: "topicPrefix",
HassPrefix: "hassPrefix",
Mqtt: mqttClient,
Modbus: modbusClient,
})
// Check the correct subscriptions and messages are sent on connect:
err = b.Start()
t.Ok(err)
t.EqualsFile("subscriptions.json", getKeys(mqttClient.subscriptions))
sort.Slice(mqttClient.messages, func(i, j int) bool {
return strings.Compare(mqttClient.messages[i].Topic, mqttClient.messages[j].Topic) < 0
})
t.EqualsFile("connect-messages.json", mqttClient.messages)
mqttClient.Clear()
// Check modbus state is altered when various control messages are received over MQTT
var messages []TestMessage
simulateMessage := func(topic, payload string) {
state := append([]uint16(nil), modbusClient.State[49]...)
mqttClient.simulateMessage(topic, payload)
messages = append(messages, TestMessage{
ID: len(messages) + 1,
Topic: topic,
Payload: payload,
Diffs: diffState(state, modbusClient.State[49]),
})
}
simulateMessage("topicPrefix/TestModule/sys/holdMode/set", kn.HOLD_MODE_FAN_ONLY)
simulateMessage("topicPrefix/TestModule/sys/holdMode/set", kn.HOLD_MODE_UNDERFLOOR_AND_FAN)
simulateMessage("topicPrefix/TestModule/sys/holdMode/set", kn.HOLD_MODE_UNDERFLOOR_ONLY)
simulateMessage("topicPrefix/TestModule/sys/holdMode/set", "bad mode")
hvacModes := []string{kn.HVAC_MODE_COOL, kn.HVAC_MODE_HEAT, kn.HVAC_MODE_OFF}
holdModes := []string{kn.HOLD_MODE_UNDERFLOOR_ONLY, kn.HOLD_MODE_FAN_ONLY, kn.HOLD_MODE_UNDERFLOOR_AND_FAN}
for z := 1; z < 11; z++ {
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/fanMode/set", z), kn.FanMode2Str(kn.FAN_HIGH))
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/fanMode/set", z), kn.FanMode2Str(kn.FAN_MED))
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/fanMode/set", z), kn.FanMode2Str(kn.FAN_LOW))
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/fanMode/set", z), kn.FanMode2Str(kn.FAN_AUTO))
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/fanMode/set", z), "bad mode")
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/targetTemp/set", z), strconv.Itoa(20+z))
for i := 0; i < len(hvacModes); i++ {
simulateMessage(fmt.Sprintf("topicPrefix/TestModule/zone%d/hvacMode/set", z), hvacModes[i])
for j := 0; j < len(holdModes); j++ {
simulateMessage("topicPrefix/TestModule/sys/holdMode/set", holdModes[j])
}
}
}
// diffs.json will contain a list of changes. Each item in the array is the result
// of each simulateMessage call above.
t.EqualsFile("diffs.json", messages)
t.EqualsFile("messages.json", mqttClient.messages)
// simulate changes in temperature by writing random values to temperature registers
mqttClient.Clear()
n := 0
for i := 0; i < 20; i++ {
for z := 1; z < 11; z++ {
t := (i + 15) * 2
n++
modbusClient.WriteRegister(49, uint16((z-1)*kn.REG_PER_ZONE+kn.REG_CURRENT_TEMP), uint16(t))
}
b.Tick()
}
t.EqualsFile("current-temp.json", mqttClient.messages)
}

View File

@@ -2,7 +2,6 @@ package kn
import ( import (
"errors" "errors"
"koolnova2mqtt/bimap"
) )
const NUM_ZONES = 16 const NUM_ZONES = 16
@@ -43,22 +42,6 @@ const MODE_UNDERFLOOR_HEATING KnMode = 0x04
const MODE_UNDERFLOOR_AIR_COOLING KnMode = 0x05 const MODE_UNDERFLOOR_AIR_COOLING KnMode = 0x05
const MODE_UNDERFLOOR_AIR_HEATING KnMode = 0x06 const MODE_UNDERFLOOR_AIR_HEATING KnMode = 0x06
var FanModes = bimap.New(map[interface{}]interface{}{
"off": FAN_OFF,
"low": FAN_LOW,
"medium": FAN_MED,
"high": FAN_HIGH,
"auto": FAN_AUTO,
})
var KnModes = bimap.New(map[interface{}]interface{}{
"air cooling": MODE_AIR_COOLING,
"air heating": MODE_AIR_HEATING,
"underfloor heating": MODE_UNDERFLOOR_HEATING,
"underfloor air cooling": MODE_UNDERFLOOR_AIR_COOLING,
"underfloor air heating": MODE_UNDERFLOOR_AIR_HEATING,
})
const HOLD_MODE_UNDERFLOOR_ONLY = "underfloor" const HOLD_MODE_UNDERFLOOR_ONLY = "underfloor"
const HOLD_MODE_FAN_ONLY = "fan" const HOLD_MODE_FAN_ONLY = "fan"
const HOLD_MODE_UNDERFLOOR_AND_FAN = "underfloor and fan" const HOLD_MODE_UNDERFLOOR_AND_FAN = "underfloor and fan"
@@ -76,28 +59,41 @@ const AC2 ACMachine = 2
const AC3 ACMachine = 3 const AC3 ACMachine = 3
const AC4 ACMachine = 4 const AC4 ACMachine = 4
const HA_COMPONENT_SENSOR = "sensor"
const HA_COMPONENT_CLIMATE = "climate"
func FanMode2Str(fm FanMode) string { func FanMode2Str(fm FanMode) string {
st, ok := FanModes.GetInverse(fm) switch fm {
if !ok { case FAN_OFF:
st = "unknown" return "off"
case FAN_LOW:
return "low"
case FAN_MED:
return "medium"
case FAN_HIGH:
return "high"
case FAN_AUTO:
return "auto"
default:
return "unknown"
} }
return st.(string)
} }
func Str2FanMode(st string) (FanMode, error) { func Str2FanMode(st string) (FanMode, error) {
fm, ok := FanModes.Get(st) switch st {
if !ok { case "off":
return FAN_OFF, nil
case "low":
return FAN_LOW, nil
case "medium":
return FAN_MED, nil
case "high":
return FAN_HIGH, nil
case "auto":
return FAN_AUTO, nil
default:
return FAN_OFF, errors.New("Unknown fan mode") return FAN_OFF, errors.New("Unknown fan mode")
} }
return fm.(FanMode), nil
}
func KnMode2Str(hm KnMode) string {
st, ok := KnModes.GetInverse(hm)
if !ok {
st = "unknown"
}
return st.(string)
} }
func ApplyHvacMode(knMode KnMode, hvacMode string) KnMode { func ApplyHvacMode(knMode KnMode, hvacMode string) KnMode {

View File

@@ -6,7 +6,10 @@ type SysConfig struct {
Watcher Watcher Watcher Watcher
} }
type Sys struct { // SysDriver watches system registers and allows
// to read/change the module configuration
// notifies callbacks when specific system registers change
type SysDriver struct {
SysConfig SysConfig
OnACAirflowChange func(ac ACMachine) OnACAirflowChange func(ac ACMachine)
OnACTargetTempChange func(ac ACMachine) OnACTargetTempChange func(ac ACMachine)
@@ -18,8 +21,8 @@ type Sys struct {
var ErrUnknownSerialConfig = errors.New("Uknown serial configuration") var ErrUnknownSerialConfig = errors.New("Uknown serial configuration")
func NewSys(config *SysConfig) *Sys { func NewSys(config *SysConfig) *SysDriver {
s := &Sys{ s := &SysDriver{
SysConfig: *config, SysConfig: *config,
} }
for n := byte(0); n < ACMachines; n++ { for n := byte(0); n < ACMachines; n++ {
@@ -63,31 +66,31 @@ func NewSys(config *SysConfig) *Sys {
return s return s
} }
func (s *Sys) ReadRegister(n int) int { func (s *SysDriver) ReadRegister(n int) int {
r := s.Watcher.ReadRegister(uint16(n)) r := s.Watcher.ReadRegister(uint16(n))
return int(r) return int(r)
} }
func (s *Sys) WriteRegister(n int, value uint16) error { func (s *SysDriver) WriteRegister(n int, value uint16) error {
return s.Watcher.WriteRegister(uint16(n), value) return s.Watcher.WriteRegister(uint16(n), value)
} }
func (s *Sys) GetAirflow(ac ACMachine) int { func (s *SysDriver) GetAirflow(ac ACMachine) int {
r := s.ReadRegister(REG_AIRFLOW + int(ac) - 1) r := s.ReadRegister(REG_AIRFLOW + int(ac) - 1)
return r return r
} }
func (s *Sys) GetMachineTargetTemp(ac ACMachine) float32 { func (s *SysDriver) GetMachineTargetTemp(ac ACMachine) float32 {
r := s.ReadRegister(REG_AC_TARGET_TEMP + int(ac) - 1) r := s.ReadRegister(REG_AC_TARGET_TEMP + int(ac) - 1)
return reg2temp(uint16(r)) return reg2temp(uint16(r))
} }
func (s *Sys) GetTargetFanMode(ac ACMachine) FanMode { func (s *SysDriver) GetTargetFanMode(ac ACMachine) FanMode {
r := s.ReadRegister(REG_AC_TARGET_FAN_MODE + int(ac) - 1) r := s.ReadRegister(REG_AC_TARGET_FAN_MODE + int(ac) - 1)
return FanMode(r) return FanMode(r)
} }
func (s *Sys) GetBaudRate() int { func (s *SysDriver) GetBaudRate() int {
r := s.ReadRegister(REG_SERIAL_CONFIG) r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r { switch r {
case 2, 6: case 2, 6:
@@ -98,7 +101,7 @@ func (s *Sys) GetBaudRate() int {
return 0 return 0
} }
func (s *Sys) GetParity() string { func (s *SysDriver) GetParity() string {
r := s.ReadRegister(REG_SERIAL_CONFIG) r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r { switch r {
case 2, 3: case 2, 3:
@@ -109,26 +112,55 @@ func (s *Sys) GetParity() string {
return "unknown" return "unknown"
} }
func (s *Sys) GetSlaveID() int { func (s *SysDriver) GetSlaveID() int {
r := s.ReadRegister(REG_SLAVE_ID) r := s.ReadRegister(REG_SLAVE_ID)
return r return r
} }
func (s *Sys) GetEfficiency() int { func (s *SysDriver) GetEfficiency() int {
r := s.ReadRegister(REG_EFFICIENCY) r := s.ReadRegister(REG_EFFICIENCY)
return r return r
} }
func (s *Sys) GetSystemEnabled() bool { func (s *SysDriver) GetSystemEnabled() bool {
r := s.ReadRegister(REG_SYSTEM_ENABLED) r := s.ReadRegister(REG_SYSTEM_ENABLED)
return r != 0 return r != 0
} }
func (s *Sys) GetSystemKNMode() KnMode { func (s *SysDriver) GetSystemKNMode() KnMode {
r := s.ReadRegister(REG_SYS_KN_MODE) r := s.ReadRegister(REG_SYS_KN_MODE)
return KnMode(r) return KnMode(r)
} }
func (s *Sys) SetSystemKNMode(knMode KnMode) error { func (s *SysDriver) SetSystemKNMode(knMode KnMode) error {
return s.WriteRegister(REG_SYS_KN_MODE, uint16(knMode)) return s.WriteRegister(REG_SYS_KN_MODE, uint16(knMode))
} }
// HVACMode returns the HA HVAC mode based on the
// module state
func (s *SysDriver) HVACMode() string {
if !s.GetSystemEnabled() {
return HVAC_MODE_OFF
}
switch s.GetSystemKNMode() {
case MODE_AIR_COOLING, MODE_UNDERFLOOR_AIR_COOLING:
return HVAC_MODE_COOL
case MODE_AIR_HEATING, MODE_UNDERFLOOR_HEATING, MODE_UNDERFLOOR_AIR_HEATING:
return HVAC_MODE_HEAT
}
return "unknown"
}
// HoldMode returns the HA Hold Mode based on the
// module state
func (s *SysDriver) HoldMode() string {
switch s.GetSystemKNMode() {
case MODE_AIR_COOLING, MODE_AIR_HEATING:
return HOLD_MODE_FAN_ONLY
case MODE_UNDERFLOOR_HEATING:
return HOLD_MODE_UNDERFLOOR_ONLY
case MODE_UNDERFLOOR_AIR_COOLING, MODE_UNDERFLOOR_AIR_HEATING:
return HOLD_MODE_UNDERFLOOR_AND_FAN
}
return "unknown"
}

View File

@@ -0,0 +1,844 @@
[
{
"Topic": "hassPrefix/climate/TestModule/zone1/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone1/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone1/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone1/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone1/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone1/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone1",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone1/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone1/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone1"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone10/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone10/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone10/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone10/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone10/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone10/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone10",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone10/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone10/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone10"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone2/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone2/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone2/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone2/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone2/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone2/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone2",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone2/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone2/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone2"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone3/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone3/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone3/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone3/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone3/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone3/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone3",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone3/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone3/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone3"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone4/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone4/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone4/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone4/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone4/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone4/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone4",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone4/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone4/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone4"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone5/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone5/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone5/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone5/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone5/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone5/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone5",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone5/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone5/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone5"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone6/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone6/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone6/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone6/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone6/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone6/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone6",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone6/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone6/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone6"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone7/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone7/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone7/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone7/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone7/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone7/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone7",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone7/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone7/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone7"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone8/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone8/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone8/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone8/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone8/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone8/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone8",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone8/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone8/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone8"
}
},
{
"Topic": "hassPrefix/climate/TestModule/zone9/config",
"Payload": {
"current_temperature_topic": "topicPrefix/TestModule/zone9/currentTemp",
"fan_mode_command_topic": "topicPrefix/TestModule/zone9/fanMode/set",
"fan_mode_state_topic": "topicPrefix/TestModule/zone9/fanMode",
"fan_modes": [
"auto",
"low",
"medium",
"high"
],
"hold_command_topic": "topicPrefix/TestModule/sys/holdMode/set",
"hold_modes": [
"underfloor",
"fan",
"underfloor and fan"
],
"hold_state_topic": "topicPrefix/TestModule/sys/holdMode",
"max_temp": 35,
"min_temp": 15,
"mode_command_topic": "topicPrefix/TestModule/zone9/hvacMode/set",
"mode_state_topic": "topicPrefix/TestModule/zone9/hvacMode",
"modes": [
"cool",
"heat",
"off"
],
"name": "TestModule_zone9",
"precision": 0.1,
"temp_step": 0.5,
"temperature_command_topic": "topicPrefix/TestModule/zone9/targetTemp/set",
"temperature_state_topic": "topicPrefix/TestModule/zone9/targetTemp",
"temperature_unit": "C",
"unique_id": "TestModule_zone9"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone10_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone10_target_temp",
"state_topic": "topicPrefix/TestModule/zone10/targetTemp",
"unique_id": "TestModule_zone10_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone10_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone10_temp",
"state_topic": "topicPrefix/TestModule/zone10/currentTemp",
"unique_id": "TestModule_zone10_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone1_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone1_target_temp",
"state_topic": "topicPrefix/TestModule/zone1/targetTemp",
"unique_id": "TestModule_zone1_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone1_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone1_temp",
"state_topic": "topicPrefix/TestModule/zone1/currentTemp",
"unique_id": "TestModule_zone1_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone2_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone2_target_temp",
"state_topic": "topicPrefix/TestModule/zone2/targetTemp",
"unique_id": "TestModule_zone2_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone2_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone2_temp",
"state_topic": "topicPrefix/TestModule/zone2/currentTemp",
"unique_id": "TestModule_zone2_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone3_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone3_target_temp",
"state_topic": "topicPrefix/TestModule/zone3/targetTemp",
"unique_id": "TestModule_zone3_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone3_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone3_temp",
"state_topic": "topicPrefix/TestModule/zone3/currentTemp",
"unique_id": "TestModule_zone3_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone4_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone4_target_temp",
"state_topic": "topicPrefix/TestModule/zone4/targetTemp",
"unique_id": "TestModule_zone4_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone4_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone4_temp",
"state_topic": "topicPrefix/TestModule/zone4/currentTemp",
"unique_id": "TestModule_zone4_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone5_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone5_target_temp",
"state_topic": "topicPrefix/TestModule/zone5/targetTemp",
"unique_id": "TestModule_zone5_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone5_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone5_temp",
"state_topic": "topicPrefix/TestModule/zone5/currentTemp",
"unique_id": "TestModule_zone5_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone6_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone6_target_temp",
"state_topic": "topicPrefix/TestModule/zone6/targetTemp",
"unique_id": "TestModule_zone6_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone6_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone6_temp",
"state_topic": "topicPrefix/TestModule/zone6/currentTemp",
"unique_id": "TestModule_zone6_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone7_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone7_target_temp",
"state_topic": "topicPrefix/TestModule/zone7/targetTemp",
"unique_id": "TestModule_zone7_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone7_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone7_temp",
"state_topic": "topicPrefix/TestModule/zone7/currentTemp",
"unique_id": "TestModule_zone7_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone8_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone8_target_temp",
"state_topic": "topicPrefix/TestModule/zone8/targetTemp",
"unique_id": "TestModule_zone8_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone8_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone8_temp",
"state_topic": "topicPrefix/TestModule/zone8/currentTemp",
"unique_id": "TestModule_zone8_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone9_target_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone9_target_temp",
"state_topic": "topicPrefix/TestModule/zone9/targetTemp",
"unique_id": "TestModule_zone9_target_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "hassPrefix/sensor/TestModule/zone9_temp/config",
"Payload": {
"device_class": "temperature",
"name": "TestModule_zone9_temp",
"state_topic": "topicPrefix/TestModule/zone9/currentTemp",
"unique_id": "TestModule_zone9_temp",
"unit_of_measurement": "ºC"
}
},
{
"Topic": "topicPrefix/TestModule/sys/ac1/airflow",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac1/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/sys/ac1/targetTemp",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac2/airflow",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac2/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/sys/ac2/targetTemp",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac3/airflow",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac3/fanMode",
"Payload": "high"
},
{
"Topic": "topicPrefix/TestModule/sys/ac3/targetTemp",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac4/airflow",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/ac4/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/sys/ac4/targetTemp",
"Payload": "0"
},
{
"Topic": "topicPrefix/TestModule/sys/efficiency",
"Payload": "3"
},
{
"Topic": "topicPrefix/TestModule/sys/enabled",
"Payload": "true"
},
{
"Topic": "topicPrefix/TestModule/sys/holdMode",
"Payload": "underfloor"
},
{
"Topic": "topicPrefix/TestModule/sys/serialBaud",
"Payload": "9600"
},
{
"Topic": "topicPrefix/TestModule/sys/serialParity",
"Payload": "even"
},
{
"Topic": "topicPrefix/TestModule/sys/slaveId",
"Payload": "49"
},
{
"Topic": "topicPrefix/TestModule/zone1/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone1/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone1/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone1/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone1/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone10/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone10/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone10/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone10/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone2/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone2/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone2/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone2/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone3/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone3/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone3/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone3/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone4/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone4/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone4/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone4/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone5/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone5/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone5/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone5/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/fanMode",
"Payload": "high"
},
{
"Topic": "topicPrefix/TestModule/zone6/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone6/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone6/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone6/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/fanMode",
"Payload": "high"
},
{
"Topic": "topicPrefix/TestModule/zone7/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone7/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone7/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone7/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone8/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone8/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone8/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone8/targetTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/fanMode",
"Payload": "auto"
},
{
"Topic": "topicPrefix/TestModule/zone9/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone9/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone9/hvacMode",
"Payload": "heat"
},
{
"Topic": "topicPrefix/TestModule/zone9/targetTemp",
"Payload": "20.5"
}
]

802
kn/testdata/TestBridge/current-temp.json vendored Normal file
View File

@@ -0,0 +1,802 @@
[
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "15"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "15.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "16"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "16.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "17"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "17.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "18"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "18.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "19"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "19.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "20"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "20.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "21"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "21.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "22"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "22.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "23"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "23.5"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "24"
},
{
"Topic": "topicPrefix/TestModule/zone1/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone2/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone3/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone4/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone5/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone6/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone7/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone8/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone9/currentTemp",
"Payload": "24.5"
},
{
"Topic": "topicPrefix/TestModule/zone10/currentTemp",
"Payload": "24.5"
}
]

2132
kn/testdata/TestBridge/diffs.json vendored Normal file

File diff suppressed because it is too large Load Diff

3278
kn/testdata/TestBridge/messages.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
[
"topicPrefix/TestModule/sys/holdMode/set",
"topicPrefix/TestModule/zone1/fanMode/set",
"topicPrefix/TestModule/zone1/hvacMode/set",
"topicPrefix/TestModule/zone1/targetTemp/set",
"topicPrefix/TestModule/zone10/fanMode/set",
"topicPrefix/TestModule/zone10/hvacMode/set",
"topicPrefix/TestModule/zone10/targetTemp/set",
"topicPrefix/TestModule/zone2/fanMode/set",
"topicPrefix/TestModule/zone2/hvacMode/set",
"topicPrefix/TestModule/zone2/targetTemp/set",
"topicPrefix/TestModule/zone3/fanMode/set",
"topicPrefix/TestModule/zone3/hvacMode/set",
"topicPrefix/TestModule/zone3/targetTemp/set",
"topicPrefix/TestModule/zone4/fanMode/set",
"topicPrefix/TestModule/zone4/hvacMode/set",
"topicPrefix/TestModule/zone4/targetTemp/set",
"topicPrefix/TestModule/zone5/fanMode/set",
"topicPrefix/TestModule/zone5/hvacMode/set",
"topicPrefix/TestModule/zone5/targetTemp/set",
"topicPrefix/TestModule/zone6/fanMode/set",
"topicPrefix/TestModule/zone6/hvacMode/set",
"topicPrefix/TestModule/zone6/targetTemp/set",
"topicPrefix/TestModule/zone7/fanMode/set",
"topicPrefix/TestModule/zone7/hvacMode/set",
"topicPrefix/TestModule/zone7/targetTemp/set",
"topicPrefix/TestModule/zone8/fanMode/set",
"topicPrefix/TestModule/zone8/hvacMode/set",
"topicPrefix/TestModule/zone8/targetTemp/set",
"topicPrefix/TestModule/zone9/fanMode/set",
"topicPrefix/TestModule/zone9/hvacMode/set",
"topicPrefix/TestModule/zone9/targetTemp/set"
]

View File

@@ -17,6 +17,7 @@ type ZoneConfig struct {
Watcher Watcher Watcher Watcher
} }
// Zone is a driver to interact with climate zones
type Zone struct { type Zone struct {
ZoneConfig ZoneConfig
OnEnabledChange func() OnEnabledChange func()
@@ -28,27 +29,31 @@ type Zone struct {
temp *average.MovingAverage temp *average.MovingAverage
} }
func NewZone(config *ZoneConfig) *Zone { // newZone creates a new climate zone with the supplied configuration
// Invokes callbacks when specific registers change
// reads registers to return current state
// sets the appropriate registers when set* methods are invoked
func newZone(config *ZoneConfig) *Zone {
z := &Zone{ z := &Zone{
ZoneConfig: *config, ZoneConfig: *config,
temp: average.New(300), temp: average.New(300),
} }
z.RegisterCallback(REG_ENABLED, func() { z.registerCallback(REG_ENABLED, func() {
if z.OnEnabledChange != nil { if z.OnEnabledChange != nil {
z.OnEnabledChange() z.OnEnabledChange()
} }
}) })
z.RegisterCallback(REG_TARGET_TEMP, func() { z.registerCallback(REG_TARGET_TEMP, func() {
if z.OnTargetTempChange == nil { if z.OnTargetTempChange == nil {
return return
} }
temp := z.GetTargetTemperature() temp := z.getTargetTemperature()
z.OnTargetTempChange(temp) z.OnTargetTempChange(temp)
}) })
z.RegisterCallback(REG_MODE, func() { z.registerCallback(REG_MODE, func() {
fanMode := z.GetFanMode() fanMode := z.getFanMode()
hvacMode := z.GetKnMode() hvacMode := z.getKnMode()
if z.OnFanModeChange != nil { if z.OnFanModeChange != nil {
z.OnFanModeChange(fanMode) z.OnFanModeChange(fanMode)
} }
@@ -59,54 +64,54 @@ func NewZone(config *ZoneConfig) *Zone {
return z return z
} }
func (z *Zone) RegisterCallback(num int, f func()) { func (z *Zone) registerCallback(num int, f func()) {
z.Watcher.RegisterCallback(uint16((z.ZoneNumber-1)*REG_PER_ZONE+num), func(address uint16) { z.Watcher.RegisterCallback(uint16((z.ZoneNumber-1)*REG_PER_ZONE+num), func(address uint16) {
f() f()
}) })
} }
func (z *Zone) ReadRegister(num int) uint16 { func (z *Zone) readRegister(num int) uint16 {
return z.Watcher.ReadRegister(uint16((z.ZoneNumber-1)*REG_PER_ZONE + num)) return z.Watcher.ReadRegister(uint16((z.ZoneNumber-1)*REG_PER_ZONE + num))
} }
func (z *Zone) WriteRegister(num int, value uint16) error { func (z *Zone) writeRegister(num int, value uint16) error {
return z.Watcher.WriteRegister(uint16((z.ZoneNumber-1)*REG_PER_ZONE+num), value) return z.Watcher.WriteRegister(uint16((z.ZoneNumber-1)*REG_PER_ZONE+num), value)
} }
func (z *Zone) IsOn() bool { func (z *Zone) isOn() bool {
r1 := z.ReadRegister(REG_ENABLED) r1 := z.readRegister(REG_ENABLED)
return r1&0x1 != 0 return r1&0x1 != 0
} }
func (z *Zone) SetOn(on bool) error { func (z *Zone) setOn(on bool) error {
var r1 uint16 var r1 uint16
if on { if on {
r1 = 0x3 r1 = 0x3
} else { } else {
r1 = 0x2 r1 = 0x2
} }
return z.WriteRegister(REG_ENABLED, r1) return z.writeRegister(REG_ENABLED, r1)
} }
func (z *Zone) IsPresent() bool { func (z *Zone) isPresent() bool {
r1 := z.ReadRegister(REG_ENABLED) r1 := z.readRegister(REG_ENABLED)
return r1&0x2 != 0 return r1&0x2 != 0
} }
func (z *Zone) getCurrentTemperature() float32 { func (z *Zone) getCurrentTemperature() float32 {
r4 := z.ReadRegister(REG_CURRENT_TEMP) r4 := z.readRegister(REG_CURRENT_TEMP)
return reg2temp(r4) return reg2temp(r4)
} }
func (z *Zone) GetCurrentTemperature() float32 { func (z *Zone) getAverageCurrentTemperature() float32 {
return float32(math.Round(z.temp.Avg()*10) / 10) return float32(math.Round(z.temp.Avg()*10) / 10)
} }
func (z *Zone) SampleTemperature() { func (z *Zone) sampleTemperature() {
sample := z.getCurrentTemperature() sample := z.getCurrentTemperature()
z.temp.Add(float64(sample)) z.temp.Add(float64(sample))
if z.OnCurrentTempChange != nil { if z.OnCurrentTempChange != nil {
t := z.GetCurrentTemperature() t := z.getAverageCurrentTemperature()
if t != z.lastTemp { if t != z.lastTemp {
z.lastTemp = t z.lastTemp = t
z.OnCurrentTempChange(t) z.OnCurrentTempChange(t)
@@ -114,28 +119,28 @@ func (z *Zone) SampleTemperature() {
} }
} }
func (z *Zone) GetTargetTemperature() float32 { func (z *Zone) getTargetTemperature() float32 {
r3 := z.ReadRegister(REG_TARGET_TEMP) r3 := z.readRegister(REG_TARGET_TEMP)
return reg2temp(r3) return reg2temp(r3)
} }
func (z *Zone) SetTargetTemperature(targetTemp float32) error { func (z *Zone) setTargetTemperature(targetTemp float32) error {
return z.WriteRegister(REG_TARGET_TEMP, temp2reg(targetTemp)) return z.writeRegister(REG_TARGET_TEMP, temp2reg(targetTemp))
} }
func (z *Zone) GetFanMode() FanMode { func (z *Zone) getFanMode() FanMode {
r2 := z.ReadRegister(REG_MODE) r2 := z.readRegister(REG_MODE)
return (FanMode)(r2&0x00F0) >> 4 return (FanMode)(r2&0x00F0) >> 4
} }
func (z *Zone) SetFanMode(fanMode FanMode) error { func (z *Zone) setFanMode(fanMode FanMode) error {
r2 := z.ReadRegister(REG_MODE) & 0x000F r2 := z.readRegister(REG_MODE) & 0x000F
fm := (uint16(fanMode) & 0x000F) << 4 fm := (uint16(fanMode) & 0x000F) << 4
return z.WriteRegister(REG_MODE, r2|fm) return z.writeRegister(REG_MODE, r2|fm)
} }
func (z *Zone) GetKnMode() KnMode { func (z *Zone) getKnMode() KnMode {
r2 := z.ReadRegister(REG_MODE) r2 := z.readRegister(REG_MODE)
return (KnMode)(r2 & 0x000F) return (KnMode)(r2 & 0x000F)
} }

View File

@@ -9,7 +9,8 @@ import (
"time" "time"
) )
func NewBridges(slaves map[byte]string, templateConfig *kn.Config) []*kn.Bridge { // newBridges builds all bridges from a list of Modbus slaves
func newBridges(slaves map[byte]string, templateConfig *kn.Config) []*kn.Bridge {
var bridges []*kn.Bridge var bridges []*kn.Bridge
for id, name := range slaves { for id, name := range slaves {
config := *templateConfig config := *templateConfig
@@ -23,9 +24,11 @@ func NewBridges(slaves map[byte]string, templateConfig *kn.Config) []*kn.Bridge
func main() { func main() {
// configure CTRL+C as a way to stop the application
ctrlC := make(chan os.Signal, 1) ctrlC := make(chan os.Signal, 1)
signal.Notify(ctrlC, os.Interrupt, syscall.SIGTERM) signal.Notify(ctrlC, os.Interrupt, syscall.SIGTERM)
// read configuration from the command line
config := ParseCommandLine() config := ParseCommandLine()
go func() { go func() {
@@ -35,7 +38,7 @@ func main() {
for range ticker.C { for range ticker.C {
newSessionID := config.MqttClient.ID newSessionID := config.MqttClient.ID
if sessionID != newSessionID { if sessionID != newSessionID {
bridges = NewBridges(config.slaves, config.BridgeTemplateConfig) bridges = newBridges(config.slaves, config.BridgeTemplateConfig)
for _, b := range bridges { for _, b := range bridges {
err := b.Start() err := b.Start()
if err != nil { if err != nil {
@@ -56,5 +59,6 @@ func main() {
<-ctrlC <-ctrlC
config.MqttClient.Close() config.MqttClient.Close()
config.BridgeTemplateConfig.Modbus.Close()
} }

View File

@@ -8,7 +8,7 @@ type Mock struct {
State map[byte][]uint16 State map[byte][]uint16
} }
func NewMock() Modbus { func NewMock() *Mock {
return &Mock{ return &Mock{
State: map[byte][]uint16{ State: map[byte][]uint16{
49: {3, 68, 41, 41, 3, 68, 41, 41, 3, 68, 41, 45, 3, 68, 41, 45, 3, 68, 41, 42, 3, 52, 41, 40, 3, 52, 41, 44, 3, 68, 41, 41, 3, 68, 41, 40, 3, 68, 41, 41, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 4, 2, 49, 3, 7, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 49: {3, 68, 41, 41, 3, 68, 41, 41, 3, 68, 41, 45, 3, 68, 41, 45, 3, 68, 41, 42, 3, 52, 41, 40, 3, 52, 41, 44, 3, 68, 41, 41, 3, 68, 41, 40, 3, 68, 41, 41, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 4, 2, 49, 3, 7, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},

View File

@@ -10,12 +10,6 @@ import (
gmodbus "github.com/wz2b/modbus" gmodbus "github.com/wz2b/modbus"
) )
type Modbus interface {
ReadRegister(slaveID byte, address uint16, quantity uint16) (results []uint16, err error)
WriteRegister(slaveID byte, address uint16, value uint16) (results []uint16, err error)
Close() error
}
type Config struct { type Config struct {
Port string Port string
BaudRate int BaudRate int
@@ -25,7 +19,7 @@ type Config struct {
Timeout time.Duration Timeout time.Duration
} }
type modbus struct { type Modbus struct {
handler *gmodbus.RTUClientHandler handler *gmodbus.RTUClientHandler
client gmodbus.Client client gmodbus.Client
lock sync.RWMutex lock sync.RWMutex
@@ -37,7 +31,7 @@ func throttle(ms int) {
var ErrIncorrectResultSize = errors.New("Incorrect number of results returned") var ErrIncorrectResultSize = errors.New("Incorrect number of results returned")
func New(config *Config) (Modbus, error) { func New(config *Config) (*Modbus, error) {
handler := gmodbus.NewRTUClientHandler(config.Port) handler := gmodbus.NewRTUClientHandler(config.Port)
handler.BaudRate = config.BaudRate handler.BaudRate = config.BaudRate
handler.DataBits = config.DataBits handler.DataBits = config.DataBits
@@ -45,13 +39,13 @@ func New(config *Config) (Modbus, error) {
handler.StopBits = config.StopBits handler.StopBits = config.StopBits
handler.Timeout = config.Timeout handler.Timeout = config.Timeout
return &modbus{ return &Modbus{
handler: handler, handler: handler,
client: gmodbus.NewClient(handler), client: gmodbus.NewClient(handler),
}, handler.Connect() }, handler.Connect()
} }
func (mb *modbus) Close() error { func (mb *Modbus) Close() error {
return mb.handler.Close() return mb.handler.Close()
} }
@@ -66,7 +60,7 @@ func parseResults(r []byte, quantity uint16) ([]uint16, error) {
return results, nil return results, nil
} }
func (mb *modbus) ReadRegister(slaveID byte, address uint16, quantity uint16) (results []uint16, err error) { func (mb *Modbus) ReadRegister(slaveID byte, address uint16, quantity uint16) (results []uint16, err error) {
err = mb.try(slaveID, func() (err error) { err = mb.try(slaveID, func() (err error) {
r, err := mb.client.ReadHoldingRegisters(address-1, quantity) r, err := mb.client.ReadHoldingRegisters(address-1, quantity)
if err != nil { if err != nil {
@@ -78,7 +72,7 @@ func (mb *modbus) ReadRegister(slaveID byte, address uint16, quantity uint16) (r
return results, err return results, err
} }
func (mb *modbus) WriteRegister(slaveID byte, address uint16, value uint16) (results []uint16, err error) { func (mb *Modbus) WriteRegister(slaveID byte, address uint16, value uint16) (results []uint16, err error) {
err = mb.try(slaveID, func() (err error) { err = mb.try(slaveID, func() (err error) {
r, err := mb.client.WriteSingleRegister(address-1, value) r, err := mb.client.WriteSingleRegister(address-1, value)
if err != nil { if err != nil {
@@ -90,7 +84,7 @@ func (mb *modbus) WriteRegister(slaveID byte, address uint16, value uint16) (res
return results, err return results, err
} }
func (mb *modbus) try(slaveID byte, f func() error) (err error) { func (mb *Modbus) try(slaveID byte, f func() error) (err error) {
mb.lock.Lock() mb.lock.Lock()
defer mb.lock.Unlock() defer mb.lock.Unlock()
defer throttle(100) defer throttle(100)

View File

@@ -4,16 +4,21 @@ package watcher
import ( import (
"errors" "errors"
"koolnova2mqtt/modbus"
"sync" "sync"
) )
type Modbus interface {
ReadRegister(slaveID byte, address uint16, quantity uint16) (results []uint16, err error)
WriteRegister(slaveID byte, address uint16, value uint16) (results []uint16, err error)
Close() error
}
// Config contains the configuration parameters for a new Watcher instance // Config contains the configuration parameters for a new Watcher instance
type Config struct { type Config struct {
Address uint16 // Start address Address uint16 // Start address
Quantity uint16 // Number of registers to watch Quantity uint16 // Number of registers to watch
SlaveID byte // SlaveID to watch SlaveID byte // SlaveID to watch
Modbus modbus.Modbus // Modbus interface Modbus Modbus // Modbus interface
} }
// Watcher represents a cache of modbus registers in a device // Watcher represents a cache of modbus registers in a device