COntinue on values

This commit is contained in:
2025-06-21 17:22:56 +02:00
parent aeda0fb7e2
commit 401dcd67af
6 changed files with 567 additions and 13 deletions

8
go.mod
View File

@@ -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

14
go.sum
View File

@@ -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=

384
internal/upnp/statevalue.go Normal file
View File

@@ -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(),
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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{}
}