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 (
"encoding/json"
"fmt"
"koolnova2mqtt/modbus"
"koolnova2mqtt/watcher"
"log"
"strconv"
)
// MqttClient defines the expected MQTT client pub sub interface
type MqttClient interface {
Publish(topic string, qos byte, retained bool, payload string) error
Subscribe(topic string, callback func(message string)) error
}
// Config defines de Modbus<>MQTT bridge configuration
type Config struct {
ModuleName string
SlaveID byte
Mqtt MqttClient
TopicPrefix string
HassPrefix string
Modbus modbus.Modbus
ModuleName string // name of the module the modbus interface is connected to
SlaveID byte // SlaveID of the module in the bus
TopicPrefix string // MQTT topic prefix to publish information
HassPrefix string // Home Assistant sensor discovery prefix
Mqtt MqttClient // MQTT client
Modbus watcher.Modbus // Modbus client
}
// Bridge bridges Modbus and MQTT protocols
type Bridge struct {
Config
zw *watcher.Watcher
sysw *watcher.Watcher
refresh func()
zones []*Zone
Config // embedded configuration
zw *watcher.Watcher // watcher to detect register changes in zones
sysw *watcher.Watcher // watcher to detect register changes in system registers
zones []*Zone // List of present zones in this module
sys *SysDriver
}
// getActiveZones returns the list of active zones in this module
func getActiveZones(w Watcher) ([]*Zone, error) {
var zones []*Zone
for n := 0; n < NUM_ZONES; n++ {
zone := NewZone(&ZoneConfig{
zone := newZone(&ZoneConfig{
ZoneNumber: n + 1,
Watcher: w,
})
isPresent := zone.IsPresent()
isPresent := zone.isPresent()
if isPresent {
zones = append(zones, zone)
}
@@ -47,6 +50,7 @@ func getActiveZones(w Watcher) ([]*Zone, error) {
return zones, nil
}
// NewBridge returns a new Modbus<>MQTT bridge
func NewBridge(config *Config) *Bridge {
b := &Bridge{
Config: *config,
@@ -54,8 +58,11 @@ func NewBridge(config *Config) *Bridge {
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 {
// Define a watcher to watch the zone registers
zw := watcher.New(&watcher.Config{
Address: FIRST_ZONE_REGISTER,
Quantity: TOTAL_ZONE_REGISTERS,
@@ -63,17 +70,20 @@ func (b *Bridge) Start() error {
Modbus: b.Modbus,
})
// Define a watcher to watch the system registers
sysw := watcher.New(&watcher.Config{
Address: FIRST_SYS_REGISTER,
Quantity: TOTAL_SYS_REGISTERS,
SlaveID: b.SlaveID,
Modbus: b.Modbus,
})
b.zw = zw
b.sysw = sysw
sys := NewSys(&SysConfig{
Watcher: b.sysw,
})
b.sys = sys
log.Printf("Starting bridge for %s\n", b.ModuleName)
err := b.poll()
@@ -81,49 +91,18 @@ func (b *Bridge) Start() error {
return err
}
// Get Active zones
zones, err := getActiveZones(b.zw)
b.zones = zones
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)
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")
holdModeSetTopic := holdModeTopic + "/set"
// configure publishing when modbus registers change
for _, zone := range zones {
zone := zone
currentTempTopic := b.getZoneTopic(zone.ZoneNumber, "currentTemp")
@@ -134,36 +113,45 @@ func (b *Bridge) Start() error {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
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() {
hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode")
var mode string
if zone.IsOn() {
mode = getHVACMode()
if zone.isOn() {
mode = sys.HVACMode()
} else {
mode = HVAC_MODE_OFF
}
b.Mqtt.Publish(hvacModeTopic, 0, true, mode)
}
// if the current temperature changes, forward value to the
// correspondig MQTT topic
zone.OnCurrentTempChange = func(currentTemp float32) {
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) {
b.Mqtt.Publish(targetTempTopic, 0, true, fmt.Sprintf("%g", targetTemp))
}
// Publish changes to the fan mode
zone.OnFanModeChange = func(fanMode 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) {
targetTemp, err := strconv.ParseFloat(message, 32)
if err != nil {
log.Printf("Error parsing targetTemperature in topic %s: %s", targetTempSetTopic, err)
return
}
err = zone.SetTargetTemperature(float32(targetTemp))
err = zone.setTargetTemperature(float32(targetTemp))
if err != nil {
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
}
// Subscribe to fan mode set topic in MQTT
err = b.Mqtt.Subscribe(fanModeSetTopic, func(message string) {
fm, err := Str2FanMode(message)
if err != nil {
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 {
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
}
// Subscribe to HVAC Mode set topic in MQTT
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 {
err := zone.SetOn(false)
err := zone.setOn(false) // turn zone off (REG_ENABLED)
if err != nil {
log.Printf("Cannot set zone %d to off", zone.ZoneNumber)
}
return
}
// Translate HA HVAC mode to Koolnova's
knMode := sys.GetSystemKNMode()
knMode = ApplyHvacMode(knMode, message)
err = sys.SetSystemKNMode(knMode)
if err != nil {
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 {
log.Printf("Cannot set zone %d to on", zone.ZoneNumber)
return
@@ -210,7 +202,9 @@ func (b *Bridge) Start() error {
return err
}
// Subscribe to changes in hold mode:
err = b.Mqtt.Subscribe(holdModeSetTopic, func(message string) {
// Translate HA's hold mode to Koolnova's
knMode := sys.GetSystemKNMode()
knMode = ApplyHoldMode(knMode, message)
err := sys.SetSystemKNMode(knMode)
@@ -222,8 +216,9 @@ func (b *Bridge) Start() error {
return err
}
// Define a Home Assistant thermostat
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,
"current_temperature_topic": currentTempTopic,
"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_state_topic": holdModeTopic,
"hold_command_topic": holdModeSetTopic,
}
})
configJSON, _ := json.Marshal(config)
// <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:
// Define a current temperature sensor:
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,
"device_class": "temperature",
"state_topic": currentTempTopic,
"unit_of_measurement": "ºC",
"unique_id": name,
}
})
configJSON, _ = json.Marshal(config)
b.Mqtt.Publish(fmt.Sprintf("%s/sensor/%s/zone%d_temp/config", b.HassPrefix, b.ModuleName, zone.ZoneNumber), 0, true, string(configJSON))
// Define a target temperature sensor:
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) {
airflow := sys.GetAirflow(ac)
b.Mqtt.Publish(b.getACTopic(ac, "airflow"), 0, true, strconv.Itoa(airflow))
}
sys.OnACTargetTempChange = func(ac ACMachine) {
targetTemp := sys.GetMachineTargetTemp(ac)
b.Mqtt.Publish(b.getACTopic(ac, "targetTemp"), 0, true, fmt.Sprintf("%g", targetTemp))
}
sys.OnACTargetFanModeChange = func(ac ACMachine) {
targetAirflow := sys.GetTargetFanMode(ac)
b.Mqtt.Publish(b.getACTopic(ac, "fanMode"), 0, true, FanMode2Str(targetAirflow))
}
sys.OnEfficiencyChange = func() {
efficiency := sys.GetEfficiency()
b.Mqtt.Publish(b.getSysTopic("efficiency"), 0, true, strconv.Itoa(efficiency))
}
sys.OnSystemEnabledChange = func() {
enabled := sys.GetSystemEnabled()
b.Mqtt.Publish(b.getSysTopic("enabled"), 0, true, fmt.Sprintf("%t", enabled))
publishHvacMode()
}
sys.OnKnModeChange = func() {
publishHvacMode()
b.Mqtt.Publish(holdModeTopic, 0, true, getHoldMode())
b.publishHvacMode()
}
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.sysw.TriggerCallbacks()
// Publish one-off static information
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("slaveId"), 0, true, strconv.Itoa(sys.GetSlaveID()))
return nil
}
// poll polls modbus for changes
func (b *Bridge) poll() error {
err := b.zw.Poll()
if err != nil {
@@ -314,13 +321,15 @@ func (b *Bridge) poll() error {
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 {
err := b.poll()
if err != nil {
return err
}
for _, z := range b.zones {
z.SampleTemperature()
z.sampleTemperature()
}
return nil
}
@@ -336,3 +345,19 @@ func (b *Bridge) getSysTopic(subtopic string) string {
func (b *Bridge) getACTopic(ac ACMachine, subtopic string) string {
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 (
"errors"
"koolnova2mqtt/bimap"
)
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_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_FAN_ONLY = "fan"
const HOLD_MODE_UNDERFLOOR_AND_FAN = "underfloor and fan"
@@ -76,28 +59,41 @@ const AC2 ACMachine = 2
const AC3 ACMachine = 3
const AC4 ACMachine = 4
const HA_COMPONENT_SENSOR = "sensor"
const HA_COMPONENT_CLIMATE = "climate"
func FanMode2Str(fm FanMode) string {
st, ok := FanModes.GetInverse(fm)
if !ok {
st = "unknown"
switch fm {
case FAN_OFF:
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) {
fm, ok := FanModes.Get(st)
if !ok {
switch st {
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 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 {

View File

@@ -6,7 +6,10 @@ type SysConfig struct {
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
OnACAirflowChange func(ac ACMachine)
OnACTargetTempChange func(ac ACMachine)
@@ -18,8 +21,8 @@ type Sys struct {
var ErrUnknownSerialConfig = errors.New("Uknown serial configuration")
func NewSys(config *SysConfig) *Sys {
s := &Sys{
func NewSys(config *SysConfig) *SysDriver {
s := &SysDriver{
SysConfig: *config,
}
for n := byte(0); n < ACMachines; n++ {
@@ -63,31 +66,31 @@ func NewSys(config *SysConfig) *Sys {
return s
}
func (s *Sys) ReadRegister(n int) int {
func (s *SysDriver) ReadRegister(n int) int {
r := s.Watcher.ReadRegister(uint16(n))
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)
}
func (s *Sys) GetAirflow(ac ACMachine) int {
func (s *SysDriver) GetAirflow(ac ACMachine) int {
r := s.ReadRegister(REG_AIRFLOW + int(ac) - 1)
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)
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)
return FanMode(r)
}
func (s *Sys) GetBaudRate() int {
func (s *SysDriver) GetBaudRate() int {
r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r {
case 2, 6:
@@ -98,7 +101,7 @@ func (s *Sys) GetBaudRate() int {
return 0
}
func (s *Sys) GetParity() string {
func (s *SysDriver) GetParity() string {
r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r {
case 2, 3:
@@ -109,26 +112,55 @@ func (s *Sys) GetParity() string {
return "unknown"
}
func (s *Sys) GetSlaveID() int {
func (s *SysDriver) GetSlaveID() int {
r := s.ReadRegister(REG_SLAVE_ID)
return r
}
func (s *Sys) GetEfficiency() int {
func (s *SysDriver) GetEfficiency() int {
r := s.ReadRegister(REG_EFFICIENCY)
return r
}
func (s *Sys) GetSystemEnabled() bool {
func (s *SysDriver) GetSystemEnabled() bool {
r := s.ReadRegister(REG_SYSTEM_ENABLED)
return r != 0
}
func (s *Sys) GetSystemKNMode() KnMode {
func (s *SysDriver) GetSystemKNMode() KnMode {
r := s.ReadRegister(REG_SYS_KN_MODE)
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))
}
// 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
}
// Zone is a driver to interact with climate zones
type Zone struct {
ZoneConfig
OnEnabledChange func()
@@ -28,27 +29,31 @@ type Zone struct {
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{
ZoneConfig: *config,
temp: average.New(300),
}
z.RegisterCallback(REG_ENABLED, func() {
z.registerCallback(REG_ENABLED, func() {
if z.OnEnabledChange != nil {
z.OnEnabledChange()
}
})
z.RegisterCallback(REG_TARGET_TEMP, func() {
z.registerCallback(REG_TARGET_TEMP, func() {
if z.OnTargetTempChange == nil {
return
}
temp := z.GetTargetTemperature()
temp := z.getTargetTemperature()
z.OnTargetTempChange(temp)
})
z.RegisterCallback(REG_MODE, func() {
fanMode := z.GetFanMode()
hvacMode := z.GetKnMode()
z.registerCallback(REG_MODE, func() {
fanMode := z.getFanMode()
hvacMode := z.getKnMode()
if z.OnFanModeChange != nil {
z.OnFanModeChange(fanMode)
}
@@ -59,54 +64,54 @@ func NewZone(config *ZoneConfig) *Zone {
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) {
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))
}
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)
}
func (z *Zone) IsOn() bool {
r1 := z.ReadRegister(REG_ENABLED)
func (z *Zone) isOn() bool {
r1 := z.readRegister(REG_ENABLED)
return r1&0x1 != 0
}
func (z *Zone) SetOn(on bool) error {
func (z *Zone) setOn(on bool) error {
var r1 uint16
if on {
r1 = 0x3
} else {
r1 = 0x2
}
return z.WriteRegister(REG_ENABLED, r1)
return z.writeRegister(REG_ENABLED, r1)
}
func (z *Zone) IsPresent() bool {
r1 := z.ReadRegister(REG_ENABLED)
func (z *Zone) isPresent() bool {
r1 := z.readRegister(REG_ENABLED)
return r1&0x2 != 0
}
func (z *Zone) getCurrentTemperature() float32 {
r4 := z.ReadRegister(REG_CURRENT_TEMP)
r4 := z.readRegister(REG_CURRENT_TEMP)
return reg2temp(r4)
}
func (z *Zone) GetCurrentTemperature() float32 {
func (z *Zone) getAverageCurrentTemperature() float32 {
return float32(math.Round(z.temp.Avg()*10) / 10)
}
func (z *Zone) SampleTemperature() {
func (z *Zone) sampleTemperature() {
sample := z.getCurrentTemperature()
z.temp.Add(float64(sample))
if z.OnCurrentTempChange != nil {
t := z.GetCurrentTemperature()
t := z.getAverageCurrentTemperature()
if t != z.lastTemp {
z.lastTemp = t
z.OnCurrentTempChange(t)
@@ -114,28 +119,28 @@ func (z *Zone) SampleTemperature() {
}
}
func (z *Zone) GetTargetTemperature() float32 {
r3 := z.ReadRegister(REG_TARGET_TEMP)
func (z *Zone) getTargetTemperature() float32 {
r3 := z.readRegister(REG_TARGET_TEMP)
return reg2temp(r3)
}
func (z *Zone) SetTargetTemperature(targetTemp float32) error {
return z.WriteRegister(REG_TARGET_TEMP, temp2reg(targetTemp))
func (z *Zone) setTargetTemperature(targetTemp float32) error {
return z.writeRegister(REG_TARGET_TEMP, temp2reg(targetTemp))
}
func (z *Zone) GetFanMode() FanMode {
r2 := z.ReadRegister(REG_MODE)
func (z *Zone) getFanMode() FanMode {
r2 := z.readRegister(REG_MODE)
return (FanMode)(r2&0x00F0) >> 4
}
func (z *Zone) SetFanMode(fanMode FanMode) error {
r2 := z.ReadRegister(REG_MODE) & 0x000F
func (z *Zone) setFanMode(fanMode FanMode) error {
r2 := z.readRegister(REG_MODE) & 0x000F
fm := (uint16(fanMode) & 0x000F) << 4
return z.WriteRegister(REG_MODE, r2|fm)
return z.writeRegister(REG_MODE, r2|fm)
}
func (z *Zone) GetKnMode() KnMode {
r2 := z.ReadRegister(REG_MODE)
func (z *Zone) getKnMode() KnMode {
r2 := z.readRegister(REG_MODE)
return (KnMode)(r2 & 0x000F)
}

View File

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

View File

@@ -8,7 +8,7 @@ type Mock struct {
State map[byte][]uint16
}
func NewMock() Modbus {
func NewMock() *Mock {
return &Mock{
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},

View File

@@ -10,12 +10,6 @@ import (
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 {
Port string
BaudRate int
@@ -25,7 +19,7 @@ type Config struct {
Timeout time.Duration
}
type modbus struct {
type Modbus struct {
handler *gmodbus.RTUClientHandler
client gmodbus.Client
lock sync.RWMutex
@@ -37,7 +31,7 @@ func throttle(ms int) {
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.BaudRate = config.BaudRate
handler.DataBits = config.DataBits
@@ -45,13 +39,13 @@ func New(config *Config) (Modbus, error) {
handler.StopBits = config.StopBits
handler.Timeout = config.Timeout
return &modbus{
return &Modbus{
handler: handler,
client: gmodbus.NewClient(handler),
}, handler.Connect()
}
func (mb *modbus) Close() error {
func (mb *Modbus) Close() error {
return mb.handler.Close()
}
@@ -66,7 +60,7 @@ func parseResults(r []byte, quantity uint16) ([]uint16, error) {
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) {
r, err := mb.client.ReadHoldingRegisters(address-1, quantity)
if err != nil {
@@ -78,7 +72,7 @@ func (mb *modbus) ReadRegister(slaveID byte, address uint16, quantity uint16) (r
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) {
r, err := mb.client.WriteSingleRegister(address-1, value)
if err != nil {
@@ -90,7 +84,7 @@ func (mb *modbus) WriteRegister(slaveID byte, address uint16, value uint16) (res
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()
defer mb.lock.Unlock()
defer throttle(100)

View File

@@ -4,16 +4,21 @@ package watcher
import (
"errors"
"koolnova2mqtt/modbus"
"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
type Config struct {
Address uint16 // Start address
Quantity uint16 // Number of registers to watch
SlaveID byte // SlaveID to watch
Modbus modbus.Modbus // Modbus interface
Address uint16 // Start address
Quantity uint16 // Number of registers to watch
SlaveID byte // SlaveID to watch
Modbus Modbus // Modbus interface
}
// Watcher represents a cache of modbus registers in a device