2023-01-28 23:28:37 +02:00
|
|
|
package pingvinKL
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2023-02-02 00:07:36 +02:00
|
|
|
"fmt"
|
2023-01-28 23:28:37 +02:00
|
|
|
"log"
|
|
|
|
"os"
|
2023-01-29 23:31:21 +02:00
|
|
|
"strconv"
|
2023-01-28 23:28:37 +02:00
|
|
|
"strings"
|
2023-01-29 22:09:37 +02:00
|
|
|
"sync"
|
2023-01-29 14:42:03 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/goburrow/modbus"
|
2023-01-28 23:28:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// single coil data
|
|
|
|
type pingvinCoil struct {
|
2023-01-29 23:31:21 +02:00
|
|
|
Address int `json:"address"`
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Value bool `json:"value"`
|
|
|
|
Description string `json:"description"`
|
|
|
|
Reserved bool `json:"reserved"`
|
2023-01-28 23:28:37 +02:00
|
|
|
}
|
|
|
|
|
2023-01-31 23:01:09 +02:00
|
|
|
// unit modbus data
|
|
|
|
type PingvinKL struct {
|
2023-02-18 21:06:02 +02:00
|
|
|
Coils []pingvinCoil
|
|
|
|
Registers []pingvinRegister
|
|
|
|
Status pingvinStatus
|
|
|
|
buslock *sync.Mutex
|
|
|
|
statuslock *sync.Mutex
|
2023-02-19 21:28:50 +02:00
|
|
|
debug bool
|
2023-01-31 23:01:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// single register data
|
|
|
|
type pingvinRegister struct {
|
2023-01-31 23:49:09 +02:00
|
|
|
Address int `json:"address"`
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Value int `json:"value"`
|
2023-02-02 00:07:36 +02:00
|
|
|
Bitfield string `json:"bitfield"`
|
|
|
|
Type string `json:"type"`
|
2023-01-31 23:49:09 +02:00
|
|
|
Description string `json:"description"`
|
|
|
|
Reserved bool `json:"reserved"`
|
2023-02-18 19:37:43 +02:00
|
|
|
Multiplier int `json:"multiplier"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type pingvinVentInfo struct {
|
2023-02-23 19:57:39 +02:00
|
|
|
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
|
2023-02-18 19:37:43 +02:00
|
|
|
}
|
|
|
|
|
2023-02-18 21:06:02 +02:00
|
|
|
type pingvinStatus struct {
|
2023-02-23 19:57:39 +02:00
|
|
|
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
|
2023-01-31 23:01:09 +02:00
|
|
|
}
|
|
|
|
|
2023-01-29 23:31:21 +02:00
|
|
|
func newCoil(address string, symbol string, description string) pingvinCoil {
|
|
|
|
addr, err := strconv.Atoi(address)
|
|
|
|
if err != nil {
|
2023-01-29 23:34:43 +02:00
|
|
|
log.Fatal("newCoil: Atoi: ", err)
|
2023-01-29 23:31:21 +02:00
|
|
|
}
|
2023-01-28 23:28:37 +02:00
|
|
|
reserved := symbol == "-" && description == "-"
|
2023-01-29 23:31:21 +02:00
|
|
|
coil := pingvinCoil{addr, symbol, false, description, reserved}
|
2023-01-28 23:28:37 +02:00
|
|
|
return coil
|
|
|
|
}
|
|
|
|
|
2023-02-18 21:06:02 +02:00
|
|
|
func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
|
2023-01-31 23:01:09 +02:00
|
|
|
addr, err := strconv.Atoi(address)
|
|
|
|
if err != nil {
|
2023-02-18 21:52:53 +02:00
|
|
|
log.Fatal("newRegister: Atoi(address): ", err)
|
2023-01-31 23:01:09 +02:00
|
|
|
}
|
2023-02-18 21:52:53 +02:00
|
|
|
multipl := 1
|
|
|
|
if len(multiplier) > 0 {
|
|
|
|
multipl, err = strconv.Atoi(multiplier)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("newRegister: Atoi(multiplier): ", err)
|
|
|
|
}
|
2023-02-18 21:06:02 +02:00
|
|
|
}
|
2023-01-31 23:01:09 +02:00
|
|
|
reserved := symbol == "Reserved" && description == "Reserved"
|
2023-02-19 22:34:34 +02:00
|
|
|
register := pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl}
|
2023-01-31 23:01:09 +02:00
|
|
|
return register
|
2023-01-28 23:28:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// read a CSV file containing data for coils or registers
|
|
|
|
func readCsvLines(file string) [][]string {
|
2023-01-31 22:21:15 +02:00
|
|
|
delim := ";"
|
2023-01-28 23:28:37 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-01-29 21:56:22 +02:00
|
|
|
// Configure the modbus parameters
|
2023-01-29 14:42:03 +02:00
|
|
|
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
|
2023-01-29 21:56:22 +02:00
|
|
|
handler.Timeout = 1500 * time.Millisecond
|
2023-01-29 14:42:03 +02:00
|
|
|
return handler
|
|
|
|
}
|
|
|
|
|
2023-02-18 21:52:53 +02:00
|
|
|
func (p *PingvinKL) updateCoils() {
|
2023-01-29 21:56:22 +02:00
|
|
|
handler := p.getHandler()
|
2023-01-29 22:09:37 +02:00
|
|
|
p.buslock.Lock()
|
2023-01-29 21:56:22 +02:00
|
|
|
err := handler.Connect()
|
|
|
|
if err != nil {
|
2023-01-31 23:49:09 +02:00
|
|
|
log.Fatal("updateCoils: handler.Connect: ", err)
|
2023-01-29 21:56:22 +02:00
|
|
|
}
|
|
|
|
defer handler.Close()
|
|
|
|
client := modbus.NewClient(handler)
|
|
|
|
results, err := client.ReadCoils(0, uint16(len(p.Coils)))
|
|
|
|
if err != nil {
|
2023-01-31 23:49:09 +02:00
|
|
|
log.Fatal("updateCoils: client.ReadCoils: ", err)
|
2023-01-29 21:56:22 +02:00
|
|
|
}
|
2023-01-29 22:09:37 +02:00
|
|
|
p.buslock.Unlock()
|
2023-01-29 21:56:22 +02:00
|
|
|
// 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++
|
|
|
|
}
|
|
|
|
}
|
2023-01-29 14:42:03 +02:00
|
|
|
}
|
|
|
|
|
2023-02-18 21:52:53 +02:00
|
|
|
func (p *PingvinKL) updateRegisters() {
|
2023-01-31 23:49:09 +02:00
|
|
|
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
|
2023-02-01 00:04:13 +02:00
|
|
|
// 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
|
2023-01-31 23:49:09 +02:00
|
|
|
for k < regs {
|
|
|
|
r := 125
|
|
|
|
if regs-k < 125 {
|
|
|
|
r = regs - k
|
|
|
|
}
|
2023-02-19 21:28:50 +02:00
|
|
|
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)
|
|
|
|
}
|
2023-01-31 23:49:09 +02:00
|
|
|
}
|
2023-02-01 00:04:13 +02:00
|
|
|
// 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
|
2023-01-31 23:49:09 +02:00
|
|
|
msb := true
|
2023-02-01 23:09:50 +02:00
|
|
|
value := int16(0)
|
|
|
|
uvalue := uint16(0)
|
2023-01-31 23:49:09 +02:00
|
|
|
for i := 0; i < len(results); i++ {
|
|
|
|
if msb {
|
2023-02-01 23:09:50 +02:00
|
|
|
value = int16(results[i]) << 8
|
|
|
|
uvalue = uint16(results[i]) << 8
|
2023-01-31 23:49:09 +02:00
|
|
|
} else {
|
2023-02-01 23:09:50 +02:00
|
|
|
value += int16(results[i])
|
|
|
|
uvalue += uint16(results[i])
|
2023-02-02 00:07:36 +02:00
|
|
|
if p.Registers[k].Type == "int16" {
|
2023-02-01 23:09:50 +02:00
|
|
|
p.Registers[k].Value = int(value)
|
2023-02-02 00:07:36 +02:00
|
|
|
}
|
|
|
|
if p.Registers[k].Type == "uint16" || p.Registers[k].Type == "enumeration" {
|
2023-02-01 23:09:50 +02:00
|
|
|
p.Registers[k].Value = int(uvalue)
|
|
|
|
}
|
2023-02-02 00:07:36 +02:00
|
|
|
if p.Registers[k].Type == "bitfield" {
|
|
|
|
p.Registers[k].Value = int(value)
|
2023-02-19 22:34:34 +02:00
|
|
|
// 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))
|
|
|
|
}
|
2023-02-02 00:07:36 +02:00
|
|
|
}
|
2023-01-31 23:49:09 +02:00
|
|
|
k++
|
|
|
|
}
|
|
|
|
msb = !msb
|
|
|
|
}
|
|
|
|
}
|
|
|
|
p.buslock.Unlock()
|
|
|
|
}
|
|
|
|
|
2023-02-18 21:52:53 +02:00
|
|
|
func (p *PingvinKL) Update() {
|
2023-01-31 23:01:09 +02:00
|
|
|
p.updateCoils()
|
2023-01-31 23:49:09 +02:00
|
|
|
p.updateRegisters()
|
2023-02-18 21:52:53 +02:00
|
|
|
p.populateStatus()
|
2023-01-31 23:01:09 +02:00
|
|
|
}
|
|
|
|
|
2023-01-29 14:42:03 +02:00
|
|
|
func (p PingvinKL) ReadCoil(n uint16) []byte {
|
|
|
|
handler := p.getHandler()
|
2023-01-29 22:09:37 +02:00
|
|
|
p.buslock.Lock()
|
2023-01-29 14:42:03 +02:00
|
|
|
err := handler.Connect()
|
|
|
|
if err != nil {
|
2023-01-29 21:56:22 +02:00
|
|
|
log.Fatal("ReadCoil: handler.Connect: ", err)
|
2023-01-29 14:42:03 +02:00
|
|
|
}
|
|
|
|
defer handler.Close()
|
|
|
|
client := modbus.NewClient(handler)
|
|
|
|
results, err := client.ReadCoils(n, 1)
|
2023-01-29 22:09:37 +02:00
|
|
|
p.buslock.Unlock()
|
2023-01-29 14:42:03 +02:00
|
|
|
if err != nil {
|
2023-01-29 23:34:43 +02:00
|
|
|
log.Fatal("ReadCoil: client.ReadCoils: ", err)
|
2023-01-29 14:42:03 +02:00
|
|
|
}
|
2023-01-29 22:09:37 +02:00
|
|
|
p.Coils[n].Value = results[0] == 1
|
2023-01-29 14:42:03 +02:00
|
|
|
return results
|
|
|
|
}
|
|
|
|
|
2023-02-18 21:52:53 +02:00
|
|
|
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
|
|
|
|
}
|
2023-02-18 22:18:37 +02:00
|
|
|
p.Status.TempSetting = p.Registers[135].Value / p.Registers[135].Multiplier
|
|
|
|
p.Status.FanPct = p.Registers[774].Value / p.Registers[774].Multiplier
|
2023-02-23 19:57:39 +02:00
|
|
|
p.Status.VentInfo.Roomtemp1 = p.Registers[1].Value / p.Registers[1].Multiplier
|
2023-02-18 22:18:37 +02:00
|
|
|
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
|
2023-02-19 22:34:34 +02:00
|
|
|
p.Status.OpMode = parseStatus(p.Registers[44].Value)
|
2023-02-18 22:18:37 +02:00
|
|
|
// TODO: Alarms, n of alarms
|
|
|
|
p.Status.DaysUntilService = p.Registers[538].Value / p.Registers[538].Multiplier
|
|
|
|
// TODO: Uptime & date in separate functions
|
2023-02-18 21:52:53 +02:00
|
|
|
}
|
|
|
|
|
2023-02-19 22:34:34 +02:00
|
|
|
func parseStatus(value int) string {
|
|
|
|
val := int16(value)
|
|
|
|
pingvinStatuses := []string{
|
|
|
|
"Max cooling",
|
2023-02-23 20:42:00 +02:00
|
|
|
"Max heating",
|
2023-02-19 22:34:34 +02:00
|
|
|
"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",
|
|
|
|
}
|
2023-02-23 20:42:00 +02:00
|
|
|
for i := 0; i < 15; i++ {
|
2023-02-19 22:34:34 +02:00
|
|
|
if val>>i&0x1 == 1 {
|
|
|
|
return pingvinStatuses[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "Normal"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-02-19 12:28:26 +02:00
|
|
|
func (p *PingvinKL) Monitor(interval int) {
|
|
|
|
for {
|
|
|
|
time.Sleep(time.Duration(interval) * time.Second)
|
|
|
|
p.Update()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-28 23:28:37 +02:00
|
|
|
// create a PingvinKL struct, read coils and registers from CSVs
|
2023-02-19 21:28:50 +02:00
|
|
|
func New(debug bool) PingvinKL {
|
2023-01-28 23:28:37 +02:00
|
|
|
pingvin := PingvinKL{}
|
2023-02-19 21:28:50 +02:00
|
|
|
pingvin.debug = debug
|
2023-01-29 22:09:37 +02:00
|
|
|
pingvin.buslock = &sync.Mutex{}
|
2023-01-31 23:01:09 +02:00
|
|
|
log.Println("Parsing coil data...")
|
2023-01-28 23:28:37 +02:00
|
|
|
coilData := readCsvLines("coils.csv")
|
|
|
|
for i := 0; i < len(coilData); i++ {
|
2023-01-29 23:31:21 +02:00
|
|
|
pingvin.Coils = append(pingvin.Coils, newCoil(coilData[i][0], coilData[i][1], coilData[i][2]))
|
2023-01-28 23:28:37 +02:00
|
|
|
}
|
2023-02-01 22:19:57 +02:00
|
|
|
log.Println("Parsed", len(pingvin.Coils), "coils")
|
2023-01-31 23:01:09 +02:00
|
|
|
log.Println("Parsing register data...")
|
|
|
|
registerData := readCsvLines("registers.csv")
|
|
|
|
for i := 0; i < len(registerData); i++ {
|
|
|
|
pingvin.Registers = append(pingvin.Registers,
|
2023-02-18 21:06:02 +02:00
|
|
|
newRegister(registerData[i][0], registerData[i][1], registerData[i][2], registerData[i][3], registerData[i][6]))
|
2023-01-31 23:01:09 +02:00
|
|
|
}
|
2023-02-01 22:19:57 +02:00
|
|
|
log.Println("Parsed", len(pingvin.Registers), "registers")
|
2023-01-28 23:28:37 +02:00
|
|
|
return pingvin
|
|
|
|
}
|