diff --git a/kn/constants.go b/kn/constants.go index 5ab2fce..07b41f6 100644 --- a/kn/constants.go +++ b/kn/constants.go @@ -8,13 +8,22 @@ const REG_MODE = 2 const REG_TARGET_TEMP = 3 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 -const FAN_OFF FanMode = 0x00 -const FAN_LOW FanMode = 0x10 -const FAN_MED FanMode = 0x20 -const FAN_HIGH FanMode = 0x30 -const FAN_AUTO FanMode = 0x40 +const FAN_OFF FanMode = 0 +const FAN_LOW FanMode = 1 +const FAN_MED FanMode = 2 +const FAN_HIGH FanMode = 3 +const FAN_AUTO FanMode = 4 type HvacMode byte @@ -39,3 +48,28 @@ var HvacModes = bimap.New(map[interface{}]interface{}{ "underfloor air cooling": MODE_UNDERFLOOR_AIR_COOLING, "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) +} diff --git a/kn/sys.go b/kn/sys.go new file mode 100644 index 0000000..824b50d --- /dev/null +++ b/kn/sys.go @@ -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 +} diff --git a/kn/zone.go b/kn/zone.go index 9c94e05..20b1ab5 100644 --- a/kn/zone.go +++ b/kn/zone.go @@ -93,7 +93,7 @@ func (z *Zone) IsOn() (bool, error) { return false, err } - return r1&uint16(0x1) != 0, nil + return r1&0x1 != 0, nil } func (z *Zone) IsPresent() (bool, error) { @@ -102,7 +102,7 @@ func (z *Zone) IsPresent() (bool, error) { return false, err } - return r1&uint16(0x2) != 0, nil + return r1&0x2 != 0, nil } func (z *Zone) GetCurrentTemperature() (float32, error) { @@ -111,7 +111,7 @@ func (z *Zone) GetCurrentTemperature() (float32, error) { return 0.0, err } - return float32(r4) / 2.0, nil + return reg2temp(r4), nil } func (z *Zone) GetTargetTemperature() (float32, error) { @@ -119,7 +119,7 @@ func (z *Zone) GetTargetTemperature() (float32, error) { if err != nil { return 0.0, err } - return float32(r3) / 2.0, nil + return reg2temp(r3), nil } func (z *Zone) GetFanMode() (FanMode, error) { @@ -127,7 +127,7 @@ func (z *Zone) GetFanMode() (FanMode, error) { if err != nil { return 0, err } - return (FanMode)(r2 & 0x00F0), nil + return (FanMode)(r2&0x00F0) >> 4, nil } func (z *Zone) GetHvacMode() (HvacMode, error) { @@ -137,3 +137,7 @@ func (z *Zone) GetHvacMode() (HvacMode, error) { } return (HvacMode)(r2 & 0x000F), nil } + +func reg2temp(r uint16) float32 { + return float32(0x00FF&r) / 2.0 +} diff --git a/main.go b/main.go index c2cc9c9..772c49c 100644 --- a/main.go +++ b/main.go @@ -83,25 +83,52 @@ func main() { 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) } - 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, Quantity: 64, RegisterSize: 2, 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 { fmt.Println(err) 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) if *username != "" { @@ -117,35 +144,92 @@ func main() { /* if token := c.Subscribe(*topic, byte(*qos), onMessageReceived); token.Wait() && token.Error() != nil { 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) for _, zone := range zones { zone := zone 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) { - 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) { - fanModeStr, found := kn.FanModes.GetInverse(fanMode) - if !found { - log.Printf("Unknown fan mode %d", fanMode) - } - mqttClient.Publish(getTopic(zone.ZoneNumber, "fanMode"), 0, false, fanModeStr) + mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "fanMode"), 0, false, kn.FanMode2Str(fanMode)) } zone.OnHvacModeChange = func(hvacMode kn.HvacMode) { - hvacModeStr, found := kn.HvacModes.GetInverse(hvacMode) - if !found { - log.Printf("Unknown hvac mode %d", hvacMode) - } - mqttClient.Publish(getTopic(zone.ZoneNumber, "hvacMode"), 0, false, hvacModeStr) + mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "hvacMode"), 0, false, kn.HvacMode2Str(hvacMode)) } } + 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 { panic(token.Error()) } else { @@ -156,9 +240,14 @@ func main() { go func() { for range ticker.C { - err := w.Poll() + err := zw.Poll() 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) } } }()