mirror of
https://github.com/jpeletier/koolnova2mqtt.git
synced 2026-07-16 16:26:57 +00:00
completed readonly
This commit is contained in:
+39
-5
@@ -8,13 +8,22 @@ const REG_MODE = 2
|
|||||||
const REG_TARGET_TEMP = 3
|
const REG_TARGET_TEMP = 3
|
||||||
const REG_CURRENT_TEMP = 4
|
const REG_CURRENT_TEMP = 4
|
||||||
|
|
||||||
|
const REG_AIRFLOW = 65
|
||||||
|
const REG_AC_TARGET_TEMP = 69
|
||||||
|
const REG_AC_TARGET_FAN_MODE = 73
|
||||||
|
const REG_SERIAL_CONFIG = 77
|
||||||
|
const REG_SLAVE_ID = 78
|
||||||
|
const REG_EFFICIENCY = 79
|
||||||
|
const REG_SYSTEM_ENABLED = 81
|
||||||
|
const REG_SYS_HVAC_MODE = 82
|
||||||
|
|
||||||
type FanMode byte
|
type FanMode byte
|
||||||
|
|
||||||
const FAN_OFF FanMode = 0x00
|
const FAN_OFF FanMode = 0
|
||||||
const FAN_LOW FanMode = 0x10
|
const FAN_LOW FanMode = 1
|
||||||
const FAN_MED FanMode = 0x20
|
const FAN_MED FanMode = 2
|
||||||
const FAN_HIGH FanMode = 0x30
|
const FAN_HIGH FanMode = 3
|
||||||
const FAN_AUTO FanMode = 0x40
|
const FAN_AUTO FanMode = 4
|
||||||
|
|
||||||
type HvacMode byte
|
type HvacMode byte
|
||||||
|
|
||||||
@@ -39,3 +48,28 @@ var HvacModes = bimap.New(map[interface{}]interface{}{
|
|||||||
"underfloor air cooling": MODE_UNDERFLOOR_AIR_COOLING,
|
"underfloor air cooling": MODE_UNDERFLOOR_AIR_COOLING,
|
||||||
"underfloor air heating": MODE_UNDERFLOOR_AIR_HEATING,
|
"underfloor air heating": MODE_UNDERFLOOR_AIR_HEATING,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type ACMachine int
|
||||||
|
|
||||||
|
const ACMachines = 4
|
||||||
|
|
||||||
|
const AC1 ACMachine = 1
|
||||||
|
const AC2 ACMachine = 2
|
||||||
|
const AC3 ACMachine = 3
|
||||||
|
const AC4 ACMachine = 4
|
||||||
|
|
||||||
|
func FanMode2Str(fm FanMode) string {
|
||||||
|
st, ok := FanModes.GetInverse(fm)
|
||||||
|
if !ok {
|
||||||
|
st = "unknown"
|
||||||
|
}
|
||||||
|
return st.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HvacMode2Str(hm HvacMode) string {
|
||||||
|
st, ok := HvacModes.GetInverse(hm)
|
||||||
|
if !ok {
|
||||||
|
st = "unknown"
|
||||||
|
}
|
||||||
|
return st.(string)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package kn
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
type SysConfig struct {
|
||||||
|
Watcher Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sys struct {
|
||||||
|
SysConfig
|
||||||
|
OnACAirflowChange func(ac ACMachine)
|
||||||
|
OnACTargetTempChange func(ac ACMachine)
|
||||||
|
OnACTargetFanModeChange func(ac ACMachine)
|
||||||
|
OnEfficiencyChange func()
|
||||||
|
OnSystemEnabledChange func()
|
||||||
|
OnHvacModeChange func()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUnknownSerialConfig = errors.New("Uknown serial configuration")
|
||||||
|
|
||||||
|
func NewSys(config *SysConfig) *Sys {
|
||||||
|
s := &Sys{
|
||||||
|
SysConfig: *config,
|
||||||
|
}
|
||||||
|
for n := byte(0); n < ACMachines; n++ {
|
||||||
|
func(ac ACMachine) {
|
||||||
|
s.Watcher.RegisterCallback(uint16(REG_AIRFLOW+ac-1), func(address uint16) {
|
||||||
|
if s.OnACAirflowChange != nil {
|
||||||
|
s.OnACAirflowChange(ac)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.Watcher.RegisterCallback(uint16(REG_AC_TARGET_TEMP+ac-1), func(address uint16) {
|
||||||
|
if s.OnACTargetTempChange != nil {
|
||||||
|
s.OnACTargetTempChange(ac)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.Watcher.RegisterCallback(uint16(REG_AC_TARGET_FAN_MODE+ac-1), func(address uint16) {
|
||||||
|
if s.OnACTargetFanModeChange != nil {
|
||||||
|
s.OnACTargetFanModeChange(ac)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(ACMachine(n + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Watcher.RegisterCallback(REG_EFFICIENCY, func(address uint16) {
|
||||||
|
if s.OnEfficiencyChange != nil {
|
||||||
|
s.OnEfficiencyChange()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Watcher.RegisterCallback(REG_SYSTEM_ENABLED, func(address uint16) {
|
||||||
|
if s.OnSystemEnabledChange != nil {
|
||||||
|
s.OnSystemEnabledChange()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Watcher.RegisterCallback(REG_SYS_HVAC_MODE, func(address uint16) {
|
||||||
|
if s.OnHvacModeChange != nil {
|
||||||
|
s.OnHvacModeChange()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) ReadRegister(n int) (int, error) {
|
||||||
|
r, err := s.Watcher.ReadRegister(uint16(n))
|
||||||
|
if err != nil || len(r) != 2 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return int(r[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetAirflow(ac ACMachine) (int, error) {
|
||||||
|
r, err := s.ReadRegister(REG_AIRFLOW + int(ac) - 1)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetMachineTargetTemp(ac ACMachine) (float32, error) {
|
||||||
|
r, err := s.ReadRegister(REG_AC_TARGET_TEMP + int(ac) - 1)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return reg2temp(uint16(r)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetTargetFanMode(ac ACMachine) (FanMode, error) {
|
||||||
|
r, err := s.ReadRegister(REG_AC_TARGET_FAN_MODE + int(ac) - 1)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return FanMode(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetBaudRate() (int, error) {
|
||||||
|
r, err := s.ReadRegister(REG_SERIAL_CONFIG)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case 2, 6:
|
||||||
|
return 9600, nil
|
||||||
|
case 3, 7:
|
||||||
|
return 19200, nil
|
||||||
|
}
|
||||||
|
return 0, ErrUnknownSerialConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetParity() (string, error) {
|
||||||
|
r, err := s.ReadRegister(REG_SERIAL_CONFIG)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case 2, 3:
|
||||||
|
return "even", nil
|
||||||
|
case 6, 7:
|
||||||
|
return "none", nil
|
||||||
|
}
|
||||||
|
return "", ErrUnknownSerialConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetSlaveID() (int, error) {
|
||||||
|
r, err := s.ReadRegister(REG_SLAVE_ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetEfficiency() (int, error) {
|
||||||
|
r, err := s.ReadRegister(REG_EFFICIENCY)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetSystemEnabled() (bool, error) {
|
||||||
|
r, err := s.ReadRegister(REG_SYSTEM_ENABLED)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return r != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sys) GetSystemHVACMode() (HvacMode, error) {
|
||||||
|
r, err := s.ReadRegister(REG_SYS_HVAC_MODE)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return HvacMode(r), nil
|
||||||
|
}
|
||||||
+9
-5
@@ -93,7 +93,7 @@ func (z *Zone) IsOn() (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r1&uint16(0x1) != 0, nil
|
return r1&0x1 != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *Zone) IsPresent() (bool, error) {
|
func (z *Zone) IsPresent() (bool, error) {
|
||||||
@@ -102,7 +102,7 @@ func (z *Zone) IsPresent() (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r1&uint16(0x2) != 0, nil
|
return r1&0x2 != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *Zone) GetCurrentTemperature() (float32, error) {
|
func (z *Zone) GetCurrentTemperature() (float32, error) {
|
||||||
@@ -111,7 +111,7 @@ func (z *Zone) GetCurrentTemperature() (float32, error) {
|
|||||||
return 0.0, err
|
return 0.0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return float32(r4) / 2.0, nil
|
return reg2temp(r4), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *Zone) GetTargetTemperature() (float32, error) {
|
func (z *Zone) GetTargetTemperature() (float32, error) {
|
||||||
@@ -119,7 +119,7 @@ func (z *Zone) GetTargetTemperature() (float32, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0.0, err
|
return 0.0, err
|
||||||
}
|
}
|
||||||
return float32(r3) / 2.0, nil
|
return reg2temp(r3), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *Zone) GetFanMode() (FanMode, error) {
|
func (z *Zone) GetFanMode() (FanMode, error) {
|
||||||
@@ -127,7 +127,7 @@ func (z *Zone) GetFanMode() (FanMode, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return (FanMode)(r2 & 0x00F0), nil
|
return (FanMode)(r2&0x00F0) >> 4, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *Zone) GetHvacMode() (HvacMode, error) {
|
func (z *Zone) GetHvacMode() (HvacMode, error) {
|
||||||
@@ -137,3 +137,7 @@ func (z *Zone) GetHvacMode() (HvacMode, error) {
|
|||||||
}
|
}
|
||||||
return (HvacMode)(r2 & 0x000F), nil
|
return (HvacMode)(r2 & 0x000F), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reg2temp(r uint16) float32 {
|
||||||
|
return float32(0x00FF&r) / 2.0
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,25 +83,52 @@ func main() {
|
|||||||
|
|
||||||
nodeName := "topFloors"
|
nodeName := "topFloors"
|
||||||
|
|
||||||
getTopic := func(zoneNum int, subtopic string) string {
|
getZoneTopic := func(zoneNum int, subtopic string) string {
|
||||||
return fmt.Sprintf("%s/%s/zone%d/%s", *prefix, nodeName, zoneNum, subtopic)
|
return fmt.Sprintf("%s/%s/zone%d/%s", *prefix, nodeName, zoneNum, subtopic)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := watcher.New(&watcher.Config{
|
getSysTopic := func(subtopic string) string {
|
||||||
|
return fmt.Sprintf("%s/%s/sys/%s", *prefix, nodeName, subtopic)
|
||||||
|
}
|
||||||
|
|
||||||
|
getACTopic := func(ac kn.ACMachine, subtopic string) string {
|
||||||
|
return getSysTopic(fmt.Sprintf("ac%d/%s", ac, subtopic))
|
||||||
|
}
|
||||||
|
|
||||||
|
registerReader := buildReader(handler, client)
|
||||||
|
|
||||||
|
zw := watcher.New(&watcher.Config{
|
||||||
Address: 1,
|
Address: 1,
|
||||||
Quantity: 64,
|
Quantity: 64,
|
||||||
RegisterSize: 2,
|
RegisterSize: 2,
|
||||||
SlaveID: 49,
|
SlaveID: 49,
|
||||||
Read: buildReader(handler, client),
|
Read: registerReader,
|
||||||
})
|
})
|
||||||
|
|
||||||
err = w.Poll()
|
sysw := watcher.New(&watcher.Config{
|
||||||
|
Address: kn.REG_AIRFLOW,
|
||||||
|
Quantity: 18,
|
||||||
|
RegisterSize: 2,
|
||||||
|
SlaveID: 49,
|
||||||
|
Read: registerReader,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = zw.Poll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zones, err := getActiveZones(w)
|
err = sysw.Poll()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zones, err := getActiveZones(zw)
|
||||||
|
sys := kn.NewSys(&kn.SysConfig{
|
||||||
|
Watcher: sysw,
|
||||||
|
})
|
||||||
|
|
||||||
connOpts := MQTT.NewClientOptions().AddBroker(*server).SetClientID(*clientid).SetCleanSession(true)
|
connOpts := MQTT.NewClientOptions().AddBroker(*server).SetClientID(*clientid).SetCleanSession(true)
|
||||||
if *username != "" {
|
if *username != "" {
|
||||||
@@ -117,35 +144,92 @@ func main() {
|
|||||||
/* if token := c.Subscribe(*topic, byte(*qos), onMessageReceived); token.Wait() && token.Error() != nil {
|
/* if token := c.Subscribe(*topic, byte(*qos), onMessageReceived); token.Wait() && token.Error() != nil {
|
||||||
panic(token.Error())
|
panic(token.Error())
|
||||||
} */
|
} */
|
||||||
w.TriggerCallbacks()
|
zw.TriggerCallbacks()
|
||||||
}
|
sysw.TriggerCallbacks()
|
||||||
|
bauds, err := sys.GetBaudRate()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading configured serial baud rate: %s", err)
|
||||||
|
}
|
||||||
|
parity, err := sys.GetParity()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading configured serial parity: %s", err)
|
||||||
|
}
|
||||||
|
slaveID, err := sys.GetSlaveID()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading configured modbus slave ID: %s", err)
|
||||||
|
}
|
||||||
|
c.Publish(getSysTopic("serialBaud"), 0, false, strconv.Itoa(bauds))
|
||||||
|
c.Publish(getSysTopic("serialParity"), 0, false, parity)
|
||||||
|
c.Publish(getSysTopic("slaveId"), 0, false, slaveID)
|
||||||
|
|
||||||
|
}
|
||||||
mqttClient := MQTT.NewClient(connOpts)
|
mqttClient := MQTT.NewClient(connOpts)
|
||||||
|
|
||||||
for _, zone := range zones {
|
for _, zone := range zones {
|
||||||
zone := zone
|
zone := zone
|
||||||
zone.OnCurrentTempChange = func(currentTemp float32) {
|
zone.OnCurrentTempChange = func(currentTemp float32) {
|
||||||
mqttClient.Publish(getTopic(zone.ZoneNumber, "currentTemperature"), 0, false, fmt.Sprintf("%g", currentTemp))
|
mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "currentTemp"), 0, false, fmt.Sprintf("%g", currentTemp))
|
||||||
}
|
}
|
||||||
zone.OnTargetTempChange = func(targetTemp float32) {
|
zone.OnTargetTempChange = func(targetTemp float32) {
|
||||||
mqttClient.Publish(getTopic(zone.ZoneNumber, "targetTemperature"), 0, false, fmt.Sprintf("%g", targetTemp))
|
mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp))
|
||||||
}
|
}
|
||||||
zone.OnFanModeChange = func(fanMode kn.FanMode) {
|
zone.OnFanModeChange = func(fanMode kn.FanMode) {
|
||||||
fanModeStr, found := kn.FanModes.GetInverse(fanMode)
|
mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "fanMode"), 0, false, kn.FanMode2Str(fanMode))
|
||||||
if !found {
|
|
||||||
log.Printf("Unknown fan mode %d", fanMode)
|
|
||||||
}
|
|
||||||
mqttClient.Publish(getTopic(zone.ZoneNumber, "fanMode"), 0, false, fanModeStr)
|
|
||||||
}
|
}
|
||||||
zone.OnHvacModeChange = func(hvacMode kn.HvacMode) {
|
zone.OnHvacModeChange = func(hvacMode kn.HvacMode) {
|
||||||
hvacModeStr, found := kn.HvacModes.GetInverse(hvacMode)
|
mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "hvacMode"), 0, false, kn.HvacMode2Str(hvacMode))
|
||||||
if !found {
|
|
||||||
log.Printf("Unknown hvac mode %d", hvacMode)
|
|
||||||
}
|
|
||||||
mqttClient.Publish(getTopic(zone.ZoneNumber, "hvacMode"), 0, false, hvacModeStr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sys.OnACAirflowChange = func(ac kn.ACMachine) {
|
||||||
|
airflow, err := sys.GetAirflow(ac)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading airflow of AC %d: %s", ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getACTopic(ac, "airflow"), 0, false, strconv.Itoa(airflow))
|
||||||
|
}
|
||||||
|
sys.OnACTargetTempChange = func(ac kn.ACMachine) {
|
||||||
|
targetTemp, err := sys.GetMachineTargetTemp(ac)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading target temp of AC %d: %s", ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getACTopic(ac, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp))
|
||||||
|
}
|
||||||
|
sys.OnACTargetFanModeChange = func(ac kn.ACMachine) {
|
||||||
|
targetAirflow, err := sys.GetTargetFanMode(ac)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading target airflow of AC %d: %s", ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getACTopic(ac, "fanMode"), 0, false, kn.FanMode2Str(targetAirflow))
|
||||||
|
}
|
||||||
|
sys.OnEfficiencyChange = func() {
|
||||||
|
efficiency, err := sys.GetEfficiency()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading efficiency value: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getSysTopic("efficiency"), 0, false, strconv.Itoa(efficiency))
|
||||||
|
}
|
||||||
|
sys.OnSystemEnabledChange = func() {
|
||||||
|
enabled, err := sys.GetSystemEnabled()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading enabled value: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getSysTopic("enabled"), 0, false, fmt.Sprintf("%t", enabled))
|
||||||
|
}
|
||||||
|
sys.OnHvacModeChange = func() {
|
||||||
|
mode, err := sys.GetSystemHVACMode()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading hvac mode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mqttClient.Publish(getSysTopic("hvacMode"), 0, false, kn.HvacMode2Str(mode))
|
||||||
|
}
|
||||||
|
|
||||||
if token := mqttClient.Connect(); token.Wait() && token.Error() != nil {
|
if token := mqttClient.Connect(); token.Wait() && token.Error() != nil {
|
||||||
panic(token.Error())
|
panic(token.Error())
|
||||||
} else {
|
} else {
|
||||||
@@ -156,9 +240,14 @@ func main() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
err := w.Poll()
|
err := zw.Poll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error polling modbus: %s\n", err)
|
log.Printf("Error polling zones over modbus: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sysw.Poll()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error polling system config over modbus: %s\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user