Modernize invertergui: MQTT write support, HA integration, UI updates
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled

This commit is contained in:
2026-02-19 12:03:52 +11:00
parent 959d1e3c1f
commit a31a0b4829
460 changed files with 19655 additions and 40205 deletions

View File

@@ -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)