From 401dcd67afc03461dbf9278f563403ca3004fef0 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 21 Jun 2025 17:22:56 +0200 Subject: [PATCH] COntinue on values --- go.mod | 8 +- go.sum | 14 +- internal/upnp/statevalue.go | 384 ++++++++++++++++++++++++++++ internal/upnp/statevalueinstance.go | 29 +++ internal/upnp/statevaluetype.go | 130 +++++++++- internal/upnp/valuerange.go | 15 ++ 6 files changed, 567 insertions(+), 13 deletions(-) create mode 100644 internal/upnp/statevalue.go create mode 100644 internal/upnp/statevalueinstance.go create mode 100644 internal/upnp/valuerange.go diff --git a/go.mod b/go.mod index 82e7cc88..83951726 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,10 @@ module gargoton.petite-maison-orange.fr/eric/pmomusic go 1.24.2 require ( + github.com/beevik/etree v1.5.1 + github.com/globusdigital/soap v1.4.0 github.com/google/uuid v1.6.0 - github.com/koron/go-ssdp v0.0.6 github.com/sirupsen/logrus v1.9.3 ) -require ( - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect -) +require golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index fcc6ea4c..85214551 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,24 @@ +github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo= +github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/globusdigital/soap v1.4.0 h1:nQDpWelZr3zKg6Oe1e5SqWd4PfiUvsl5/44oWUXy3II= +github.com/globusdigital/soap v1.4.0/go.mod h1:p8hjOZ4FmK0jXBTcIZ6e5M2QBfcsxBUKWBYsHM2eZRw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= -github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/upnp/statevalue.go b/internal/upnp/statevalue.go new file mode 100644 index 00000000..b40f19d5 --- /dev/null +++ b/internal/upnp/statevalue.go @@ -0,0 +1,384 @@ +package upnp + +import ( + "fmt" + "reflect" + "slices" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +type EventType string + +const ( + EventEnabled EventType = "yes" + EventDisabled EventType = "no" + EventConditional EventType = "conditional" +) + +type StateConditionFunc func(instance *StateValueInstance) bool + +type TypeModifier int + +const ( + ModifierAtomic TypeModifier = iota // Valeur simple (par défaut) + ModifierList // Liste d'éléments + ModifierMap // Tableau associatif + ModifierStruct // Structure nommée +) + +// StateValue represents a UPnP state variable with value constraints and eventing capabilities. +// It encapsulates: +// - Value type (StateVarType) +// - Value range constraints (min/max) +// - Allowed value list +// - Eventing configuration +// graph TD +// A[StateValue] --> B[baseType: Type sérialisé] +// A --> C[modifier: Structure des données] +// C --> D[Liste] +// C --> E[Map] +// C --> F[Structure] +// D --> G[elementType: Type des éléments] +// E --> H[keyType: Type des clés] +// E --> I[elementType: Type des valeurs] +// +// Usage: +// +// state := StateValue{ +// name: "Volume", +// baseType: StateType_UI2, +// } +// state.SetRange(0, 100) // Set 0-100 range +// state.AppendAllowedValue(25, 50, 75) // Add specific allowed values +type StateValue struct { + name string + baseType StateVarType // Renommé pour plus de clarté + modifier TypeModifier + step interface{} + minDelta interface{} + modifiable bool + eventConditions []StateConditionFunc + description string + defaultValue interface{} + valueRange *ValueRange + allowedValues []interface{} + sendEvents bool + elementType StateVarType // Pour listes et valeurs de maps + keyType StateVarType // Type des clés (pour les maps) + structFields map[string]StateVarType // Champs (pour les structures) + +} + +// Name returns the state variable's name (e.g., "Volume", "Brightness"). +func (state *StateValue) Name() string { + return state.name +} + +// Type returns the UPnP data type of the state variable. +func (state *StateValue) Type() StateVarType { + return state.baseType +} + +func (state *StateValue) AddEventCondition(condition StateConditionFunc) { + state.eventConditions = append(state.eventConditions, condition) +} + +// ClearEventConditions réinitialise toutes les conditions +func (state *StateValue) ClearEventConditions() { + state.eventConditions = nil +} + +func (state *StateValue) SetDefault(value interface{}) error { + if state.IsValidValue(value) { + cvalue, _ := state.baseType.Cast(value) + state.defaultValue = cvalue + log.Debugf("🐞 Setting default value for %v to %v", state.name, cvalue) + return nil + } + return fmt.Errorf("invalid default value for %v (%v)", state.name, value) +} + +func (state *StateValue) HasDefault() bool { + return state.defaultValue != nil +} + +func (state *StateValue) DefaultValue() interface{} { + if !state.HasDefault() { + return state.baseType.DefaultValue() + } + + return state.DefaultValue() +} + +// HasRange indicates if a value range constraint is defined. +// Returns true if min/max boundaries are set. +func (state *StateValue) HasRange() bool { + return state.valueRange != nil +} + +// Maximum returns the upper bound of the value range. +// Returns nil if no range is defined. +func (state *StateValue) Maximum() interface{} { + if state.valueRange == nil { + return nil + } + return state.valueRange.max +} + +// Minimum returns the lower bound of the value range. +// Returns nil if no range is defined. +func (state *StateValue) Minimum() interface{} { + if state.valueRange == nil { + return nil + } + return state.valueRange.min +} + +// SetRange defines the inclusive value range [min, max]. +// Validates and casts values to the state variable's type. +// +// Parameters: +// +// min: Lower boundary value +// max: Upper boundary value +// +// Returns: +// +// error: If values can't be cast to the type or are nil +// +// Example: +// +// err := volumeState.SetRange(0, 100) // 0-100 range for volume +func (state *StateValue) SetRange(min, max interface{}) error { + if min == nil || max == nil { + return fmt.Errorf("min and max must not be nil") + } + limits, err := state.baseType.ValueRange(min, max) + if err != nil { + return fmt.Errorf("setting range: %v", err) + } + state.valueRange = limits + + log.Debugf("🐞 Setting range of %s to [%v, %v]", state.name, min, max) + return nil +} + +// UpdateMinimalValue dynamically updates the lower range boundary. +// Requires an existing range to be set. +// +// Parameters: +// +// value: New minimum value +// +// Returns: +// +// error: If no range exists or value can't be cast +func (state *StateValue) UpdateMinimalValue(value interface{}) error { + if state.valueRange == nil { + return fmt.Errorf("no range set for value %v", state.name) + } + cvalue, err := state.baseType.Cast(value) + if err != nil { + return fmt.Errorf("casting value: %v", err) + } + state.valueRange.min = cvalue + + log.Debugf("🐞 Updating minimal value of %s to %v", state.name, cvalue) + return nil +} + +// UpdateMaximalValue dynamically updates the upper range boundary. +// Requires an existing range to be set. +// +// Parameters: +// +// value: New maximum value +// +// Returns: +// +// error: If no range exists or value can't be cast +func (state *StateValue) UpdateMaximalValue(value interface{}) error { + if state.valueRange == nil { + return fmt.Errorf("no range set for value %v", state.name) + } + cvalue, err := state.baseType.Cast(value) + if err != nil { + return fmt.Errorf("casting value: %v", err) + } + state.valueRange.max = cvalue + + log.Debugf("🐞 Updating maximal value of %s to %v", state.name, cvalue) + return nil +} + +// IsValueInRange checks if a value falls within the defined range. +// Always returns true if no range is set. +// +// Parameters: +// +// value: Value to check +// +// Returns: +// +// bool: True if within range or no range defined +func (state *StateValue) IsValueInRange(value interface{}) bool { + return state.baseType.InRange(value, state.valueRange) +} + +// IsSendingEvents indicates if state changes trigger UPnP events. +func (state *StateValue) IsSendingEvents() bool { + return state.sendEvents +} + +// SetSendingEvents enables event notifications for state changes. +func (state *StateValue) SetSendingEvents() { + state.sendEvents = true + log.Debugf("🐞 Enabling event sending for %s", state.name) +} + +// UnsetSendingEvents disables event notifications for state changes. +func (state *StateValue) UnsetSendingEvents() { + state.sendEvents = false + log.Debugf("🐞 Disabling event sending for %s", state.name) +} + +// HasAllowedValues indicates if an allowed value list is defined. +func (state *StateValue) HasAllowedValues() bool { + return len(state.allowedValues) > 0 +} + +// AllowedValues returns the list of permitted values. +// Returns an empty slice if no values are defined. +func (state *StateValue) AllowedValues() []interface{} { + return state.allowedValues +} + +// AppendAllowedValue adds values to the permitted value list. +// Values are cast to the state variable's type before adding. +// +// Parameters: +// +// value: One or more values to add +// +// Returns: +// +// error: If any value can't be cast to the type +// +// Example: +// +// err := state.AppendAllowedValue("PLAYING", "PAUSED", "STOPPED") +func (state *StateValue) AppendAllowedValue(value ...interface{}) error { + state.allowedValues = slices.Grow(state.allowedValues, len(value)) + for _, v := range value { + cv, err := state.baseType.Cast(v) + if err != nil { + return fmt.Errorf("casting allowed value: %v", err) + } + state.allowedValues = append(state.allowedValues, cv) + } + + log.Debugf("🐞 Added allowed values to %s: %v", state.name, value) + return nil +} + +// IsValueAllowed checks if a value exists in the allowed value list. +// Always returns true if no allowed values are defined. +// +// Parameters: +// +// value: Value to check +// +// Returns: +// +// bool: True if value is permitted or no list defined +func (state *StateValue) IsValueAllowed(value interface{}) bool { + if !state.HasAllowedValues() { + return true // No list = any value valid + } + + cvalue, err := state.baseType.Cast(value) + if err != nil { + return false + } + + for _, allowed := range state.allowedValues { + if reflect.DeepEqual(cvalue, allowed) { + return true + } + } + return false +} + +// IsValidValue performs full validation against all constraints. +// Checks (in order): +// 1. Value can be cast to the type +// 2. Value is within range (if defined) +// 3. Value is in allowed list (if defined) +// +// Returns: +// +// bool: True if value passes all applicable constraints +func (state *StateValue) IsValidValue(value interface{}) bool { + cvalue, err := state.baseType.Cast(value) + if err != nil { + return false + } + return state.IsValueInRange(cvalue) && state.IsValueAllowed(cvalue) +} + +func (state *StateValue) HasDescription() bool { + return len(state.description) > 0 +} + +func (state *StateValue) Description() string { + return state.description +} + +func (state *StateValue) SetDescription(desc string) { + state.description = strings.TrimSpace(desc) +} + +func (state *StateValue) IsConstant() bool { + return !state.modifiable +} + +func (state *StateValue) SetConstant() { + state.modifiable = false +} + +func (state *StateValue) SetModifiable() { + state.modifiable = true +} + +func (state *StateValue) SetStep(step interface{}) error { + // Validation que le step correspond au type de la variable + if _, err := state.baseType.Cast(step); err != nil { + return fmt.Errorf("invalid step type: %v", err) + } + state.step = step + return nil +} + +func (state *StateValue) UnsetStep() { + state.step = nil +} + +func (state *StateValue) HasStep() bool { + return state.step != nil +} + +func (state *StateValue) Step() interface{} { + return state.step +} + +func (state *StateValue) NewInstance() *StateValueInstance { + return &StateValueInstance{ + model: state, + value: state.DefaultValue(), + lastChange: time.Now(), + lastEvent: time.Unix(int64(1718985600), 0).UTC(), + } +} diff --git a/internal/upnp/statevalueinstance.go b/internal/upnp/statevalueinstance.go new file mode 100644 index 00000000..b26854fd --- /dev/null +++ b/internal/upnp/statevalueinstance.go @@ -0,0 +1,29 @@ +package upnp + +import "time" + +type StateValueInstance struct { + model *StateValue + value interface{} + previousValue interface{} + lastChange time.Time + lastEvent time.Time +} + +func (instance *StateValueInstance) Model() *StateValue { + return instance.model +} + +func (instance *StateValueInstance) Value() interface{} { + return instance.value +} + +// ShouldTriggerEvent vérifie toutes les conditions +func (instance *StateValueInstance) ShouldTriggerEvent() bool { + for _, condition := range instance.model.eventConditions { + if !condition(instance) { + return false + } + } + return true +} diff --git a/internal/upnp/statevaluetype.go b/internal/upnp/statevaluetype.go index 23a72a93..7bde0733 100644 --- a/internal/upnp/statevaluetype.go +++ b/internal/upnp/statevaluetype.go @@ -379,8 +379,134 @@ func (t StateVarType) Cmp(v1, v2 interface{}) int { // // range := ValueRange{min: uint16(10), max: uint16(100)} // StateType_UI2.InRange(uint16(50), range) // true -func (t StateVarType) InRange(val interface{}, interval ValueRange) bool { - return t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0 +func (t StateVarType) InRange(val interface{}, interval *ValueRange) bool { + return interval == nil || t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0 +} + +// ValueRange creates a valid value range for the UPnP type. +// +// This method casts the provided min and max values to the UPnP type and returns +// a ValueRange struct suitable for range validation. If either value cannot be +// cast to the type, it returns an error. +// +// Parameters: +// +// min: Minimum value of the range (inclusive) +// max: Maximum value of the range (inclusive) +// +// Returns: +// +// ValueRange: Valid range structure if cast succeeds +// error: If min or max cannot be cast to the type +// +// Example: +// +// // Create a range for UI2 (uint16) values +// r, err := StateType_UI2.ValueRange(10, 100) +// if err != nil { /* handle error */ } +// valid := StateType_UI2.InRange(50, r) // true +// +// Notes: +// - The range is inclusive: [min, max] +// - Values must be comparable using the type's comparison logic +// - For time types, min and max must be valid time values +func (t StateVarType) ValueRange(min, max interface{}) (*ValueRange, error) { + cmin, error := t.Cast(min) + if error != nil { + return nil, fmt.Errorf("min value %v is not castable to type %s", min, t.String()) + } + cmax, error := t.Cast(max) + if error != nil { + return nil, fmt.Errorf("max value %v is not castable to type %s", min, t.String()) + } + + return &ValueRange{min: cmin, max: cmax}, nil +} + +// NewAtomicValue crée une valeur simple +func (t StateVarType) NewAtomicValue(name string) *StateValue { + return &StateValue{ + name: name, + baseType: t, + modifier: ModifierAtomic, + } +} + +// NewListValue crée une liste +func (t StateVarType) NewListValue(name string, elementType StateVarType) *StateValue { + return &StateValue{ + name: name, + baseType: t, + modifier: ModifierList, + elementType: elementType, + } +} + +// NewMapValue crée une map +func (t StateVarType) NewMapValue( + name string, + keyType StateVarType, + valueType StateVarType, +) *StateValue { + return &StateValue{ + name: name, + baseType: t, + modifier: ModifierMap, + keyType: keyType, + elementType: valueType, // elementType = valeur de la map + } +} + +// NewStructValue crée une valeur de type struct +func (t StateVarType) NewStructValue(name string, fields map[string]StateVarType) *StateValue { + return &StateValue{ + name: name, + baseType: t, + modifier: ModifierStruct, + structFields: fields, + } +} + +func (t StateVarType) DefaultValue() interface{} { + switch t { + case StateType_Unknown: + + case StateType_UI1, StateType_UI2, StateType_UI4: + return uint64(0) + + case StateType_I1, StateType_I2, StateType_I4, StateType_Int: + return int64(0) + + case StateType_R4, StateType_R8, StateType_Number, StateType_Fixed14_4: + return float64(0) + + case StateType_Char, StateType_String: + return "" + case StateType_Boolean: + return false + + case StateType_BinBase64: + return "" + case StateType_BinHex: + return "" + case StateType_Date: + return time.Unix(int64(1718985600), 0).UTC() + case StateType_DateTime: + return time.Unix(int64(1718985600), 0).UTC() + case StateType_DateTimeTZ: + return time.Unix(int64(1718985600), 0).UTC() + case StateType_Time: + return time.Unix(int64(1718985600), 0).UTC() + case StateType_TimeTZ: + return time.Unix(int64(1718985600), 0).UTC() + case StateType_UUID: + return "" + case StateType_URI: + return "" + + } + + return nil } // toInt converts various types to signed integer with specified bit size. diff --git a/internal/upnp/valuerange.go b/internal/upnp/valuerange.go new file mode 100644 index 00000000..8639e01a --- /dev/null +++ b/internal/upnp/valuerange.go @@ -0,0 +1,15 @@ +package upnp + +// ValueRange represents an inclusive range constraint for a state variable value. +// It defines the minimum and maximum allowable values for a given UPnP type. +// +// Usage: +// - For numeric types: min/max must be numeric types +// - For time types: min/max must be time.Time values +// - For strings/UUIDs: min/max must be string values +// +// Use with StateVarType.InRange() to check if values fall within the range. +type ValueRange struct { + min interface{} + max interface{} +}