implement some features of Venus OS
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
194
mk2driver/managed_writer.go
Normal file
194
mk2driver/managed_writer.go
Normal file
@@ -0,0 +1,194 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user