Modernize invertergui: MQTT write support, HA integration, UI updates
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
This commit is contained in:
274
mk2driver/mk2.go
274
mk2driver/mk2.go
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -49,13 +50,25 @@ const (
|
||||
const (
|
||||
acL1InfoFrame = 0x08
|
||||
dcInfoFrame = 0x0C
|
||||
interfaceFrame = 0x48 // H
|
||||
setTargetFrame = 0x41
|
||||
infoReqFrame = 0x46 //F
|
||||
ledFrame = 0x4C
|
||||
stateFrame = 0x53 // S
|
||||
vFrame = 0x56
|
||||
winmonFrame = 0x57
|
||||
)
|
||||
|
||||
const (
|
||||
panelStateVariant2Flags = 0x80
|
||||
|
||||
interfacePanelDetectFlag = 0x01
|
||||
interfaceStandbyFlag = 0x02
|
||||
|
||||
panelCurrentLimitUnknown = 0x8000
|
||||
panelCurrentLimitMax = 0x7FFF
|
||||
)
|
||||
|
||||
// info frame types
|
||||
const (
|
||||
infoReqAddrDC = 0x00
|
||||
@@ -65,12 +78,21 @@ const (
|
||||
// winmon frame commands
|
||||
const (
|
||||
commandReadRAMVar = 0x30
|
||||
commandWriteRAMVar = 0x32
|
||||
commandWriteSetting = 0x33
|
||||
commandWriteData = 0x34
|
||||
commandGetRAMVarInfo = 0x36
|
||||
|
||||
commandReadRAMResponse = 0x85
|
||||
commandGetRAMVarInfoResponse = 0x8E
|
||||
commandUnsupportedResponse = 0x80
|
||||
commandReadRAMResponse = 0x85
|
||||
commandWriteRAMResponse = 0x87
|
||||
commandWriteSettingResponse = 0x88
|
||||
commandWriteNotAllowedResponse = 0x9B
|
||||
commandGetRAMVarInfoResponse = 0x8E
|
||||
)
|
||||
|
||||
const writeResponseTimeout = 3 * time.Second
|
||||
|
||||
type mk2Ser struct {
|
||||
info *Mk2Info
|
||||
p io.ReadWriter
|
||||
@@ -79,6 +101,10 @@ type mk2Ser struct {
|
||||
run chan struct{}
|
||||
frameLock bool
|
||||
infochan chan *Mk2Info
|
||||
commandMu sync.Mutex
|
||||
writeAck chan byte
|
||||
stateAck chan struct{}
|
||||
ifaceAck chan byte
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
@@ -89,6 +115,9 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
|
||||
mk2.scaleCount = 0
|
||||
mk2.frameLock = false
|
||||
mk2.scales = make([]scaling, 0, ramVarMaxOffset)
|
||||
mk2.writeAck = make(chan byte, 4)
|
||||
mk2.stateAck = make(chan struct{}, 1)
|
||||
mk2.ifaceAck = make(chan byte, 1)
|
||||
mk2.setTarget()
|
||||
mk2.run = make(chan struct{})
|
||||
mk2.infochan = make(chan *Mk2Info)
|
||||
@@ -153,6 +182,233 @@ func (m *mk2Ser) C() chan *Mk2Info {
|
||||
return m.infochan
|
||||
}
|
||||
|
||||
func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error {
|
||||
return m.writeByID(commandWriteRAMVar, commandWriteRAMResponse, id, value)
|
||||
}
|
||||
|
||||
func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
|
||||
return m.writeByID(commandWriteSetting, commandWriteSettingResponse, id, value)
|
||||
}
|
||||
|
||||
func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error {
|
||||
if !validPanelSwitchState(switchState) {
|
||||
return fmt.Errorf("invalid panel switch state: %d", switchState)
|
||||
}
|
||||
|
||||
currentLimitRaw, err := encodePanelCurrentLimit(currentLimitA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.commandMu.Lock()
|
||||
defer m.commandMu.Unlock()
|
||||
|
||||
m.clearStateResponses()
|
||||
m.sendCommandLocked([]byte{
|
||||
stateFrame,
|
||||
byte(switchState),
|
||||
byte(currentLimitRaw),
|
||||
byte(currentLimitRaw >> 8),
|
||||
0x01,
|
||||
panelStateVariant2Flags,
|
||||
})
|
||||
|
||||
return m.waitForStateResponse()
|
||||
}
|
||||
|
||||
func (m *mk2Ser) SetStandby(enabled bool) error {
|
||||
lineState := byte(interfacePanelDetectFlag)
|
||||
if enabled {
|
||||
lineState |= interfaceStandbyFlag
|
||||
}
|
||||
|
||||
m.commandMu.Lock()
|
||||
defer m.commandMu.Unlock()
|
||||
|
||||
m.clearInterfaceResponses()
|
||||
m.sendCommandLocked([]byte{
|
||||
interfaceFrame,
|
||||
lineState,
|
||||
})
|
||||
|
||||
return m.waitForInterfaceResponse(enabled)
|
||||
}
|
||||
|
||||
func validPanelSwitchState(switchState PanelSwitchState) bool {
|
||||
switch switchState {
|
||||
case PanelSwitchChargerOnly, PanelSwitchInverterOnly, PanelSwitchOn, PanelSwitchOff:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
|
||||
if currentLimitA == nil {
|
||||
return panelCurrentLimitUnknown, nil
|
||||
}
|
||||
if *currentLimitA < 0 {
|
||||
return 0, fmt.Errorf("current_limit must be >= 0, got %.3f", *currentLimitA)
|
||||
}
|
||||
|
||||
raw := math.Round(*currentLimitA * 10)
|
||||
if raw > panelCurrentLimitMax {
|
||||
return 0, fmt.Errorf("current_limit %.3f A is above MK2 maximum %.1f A", *currentLimitA, panelCurrentLimitMax/10.0)
|
||||
}
|
||||
return uint16(raw), nil
|
||||
}
|
||||
|
||||
func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, value int16) error {
|
||||
m.commandMu.Lock()
|
||||
defer m.commandMu.Unlock()
|
||||
|
||||
m.clearWriteResponses()
|
||||
|
||||
m.sendCommandLocked([]byte{
|
||||
winmonFrame,
|
||||
selectCommand,
|
||||
byte(id),
|
||||
byte(id >> 8),
|
||||
})
|
||||
|
||||
rawValue := uint16(value)
|
||||
m.sendCommandLocked([]byte{
|
||||
winmonFrame,
|
||||
commandWriteData,
|
||||
byte(rawValue),
|
||||
byte(rawValue >> 8),
|
||||
})
|
||||
|
||||
return m.waitForWriteResponse(expectedResponse)
|
||||
}
|
||||
|
||||
func (m *mk2Ser) clearWriteResponses() {
|
||||
if m.writeAck == nil {
|
||||
m.writeAck = make(chan byte, 4)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-m.writeAck:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error {
|
||||
if m.writeAck == nil {
|
||||
return errors.New("write response channel is not initialized")
|
||||
}
|
||||
|
||||
select {
|
||||
case response := <-m.writeAck:
|
||||
switch response {
|
||||
case expectedResponse:
|
||||
return nil
|
||||
case commandUnsupportedResponse:
|
||||
return errors.New("write command is not supported by this device firmware")
|
||||
case commandWriteNotAllowedResponse:
|
||||
return errors.New("write command rejected by device access level")
|
||||
default:
|
||||
return fmt.Errorf("unexpected write response 0x%02x", response)
|
||||
}
|
||||
case <-time.After(writeResponseTimeout):
|
||||
return fmt.Errorf("timed out waiting for write response after %s", writeResponseTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) pushWriteResponse(response byte) {
|
||||
if m.writeAck == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case m.writeAck <- response:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) clearStateResponses() {
|
||||
if m.stateAck == nil {
|
||||
m.stateAck = make(chan struct{}, 1)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-m.stateAck:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) waitForStateResponse() error {
|
||||
if m.stateAck == nil {
|
||||
return errors.New("panel state response channel is not initialized")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.stateAck:
|
||||
return nil
|
||||
case <-time.After(writeResponseTimeout):
|
||||
return fmt.Errorf("timed out waiting for panel state response after %s", writeResponseTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) pushStateResponse() {
|
||||
if m.stateAck == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case m.stateAck <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) clearInterfaceResponses() {
|
||||
if m.ifaceAck == nil {
|
||||
m.ifaceAck = make(chan byte, 1)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-m.ifaceAck:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) waitForInterfaceResponse(expectedStandby bool) error {
|
||||
if m.ifaceAck == nil {
|
||||
return errors.New("interface response channel is not initialized")
|
||||
}
|
||||
|
||||
select {
|
||||
case lineState := <-m.ifaceAck:
|
||||
standbyEnabled := lineState&interfaceStandbyFlag != 0
|
||||
if standbyEnabled != expectedStandby {
|
||||
return fmt.Errorf("unexpected standby line state 0x%02x", lineState)
|
||||
}
|
||||
return nil
|
||||
case <-time.After(writeResponseTimeout):
|
||||
return fmt.Errorf("timed out waiting for standby response after %s", writeResponseTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) pushInterfaceResponse(lineState byte) {
|
||||
if m.ifaceAck == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case m.ifaceAck <- lineState:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mk2Ser) readByte() byte {
|
||||
buffer := make([]byte, 1)
|
||||
_, err := io.ReadFull(m.p, buffer)
|
||||
@@ -192,6 +448,12 @@ func (m *mk2Ser) handleFrame(l byte, frame []byte) {
|
||||
m.setTarget()
|
||||
case frameHeader:
|
||||
switch frame[1] {
|
||||
case interfaceFrame:
|
||||
if len(frame) > 2 {
|
||||
m.pushInterfaceResponse(frame[2])
|
||||
}
|
||||
case stateFrame:
|
||||
m.pushStateResponse()
|
||||
case vFrame:
|
||||
m.versionDecode(frame[2:])
|
||||
case winmonFrame:
|
||||
@@ -200,6 +462,8 @@ func (m *mk2Ser) handleFrame(l byte, frame []byte) {
|
||||
m.scaleDecode(frame[2:])
|
||||
case commandReadRAMResponse:
|
||||
m.stateDecode(frame[2:])
|
||||
case commandWriteRAMResponse, commandWriteSettingResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse:
|
||||
m.pushWriteResponse(frame[2])
|
||||
default:
|
||||
logrus.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:])
|
||||
}
|
||||
@@ -430,6 +694,12 @@ func getLEDs(ledsOn, ledsBlink byte) map[Led]LEDstate {
|
||||
|
||||
// Adds header and trailing crc for frame to send.
|
||||
func (m *mk2Ser) sendCommand(data []byte) {
|
||||
m.commandMu.Lock()
|
||||
defer m.commandMu.Unlock()
|
||||
m.sendCommandLocked(data)
|
||||
}
|
||||
|
||||
func (m *mk2Ser) sendCommandLocked(data []byte) {
|
||||
l := len(data)
|
||||
dataOut := make([]byte, l+3)
|
||||
dataOut[0] = byte(l + 1)
|
||||
|
||||
Reference in New Issue
Block a user