first hass integration

This commit is contained in:
Javier Peletier
2020-12-15 00:31:31 +01:00
parent bb7dba5e85
commit 098f5c1d61
6 changed files with 184 additions and 215 deletions

View File

@@ -1,9 +1,9 @@
package kn
import (
"encoding/json"
"fmt"
"koolnova2mqtt/watcher"
"log"
"strconv"
)
@@ -14,6 +14,7 @@ type Config struct {
SlaveID byte
Publish Publish
TopicPrefix string
HassPrefix string
ReadRegister watcher.ReadRegister
}
@@ -32,13 +33,10 @@ func getActiveZones(w Watcher) ([]*Zone, error) {
ZoneNumber: n,
Watcher: w,
})
isPresent, err := zone.IsPresent()
if err != nil {
return nil, err
}
isPresent := zone.IsPresent()
if isPresent {
zones = append(zones, zone)
temp, _ := zone.GetCurrentTemperature()
temp := zone.GetCurrentTemperature()
fmt.Printf("Zone %d is present. Temperature %g ºC\n", zone.ZoneNumber, temp)
}
}
@@ -89,91 +87,123 @@ func (b *Bridge) Start() {
return
}
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"
}
zones, err := getActiveZones(b.zw)
var hvacModes []string
for k, _ := range KnModes.GetForwardMap() {
hvacModes = append(hvacModes, k.(string))
}
hvacModeTopic := b.getSysTopic("hvacMode")
hvacModeTopicSet := hvacModeTopic + "/set"
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"
zone.OnCurrentTempChange = func(currentTemp float32) {
b.Publish(b.getZoneTopic(zone.ZoneNumber, "currentTemp"), 0, false, fmt.Sprintf("%g", currentTemp))
b.Publish(currentTempTopic, 0, true, fmt.Sprintf("%g", currentTemp))
}
zone.OnTargetTempChange = func(targetTemp float32) {
b.Publish(b.getZoneTopic(zone.ZoneNumber, "targetTemp"), 0, false, fmt.Sprintf("%g", targetTemp))
b.Publish(targetTempTopic, 0, true, fmt.Sprintf("%g", targetTemp))
}
zone.OnFanModeChange = func(fanMode FanMode) {
b.Publish(b.getZoneTopic(zone.ZoneNumber, "fanMode"), 0, false, FanMode2Str(fanMode))
b.Publish(fanModeTopic, 0, true, FanMode2Str(fanMode))
}
zone.OnHvacModeChange = func(hvacMode HvacMode) {
b.Publish(b.getZoneTopic(zone.ZoneNumber, "hvacMode"), 0, false, HvacMode2Str(hvacMode))
zone.OnKnModeChange = func(knMode KnMode) {
}
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": hvacModeTopicSet,
"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)
// <discovery_prefix>/<component>/[<node_id>/]<object_id>/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, 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))
airflow := sys.GetAirflow(ac)
b.Publish(b.getACTopic(ac, "airflow"), 0, true, 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))
targetTemp := sys.GetMachineTargetTemp(ac)
b.Publish(b.getACTopic(ac, "targetTemp"), 0, true, 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))
targetAirflow := sys.GetTargetFanMode(ac)
b.Publish(b.getACTopic(ac, "fanMode"), 0, true, 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))
efficiency := sys.GetEfficiency()
b.Publish(b.getSysTopic("efficiency"), 0, true, 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))
enabled := sys.GetSystemEnabled()
b.Publish(b.getSysTopic("enabled"), 0, true, fmt.Sprintf("%t", enabled))
b.Publish(hvacModeTopic, 0, true, getHVACMode())
}
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))
sys.OnKnModeChange = func() {
b.Publish(hvacModeTopic, 0, true, getHVACMode())
b.Publish(holdModeTopic, 0, true, getHoldMode())
}
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))
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()))
}
func (b *Bridge) Tick() {

View File

@@ -17,7 +17,7 @@ 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
const REG_SYS_KN_MODE = 82
const FIRST_ZONE_REGISTER = REG_ENABLED
const TOTAL_ZONE_REGISTERS = NUM_ZONES * REG_PER_ZONE
@@ -32,13 +32,13 @@ const FAN_MED FanMode = 2
const FAN_HIGH FanMode = 3
const FAN_AUTO FanMode = 4
type HvacMode byte
type KnMode byte
const MODE_AIR_COOLING HvacMode = 0x01
const MODE_AIR_HEATING HvacMode = 0x02
const MODE_UNDERFLOOR_HEATING HvacMode = 0x04
const MODE_UNDERFLOOR_AIR_COOLING HvacMode = 0x05
const MODE_UNDERFLOOR_AIR_HEATING HvacMode = 0x06
const MODE_AIR_COOLING KnMode = 0x01
const MODE_AIR_HEATING KnMode = 0x02
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,
@@ -48,7 +48,7 @@ var FanModes = bimap.New(map[interface{}]interface{}{
"auto": FAN_AUTO,
})
var HvacModes = bimap.New(map[interface{}]interface{}{
var KnModes = bimap.New(map[interface{}]interface{}{
"air cooling": MODE_AIR_COOLING,
"air heating": MODE_AIR_HEATING,
"underfloor heating": MODE_UNDERFLOOR_HEATING,
@@ -56,6 +56,14 @@ var HvacModes = bimap.New(map[interface{}]interface{}{
"underfloor air heating": MODE_UNDERFLOOR_AIR_HEATING,
})
const HOLD_MODE_UNDERFLOOR_ONLY = "underfloor only"
const HOLD_MODE_FAN_ONLY = "fan only"
const HOLD_MODE_UNDERFLOOR_AND_FAN = "underfloor and fan"
const HVAC_MODE_OFF = "off"
const HVAC_MODE_COOL = "cool"
const HVAC_MODE_HEAT = "heat"
type ACMachine int
const ACMachines = 4
@@ -73,8 +81,8 @@ func FanMode2Str(fm FanMode) string {
return st.(string)
}
func HvacMode2Str(hm HvacMode) string {
st, ok := HvacModes.GetInverse(hm)
func KnMode2Str(hm KnMode) string {
st, ok := KnModes.GetInverse(hm)
if !ok {
st = "unknown"
}

106
kn/sys.go
View File

@@ -13,7 +13,7 @@ type Sys struct {
OnACTargetFanModeChange func(ac ACMachine)
OnEfficiencyChange func()
OnSystemEnabledChange func()
OnHvacModeChange func()
OnKnModeChange func()
}
var ErrUnknownSerialConfig = errors.New("Uknown serial configuration")
@@ -54,103 +54,73 @@ func NewSys(config *SysConfig) *Sys {
}
})
s.Watcher.RegisterCallback(REG_SYS_HVAC_MODE, func(address uint16) {
if s.OnHvacModeChange != nil {
s.OnHvacModeChange()
s.Watcher.RegisterCallback(REG_SYS_KN_MODE, func(address uint16) {
if s.OnKnModeChange != nil {
s.OnKnModeChange()
}
})
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) ReadRegister(n int) int {
r := s.Watcher.ReadRegister(uint16(n))
return int(r[1])
}
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) GetAirflow(ac ACMachine) int {
r := s.ReadRegister(REG_AIRFLOW + int(ac) - 1)
return r
}
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) 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, 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) GetTargetFanMode(ac ACMachine) FanMode {
r := s.ReadRegister(REG_AC_TARGET_FAN_MODE + int(ac) - 1)
return FanMode(r)
}
func (s *Sys) GetBaudRate() (int, error) {
r, err := s.ReadRegister(REG_SERIAL_CONFIG)
if err != nil {
return 0, err
}
func (s *Sys) GetBaudRate() int {
r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r {
case 2, 6:
return 9600, nil
return 9600
case 3, 7:
return 19200, nil
return 19200
}
return 0, ErrUnknownSerialConfig
return 0
}
func (s *Sys) GetParity() (string, error) {
r, err := s.ReadRegister(REG_SERIAL_CONFIG)
if err != nil {
return "", err
}
func (s *Sys) GetParity() string {
r := s.ReadRegister(REG_SERIAL_CONFIG)
switch r {
case 2, 3:
return "even", nil
return "even"
case 6, 7:
return "none", nil
return "none"
}
return "", ErrUnknownSerialConfig
return "unknown"
}
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) GetSlaveID() int {
r := s.ReadRegister(REG_SLAVE_ID)
return r
}
func (s *Sys) GetEfficiency() (int, error) {
r, err := s.ReadRegister(REG_EFFICIENCY)
if err != nil {
return 0, err
}
return r, nil
func (s *Sys) GetEfficiency() int {
r := s.ReadRegister(REG_EFFICIENCY)
return r
}
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) GetSystemEnabled() bool {
r := s.ReadRegister(REG_SYSTEM_ENABLED)
return r != 0
}
func (s *Sys) GetSystemHVACMode() (HvacMode, error) {
r, err := s.ReadRegister(REG_SYS_HVAC_MODE)
if err != nil {
return 0, err
}
return HvacMode(r), nil
func (s *Sys) GetSystemKNMode() KnMode {
r := s.ReadRegister(REG_SYS_KN_MODE)
return KnMode(r)
}

View File

@@ -2,11 +2,10 @@ package kn
import (
"encoding/binary"
"log"
)
type Watcher interface {
ReadRegister(address uint16) (value []byte, err error)
ReadRegister(address uint16) (value []byte)
RegisterCallback(address uint16, callback func(address uint16))
}
@@ -20,7 +19,7 @@ type Zone struct {
OnCurrentTempChange func(newTemp float32)
OnTargetTempChange func(newTemp float32)
OnFanModeChange func(newMode FanMode)
OnHvacModeChange func(newMode HvacMode)
OnKnModeChange func(newMode KnMode)
}
func NewZone(config *ZoneConfig) *Zone {
@@ -32,11 +31,7 @@ func NewZone(config *ZoneConfig) *Zone {
return
}
temp, err := z.GetCurrentTemperature()
if err != nil {
log.Printf("Cannot read current temperature: %s\n", err)
return
}
temp := z.GetCurrentTemperature()
z.OnCurrentTempChange(temp)
})
z.RegisterCallback(REG_TARGET_TEMP, func() {
@@ -44,29 +39,17 @@ func NewZone(config *ZoneConfig) *Zone {
return
}
temp, err := z.GetTargetTemperature()
if err != nil {
log.Printf("Cannot read target temperature: %s\n", err)
return
}
temp := z.GetTargetTemperature()
z.OnTargetTempChange(temp)
})
z.RegisterCallback(REG_MODE, func() {
fanMode, err := z.GetFanMode()
if err != nil {
log.Printf("Cannot read fan mode: %s\n", err)
return
}
hvacMode, err := z.GetHvacMode()
if err != nil {
log.Printf("Cannot hvac mode: %s\n", err)
return
}
fanMode := z.GetFanMode()
hvacMode := z.GetHvacMode()
if z.OnFanModeChange != nil {
z.OnFanModeChange(fanMode)
}
if z.OnHvacModeChange != nil {
z.OnHvacModeChange(hvacMode)
if z.OnKnModeChange != nil {
z.OnKnModeChange(hvacMode)
}
})
return z
@@ -78,64 +61,40 @@ func (z *Zone) RegisterCallback(num int, f func()) {
})
}
func (z *Zone) ReadRegister(num int) (uint16, error) {
func (z *Zone) ReadRegister(num int) uint16 {
b, err := z.Watcher.ReadRegister(uint16(z.ZoneNumber*REG_PER_ZONE + num))
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b), nil
b := z.Watcher.ReadRegister(uint16(z.ZoneNumber*REG_PER_ZONE + num))
return binary.BigEndian.Uint16(b)
}
func (z *Zone) IsOn() (bool, error) {
r1, err := z.ReadRegister(REG_ENABLED)
if err != nil {
return false, err
}
return r1&0x1 != 0, nil
func (z *Zone) IsOn() bool {
r1 := z.ReadRegister(REG_ENABLED)
return r1&0x1 != 0
}
func (z *Zone) IsPresent() (bool, error) {
r1, err := z.ReadRegister(REG_ENABLED)
if err != nil {
return false, err
}
return r1&0x2 != 0, nil
func (z *Zone) IsPresent() bool {
r1 := z.ReadRegister(REG_ENABLED)
return r1&0x2 != 0
}
func (z *Zone) GetCurrentTemperature() (float32, error) {
r4, err := z.ReadRegister(REG_CURRENT_TEMP)
if err != nil {
return 0.0, err
}
return reg2temp(r4), nil
func (z *Zone) GetCurrentTemperature() float32 {
r4 := z.ReadRegister(REG_CURRENT_TEMP)
return reg2temp(r4)
}
func (z *Zone) GetTargetTemperature() (float32, error) {
r3, err := z.ReadRegister(REG_TARGET_TEMP)
if err != nil {
return 0.0, err
}
return reg2temp(r3), nil
func (z *Zone) GetTargetTemperature() float32 {
r3 := z.ReadRegister(REG_TARGET_TEMP)
return reg2temp(r3)
}
func (z *Zone) GetFanMode() (FanMode, error) {
r2, err := z.ReadRegister(REG_MODE)
if err != nil {
return 0, err
}
return (FanMode)(r2&0x00F0) >> 4, nil
func (z *Zone) GetFanMode() FanMode {
r2 := z.ReadRegister(REG_MODE)
return (FanMode)(r2&0x00F0) >> 4
}
func (z *Zone) GetHvacMode() (HvacMode, error) {
r2, err := z.ReadRegister(REG_MODE)
if err != nil {
return 0, err
}
return (HvacMode)(r2 & 0x000F), nil
func (z *Zone) GetHvacMode() KnMode {
r2 := z.ReadRegister(REG_MODE)
return (KnMode)(r2 & 0x000F)
}
func reg2temp(r uint16) float32 {

View File

@@ -54,6 +54,7 @@ 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")
hassPrefix := flag.String("hassPrefix", "homeassistant", "Home assistant discovery prefix")
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")
@@ -112,6 +113,7 @@ func main() {
SlaveID: byte(slaveID),
Publish: publish,
TopicPrefix: *prefix,
HassPrefix: *hassPrefix,
ReadRegister: registerReader,
})
bridges = append(bridges, bridge)

View File

@@ -72,15 +72,15 @@ 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) {
if address < w.Address || address > w.Address+uint16(w.Quantity) {
return nil, ErrAddressOutOfRange
panic(ErrAddressOutOfRange)
}
if w.state == nil {
return nil, ErrUninitialized
panic(ErrUninitialized)
}
registerOffset := int(address-w.Address) * w.RegisterSize
return w.state[registerOffset : registerOffset+w.RegisterSize], nil
return w.state[registerOffset : registerOffset+w.RegisterSize]
}