Files
pmomusic/upnp/devices/services/statevariables/statevalueinstance.go

353 lines
8.4 KiB
Go
Raw Normal View History

2025-07-13 07:02:26 +02:00
package statevariables
2025-07-12 12:28:13 +02:00
import (
2025-09-06 17:45:28 +02:00
"encoding/base64"
"encoding/hex"
"errors"
2025-09-06 17:45:28 +02:00
"fmt"
"net/url"
"reflect"
2025-07-12 12:28:13 +02:00
"sync"
"time"
"github.com/beevik/etree"
2025-09-06 17:45:28 +02:00
"github.com/google/uuid"
2025-07-12 12:28:13 +02:00
)
2025-07-13 06:59:56 +02:00
type StateVarInstance struct {
2025-09-06 17:45:28 +02:00
model *StateVariable
name string
modifiable bool
description string
step interface{} // Step size for incremental state values (e.g., "10")
defaultValue interface{}
valueRange *ValueRange
eventConditions map[string]StateConditionFunc
allowedValues []interface{}
sendEvents bool
parse StringValueParser
marshal ValueSerializer
2025-07-12 12:28:13 +02:00
value interface{}
previousValue interface{}
lastChange time.Time
lastEvent time.Time
mu sync.RWMutex
}
2025-09-06 17:45:28 +02:00
func (instance *StateVarInstance) Name() string {
return instance.model.Name()
}
func (sv *StateVarInstance) TypeID() string {
return "StateVarInstance"
}
func (instance *StateVarInstance) BitSize() int {
return instance.model.BitSize()
}
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) Cast(val interface{}) (interface{}, error) {
2025-07-12 12:28:13 +02:00
return instance.model.Cast(val)
}
2025-09-06 17:45:28 +02:00
func (instance *StateVarInstance) HasDefault() bool {
return instance.defaultValue != nil
}
func (instance *StateVarInstance) DefaultValue() interface{} {
return instance.defaultValue
}
func (instance *StateVarInstance) HasRange() bool {
return instance.valueRange != nil
}
func (instance *StateVarInstance) Minimum() interface{} {
if instance.valueRange == nil {
return nil
}
return instance.valueRange.min
}
func (instance *StateVarInstance) Maximum() interface{} {
if instance.valueRange == nil {
return nil
}
return instance.valueRange.max
}
func (instance *StateVarInstance) IsSendingEvents() bool {
return instance.sendEvents
}
func (instance *StateVarInstance) HasAllowedValues() bool {
return len(instance.allowedValues) > 0
}
func (instance *StateVarInstance) AllowedValues() []interface{} {
return instance.allowedValues
}
func (instance *StateVarInstance) HasParser() bool {
return instance.parse != nil
}
func (instance *StateVarInstance) ParseValue(value string) (interface{}, error) {
if instance.parse == nil {
return value, errors.New("Not parsed value")
}
return instance.parse(value)
}
2025-09-06 17:45:28 +02:00
// 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 (instance *StateVarInstance) IsValueInRange(value interface{}) (bool, error) {
return instance.model.valueType.InRange(value, instance.valueRange)
}
func (instance *StateVarInstance) IsValueAllowed(value interface{}) (bool, error) {
if !instance.HasAllowedValues() {
return true, nil // No list = any value valid
}
cvalue, err := instance.Cast(value)
if err != nil {
return false, err
}
for _, allowed := range instance.allowedValues {
if reflect.DeepEqual(cvalue, allowed) {
return true, nil
}
}
return false, nil
}
func (instance *StateVarInstance) IsValidValue(value interface{}) (bool, error) {
cvalue, err := instance.Cast(value)
if err != nil {
return false, err
}
inrange, err1 := instance.IsValueInRange(cvalue)
allowed, err2 := instance.IsValueAllowed(cvalue)
if err1 != nil || err2 != nil {
if err1 != nil {
err = err1
} else {
err = err2
}
}
return inrange && allowed, err
}
func (instance *StateVarInstance) HasDescription() bool {
return len(instance.description) > 0
}
func (instance *StateVarInstance) Description() string {
return instance.description
}
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) Model() *StateVariable {
2025-07-12 12:28:13 +02:00
return instance.model
}
2025-09-06 17:45:28 +02:00
func (instance *StateVarInstance) IsConstant() bool {
return !instance.modifiable
}
func (instance *StateVarInstance) HasStep() bool {
return instance.step != nil
}
func (instance *StateVarInstance) Step() interface{} {
return instance.step
}
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) Value() interface{} {
2025-07-12 12:28:13 +02:00
instance.mu.RLock()
defer instance.mu.RUnlock()
return instance.value
}
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) SetValue(val interface{}) error {
2025-07-12 12:28:13 +02:00
cval, err := instance.Cast(val)
if err != nil {
return err
}
instance.mu.Lock()
defer instance.mu.Unlock()
instance.previousValue = instance.value
instance.value = cval
return nil
}
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) Incr() {
2025-07-12 12:28:13 +02:00
instance.mu.Lock()
defer instance.mu.Unlock()
}
// ShouldTriggerEvent vérifie toutes les conditions
2025-07-13 06:59:56 +02:00
func (instance *StateVarInstance) ShouldTriggerEvent() bool {
2025-07-12 12:28:13 +02:00
for _, condition := range instance.model.eventConditions {
if !condition(instance) {
return false
}
}
return true
}
2025-07-13 06:59:56 +02:00
func (sv *StateVarInstance) GenerateEvent() *etree.Element {
2025-07-12 12:28:13 +02:00
// Construire le XML d'événement
propSet := etree.NewElement("e:propertyset")
propSet.CreateAttr("xmlns:e", "urn:schemas-upnp-org:event-1-0")
prop := propSet.CreateElement("e:property")
elem := prop.CreateElement(sv.model.Name())
2025-09-06 17:45:28 +02:00
elem.SetText(sv.valueToString(sv.Value()))
2025-07-12 12:28:13 +02:00
return propSet
}
2025-09-06 17:45:28 +02:00
// ToXMLElement generates the complete XML representation of the state variable
// Returns an etree.Element that can be serialized to XML
func (sv *StateVarInstance) ToXMLElement() *etree.Element {
// Create root <stateVariable> element
elem := etree.NewElement("stateVariable")
// Add sendEvents attribute (UPnP eventing capability)
if sv.sendEvents {
elem.CreateAttr("sendEvents", "yes") // Enable event notifications
} else {
elem.CreateAttr("sendEvents", "no") // Disable event notifications
}
name := elem.CreateElement("name")
name.SetText(sv.Name())
// Add data type element
dataType := elem.CreateElement("dataType")
dataType.SetText(sv.model.valueType.String()) // Set UPnP type name (e.g., "ui1", "boolean")
// Add default value if specified
if sv.defaultValue != nil {
defaultValue := elem.CreateElement("defaultValue")
// Convert value to UPnP-compatible string representation
defaultValue.SetText(sv.valueToString(sv.defaultValue))
}
// Add value range constraints if defined
if sv.valueRange != nil {
rangeElem := elem.CreateElement("allowedValueRange")
// Minimum boundary value
min := rangeElem.CreateElement("minimum")
min.SetText(sv.valueToString(sv.valueRange.min))
// Maximum boundary value
max := rangeElem.CreateElement("maximum")
max.SetText(sv.valueToString(sv.valueRange.max))
// Add step value if defined (for incremental controls)
if sv.step != nil {
step := rangeElem.CreateElement("step")
step.SetText(sv.valueToString(sv.step))
}
}
// Add allowed value list if defined
if len(sv.allowedValues) > 0 {
allowedList := elem.CreateElement("allowedValueList")
for _, value := range sv.allowedValues {
// Create individual <allowedValue> elements
allowed := allowedList.CreateElement("allowedValue")
allowed.SetText(sv.valueToString(value))
}
}
// Add description if available
if sv.description != "" {
desc := elem.CreateElement("description")
desc.SetText(sv.description) // Human-readable description
}
return elem
}
// valueToString converts a value to its UPnP-compatible string representation
// Handles type-specific formatting for proper XML serialization
func (sv *StateVarInstance) valueToString(val interface{}) string {
if val == nil {
return "" // Safeguard against nil values
}
// Type-specific formatting for UPnP compliance
switch sv.model.valueType {
case StateType_Boolean:
// Boolean: "1" for true, "0" for false (UPnP standard)
if b, ok := val.(bool); ok && b {
return "1"
}
return "0"
case StateType_Date:
// Date: YYYY-MM-DD format
if t, ok := val.(time.Time); ok {
return t.Format("2006-01-02")
}
case StateType_DateTime, StateType_DateTimeTZ:
// DateTime: ISO 8601 format with timezone
if t, ok := val.(time.Time); ok {
return t.Format(time.RFC3339)
}
case StateType_Time, StateType_TimeTZ:
// Time: HH:MM:SS format
if t, ok := val.(time.Time); ok {
return t.Format("15:04:05")
}
case StateType_BinBase64:
// Binary: Base64 encoding
if b, ok := val.([]byte); ok {
return base64.StdEncoding.EncodeToString(b)
}
case StateType_BinHex:
// Binary: Hex encoding
if b, ok := val.([]byte); ok {
return hex.EncodeToString(b)
}
case StateType_URI:
// URI: Full URL string
if u, ok := val.(*url.URL); ok {
return u.String()
}
case StateType_UUID:
// UUID: Canonical string representation
if u, ok := val.(uuid.UUID); ok {
return u.String()
}
}
// Default conversion for unsupported types or fallback
return fmt.Sprintf("%v", val)
}