package kn import ( "encoding/json" "fmt" "koolnova2mqtt/modbus" "koolnova2mqtt/watcher" "log" "strconv" ) type Publish func(topic string, qos byte, retained bool, payload string) type Subscribe func(topic string, callback func(message string)) error type Config struct { ModuleName string SlaveID byte Publish Publish Subscribe Subscribe TopicPrefix string HassPrefix string Modbus modbus.Modbus } 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 + 1, Watcher: w, }) isPresent := zone.IsPresent() if isPresent { zones = append(zones, zone) } } return zones, nil } func NewBridge(config *Config) *Bridge { b := &Bridge{ Config: *config, } return b } func (b *Bridge) Start() error { zw := watcher.New(&watcher.Config{ Address: FIRST_ZONE_REGISTER, Quantity: TOTAL_ZONE_REGISTERS, RegisterSize: 2, SlaveID: b.SlaveID, Modbus: b.Modbus, }) sysw := watcher.New(&watcher.Config{ Address: FIRST_SYS_REGISTER, Quantity: TOTAL_SYS_REGISTERS, RegisterSize: 2, SlaveID: b.SlaveID, Modbus: b.Modbus, }) b.zw = zw b.sysw = sysw sys := NewSys(&SysConfig{ Watcher: b.sysw, }) log.Printf("Starting bridge for %s\n", b.ModuleName) err := b.Tick() if err != nil { return err } zones, err := getActiveZones(b.zw) log.Printf("%d zones are present in %s\n", len(zones), b.ModuleName) 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.Publish(hvacModeTopic, 0, true, mode) } } } holdModeTopic := b.getSysTopic("holdMode") holdModeSetTopic := holdModeTopic + "/set" for _, zone := range zones { zone := zone currentTempTopic := b.getZoneTopic(zone.ZoneNumber, "currentTemp") targetTempTopic := b.getZoneTopic(zone.ZoneNumber, "targetTemp") targetTempSetTopic := targetTempTopic + "/set" fanModeTopic := b.getZoneTopic(zone.ZoneNumber, "fanMode") fanModeSetTopic := fanModeTopic + "/set" hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode") hvacModeSetTopic := hvacModeTopic + "/set" zone.OnEnabledChange = func() { hvacModeTopic := b.getZoneTopic(zone.ZoneNumber, "hvacMode") var mode string if zone.IsOn() { mode = getHVACMode() } else { mode = HVAC_MODE_OFF } b.Publish(hvacModeTopic, 0, true, mode) } zone.OnCurrentTempChange = func(currentTemp float32) { b.Publish(currentTempTopic, 0, true, fmt.Sprintf("%g", currentTemp)) } zone.OnTargetTempChange = func(targetTemp float32) { b.Publish(targetTempTopic, 0, true, fmt.Sprintf("%g", targetTemp)) } zone.OnFanModeChange = func(fanMode FanMode) { b.Publish(fanModeTopic, 0, true, FanMode2Str(fanMode)) } zone.OnKnModeChange = func(knMode KnMode) { } err = b.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)) if err != nil { log.Printf("Cannot set target temperature to %g in zone %d", targetTemp, zone.ZoneNumber) } }) if err != nil { return err } err = b.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) if err != nil { log.Printf("Cannot set fan mode to %s in zone %d", message, zone.ZoneNumber) } }) if err != nil { return err } err = b.Subscribe(hvacModeSetTopic, func(message string) { if message == HVAC_MODE_OFF { err := zone.SetOn(false) if err != nil { log.Printf("Cannot set zone %d to off", zone.ZoneNumber) } return } 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) if err != nil { log.Printf("Cannot set zone %d to on", zone.ZoneNumber) return } }) if err != nil { return err } err = b.Subscribe(holdModeSetTopic, func(message string) { knMode := sys.GetSystemKNMode() knMode = ApplyHoldMode(knMode, message) err := sys.SetSystemKNMode(knMode) if err != nil { log.Printf("Cannot set knmode mode to %x in zone %d", knMode, zone.ZoneNumber) } }) if err != nil { return err } name := fmt.Sprintf("%s_zone%d", b.ModuleName, zone.ZoneNumber) config := map[string]interface{}{ "name": name, "current_temperature_topic": currentTempTopic, "precision": 0.5, "temperature_state_topic": targetTempTopic, "temperature_command_topic": targetTempSetTopic, "temperature_unit": "C", "temp_step": 0.5, "unique_id": name, "min_temp": 15, "max_temp": 35, "modes": []string{HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF}, "mode_state_topic": hvacModeTopic, "mode_command_topic": hvacModeSetTopic, "fan_modes": []string{"auto", "low", "medium", "high"}, "fan_mode_state_topic": fanModeTopic, "fan_mode_command_topic": fanModeSetTopic, "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) // //[/]/config b.Publish(fmt.Sprintf("%s/climate/%s/zone%d/config", b.HassPrefix, b.ModuleName, zone.ZoneNumber), 0, true, string(configJSON)) } sys.OnACAirflowChange = func(ac ACMachine) { airflow := sys.GetAirflow(ac) b.Publish(b.getACTopic(ac, "airflow"), 0, true, strconv.Itoa(airflow)) } sys.OnACTargetTempChange = func(ac ACMachine) { targetTemp := sys.GetMachineTargetTemp(ac) b.Publish(b.getACTopic(ac, "targetTemp"), 0, true, fmt.Sprintf("%g", targetTemp)) } sys.OnACTargetFanModeChange = func(ac ACMachine) { targetAirflow := sys.GetTargetFanMode(ac) b.Publish(b.getACTopic(ac, "fanMode"), 0, true, FanMode2Str(targetAirflow)) } sys.OnEfficiencyChange = func() { efficiency := sys.GetEfficiency() b.Publish(b.getSysTopic("efficiency"), 0, true, strconv.Itoa(efficiency)) } sys.OnSystemEnabledChange = func() { enabled := sys.GetSystemEnabled() b.Publish(b.getSysTopic("enabled"), 0, true, fmt.Sprintf("%t", enabled)) publishHvacMode() } sys.OnKnModeChange = func() { publishHvacMode() b.Publish(holdModeTopic, 0, true, getHoldMode()) } b.zw.TriggerCallbacks() b.sysw.TriggerCallbacks() b.Publish(b.getSysTopic("serialBaud"), 0, true, strconv.Itoa(sys.GetBaudRate())) b.Publish(b.getSysTopic("serialParity"), 0, true, sys.GetParity()) b.Publish(b.getSysTopic("slaveId"), 0, true, strconv.Itoa(sys.GetSlaveID())) return nil } func (b *Bridge) Tick() error { err := b.zw.Poll() if err != nil { log.Printf("Timeout polling %s zone registers: %s\n", b.ModuleName, err) return err } err = b.sysw.Poll() if err != nil { log.Printf("Timeout polling %s system registers: %s\n", b.ModuleName, err) return err } return nil } 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)) }