From 437aeb84e578ce66a41aef12908e0b70c9c0d3ef Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 7 Sep 2025 17:16:14 +0200 Subject: [PATCH] Maintenant modifier les instance de state variable pour qu'elles notifient leurs changements --- pmolog/prettyxml.go | 24 ++++++ soap/parseSoap.go | 18 ---- .../statevariables/statevalueinstance.go | 86 +++++++++++++++---- upnp/serviceinstance.go | 65 ++++++++------ 4 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 pmolog/prettyxml.go diff --git a/pmolog/prettyxml.go b/pmolog/prettyxml.go new file mode 100644 index 00000000..1c7ced96 --- /dev/null +++ b/pmolog/prettyxml.go @@ -0,0 +1,24 @@ +package pmolog + +import ( + "bytes" + "encoding/xml" +) + +func PrettyPrintXML(raw string) string { + var out bytes.Buffer + dec := xml.NewDecoder(bytes.NewReader([]byte(raw))) + enc := xml.NewEncoder(&out) + enc.Indent("", " ") + for { + t, err := dec.Token() + if err != nil { + break + } + if err := enc.EncodeToken(t); err != nil { + break + } + } + enc.Flush() + return out.String() +} diff --git a/soap/parseSoap.go b/soap/parseSoap.go index f8cc4d65..66b4fc6b 100644 --- a/soap/parseSoap.go +++ b/soap/parseSoap.go @@ -48,24 +48,6 @@ type Fault struct { // ----- Utils ----- -func prettyPrintXML(raw string) string { - var out bytes.Buffer - dec := xml.NewDecoder(bytes.NewReader([]byte(raw))) - enc := xml.NewEncoder(&out) - enc.Indent("", " ") - for { - t, err := dec.Token() - if err != nil { - break - } - if err := enc.EncodeToken(t); err != nil { - break - } - } - enc.Flush() - return out.String() -} - // ----- Parseurs ----- func ParseSOAPEnvelope(body []byte) (*Envelope, error) { diff --git a/upnp/devices/services/statevariables/statevalueinstance.go b/upnp/devices/services/statevariables/statevalueinstance.go index 7c06b518..2b465211 100644 --- a/upnp/devices/services/statevariables/statevalueinstance.go +++ b/upnp/devices/services/statevariables/statevalueinstance.go @@ -12,8 +12,14 @@ import ( "github.com/beevik/etree" "github.com/google/uuid" + log "github.com/sirupsen/logrus" ) +type notifiable interface { + Name() string + EventToBeSent(name string, value interface{}) +} + type StateVarInstance struct { model *StateVariable name string @@ -27,12 +33,12 @@ type StateVarInstance struct { sendEvents bool parse StringValueParser marshal ValueSerializer - - value interface{} - previousValue interface{} - lastChange time.Time - lastEvent time.Time - mu sync.RWMutex + service notifiable + value interface{} + previousValue interface{} + lastChange time.Time + lastEvent time.Time + mu sync.RWMutex } func (instance *StateVarInstance) Name() string { @@ -102,13 +108,11 @@ func (instance *StateVarInstance) ParseValue(value string) (interface{}, error) // IsValueInRange checks if a value falls within the defined range. // Always returns true if no range is set. - // Parameters: - -// value: Value to check - +// +// value: Value to check +// // Returns: - // bool: True if within range or no range defined func (instance *StateVarInstance) IsValueInRange(value interface{}) (bool, error) { return instance.model.valueType.InRange(value, instance.valueRange) @@ -186,27 +190,73 @@ func (instance *StateVarInstance) SetValue(val interface{}) error { return err } + if ok, err := instance.IsValidValue(cval); !ok || err != nil { + if err != nil { + return err + } + return fmt.Errorf("Not valid value %v for variable %s", cval, instance.Name()) + } + instance.mu.Lock() defer instance.mu.Unlock() instance.previousValue = instance.value instance.value = cval + + if instance.ShouldTriggerEvent() { + instance.service.EventToBeSent(instance.Name(), instance.Value()) + } + return nil } -func (instance *StateVarInstance) Incr() { - instance.mu.Lock() - defer instance.mu.Unlock() +func (instance *StateVarInstance) Incr() error { + if instance.HasStep() { + value, err := instance.model.valueType.Add(instance.Value(), instance.Step()) + if err != nil { + return err + } + return instance.SetValue(value) + } + return fmt.Errorf( + "no step for variable %s:%s", + instance.service.Name(), + instance.Name(), + ) +} +func (instance *StateVarInstance) Decr() error { + if instance.HasStep() { + value, err := instance.model.valueType.Sub(instance.Value(), instance.Step()) + if err != nil { + return err + } + return instance.SetValue(value) + } + return fmt.Errorf( + "no step for variable %s:%s", + instance.service.Name(), + instance.Name(), + ) } // ShouldTriggerEvent vérifie toutes les conditions func (instance *StateVarInstance) ShouldTriggerEvent() bool { - for _, condition := range instance.model.eventConditions { - if !condition(instance) { - return false + if instance.IsSendingEvents() { + for name, condition := range instance.model.eventConditions { + if !condition(instance) { + log.Debugf( + "State variable %s:%s event condition %s not true", + instance.service.Name(), + instance.Name(), + name, + ) + return false + } } + + return true } - return true + return false } func (sv *StateVarInstance) GenerateEvent() *etree.Element { diff --git a/upnp/serviceinstance.go b/upnp/serviceinstance.go index 5063f936..410e384f 100644 --- a/upnp/serviceinstance.go +++ b/upnp/serviceinstance.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "gargoton.petite-maison-orange.fr/eric/pmomusic/pmolog" "gargoton.petite-maison-orange.fr/eric/pmomusic/soap" "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions" "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables" @@ -162,38 +163,48 @@ func (svc *ServiceInstance) SendInitialEvent(sid string) { return } - // Ici tu peux construire un SOAP Event XML minimal - body := `Initial` + changed := make(map[string]interface{}) + for name, sv := range svc.statevariables { + if sv.IsSendingEvents() { // sendEvents="yes" + changed[name] = sv.Value() + } + } - callback = strings.TrimSpace(callback) - callback = strings.Trim(callback, "<>") // retire les < et > - - // parser pour valider - u, err := url.Parse(callback) - if err != nil { - log.Errorf("Invalid callback URL: %v", err) + if len(changed) == 0 { return } - req, err := http.NewRequest("NOTIFY", u.String(), strings.NewReader(body)) - if err != nil { - log.Errorf("Failed to create NOTIFY request: %v", err) - return - } + go func() { + callback = strings.TrimSpace(callback) + callback = strings.Trim(callback, "<>") - req.Header.Set("Content-Type", "text/xml; charset=\"utf-8\"") - req.Header.Set("NT", "upnp:event") - req.Header.Set("NTS", "upnp:propchange") - req.Header.Set("SID", sid) - req.Header.Set("SEQ", "0") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Errorf("Failed to send initial event to %s: %v", callback, err) - return - } - defer resp.Body.Close() - log.Infof("✅ Initial event sent to %s, status=%s", callback, resp.Status) + body := `` + for name, val := range changed { + body += fmt.Sprintf("<%s>%v", name, val, name) + } + body += "" + + req, _ := http.NewRequest("NOTIFY", callback, strings.NewReader(body)) + req.Header.Set("Content-Type", `text/xml; charset="utf-8"`) + req.Header.Set("NT", "upnp:event") + req.Header.Set("NTS", "upnp:propchange") + req.Header.Set("SID", sid) + req.Header.Set("SEQ", "0") // initial event + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Errorf("Failed to send initial event to %s: %v", callback, err) + return + } + defer resp.Body.Close() + log.Infof( + "✅ Initial event sent to %s, status=%s\n
\n\n```xml\n%s\n```\n
\n", + callback, + resp.Status, + pmolog.PrettyPrintXML(body), + ) + }() } func (svc *ServiceInstance) ToXMLElement() *etree.Element {