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 }