Files
invertergui/mk2driver/managed_writer.go
Nathan Coad e8153e2953
All checks were successful
continuous-integration/drone/push Build is passing
implement some features of Venus OS
2026-02-19 15:37:41 +11:00

195 lines
5.5 KiB
Go

package mk2driver
import (
"errors"
"fmt"
"sync"
"time"
)
type WriterProfile string
const (
WriterProfileNormal WriterProfile = "normal"
WriterProfileMaintenance WriterProfile = "maintenance"
WriterProfileReadOnly WriterProfile = "read_only"
)
type WriterPolicy struct {
Profile WriterProfile
MaxCurrentLimitA *float64
ModeChangeMinInterval time.Duration
LockoutWindow time.Duration
}
type CommandEvent struct {
Timestamp time.Time `json:"timestamp"`
Source CommandSource `json:"source"`
Kind string `json:"kind"`
Allowed bool `json:"allowed"`
Error string `json:"error,omitempty"`
}
type ManagedWriter struct {
writer SettingsWriter
policy WriterPolicy
mu sync.Mutex
lastModeChange time.Time
lockoutUntil time.Time
events []CommandEvent
}
var _ SettingsWriter = (*ManagedWriter)(nil)
var _ SourceAwareSettingsWriter = (*ManagedWriter)(nil)
func NewManagedWriter(writer SettingsWriter, policy WriterPolicy) *ManagedWriter {
if policy.Profile == "" {
policy.Profile = WriterProfileNormal
}
return &ManagedWriter{
writer: writer,
policy: policy,
events: make([]CommandEvent, 0, 100),
}
}
func (m *ManagedWriter) WriteRAMVar(id uint16, value int16) error {
return m.WriteRAMVarWithSource(CommandSourceUnknown, id, value)
}
func (m *ManagedWriter) WriteSetting(id uint16, value int16) error {
return m.WriteSettingWithSource(CommandSourceUnknown, id, value)
}
func (m *ManagedWriter) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error {
return m.SetPanelStateWithSource(CommandSourceUnknown, switchState, currentLimitA)
}
func (m *ManagedWriter) SetStandby(enabled bool) error {
return m.SetStandbyWithSource(CommandSourceUnknown, enabled)
}
func (m *ManagedWriter) WriteRAMVarWithSource(source CommandSource, id uint16, value int16) error {
return m.apply(source, "write_ram_var", func() error {
if err := m.ensureProfileAllows("write_ram_var"); err != nil {
return err
}
return m.baseWriter().WriteRAMVar(id, value)
})
}
func (m *ManagedWriter) WriteSettingWithSource(source CommandSource, id uint16, value int16) error {
return m.apply(source, "write_setting", func() error {
if err := m.ensureProfileAllows("write_setting"); err != nil {
return err
}
return m.baseWriter().WriteSetting(id, value)
})
}
func (m *ManagedWriter) SetPanelStateWithSource(source CommandSource, switchState PanelSwitchState, currentLimitA *float64) error {
return m.apply(source, "set_panel_state", func() error {
if err := m.ensureProfileAllows("set_panel_state"); err != nil {
return err
}
if m.policy.MaxCurrentLimitA != nil && currentLimitA != nil && *currentLimitA > *m.policy.MaxCurrentLimitA {
return fmt.Errorf("current limit %.2fA exceeds configured policy max %.2fA", *currentLimitA, *m.policy.MaxCurrentLimitA)
}
if m.policy.Profile == WriterProfileMaintenance && switchState != PanelSwitchOff {
return errors.New("maintenance profile only allows panel switch off")
}
if m.policy.ModeChangeMinInterval > 0 && !m.lastModeChange.IsZero() && time.Since(m.lastModeChange) < m.policy.ModeChangeMinInterval {
return fmt.Errorf("mode change denied due to rate limit; wait %s", m.policy.ModeChangeMinInterval-time.Since(m.lastModeChange))
}
if err := m.baseWriter().SetPanelState(switchState, currentLimitA); err != nil {
return err
}
m.lastModeChange = time.Now().UTC()
return nil
})
}
func (m *ManagedWriter) SetStandbyWithSource(source CommandSource, enabled bool) error {
return m.apply(source, "set_standby", func() error {
if err := m.ensureProfileAllows("set_standby"); err != nil {
return err
}
return m.baseWriter().SetStandby(enabled)
})
}
func (m *ManagedWriter) History(limit int) []CommandEvent {
m.mu.Lock()
defer m.mu.Unlock()
if limit <= 0 || limit > len(m.events) {
limit = len(m.events)
}
out := make([]CommandEvent, limit)
if limit > 0 {
copy(out, m.events[len(m.events)-limit:])
}
return out
}
func (m *ManagedWriter) apply(source CommandSource, kind string, fn func() error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.writer == nil {
err := errors.New("settings writer is not available")
m.recordLocked(source, kind, false, err)
return err
}
if m.policy.LockoutWindow > 0 && time.Now().UTC().Before(m.lockoutUntil) {
err := fmt.Errorf("command denied during lockout window until %s", m.lockoutUntil.Format(time.RFC3339))
m.recordLocked(source, kind, false, err)
return err
}
if err := fn(); err != nil {
m.recordLocked(source, kind, false, err)
return err
}
if m.policy.LockoutWindow > 0 {
m.lockoutUntil = time.Now().UTC().Add(m.policy.LockoutWindow)
}
m.recordLocked(source, kind, true, nil)
return nil
}
func (m *ManagedWriter) ensureProfileAllows(kind string) error {
switch m.policy.Profile {
case WriterProfileReadOnly:
return errors.New("write denied by read-only profile")
case WriterProfileMaintenance:
if kind == "set_standby" || kind == "set_panel_state" {
return nil
}
return fmt.Errorf("maintenance profile blocks %s", kind)
default:
return nil
}
}
func (m *ManagedWriter) recordLocked(source CommandSource, kind string, allowed bool, err error) {
event := CommandEvent{
Timestamp: time.Now().UTC(),
Source: source,
Kind: kind,
Allowed: allowed,
}
if err != nil {
event.Error = err.Error()
}
m.events = append(m.events, event)
if len(m.events) > 100 {
m.events = m.events[len(m.events)-100:]
}
}
func (m *ManagedWriter) baseWriter() SettingsWriter {
return m.writer
}