Compare commits

..

No commits in common. "5458c3ba86ae518c79dd97d57fea317975e3ffe0" and "32fa6d43219e592affe1845c8f72f6f486e10972" have entirely different histories.

4 changed files with 211 additions and 260 deletions

View File

@ -1,22 +1,12 @@
#!/bin/bash #!/bin/bash
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
echo -e "Usage: $0 [ARCH|-h|--help]"
echo -e "\tARCH: amd64 (default), arm, arm64"
exit
fi
ARCH=${1:-"amd64"}
VERSION=$(grep -e 'version.*=' main.go | awk '{print $3}' | tr -d '"') VERSION=$(grep -e 'version.*=' main.go | awk '{print $3}' | tr -d '"')
pushd TMP &> /dev/null || exit 1 pushd TMP &> /dev/null || exit 1
rm -rf *
tar --exclude ../TMP -ch ../* | tar xf - tar --exclude ../TMP -ch ../* | tar xf -
#env GOOS=linux GOARCH=arm go build -o ../BUILD/enervent-ctrl-${VERSION}.linux-arm32 . env GOOS=linux GOARCH=arm go build -o ../BUILD/enervent-ctrl-${VERSION}.linux-arm32 .
CGO_ENABLED=0 GOOS=linux GOARCH="$ARCH" go build -o "../BUILD/enervent-ctrl-${VERSION}.linux-$ARCH" .
rm -rf ./* popd &> /dev/null
popd &> /dev/null || exit 1

View File

@ -1,185 +0,0 @@
package main
import (
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
)
// HTTP Basic Authentication middleware for http.HandlerFunc
// This is used for the API
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.DisableAuth {
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if ok {
userHash := sha256.Sum256([]byte(user))
passHash := sha256.Sum256([]byte(pass))
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}
if len(user) == 0 {
user = "-"
}
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
// HTTP Basic Authentication middleware for http.Handler
// Used for the HTML monitor views
func authHandler(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.DisableAuth {
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if ok {
userHash := sha256.Sum256([]byte(user))
passHash := sha256.Sum256([]byte(pass))
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}
if len(user) == 0 {
user = "-"
}
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
// /api/v1/coils endpoint
func coils(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
if len(pathparams[0]) == 0 {
_ = json.NewEncoder(w).Encode(device.Coils)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
err = device.ReadCoil(uint16(intaddr))
if err != nil {
log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
}
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
boolval, err := strconv.ParseBool(pathparams[1])
if err != nil {
log.Println("ERROR: Could not parse coil value", pathparams[1])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
device.WriteCoil(uint16(intaddr), boolval)
}
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
device.WriteCoil(uint16(intaddr), !device.Coils[intaddr].Value)
}
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
}
}
// /api/v1/registers endpoint
func registers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
if len(pathparams[0]) == 0 {
_ = json.NewEncoder(w).Encode(device.Registers)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse register address", pathparams[0])
log.Println(err)
return
}
_, err = device.ReadRegister(uint16(intaddr))
if err != nil {
log.Println("ERROR: ReadRegister:", err)
}
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse register address", pathparams[0])
log.Println(err)
return
}
intval, err := strconv.Atoi(pathparams[1])
if err != nil {
log.Println("ERROR: Could not parse register value", pathparams[1])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
_, err = device.WriteRegister(uint16(intaddr), uint16(intval))
if err != nil {
log.Println(err)
}
}
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
}
}
// /status endpoint
func status(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(device.Status)
}
// /api/v1/temperature endpoint
func temperature(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
err := device.Temperature(pathparams[0])
if err != nil {
log.Println("ERROR: ", err)
}
_ = json.NewEncoder(w).Encode(device.Registers[135])
} else {
return
}
}

176
main.go
View File

@ -2,12 +2,17 @@ package main
import ( import (
"crypto/sha256" "crypto/sha256"
"crypto/subtle"
"embed" "embed"
"encoding/json"
"flag" "flag"
"io/fs" "io/fs"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv"
"strings"
"time" "time"
"github.com/0ranki/enervent-ctrl/pingvin" "github.com/0ranki/enervent-ctrl/pingvin"
@ -25,7 +30,7 @@ import (
var static embed.FS var static embed.FS
var ( var (
version = "0.1.0" version = "0.0.28"
device pingvin.Pingvin device pingvin.Pingvin
config Conf config Conf
usernamehash [32]byte usernamehash [32]byte
@ -48,6 +53,171 @@ type Conf struct {
ReadOnly bool `yaml:"read_only"` ReadOnly bool `yaml:"read_only"`
} }
// HTTP Basic Authentication middleware for http.HandlerFunc
// This is used for the API
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.DisableAuth {
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if ok {
userHash := sha256.Sum256([]byte(user))
passHash := sha256.Sum256([]byte(pass))
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}
if len(user) == 0 {
user = "-"
}
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
// HTTP Basic Authentication middleware for http.Handler
// Used for the HTML monitor views
func authHandler(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.DisableAuth {
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if ok {
userHash := sha256.Sum256([]byte(user))
passHash := sha256.Sum256([]byte(pass))
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}
if len(user) == 0 {
user = "-"
}
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
// \/api/v1/coils endpoint
func coils(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
if len(pathparams[0]) == 0 {
json.NewEncoder(w).Encode(device.Coils)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
device.ReadCoil(uint16(intaddr))
json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
boolval, err := strconv.ParseBool(pathparams[1])
if err != nil {
log.Println("ERROR: Could not parse coil value", pathparams[1])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
device.WriteCoil(uint16(intaddr), boolval)
}
json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse coil address", pathparams[0])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
device.WriteCoil(uint16(intaddr), !device.Coils[intaddr].Value)
}
json.NewEncoder(w).Encode(device.Coils[intaddr])
}
}
// \/api/v1/registers endpoint
func registers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
if len(pathparams[0]) == 0 {
json.NewEncoder(w).Encode(device.Registers)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse register address", pathparams[0])
log.Println(err)
return
}
device.ReadRegister(uint16(intaddr))
json.NewEncoder(w).Encode(device.Registers[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
log.Println("ERROR: Could not parse register address", pathparams[0])
log.Println(err)
return
}
intval, err := strconv.Atoi(pathparams[1])
if err != nil {
log.Println("ERROR: Could not parse register value", pathparams[1])
log.Println(err)
return
}
if config.ReadOnly {
log.Println("WARNING: Read only mode, refusing to write to device")
} else {
_, err = device.WriteRegister(uint16(intaddr), uint16(intval))
if err != nil {
log.Println(err)
}
}
json.NewEncoder(w).Encode(device.Registers[intaddr])
}
}
// \/status endpoint
func status(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(device.Status)
}
// \/api/v1/temperature endpoint
func temperature(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
device.Temperature(pathparams[0])
json.NewEncoder(w).Encode(device.Registers[135])
} else {
return
}
}
// Start the HTTP server // Start the HTTP server
func serve(cert, key *string) { func serve(cert, key *string) {
log.Println("Starting service") log.Println("Starting service")
@ -113,12 +283,12 @@ func parseConfigFile() {
} }
} }
conffile := confpath + "/configuration.yaml" conffile := confpath + "/configuration.yaml"
yamldata, err := os.ReadFile(conffile) yamldata, err := ioutil.ReadFile(conffile)
if err != nil { if err != nil {
log.Println("Configuration file", conffile, "not found") log.Println("Configuration file", conffile, "not found")
log.Println("Generating", conffile, "with default values") log.Println("Generating", conffile, "with default values")
initDefaultConfig(confpath) initDefaultConfig(confpath)
if yamldata, err = os.ReadFile(conffile); err != nil { if yamldata, err = ioutil.ReadFile(conffile); err != nil {
log.Fatal("Error parsing configuration:", err) log.Fatal("Error parsing configuration:", err)
} }
} }

View File

@ -26,9 +26,9 @@ type pingvinCoil struct {
// unit modbus data // unit modbus data
type Pingvin struct { type Pingvin struct {
Coils []*pingvinCoil Coils []pingvinCoil
Registers []*pingvinRegister Registers []pingvinRegister
Status *pingvinStatus Status pingvinStatus
buslock *sync.Mutex buslock *sync.Mutex
handler *modbus.RTUClientHandler handler *modbus.RTUClientHandler
modbusclient modbus.Client modbusclient modbus.Client
@ -73,7 +73,7 @@ type pingvinStatus struct {
OpMode string `json:"op_mode"` // Current operating mode, text representation OpMode string `json:"op_mode"` // Current operating mode, text representation
Uptime string `json:"uptime"` // Unit uptime Uptime string `json:"uptime"` // Unit uptime
SystemTime string `json:"system_time"` // Time and date in unit SystemTime string `json:"system_time"` // Time and date in unit
Coils []*pingvinCoil `json:"coils"` Coils []pingvinCoil `json:"coils"`
} }
type PingvinLogger struct { type PingvinLogger struct {
@ -101,7 +101,7 @@ func (logger *PingvinLogger) Println(msg ...any) {
} }
} }
func newCoil(address string, symbol string, description string) *pingvinCoil { func newCoil(address string, symbol string, description string) pingvinCoil {
addr, err := strconv.Atoi(address) addr, err := strconv.Atoi(address)
if err != nil { if err != nil {
log.Fatal("newCoil: Atoi: ", err) log.Fatal("newCoil: Atoi: ", err)
@ -111,7 +111,7 @@ func newCoil(address string, symbol string, description string) *pingvinCoil {
promdesc := strings.ToLower(symbol) promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%02d", addr) zpadaddr := fmt.Sprintf("%02d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1) promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return &pingvinCoil{addr, symbol, false, description, reserved, return pingvinCoil{addr, symbol, false, description, reserved,
prometheus.NewDesc( prometheus.NewDesc(
prometheus.BuildFQName("", "pingvin", promdesc), prometheus.BuildFQName("", "pingvin", promdesc),
description, description,
@ -120,10 +120,10 @@ func newCoil(address string, symbol string, description string) *pingvinCoil {
), ),
} }
} }
return &pingvinCoil{addr, symbol, false, description, reserved, nil} return pingvinCoil{addr, symbol, false, description, reserved, nil}
} }
func newRegister(address, symbol, typ, multiplier, description string) *pingvinRegister { func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
addr, err := strconv.Atoi(address) addr, err := strconv.Atoi(address)
if err != nil { if err != nil {
log.Fatal("newRegister: Atoi(address): ", err) log.Fatal("newRegister: Atoi(address): ", err)
@ -141,7 +141,7 @@ func newRegister(address, symbol, typ, multiplier, description string) *pingvinR
promdesc := strings.ToLower(symbol) promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%03d", addr) zpadaddr := fmt.Sprintf("%03d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1) promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return &pingvinRegister{ return pingvinRegister{
addr, addr,
symbol, symbol,
0, 0,
@ -158,7 +158,7 @@ func newRegister(address, symbol, typ, multiplier, description string) *pingvinR
), ),
} }
} }
return &pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil} return pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
} }
// read a CSV file containing data for coils or registers // read a CSV file containing data for coils or registers
@ -210,23 +210,11 @@ func (p *Pingvin) Quit() {
// Update all coil values // Update all coil values
func (p *Pingvin) updateCoils() { func (p *Pingvin) updateCoils() {
var results []byte p.buslock.Lock()
var err error results, err := p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
for retries := 1; retries <= 5; retries++ { p.buslock.Unlock()
p.Debug.Println("Reading coils, attempt", retries) if err != nil {
p.buslock.Lock() log.Fatal("updateCoils: client.ReadCoils: ", err)
results, err = p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
p.buslock.Unlock()
if len(results) > 0 {
break
} else if retries == 4 {
log.Println("ERROR: updateCoils: client.Readcoils: ", err)
return
}
if err != nil {
log.Printf("WARNING updateCoils: client.ReadCoils attempt %d: %s\n", retries, err)
}
time.Sleep(100 * time.Millisecond)
} }
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7, // modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc. // second byte coils 8-15 etc.
@ -254,8 +242,8 @@ func (p *Pingvin) ReadRegister(addr uint16) (int, error) {
results, err := p.modbusclient.ReadHoldingRegisters(addr, 1) results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
p.buslock.Unlock() p.buslock.Unlock()
if err != nil { if err != nil {
//log.Println("ERROR: ReadRegister:", err) log.Println("ERROR: ReadRegister:", err)
return p.Registers[addr].Value, err return 0, err
} }
if p.Registers[addr].Type == "uint16" { if p.Registers[addr].Type == "uint16" {
p.Registers[addr].Value = int(uint16(results[0]) << 8) p.Registers[addr].Value = int(uint16(results[0]) << 8)
@ -282,7 +270,6 @@ func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
return 0, err return 0, err
} }
if val == int(value) { if val == int(value) {
log.Printf("Wrote register %d to value %d (%s: %s)", addr, p.Registers[addr].Value, p.Registers[addr].Symbol, p.Registers[addr].Description)
return value, nil return value, nil
} }
return 0, fmt.Errorf("Failed to write register") return 0, fmt.Errorf("Failed to write register")
@ -301,8 +288,8 @@ func (p *Pingvin) updateRegisters() {
if regs-k < 125 { if regs-k < 125 {
r = regs - k r = regs - k
} }
var results []byte results := []byte{}
for retries := 1; retries <= 5; retries++ { for retries := 0; retries < 5; retries++ {
p.Debug.Println("Reading registers, attempt", retries, "k:", k) p.Debug.Println("Reading registers, attempt", retries, "k:", k)
p.buslock.Lock() p.buslock.Lock()
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r)) results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
@ -312,9 +299,8 @@ func (p *Pingvin) updateRegisters() {
} else if retries == 4 { } else if retries == 4 {
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err) log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
} else if err != nil { } else if err != nil {
log.Printf("WARNING: updateRegisters: client.ReadHoldingRegisters attempt %d: %s", retries, err) log.Println("WARNING: updateRegisters: client.ReadHoldingRegisters: ", err)
} }
time.Sleep(100 * time.Millisecond)
} }
// The values represent 16 bit integers, but modbus works with bytes // 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 // Each even byte of the returned []byte is the 8 MSBs of a new 16-bit
@ -364,30 +350,22 @@ func (p *Pingvin) Update() {
} }
// Read single coil // Read single coil
func (p *Pingvin) ReadCoil(n uint16) (err error) { func (p *Pingvin) ReadCoil(n uint16) ([]byte, error) {
var results []byte p.buslock.Lock()
for retries := 1; retries <= 5; retries++ { results, err := p.modbusclient.ReadCoils(n, 1)
p.buslock.Lock() p.buslock.Unlock()
results, err = p.modbusclient.ReadCoils(n, 1) if err != nil {
p.buslock.Unlock() log.Fatal("ReadCoil: client.ReadCoils: ", err)
if len(results) > 0 && err == nil { return nil, err
break
} else if retries == 4 {
//log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
return
} else if err != nil {
log.Printf("WARNING: ReadCoil: client.ReadCoils attempt %d: %s", retries, err)
}
time.Sleep(100 * time.Millisecond)
} }
p.Coils[n].Value = results[0] == 1 p.Coils[n].Value = results[0] == 1
return return results, nil
} }
// Force a single coil // Force a single coil
func (p *Pingvin) WriteCoil(n uint16, val bool) bool { func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if val { if val {
_ = p.checkMutexCoils(n) //, p.handler) p.checkMutexCoils(n, p.handler)
} }
var value uint16 = 0 var value uint16 = 0
if val { if val {
@ -399,15 +377,14 @@ func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if err != nil { if err != nil {
log.Println("ERROR: WriteCoil: ", err) log.Println("ERROR: WriteCoil: ", err)
} }
if (val && results[0] != 255) && (!val && results[0] != 0) { 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") log.Println("ERROR: WriteCoil: failed to write coil")
return false return false
} }
err = p.ReadCoil(n) p.ReadCoil(n)
if err != nil {
log.Printf("ERROR WriteCoil: p.ReadCoil: %s", err)
}
log.Printf("Wrote coil %d to value %v (%s: %s)", n, p.Coils[n].Value, p.Coils[n].Symbol, p.Coils[n].Description)
return true return true
} }
@ -463,7 +440,7 @@ func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) err
// Some of the coils are mutually exclusive, and can only be 1 one at a time. // Some of the coils are mutually exclusive, and can only be 1 one at a time.
// Check if coil is one of them and force all of them to 0 if so // Check if coil is one of them and force all of them to 0 if so
func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUClientHandler) error { func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler) error {
for _, mutexcoil := range mutexcoils { for _, mutexcoil := range mutexcoils {
if mutexcoil == addr { if mutexcoil == addr {
for _, n := range mutexcoils { for _, n := range mutexcoils {
@ -485,7 +462,6 @@ func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUCl
// populate p.Status struct for Home Assistant // populate p.Status struct for Home Assistant
func (p *Pingvin) populateStatus() { func (p *Pingvin) populateStatus() {
p.Status = &pingvinStatus{}
hpct := p.Registers[49].Value / p.Registers[49].Multiplier hpct := p.Registers[49].Value / p.Registers[49].Multiplier
if hpct > 100 { if hpct > 100 {
p.Status.HeaterPct = hpct - 100 p.Status.HeaterPct = hpct - 100