enervent-ctrl/enervent-ctrl-go/pingvinKL/pingvinKL.go

383 lines
12 KiB
Go

package pingvinKL
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/goburrow/modbus"
)
// single coil data
type pingvinCoil struct {
Address int `json:"address"`
Symbol string `json:"symbol"`
Value bool `json:"value"`
Description string `json:"description"`
Reserved bool `json:"reserved"`
}
// unit modbus data
type PingvinKL struct {
Coils []pingvinCoil
Registers []pingvinRegister
Status pingvinStatus
buslock *sync.Mutex
statuslock *sync.Mutex
debug bool
}
// single register data
type pingvinRegister struct {
Address int `json:"address"`
Symbol string `json:"symbol"`
Value int `json:"value"`
Bitfield string `json:"bitfield"`
Type string `json:"type"`
Description string `json:"description"`
Reserved bool `json:"reserved"`
Multiplier int `json:"multiplier"`
}
type pingvinVentInfo struct {
Roomtemp1 int `json:"room_temp1"` // Room temperature at panel 1
SupplyHeated int `json:"supply_heated"` // Temperature of supply air after heating
SupplyHrc int `json:"supply_hrc"` // Temperature of supply air after heat recovery
SupplyIntake int `json:"supply_intake"` // Temperature of outside air at device
SupplyIntake24h int `json:"supply_intake_24h"` // 24h avg of outside air humidity
SupplyHum int `json:"supply_hum"` // Supply air humidity
ExtractIntake int `json:"extract_intake"` // Temperature of extract air
ExtractHrc int `json:"extract_hrc"` // Temperature of extract air after heat recovery
ExtractHum int `json:"extract_hum"` // Relative humidity of extract air
ExtractHum48h int `json:"extract_hum_48h"` // 48h avg extract air humidity
}
type pingvinStatus struct {
HeaterPct int `json:"heater_pct"` // After heater valve position
HrcPct int `json:"hrc_pct"` // Heat recovery turn speed
TempSetting int `json:"temp_setting"` // Requested room temperature
FanPct int `json:"fan_pct"` // Circulation fan setting
VentInfo pingvinVentInfo `json:"vent_info"` // Measurements
HrcEffIn int `json:"hrc_efficiency_in"` // Calculated HRC efficiency, intake
HrcEffEx int `json:"hrc_efficiency_ex"` // Calculated HRC efficiency, extract
OpMode string `json:"op_mode"` // Current operating mode, text representation
DaysUntilService int `json:"days_until_service"` // Days until next filter service
Uptime string `json:"uptime"` // Unit uptime
SystemTime string `json:"system_time"` // Time and date in unit
}
func newCoil(address string, symbol string, description string) pingvinCoil {
addr, err := strconv.Atoi(address)
if err != nil {
log.Fatal("newCoil: Atoi: ", err)
}
reserved := symbol == "-" && description == "-"
coil := pingvinCoil{addr, symbol, false, description, reserved}
return coil
}
func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
addr, err := strconv.Atoi(address)
if err != nil {
log.Fatal("newRegister: Atoi(address): ", err)
}
multipl := 1
if len(multiplier) > 0 {
multipl, err = strconv.Atoi(multiplier)
if err != nil {
log.Fatal("newRegister: Atoi(multiplier): ", err)
}
}
reserved := symbol == "Reserved" && description == "Reserved"
register := pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl}
return register
}
// read a CSV file containing data for coils or registers
func readCsvLines(file string) [][]string {
delim := ";"
data := [][]string{}
csv, err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer csv.Close()
scanner := bufio.NewScanner(csv)
for scanner.Scan() {
elements := strings.Split(scanner.Text(), delim)
data = append(data, elements)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return data
}
// Configure the modbus parameters
func (p PingvinKL) getHandler() *modbus.RTUClientHandler {
// TODO: read configuration from file, hardcoded for now
handler := modbus.NewRTUClientHandler("/dev/ttyS0")
handler.BaudRate = 19200
handler.DataBits = 8
handler.Parity = "N"
handler.StopBits = 1
handler.SlaveId = 1
handler.Timeout = 1500 * time.Millisecond
return handler
}
func (p *PingvinKL) updateCoils() {
handler := p.getHandler()
p.buslock.Lock()
err := handler.Connect()
if err != nil {
log.Fatal("updateCoils: handler.Connect: ", err)
}
defer handler.Close()
client := modbus.NewClient(handler)
results, err := client.ReadCoils(0, uint16(len(p.Coils)))
if err != nil {
log.Fatal("updateCoils: client.ReadCoils: ", err)
}
p.buslock.Unlock()
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc.
// Within each byte, LSB represents the lowest n coil while MSB is the highest
// e.g. reading the first 8 coils might return a byte array of length 1, with the following:
// [4], which is 00000100, meaning all other coils are 0 except coil #2 (3rd coil)
//
k := 0 // pingvinCoil index
for i := 0; i < len(results); i++ { // loop through the byte array
for j := 0; j < 8; j++ {
// Here we loop through each bit in the byte, shifting right
// and checking if the LSB after the shift is 1 with a bitwise AND
// A coil value of 1 means on/true/yes, so == 1 returns the bool value
// for each coil
p.Coils[k].Value = (results[i] >> j & 0x1) == 1
k++
}
}
}
func (p *PingvinKL) updateRegisters() {
handler := p.getHandler()
p.buslock.Lock()
err := handler.Connect()
if err != nil {
log.Fatal("updateRegisters: handler.Connect: ", err)
}
defer handler.Close()
client := modbus.NewClient(handler)
regs := len(p.Registers)
k := 0
// modbus.ReadHoldingRegisters can read 125 regs at a time, so first we loop
// until all the values are fethed, increasing the value of k for each register
// When there are less than 125 registers to go, it's the last pass
for k < regs {
r := 125
if regs-k < 125 {
r = regs - k
}
results := []byte{}
for retries := 0; retries < 5; retries++ {
results, err = client.ReadHoldingRegisters(uint16(k), uint16(r))
if len(results) > 0 {
break
} else if retries == 4 {
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
} else if err != nil {
log.Println("WARNING: updateRegisters: client.ReadHoldingRegisters: ", err)
}
}
// The values represent 16 bit integers, but modbus works with bytes
// Each even byte of the returned []byte is the 8 MSBs of a new 16-bit
// value, so for each even byte in the reponse slice we bitshift the byte
// left by 8, then add the odd byte as is to the shifted 16-bit value
msb := true
value := int16(0)
uvalue := uint16(0)
for i := 0; i < len(results); i++ {
if msb {
value = int16(results[i]) << 8
uvalue = uint16(results[i]) << 8
} else {
value += int16(results[i])
uvalue += uint16(results[i])
if p.Registers[k].Type == "int16" {
p.Registers[k].Value = int(value)
}
if p.Registers[k].Type == "uint16" || p.Registers[k].Type == "enumeration" {
p.Registers[k].Value = int(uvalue)
}
if p.Registers[k].Type == "bitfield" {
p.Registers[k].Value = int(value)
// p.Registers[k].Bitfield = fmt.Sprintf("%16b", uvalue)
p.Registers[k].Bitfield = ""
for i := 16; i >= 0; i-- {
x := 0
if p.Registers[k].Value>>i&0x1 == 1 {
x = 1
}
p.Registers[k].Bitfield = fmt.Sprintf("%s%s", p.Registers[k].Bitfield, strconv.Itoa(x))
}
}
k++
}
msb = !msb
}
}
p.buslock.Unlock()
}
func (p *PingvinKL) Update() {
p.updateCoils()
p.updateRegisters()
p.populateStatus()
}
func (p PingvinKL) ReadCoil(n uint16) []byte {
handler := p.getHandler()
p.buslock.Lock()
err := handler.Connect()
if err != nil {
log.Fatal("ReadCoil: handler.Connect: ", err)
}
defer handler.Close()
client := modbus.NewClient(handler)
results, err := client.ReadCoils(n, 1)
p.buslock.Unlock()
if err != nil {
log.Fatal("ReadCoil: client.ReadCoils: ", err)
}
p.Coils[n].Value = results[0] == 1
return results
}
func (p *PingvinKL) WriteCoil(n uint16, val bool) bool {
handler := p.getHandler()
p.buslock.Lock()
err := handler.Connect()
if err != nil {
log.Println("WARNING: WriteCoil: failed to connect handler")
return false
}
defer handler.Close()
var value uint16 = 0
if val {
value = 0xff00
}
client := modbus.NewClient(handler)
results, err := client.WriteSingleCoil(n, value)
p.buslock.Unlock()
if err != nil {
log.Println("ERROR: WriteCoil: ", err)
}
if (val && results[0] == 255) || (!val && results[0] == 0) {
log.Println("WriteCoil: wrote coil", n, "to value", val)
} else {
log.Println("ERROR: WriteCoil: failed to write coil")
return false
}
p.ReadCoil(n)
return true
}
func (p *PingvinKL) populateStatus() {
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
if hpct > 100 {
p.Status.HeaterPct = hpct - 100
p.Status.HrcPct = 100
} else {
p.Status.HeaterPct = 0
p.Status.HrcPct = hpct
}
p.Status.TempSetting = p.Registers[135].Value / p.Registers[135].Multiplier
p.Status.FanPct = p.Registers[774].Value / p.Registers[774].Multiplier
p.Status.VentInfo.Roomtemp1 = p.Registers[1].Value / p.Registers[1].Multiplier
p.Status.VentInfo.SupplyHeated = p.Registers[8].Value / p.Registers[8].Multiplier
p.Status.VentInfo.SupplyHrc = p.Registers[7].Value / p.Registers[7].Multiplier
p.Status.VentInfo.SupplyIntake = p.Registers[6].Value / p.Registers[6].Multiplier
p.Status.VentInfo.SupplyIntake24h = p.Registers[134].Value / p.Registers[134].Multiplier
p.Status.VentInfo.SupplyHum = p.Registers[36].Value / p.Registers[46].Multiplier
p.Status.VentInfo.ExtractIntake = p.Registers[10].Value / p.Registers[10].Multiplier
p.Status.VentInfo.ExtractHrc = p.Registers[9].Value / p.Registers[9].Multiplier
p.Status.VentInfo.ExtractHum = p.Registers[28].Value / p.Registers[28].Multiplier
p.Status.VentInfo.ExtractHum48h = p.Registers[50].Value / p.Registers[50].Multiplier
p.Status.HrcEffIn = p.Registers[29].Value / p.Registers[29].Multiplier
p.Status.HrcEffEx = p.Registers[30].Value / p.Registers[30].Multiplier
p.Status.OpMode = parseStatus(p.Registers[44].Value)
// TODO: Alarms, n of alarms
p.Status.DaysUntilService = p.Registers[538].Value / p.Registers[538].Multiplier
// TODO: Uptime & date in separate functions
}
func parseStatus(value int) string {
val := int16(value)
pingvinStatuses := []string{
"Max cooling",
"Max heating",
"Stopped by alarm",
"Stopped by user",
"Away",
"reserved",
"Temperature boost",
"CO2 boost",
"RH boost",
"Manual boost",
"Overpressure",
"Cooker hood mode",
"Central vac mode",
"Electric heater cooloff",
"Summer night cooling",
"HRC defrost",
}
for i := 0; i < 15; i++ {
if val>>i&0x1 == 1 {
return pingvinStatuses[i]
}
}
return "Normal"
}
func (p *PingvinKL) Monitor(interval int) {
for {
time.Sleep(time.Duration(interval) * time.Second)
if p.debug {
log.Println("DEBUG: Updating values")
}
p.Update()
if p.debug {
log.Println("DEBUG: coils:", p.Coils)
log.Println("DEBUG: registers:", p.Registers)
}
}
}
// create a PingvinKL struct, read coils and registers from CSVs
func New(debug bool) PingvinKL {
pingvin := PingvinKL{}
pingvin.debug = debug
pingvin.buslock = &sync.Mutex{}
log.Println("Parsing coil data...")
coilData := readCsvLines("coils.csv")
for i := 0; i < len(coilData); i++ {
pingvin.Coils = append(pingvin.Coils, newCoil(coilData[i][0], coilData[i][1], coilData[i][2]))
}
log.Println("Parsed", len(pingvin.Coils), "coils")
log.Println("Parsing register data...")
registerData := readCsvLines("registers.csv")
for i := 0; i < len(registerData); i++ {
pingvin.Registers = append(pingvin.Registers,
newRegister(registerData[i][0], registerData[i][1], registerData[i][2], registerData[i][3], registerData[i][6]))
}
log.Println("Parsed", len(pingvin.Registers), "registers")
return pingvin
}