Add read-only mode support and enhance logging throughout the application
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -99,6 +99,17 @@ type panelStateCache struct {
|
||||
|
||||
// New creates an MQTT client that publishes MK2 updates and optionally handles setting write commands.
|
||||
func New(mk2 mk2driver.Mk2, writer mk2driver.SettingsWriter, config Config) error {
|
||||
log.WithFields(logrus.Fields{
|
||||
"broker": config.Broker,
|
||||
"client_id": config.ClientID,
|
||||
"topic": config.Topic,
|
||||
"command_topic": config.CommandTopic,
|
||||
"status_topic": config.StatusTopic,
|
||||
"ha_enabled": config.HomeAssistant.Enabled,
|
||||
"ha_node_id": config.HomeAssistant.NodeID,
|
||||
"ha_device_name": config.HomeAssistant.DeviceName,
|
||||
}).Info("Initializing MQTT client")
|
||||
|
||||
c := mqtt.NewClient(getOpts(config))
|
||||
if token := c.Connect(); token.Wait() && token.Error() != nil {
|
||||
return token.Error()
|
||||
@@ -135,14 +146,18 @@ func New(mk2 mk2driver.Mk2, writer mk2driver.SettingsWriter, config Config) erro
|
||||
go func() {
|
||||
for e := range mk2.C() {
|
||||
if e == nil || !e.Valid {
|
||||
log.Debug("Skipping invalid/nil MK2 event for MQTT publish")
|
||||
continue
|
||||
}
|
||||
if err := publishJSON(c, config.Topic, e, 0, false); err != nil {
|
||||
log.Errorf("Could not publish update to MQTT topic %q: %v", config.Topic, err)
|
||||
} else {
|
||||
log.WithField("topic", config.Topic).Debug("Published MK2 update to MQTT")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("MQTT client setup complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,9 +167,14 @@ func subscribeHAPanelModeState(client mqtt.Client, config Config, cache *panelSt
|
||||
}
|
||||
|
||||
stateTopic := haPanelSwitchStateTopic(config)
|
||||
log.WithField("topic", stateTopic).Info("Subscribing to Home Assistant mode state topic for panel cache")
|
||||
t := client.Subscribe(stateTopic, 1, func(_ mqtt.Client, msg mqtt.Message) {
|
||||
switchState, switchName, err := normalizePanelSwitch(string(msg.Payload()))
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": msg.Topic(),
|
||||
"payload": string(msg.Payload()),
|
||||
}).WithError(err).Warn("Ignoring invalid Home Assistant mode state payload")
|
||||
return
|
||||
}
|
||||
cache.remember(writeCommand{
|
||||
@@ -163,6 +183,10 @@ func subscribeHAPanelModeState(client mqtt.Client, config Config, cache *panelSt
|
||||
SwitchState: switchState,
|
||||
SwitchName: switchName,
|
||||
})
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": msg.Topic(),
|
||||
"switch_name": switchName,
|
||||
}).Debug("Updated panel mode cache from Home Assistant state topic")
|
||||
})
|
||||
t.Wait()
|
||||
return t.Error()
|
||||
@@ -174,6 +198,11 @@ func commandHandler(client mqtt.Client, writer mk2driver.SettingsWriter, config
|
||||
}
|
||||
|
||||
return func(_ mqtt.Client, msg mqtt.Message) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": msg.Topic(),
|
||||
"payload": string(msg.Payload()),
|
||||
}).Debug("Received MQTT command message")
|
||||
|
||||
cmd, err := decodeWriteCommand(msg.Payload())
|
||||
if err != nil {
|
||||
log.Errorf("Invalid MQTT write command payload from topic %q: %v", msg.Topic(), err)
|
||||
@@ -247,6 +276,7 @@ func (c *panelStateCache) resolvePanelCommand(cmd writeCommand) (writeCommand, e
|
||||
cmd.HasSwitch = true
|
||||
cmd.SwitchName = c.switchName
|
||||
cmd.SwitchState = c.switchState
|
||||
log.WithField("switch", cmd.SwitchName).Debug("Resolved panel command switch from cache")
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@@ -260,6 +290,7 @@ func (c *panelStateCache) remember(cmd writeCommand) {
|
||||
c.switchName = cmd.SwitchName
|
||||
c.switchState = cmd.SwitchState
|
||||
c.mu.Unlock()
|
||||
log.WithField("switch", cmd.SwitchName).Debug("Remembered panel switch state in cache")
|
||||
}
|
||||
|
||||
func decodeWriteCommand(payload []byte) (writeCommand, error) {
|
||||
@@ -465,6 +496,7 @@ func decodeStandbyValue(msg writeCommandPayload) (bool, error) {
|
||||
|
||||
func publishWriteStatus(client mqtt.Client, topic string, status writeStatus) {
|
||||
if topic == "" {
|
||||
log.Debug("Skipping write status publish; status topic is empty")
|
||||
return
|
||||
}
|
||||
if err := publishJSON(client, topic, status, 1, false); err != nil {
|
||||
@@ -479,6 +511,11 @@ func publishHADiscovery(client mqtt.Client, config Config) error {
|
||||
|
||||
for _, def := range definitions {
|
||||
topic := fmt.Sprintf("%s/%s/%s/%s/config", prefix, def.Component, nodeID, def.ObjectID)
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": topic,
|
||||
"component": def.Component,
|
||||
"object_id": def.ObjectID,
|
||||
}).Debug("Publishing Home Assistant discovery definition")
|
||||
if err := publishJSON(client, topic, def.Config, 1, true); err != nil {
|
||||
return fmt.Errorf("could not publish discovery for %s/%s: %w", def.Component, def.ObjectID, err)
|
||||
}
|
||||
@@ -665,6 +702,12 @@ func publishJSON(client mqtt.Client, topic string, payload any, qos byte, retain
|
||||
if t.Error() != nil {
|
||||
return t.Error()
|
||||
}
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": topic,
|
||||
"qos": qos,
|
||||
"retained": retained,
|
||||
"bytes": len(data),
|
||||
}).Debug("Published JSON message")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -679,6 +722,12 @@ func publishString(client mqtt.Client, topic, payload string, qos byte, retained
|
||||
if t.Error() != nil {
|
||||
return t.Error()
|
||||
}
|
||||
log.WithFields(logrus.Fields{
|
||||
"topic": topic,
|
||||
"qos": qos,
|
||||
"retained": retained,
|
||||
"payload": payload,
|
||||
}).Debug("Published string message")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ func NewWebGui(source mk2driver.Mk2, writer mk2driver.SettingsWriter) *WebGui {
|
||||
Mode: modeUnknown,
|
||||
},
|
||||
}
|
||||
log.WithField("remote_writable", writer != nil).Info("Web UI initialized")
|
||||
w.wg.Add(1)
|
||||
go w.dataPoll()
|
||||
return w
|
||||
@@ -138,50 +139,77 @@ type setRemotePanelStandbyRequest struct {
|
||||
}
|
||||
|
||||
func (w *WebGui) ServeHub(rw http.ResponseWriter, r *http.Request) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"remote": r.RemoteAddr,
|
||||
"path": r.URL.Path,
|
||||
}).Debug("WebSocket hub request")
|
||||
w.hub.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (w *WebGui) ServeRemotePanelState(rw http.ResponseWriter, r *http.Request) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"remote": r.RemoteAddr,
|
||||
}).Debug("Remote panel state API request")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
|
||||
case http.MethodPost:
|
||||
w.handleSetRemotePanelState(rw, r)
|
||||
default:
|
||||
log.WithField("method", r.Method).Warn("Remote panel state API received unsupported method")
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebGui) ServeRemotePanelStandby(rw http.ResponseWriter, r *http.Request) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"remote": r.RemoteAddr,
|
||||
}).Debug("Remote panel standby API request")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
|
||||
case http.MethodPost:
|
||||
w.handleSetRemotePanelStandby(rw, r)
|
||||
default:
|
||||
log.WithField("method", r.Method).Warn("Remote panel standby API received unsupported method")
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebGui) handleSetRemotePanelState(rw http.ResponseWriter, r *http.Request) {
|
||||
if w.writer == nil {
|
||||
log.Warn("Remote panel state write requested, but writer is unavailable")
|
||||
http.Error(rw, "remote control is not supported by this data source", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
req := setRemotePanelStateRequest{}
|
||||
if err := decodeJSONBody(r, &req); err != nil {
|
||||
log.WithError(err).Warn("Invalid remote panel state request body")
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
logEntry := log.WithField("mode", req.Mode)
|
||||
if req.CurrentLimit != nil {
|
||||
logEntry = logEntry.WithField("current_limit_a", *req.CurrentLimit)
|
||||
}
|
||||
logEntry.Info("Applying remote panel state from API")
|
||||
|
||||
switchState, normalizedMode, err := parsePanelMode(req.Mode)
|
||||
if err != nil {
|
||||
logEntry.WithError(err).Warn("Unsupported remote panel mode")
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := w.writer.SetPanelState(switchState, req.CurrentLimit); err != nil {
|
||||
logEntry.WithError(err).Error("Failed to apply remote panel state")
|
||||
w.updateRemotePanelState(func(state *remotePanelState) {
|
||||
state.LastCommand = "set_remote_panel_state"
|
||||
state.LastError = err.Error()
|
||||
@@ -196,22 +224,27 @@ func (w *WebGui) handleSetRemotePanelState(rw http.ResponseWriter, r *http.Reque
|
||||
state.LastCommand = "set_remote_panel_state"
|
||||
state.LastError = ""
|
||||
})
|
||||
logEntry.WithField("normalized_mode", normalizedMode).Info("Remote panel state applied")
|
||||
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
|
||||
}
|
||||
|
||||
func (w *WebGui) handleSetRemotePanelStandby(rw http.ResponseWriter, r *http.Request) {
|
||||
if w.writer == nil {
|
||||
log.Warn("Remote panel standby write requested, but writer is unavailable")
|
||||
http.Error(rw, "remote control is not supported by this data source", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
req := setRemotePanelStandbyRequest{}
|
||||
if err := decodeJSONBody(r, &req); err != nil {
|
||||
log.WithError(err).Warn("Invalid remote panel standby request body")
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.WithField("standby", req.Standby).Info("Applying standby state from API")
|
||||
|
||||
if err := w.writer.SetStandby(req.Standby); err != nil {
|
||||
log.WithError(err).WithField("standby", req.Standby).Error("Failed to apply standby state")
|
||||
w.updateRemotePanelState(func(state *remotePanelState) {
|
||||
state.LastCommand = "set_remote_panel_standby"
|
||||
state.LastError = err.Error()
|
||||
@@ -225,6 +258,7 @@ func (w *WebGui) handleSetRemotePanelStandby(rw http.ResponseWriter, r *http.Req
|
||||
state.LastCommand = "set_remote_panel_standby"
|
||||
state.LastError = ""
|
||||
})
|
||||
log.WithField("standby", req.Standby).Info("Standby state applied")
|
||||
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
|
||||
}
|
||||
|
||||
@@ -301,23 +335,32 @@ func buildTemplateInput(status *mk2driver.Mk2Info) *templateInput {
|
||||
}
|
||||
|
||||
func (w *WebGui) Stop() {
|
||||
log.Info("Stopping Web UI polling")
|
||||
close(w.stopChan)
|
||||
w.wg.Wait()
|
||||
log.Info("Web UI polling stopped")
|
||||
}
|
||||
|
||||
func (w *WebGui) dataPoll() {
|
||||
for {
|
||||
select {
|
||||
case s := <-w.C():
|
||||
if s.Valid {
|
||||
payload := buildTemplateInput(s)
|
||||
w.stateMu.Lock()
|
||||
payload.RemotePanel = w.remote
|
||||
w.latest = payload
|
||||
w.stateMu.Unlock()
|
||||
if err := w.hub.Broadcast(payload); err != nil {
|
||||
log.Errorf("Could not send update to clients: %v", err)
|
||||
}
|
||||
if s == nil {
|
||||
log.Debug("Skipping nil MK2 update in Web UI poller")
|
||||
continue
|
||||
}
|
||||
if !s.Valid {
|
||||
log.WithField("errors", len(s.Errors)).Debug("Skipping invalid MK2 update in Web UI poller")
|
||||
continue
|
||||
}
|
||||
|
||||
payload := buildTemplateInput(s)
|
||||
w.stateMu.Lock()
|
||||
payload.RemotePanel = w.remote
|
||||
w.latest = payload
|
||||
w.stateMu.Unlock()
|
||||
if err := w.hub.Broadcast(payload); err != nil {
|
||||
log.Errorf("Could not send update to clients: %v", err)
|
||||
}
|
||||
case <-w.stopChan:
|
||||
w.wg.Done()
|
||||
@@ -336,6 +379,13 @@ func (w *WebGui) updateRemotePanelState(update func(state *remotePanelState)) {
|
||||
w.stateMu.Lock()
|
||||
update(&w.remote)
|
||||
w.remote.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
||||
log.WithFields(logrus.Fields{
|
||||
"mode": w.remote.Mode,
|
||||
"has_limit": w.remote.CurrentLimit != nil,
|
||||
"has_standby": w.remote.Standby != nil,
|
||||
"last_command": w.remote.LastCommand,
|
||||
"last_error": w.remote.LastError,
|
||||
}).Debug("Updated remote panel state cache")
|
||||
snapshot := w.snapshotLocked()
|
||||
w.stateMu.Unlock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user