diff --git a/kn/bridge.go b/kn/bridge.go new file mode 100644 index 0000000..e5a5029 --- /dev/null +++ b/kn/bridge.go @@ -0,0 +1,203 @@ +package kn + +import ( + "fmt" + "koolnova2mqtt/watcher" + "log" + "strconv" +) + +type Publish func(topic string, qos byte, retained bool, payload string) + +type Config struct { + ModuleName string + SlaveID byte + Publish Publish + TopicPrefix string + ReadRegister watcher.ReadRegister +} + +type Bridge struct { + Config + zw *watcher.Watcher + sysw *watcher.Watcher + refresh func() +} + +func getActiveZones(w Watcher) ([]*Zone, error) { + var zones []*Zone + + for n := 0; n < NUM_ZONES; n++ { + zone := NewZone(&ZoneConfig{ + ZoneNumber: n, + Watcher: w, + }) + isPresent, err := zone.IsPresent() + if err != nil { + return nil, err + } + if isPresent { + zones = append(zones, zone) + temp, _ := zone.GetCurrentTemperature() + fmt.Printf("Zone %d is present. Temperature %g ºC\n", zone.ZoneNumber, temp) + } + } + return zones, nil +} + +func NewBridge(config *Config) *Bridge { + + zw := watcher.New(&watcher.Config{ + Address: FIRST_ZONE_REGISTER, + Quantity: TOTAL_ZONE_REGISTERS, + RegisterSize: 2, + SlaveID: config.SlaveID, + Read: config.ReadRegister, + }) + + sysw := watcher.New(&watcher.Config{ + Address: FIRST_SYS_REGISTER, + Quantity: TOTAL_SYS_REGISTERS, + RegisterSize: 2, + SlaveID: config.SlaveID, + Read: config.ReadRegister, + }) + + b := &Bridge{ + Config: *config, + zw: zw, + sysw: sysw, + } + + return b +} + +func (b *Bridge) Start() { + sys := NewSys(&SysConfig{ + Watcher: b.sysw, + }) + + err := b.zw.Poll() + if err != nil { + fmt.Println(err) + return + } + + err = b.sysw.Poll() + if err != nil { + fmt.Println(err) + return + } + + zones, err := getActiveZones(b.zw) + + for _, zone := range zones { + zone := zone + zone.OnCurrentTempChange = func(currentTemp float32) { + b.Publish(b.getZoneTopic(zone.ZoneNumber, "currentTemp"), 0, false, fmt.Sprintf("%g", currentTemp)) + } + zone.OnTargetTempChange = func(targetTemp float32) { + b.Publish(b.getZoneTopic(zone.ZoneNumber, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp)) + } + zone.OnFanModeChange = func(fanMode FanMode) { + b.Publish(b.getZoneTopic(zone.ZoneNumber, "fanMode"), 0, false, FanMode2Str(fanMode)) + } + zone.OnHvacModeChange = func(hvacMode HvacMode) { + b.Publish(b.getZoneTopic(zone.ZoneNumber, "hvacMode"), 0, false, HvacMode2Str(hvacMode)) + } + } + + sys.OnACAirflowChange = func(ac ACMachine) { + airflow, err := sys.GetAirflow(ac) + if err != nil { + log.Printf("Error reading airflow of AC %d: %s", ac, err) + return + } + b.Publish(b.getACTopic(ac, "airflow"), 0, false, strconv.Itoa(airflow)) + } + sys.OnACTargetTempChange = func(ac ACMachine) { + targetTemp, err := sys.GetMachineTargetTemp(ac) + if err != nil { + log.Printf("Error reading target temp of AC %d: %s", ac, err) + return + } + b.Publish(b.getACTopic(ac, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp)) + } + sys.OnACTargetFanModeChange = func(ac ACMachine) { + targetAirflow, err := sys.GetTargetFanMode(ac) + if err != nil { + log.Printf("Error reading target airflow of AC %d: %s", ac, err) + return + } + b.Publish(b.getACTopic(ac, "fanMode"), 0, false, FanMode2Str(targetAirflow)) + } + sys.OnEfficiencyChange = func() { + efficiency, err := sys.GetEfficiency() + if err != nil { + log.Printf("Error reading efficiency value: %s", err) + return + } + b.Publish(b.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 + } + b.Publish(b.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 + } + b.Publish(b.getSysTopic("hvacMode"), 0, false, HvacMode2Str(mode)) + } + + b.zw.TriggerCallbacks() + b.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) + } + b.Publish(b.getSysTopic("serialBaud"), 0, false, strconv.Itoa(bauds)) + b.Publish(b.getSysTopic("serialParity"), 0, false, parity) + b.Publish(b.getSysTopic("slaveId"), 0, false, strconv.Itoa(slaveID)) +} + +func (b *Bridge) Tick() { + err := b.zw.Poll() + if err != nil { + fmt.Println(err) + return + } + + err = b.sysw.Poll() + if err != nil { + fmt.Println(err) + return + } +} + +func (b *Bridge) getZoneTopic(zoneNum int, subtopic string) string { + return fmt.Sprintf("%s/%s/zone%d/%s", b.TopicPrefix, b.ModuleName, zoneNum, subtopic) +} + +func (b *Bridge) getSysTopic(subtopic string) string { + return fmt.Sprintf("%s/%s/sys/%s", b.TopicPrefix, b.ModuleName, subtopic) +} + +func (b *Bridge) getACTopic(ac ACMachine, subtopic string) string { + return b.getSysTopic(fmt.Sprintf("ac%d/%s", ac, subtopic)) +} diff --git a/kn/constants.go b/kn/constants.go index 07b41f6..c7c4368 100644 --- a/kn/constants.go +++ b/kn/constants.go @@ -2,6 +2,8 @@ package kn import "koolnova2mqtt/bimap" +const NUM_ZONES = 16 + const REG_PER_ZONE = 4 const REG_ENABLED = 1 const REG_MODE = 2 @@ -17,6 +19,11 @@ const REG_EFFICIENCY = 79 const REG_SYSTEM_ENABLED = 81 const REG_SYS_HVAC_MODE = 82 +const FIRST_ZONE_REGISTER = REG_ENABLED +const TOTAL_ZONE_REGISTERS = NUM_ZONES * REG_PER_ZONE +const FIRST_SYS_REGISTER = REG_AIRFLOW +const TOTAL_SYS_REGISTERS = 18 + type FanMode byte const FAN_OFF FanMode = 0 diff --git a/main.go b/main.go index 772c49c..5a48684 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,9 @@ import ( "log" "os" "os/signal" + "regexp" "strconv" + "strings" "syscall" "time" @@ -17,8 +19,6 @@ import ( "github.com/goburrow/modbus" ) -const NUM_ZONES = 16 - func buildReader(handler *modbus.RTUClientHandler, client modbus.Client) watcher.ReadRegister { return func(slaveID byte, address uint16, quantity uint16) (results []byte, err error) { handler.SlaveId = slaveID @@ -27,31 +27,23 @@ func buildReader(handler *modbus.RTUClientHandler, client modbus.Client) watcher } } -func getActiveZones(w kn.Watcher) ([]*kn.Zone, error) { - var zones []*kn.Zone - - for n := 0; n < NUM_ZONES; n++ { - zone := kn.NewZone(&kn.ZoneConfig{ - ZoneNumber: n, - Watcher: w, - }) - isPresent, err := zone.IsPresent() - if err != nil { - return nil, err - } - if isPresent { - zones = append(zones, zone) - temp, _ := zone.GetCurrentTemperature() - fmt.Printf("Zone %d is present. Temperature %g ºC\n", zone.ZoneNumber, temp) - } +func generateNodeName(slaveID string, port string) string { + reg, err := regexp.Compile("[^a-zA-Z0-9]+") + if err != nil { + log.Fatal(err) } - return zones, nil + hostname, _ := os.Hostname() + + port = strings.Replace(port, "/dev/", "", -1) + port = reg.ReplaceAllString(port, "") + return strings.ToLower(fmt.Sprintf("%s_%s_%s", hostname, port, slaveID)) + } func main() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) + ctrlC := make(chan os.Signal, 1) + signal.Notify(ctrlC, os.Interrupt, syscall.SIGTERM) hostname, _ := os.Hostname() @@ -62,15 +54,21 @@ func main() { username := flag.String("username", "", "A username to authenticate to the MQTT server") password := flag.String("password", "", "Password to match username") prefix := flag.String("prefix", "koolnova2mqtt", "MQTT topic root where to publish/read topics") + modbusPort := flag.String("modbusPort", "/dev/ttyUSB0", "Serial port where modbus hardware is connected") + modbusPortBaudRate := flag.Int("modbusRate", 9600, "Modbus port data rate") + modbusDataBits := flag.Int("modbusDataBits", 8, "Modbus port data bits") + modbusPortParity := flag.String("modbusParity", "E", "N - None, E - Even, O - Odd (default E) (The use of no parity requires 2 stop bits.)") + modbusStopBits := flag.Int("modbusStopBits", 1, "Modbus port stop bits") + modbusSlaveList := flag.String("modbusSlaveIDs", "49", "Comma-separated list of modbus slave IDs to manage") + modbusSlaveNames := flag.String("modbusSlaveNames", "", "Comma-separated list of modbus slave names. Defaults to 'slave#'") + flag.Parse() - // Modbus RTU/ASCII - handler := modbus.NewRTUClientHandler("/dev/remserial1") - handler.BaudRate = 9600 - handler.DataBits = 8 - handler.Parity = "E" - handler.StopBits = 1 - handler.SlaveId = 49 + handler := modbus.NewRTUClientHandler(*modbusPort) + handler.BaudRate = *modbusPortBaudRate + handler.DataBits = *modbusDataBits + handler.Parity = *modbusPortParity + handler.StopBits = *modbusStopBits handler.Timeout = 5 * time.Second err := handler.Connect() @@ -79,57 +77,46 @@ func main() { } defer handler.Close() - client := modbus.NewClient(handler) + modbusClient := modbus.NewClient(handler) - nodeName := "topFloors" + registerReader := buildReader(handler, modbusClient) - getZoneTopic := func(zoneNum int, subtopic string) string { - return fmt.Sprintf("%s/%s/zone%d/%s", *prefix, nodeName, zoneNum, subtopic) + var mqttClient MQTT.Client + publish := func(topic string, qos byte, retained bool, payload string) { + mqttClient.Publish(topic, qos, retained, payload) } - getSysTopic := func(subtopic string) string { - return fmt.Sprintf("%s/%s/sys/%s", *prefix, nodeName, subtopic) + var snameList []string + slist := strings.Split(*modbusSlaveList, ",") + + if *modbusSlaveNames == "" { + for _, slaveIDStr := range slist { + snameList = append(snameList, generateNodeName(slaveIDStr, *modbusPort)) + } + } else { + snameList = strings.Split(*modbusSlaveNames, ",") + if len(slist) != len(snameList) { + log.Fatalf("modbusSlaveIDs and modbusSlaveNames lists must have the same length") + } } - getACTopic := func(ac kn.ACMachine, subtopic string) string { - return getSysTopic(fmt.Sprintf("ac%d/%s", ac, subtopic)) + var bridges []*kn.Bridge + for i, slaveIDStr := range slist { + slaveID, err := strconv.Atoi(slaveIDStr) + slaveName := snameList[i] + if err != nil { + log.Fatalf("Error parsing slaveID list") + } + bridge := kn.NewBridge(&kn.Config{ + ModuleName: slaveName, + SlaveID: byte(slaveID), + Publish: publish, + TopicPrefix: *prefix, + ReadRegister: registerReader, + }) + bridges = append(bridges, bridge) } - registerReader := buildReader(handler, client) - - zw := watcher.New(&watcher.Config{ - Address: 1, - Quantity: 64, - RegisterSize: 2, - SlaveID: 49, - Read: registerReader, - }) - - 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 - } - - 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 != "" { connOpts.SetUsername(*username) @@ -139,96 +126,13 @@ func main() { } tlsConfig := &tls.Config{InsecureSkipVerify: true, ClientAuth: tls.NoClientCert} connOpts.SetTLSConfig(tlsConfig) - connOpts.OnConnect = func(c MQTT.Client) { - /* if token := c.Subscribe(*topic, byte(*qos), onMessageReceived); token.Wait() && token.Error() != nil { - panic(token.Error()) - } */ - 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(getZoneTopic(zone.ZoneNumber, "currentTemp"), 0, false, fmt.Sprintf("%g", currentTemp)) - } - zone.OnTargetTempChange = func(targetTemp float32) { - mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp)) - } - zone.OnFanModeChange = func(fanMode kn.FanMode) { - mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "fanMode"), 0, false, kn.FanMode2Str(fanMode)) - } - zone.OnHvacModeChange = func(hvacMode kn.HvacMode) { - mqttClient.Publish(getZoneTopic(zone.ZoneNumber, "hvacMode"), 0, false, kn.HvacMode2Str(hvacMode)) + for _, b := range bridges { + b.Start() } } - 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)) - } + mqttClient = MQTT.NewClient(connOpts) if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { panic(token.Error()) @@ -240,18 +144,12 @@ func main() { go func() { for range ticker.C { - err := zw.Poll() - if err != nil { - 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) + for _, b := range bridges { + b.Tick() } } }() - <-c + <-ctrlC } diff --git a/watcher/watcher.go b/watcher/watcher.go index c9506c7..a54da77 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -15,7 +15,7 @@ type Config struct { RegisterSize int } -type watcher struct { +type Watcher struct { Config state []byte callbacks map[uint16]func(address uint16) @@ -25,18 +25,18 @@ var ErrIncorrectRegisterSize = errors.New("Incorrect register size") var ErrAddressOutOfRange = errors.New("Register address out of range") var ErrUninitialized = errors.New("State uninitialized. Call Poll() first.") -func New(config *Config) *watcher { - return &watcher{ +func New(config *Config) *Watcher { + return &Watcher{ Config: *config, callbacks: make(map[uint16]func(address uint16)), } } -func (w *watcher) RegisterCallback(address uint16, callback func(address uint16)) { +func (w *Watcher) RegisterCallback(address uint16, callback func(address uint16)) { w.callbacks[address] = callback } -func (w *watcher) Poll() error { +func (w *Watcher) Poll() error { newState, err := w.Read(w.SlaveID, w.Address, w.Quantity) if err != nil { return err @@ -72,7 +72,7 @@ func (w *watcher) Poll() error { return nil } -func (w *watcher) ReadRegister(address uint16) (value []byte, err error) { +func (w *Watcher) ReadRegister(address uint16) (value []byte, err error) { if address < w.Address || address > w.Address+uint16(w.Quantity) { return nil, ErrAddressOutOfRange } @@ -84,7 +84,7 @@ func (w *watcher) ReadRegister(address uint16) (value []byte, err error) { } -func (w *watcher) TriggerCallbacks() { +func (w *Watcher) TriggerCallbacks() { for address, callback := range w.callbacks { callback(address) }