Merge pull request 'push-krxtpvssqpqz' (#4) from push-krxtpvssqpqz into main
Reviewed-on: #4
This commit is contained in:
24
pmolog/prettyxml.go
Normal file
24
pmolog/prettyxml.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
@@ -48,24 +48,6 @@ type Fault struct {
|
|||||||
|
|
||||||
// ----- Utils -----
|
// ----- 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 -----
|
// ----- Parseurs -----
|
||||||
|
|
||||||
func ParseSOAPEnvelope(body []byte) (*Envelope, error) {
|
func ParseSOAPEnvelope(body []byte) (*Envelope, error) {
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ import (
|
|||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type notifiable interface {
|
||||||
|
Name() string
|
||||||
|
EventToBeSent(name string, value interface{})
|
||||||
|
}
|
||||||
|
|
||||||
type StateVarInstance struct {
|
type StateVarInstance struct {
|
||||||
model *StateVariable
|
model *StateVariable
|
||||||
name string
|
name string
|
||||||
@@ -27,12 +33,12 @@ type StateVarInstance struct {
|
|||||||
sendEvents bool
|
sendEvents bool
|
||||||
parse StringValueParser
|
parse StringValueParser
|
||||||
marshal ValueSerializer
|
marshal ValueSerializer
|
||||||
|
service notifiable
|
||||||
value interface{}
|
value interface{}
|
||||||
previousValue interface{}
|
previousValue interface{}
|
||||||
lastChange time.Time
|
lastChange time.Time
|
||||||
lastEvent time.Time
|
lastEvent time.Time
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *StateVarInstance) Name() string {
|
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.
|
// IsValueInRange checks if a value falls within the defined range.
|
||||||
// Always returns true if no range is set.
|
// Always returns true if no range is set.
|
||||||
|
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
//
|
||||||
// value: Value to check
|
// value: Value to check
|
||||||
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
|
|
||||||
// bool: True if within range or no range defined
|
// bool: True if within range or no range defined
|
||||||
func (instance *StateVarInstance) IsValueInRange(value interface{}) (bool, error) {
|
func (instance *StateVarInstance) IsValueInRange(value interface{}) (bool, error) {
|
||||||
return instance.model.valueType.InRange(value, instance.valueRange)
|
return instance.model.valueType.InRange(value, instance.valueRange)
|
||||||
@@ -186,27 +190,73 @@ func (instance *StateVarInstance) SetValue(val interface{}) error {
|
|||||||
return err
|
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()
|
instance.mu.Lock()
|
||||||
defer instance.mu.Unlock()
|
defer instance.mu.Unlock()
|
||||||
instance.previousValue = instance.value
|
instance.previousValue = instance.value
|
||||||
instance.value = cval
|
instance.value = cval
|
||||||
|
|
||||||
|
if instance.ShouldTriggerEvent() {
|
||||||
|
instance.service.EventToBeSent(instance.Name(), instance.Value())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *StateVarInstance) Incr() {
|
func (instance *StateVarInstance) Incr() error {
|
||||||
instance.mu.Lock()
|
if instance.HasStep() {
|
||||||
defer instance.mu.Unlock()
|
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
|
// ShouldTriggerEvent vérifie toutes les conditions
|
||||||
func (instance *StateVarInstance) ShouldTriggerEvent() bool {
|
func (instance *StateVarInstance) ShouldTriggerEvent() bool {
|
||||||
for _, condition := range instance.model.eventConditions {
|
if instance.IsSendingEvents() {
|
||||||
if !condition(instance) {
|
for name, condition := range instance.model.eventConditions {
|
||||||
return false
|
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 {
|
func (sv *StateVarInstance) GenerateEvent() *etree.Element {
|
||||||
|
|||||||
@@ -131,8 +131,11 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for d := range s.devices.All() {
|
for d := range s.devices.All() {
|
||||||
log.Infof("coucou from %s", d.Name())
|
|
||||||
d.RegisterSSPD()
|
d.RegisterSSPD()
|
||||||
|
|
||||||
|
for svc := range d.services.All() {
|
||||||
|
svc.StartNotifier(ctx, 1*time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attente d’annulation du contexte
|
// attente d’annulation du contexte
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ func (svc *Service) NewInstance() *ServiceInstance {
|
|||||||
|
|
||||||
statevariables: make(sv.StateVarInstanceSet),
|
statevariables: make(sv.StateVarInstanceSet),
|
||||||
actions: make(actions.ActionInstanceSet),
|
actions: make(actions.ActionInstanceSet),
|
||||||
|
|
||||||
|
subscribers: make(map[string]string),
|
||||||
|
changedBuffer: make(map[string]interface{}),
|
||||||
|
seqid: make(map[string]uint32),
|
||||||
}
|
}
|
||||||
|
|
||||||
for v := range svc.stateTable.All() {
|
for v := range svc.stateTable.All() {
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
package upnp
|
package upnp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"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/soap"
|
||||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,8 +27,18 @@ type ServiceInstance struct {
|
|||||||
device *DeviceInstance
|
device *DeviceInstance
|
||||||
statevariables statevariables.StateVarInstanceSet
|
statevariables statevariables.StateVarInstanceSet
|
||||||
actions actions.ActionInstanceSet
|
actions actions.ActionInstanceSet
|
||||||
|
subscribers map[string]string // SID → Callback URL
|
||||||
|
changedBuffer map[string]interface{}
|
||||||
|
seqid map[string]uint32
|
||||||
|
cbmu sync.Mutex
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MethodSubscribe = "SUBSCRIBE"
|
||||||
|
MethodUnsubscribe = "UNSUBSCRIBE"
|
||||||
|
)
|
||||||
|
|
||||||
func (si *ServiceInstance) Name() string {
|
func (si *ServiceInstance) Name() string {
|
||||||
return si.name
|
return si.name
|
||||||
}
|
}
|
||||||
@@ -115,6 +132,81 @@ func (svc *ServiceInstance) SPCDElement() *etree.Element {
|
|||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) AddSubscriber(sid, callback string) {
|
||||||
|
svc.mu.Lock()
|
||||||
|
defer svc.mu.Unlock()
|
||||||
|
if svc.subscribers == nil {
|
||||||
|
svc.subscribers = make(map[string]string)
|
||||||
|
}
|
||||||
|
svc.subscribers[sid] = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) RenewSubscriber(sid, timeout string) {
|
||||||
|
// Pour l'instant juste log, on peut étendre avec expiration
|
||||||
|
svc.mu.Lock()
|
||||||
|
defer svc.mu.Unlock()
|
||||||
|
log.Infof("Renewed SID %s for timeout %s", sid, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) RemoveSubscriber(sid string) {
|
||||||
|
svc.mu.Lock()
|
||||||
|
defer svc.mu.Unlock()
|
||||||
|
delete(svc.subscribers, sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoi d'un événement initial (optionnel)
|
||||||
|
func (svc *ServiceInstance) SendInitialEvent(sid string) {
|
||||||
|
svc.mu.Lock()
|
||||||
|
callback := svc.subscribers[sid]
|
||||||
|
svc.mu.Unlock()
|
||||||
|
if callback == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := make(map[string]interface{})
|
||||||
|
for name, sv := range svc.statevariables {
|
||||||
|
if sv.IsSendingEvents() { // sendEvents="yes"
|
||||||
|
changed[name] = sv.Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changed) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
callback = strings.TrimSpace(callback)
|
||||||
|
callback = strings.Trim(callback, "<>")
|
||||||
|
|
||||||
|
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">`
|
||||||
|
for name, val := range changed {
|
||||||
|
body += fmt.Sprintf("<e:property><%s>%v</%s></e:property>", name, val, name)
|
||||||
|
}
|
||||||
|
body += "</e:propertyset>"
|
||||||
|
|
||||||
|
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<details>\n\n```xml\n%s\n```\n</details>\n",
|
||||||
|
callback,
|
||||||
|
resp.Status,
|
||||||
|
pmolog.PrettyPrintXML(body),
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *ServiceInstance) ToXMLElement() *etree.Element {
|
func (svc *ServiceInstance) ToXMLElement() *etree.Element {
|
||||||
elem := etree.NewElement("service")
|
elem := etree.NewElement("service")
|
||||||
|
|
||||||
@@ -136,11 +228,146 @@ func (svc *ServiceInstance) ToXMLElement() *etree.Element {
|
|||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) EventToBeSent(name string, value interface{}) {
|
||||||
|
svc.cbmu.Lock()
|
||||||
|
defer svc.cbmu.Unlock()
|
||||||
|
|
||||||
|
svc.changedBuffer[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) nextSeq(sid string) string {
|
||||||
|
svc.mu.Lock()
|
||||||
|
defer svc.mu.Unlock()
|
||||||
|
|
||||||
|
svc.seqid[sid]++
|
||||||
|
return fmt.Sprintf("%d", svc.seqid[sid])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) NotifySubscribers() {
|
||||||
|
svc.cbmu.Lock()
|
||||||
|
if len(svc.subscribers) == 0 || len(svc.changedBuffer) == 0 {
|
||||||
|
svc.cbmu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copier et réinitialiser le buffer
|
||||||
|
changed := svc.changedBuffer
|
||||||
|
svc.changedBuffer = make(map[string]interface{})
|
||||||
|
svc.cbmu.Unlock()
|
||||||
|
|
||||||
|
for sid, callback := range svc.subscribers {
|
||||||
|
go func(sid, callback string, changed map[string]interface{}) {
|
||||||
|
callback = strings.TrimSpace(callback)
|
||||||
|
callback = strings.Trim(callback, "<>")
|
||||||
|
|
||||||
|
u, err := url.Parse(callback)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Invalid callback URL %s: %v", callback, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">`
|
||||||
|
for name, val := range changed {
|
||||||
|
body += fmt.Sprintf("<e:property><%s>%v</%s></e:property>", name, val, name)
|
||||||
|
}
|
||||||
|
body += "</e:propertyset>"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("NOTIFY", u.String(), strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to create NOTIFY request to %s: %v", callback, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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", svc.nextSeq(sid))
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to notify subscriber %s: %v", callback, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Infof("✅ Notified subscriber %s of changes: %v", callback, changed)
|
||||||
|
}(sid, callback, changed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ServiceInstance) StartNotifier(ctx context.Context, interval time.Duration) {
|
||||||
|
log.Infof("✅ Starting notifier for %s:%s every %.2f s", svc.device.Name(), svc.Name(), interval.Seconds())
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Infof("✅ Notifier stopped for %s:%s", svc.device.Name(), svc.Name())
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
svc.NotifySubscribers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *ServiceInstance) EventSubHandler() func(w http.ResponseWriter, r *http.Request) {
|
func (svc *ServiceInstance) EventSubHandler() func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
log.Infof("📡 Event Subscription request for %s:%s", svc.device.Name(), svc.Name())
|
||||||
log.Infof("Event Subscription handler for %s:%s", svc.device.Name(), svc.Name())
|
|
||||||
// corps vide volontairement
|
sid := r.Header.Get("SID")
|
||||||
|
timeout := r.Header.Get("Timeout")
|
||||||
|
callback := r.Header.Get("Callback")
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case MethodSubscribe:
|
||||||
|
if sid == "" {
|
||||||
|
// Nouvelle subscription
|
||||||
|
sid = fmt.Sprintf("uuid:%s", uuid.New().String())
|
||||||
|
if callback != "" {
|
||||||
|
svc.AddSubscriber(sid, callback)
|
||||||
|
}
|
||||||
|
if timeout == "" {
|
||||||
|
timeout = "Second-1800"
|
||||||
|
}
|
||||||
|
log.Infof("🔔 New subscription: SID=%s, Callback=%s, Timeout=%s", sid, callback, timeout)
|
||||||
|
go svc.SendInitialEvent(sid) // envoyer l’état initial
|
||||||
|
} else {
|
||||||
|
// Renouvellement
|
||||||
|
svc.RenewSubscriber(sid, timeout)
|
||||||
|
log.Infof("♻️ Renew subscription: SID=%s, Timeout=%s", sid, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("SID", sid)
|
||||||
|
w.Header().Set("Timeout", timeout)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
case MethodUnsubscribe:
|
||||||
|
if sid != "" {
|
||||||
|
svc.RemoveSubscriber(sid)
|
||||||
|
log.Infof("❌ Unsubscribe SID=%s", sid)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Warnf("Unsupported EventSub method: %s", r.Method)
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug log headers
|
||||||
|
for k, v := range r.Header {
|
||||||
|
log.Debugf("Header: %s=%v", k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire le body au besoin
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err == nil && len(body) > 0 {
|
||||||
|
log.Debugf("Body: %s", string(body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user