rename package

This commit is contained in:
2025-07-13 07:02:26 +02:00
parent 63284ab51c
commit f063aadcd8
24 changed files with 24 additions and 24 deletions

View File

@@ -0,0 +1,75 @@
package statevariables
import (
"sync"
"time"
"github.com/beevik/etree"
)
type StateVarInstance struct {
model *StateVariable
value interface{}
previousValue interface{}
lastChange time.Time
lastEvent time.Time
mu sync.RWMutex
}
func (instance *StateVarInstance) Cast(val interface{}) (interface{}, error) {
return instance.model.Cast(val)
}
func (instance *StateVarInstance) Model() *StateVariable {
return instance.model
}
func (instance *StateVarInstance) Value() interface{} {
instance.mu.RLock()
defer instance.mu.RUnlock()
return instance.value
}
func (instance *StateVarInstance) SetValue(val interface{}) error {
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
}
func (instance *StateVarInstance) Incr() {
instance.mu.Lock()
defer instance.mu.Unlock()
}
// ShouldTriggerEvent vérifie toutes les conditions
func (instance *StateVarInstance) ShouldTriggerEvent() bool {
for _, condition := range instance.model.eventConditions {
if !condition(instance) {
return false
}
}
return true
}
func (sv *StateVarInstance) GenerateEvent() *etree.Element {
// 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())
elem.SetText(sv.model.valueToString(sv.Value()))
return propSet
}

View File

@@ -0,0 +1,553 @@
package statevariables
import (
"encoding/base64"
"encoding/hex"
"fmt"
"net/url"
"reflect"
"slices"
"strings"
"time"
"github.com/beevik/etree"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
type EventType string
type StateConditionFunc func(instance *StateVarInstance) bool
type StringValueParser func(value string) (interface{}, error)
type ValueSerializer func(value interface{}) (string, error)
type StateVariable struct {
name string // Name of the state value (e.g., "Volume", "Brightness")
valueType StateVarType // Type of state value, see the upnp/statevaluetype package for more information on this.
step interface{} // Step size for incremental state values (e.g., "10")
modifiable bool
eventConditions map[string]StateConditionFunc
description string
defaultValue interface{}
valueRange *ValueRange
allowedValues []interface{}
sendEvents bool
parse StringValueParser
marshal ValueSerializer
}
// BitSize returns the number of bits that will be used to represent values
// when this type is used in an UPnP State Variable. The returned value can be
// either 8, 16, 24, 32, or 64, depending on whether t is Byte, Boolean, I2, Ui2,
// I4, Ui4 respectively. If none of these types match, it will return -1.
func (sv StateVariable) BitSize() int {
return sv.valueType.BitSize()
}
// Name returns the state variable's name (e.g., "Volume", "Brightness").
func (sv StateVariable) Name() string {
return sv.name
}
// Type returns the UPnP data type of the state variable.
func (state *StateVariable) Type() StateVarType {
return state.valueType
}
func (state *StateVariable) AddEventCondition(name string, condition StateConditionFunc) {
state.eventConditions[name] = condition
}
func (state *StateVariable) DeleteEventConditions(name string) error {
if _, ok := state.eventConditions[name]; !ok {
return fmt.Errorf("%s: no such event condition (%s)", state.name, name)
}
delete(state.eventConditions, name)
return nil
}
// ClearEventConditions réinitialise toutes les conditions
func (state *StateVariable) ClearEventConditions() {
state.eventConditions = make(map[string]StateConditionFunc)
}
func (sv *StateVariable) SetMinDelta(minDelta interface{}) error {
if minDelta == nil {
return fmt.Errorf("%s: nil is an invalid minimum delta value", sv.name)
}
minDelta, err := sv.Cast(minDelta)
if err != nil {
return fmt.Errorf("%s: invalid minimum delta value %v : %v", sv.name, minDelta, err)
}
// mdf := func(instance *StateValueInstance) bool {
// o := instance.previousValue
// n := instance.value
// r,err := instance.model.valueType.ValueRange(o, n)
// if err != nil {
// return false
// }
// return instance.model.valueType.InRange()
// }
// sv.eventConditions["MinDelta"] = mdf
return nil
}
func (state *StateVariable) SetDefault(value interface{}) error {
var err error
var valid bool
if valid, err = state.IsValidValue(value); valid && err == nil {
cvalue, _ := state.valueType.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) : %v", state.name, value, err)
}
func (state *StateVariable) HasDefault() bool {
return state.defaultValue != nil
}
func (state *StateVariable) DefaultValue() interface{} {
if !state.HasDefault() {
return state.valueType.DefaultValue()
}
return state.DefaultValue()
}
// HasRange indicates if a value range constraint is defined.
// Returns true if min/max boundaries are set.
func (state *StateVariable) HasRange() bool {
return state.valueRange != nil
}
// Maximum returns the upper bound of the value range.
// Returns nil if no range is defined.
func (state *StateVariable) 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 *StateVariable) 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 *StateVariable) SetRange(min, max interface{}) error {
if min == nil || max == nil {
return fmt.Errorf("min and max must not be nil")
}
limits, err := state.valueType.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 *StateVariable) UpdateMinimalValue(value interface{}) error {
if state.valueRange == nil {
return fmt.Errorf("no range set for value %v", state.name)
}
cvalue, err := state.valueType.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 *StateVariable) UpdateMaximalValue(value interface{}) error {
if state.valueRange == nil {
return fmt.Errorf("no range set for value %v", state.name)
}
cvalue, err := state.valueType.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 *StateVariable) IsValueInRange(value interface{}) (bool, error) {
return state.valueType.InRange(value, state.valueRange)
}
// IsSendingEvents indicates if state changes trigger UPnP events.
func (state *StateVariable) IsSendingEvents() bool {
return state.sendEvents
}
// SetSendingEvents enables event notifications for state changes.
func (state *StateVariable) SetSendingEvents() {
state.sendEvents = true
log.Debugf("🐞 Enabling event sending for %s", state.name)
}
// UnsetSendingEvents disables event notifications for state changes.
func (state *StateVariable) UnsetSendingEvents() {
state.sendEvents = false
log.Debugf("🐞 Disabling event sending for %s", state.name)
}
// HasAllowedValues indicates if an allowed value list is defined.
func (state *StateVariable) 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 *StateVariable) 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 *StateVariable) AppendAllowedValue(value ...interface{}) error {
state.allowedValues = slices.Grow(state.allowedValues, len(value))
for _, v := range value {
cv, err := state.valueType.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 *StateVariable) IsValueAllowed(value interface{}) (bool, error) {
if !state.HasAllowedValues() {
return true, nil // No list = any value valid
}
cvalue, err := state.valueType.Cast(value)
if err != nil {
return false, err
}
for _, allowed := range state.allowedValues {
if reflect.DeepEqual(cvalue, allowed) {
return true, nil
}
}
return false, nil
}
// 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 *StateVariable) IsValidValue(value interface{}) (bool, error) {
cvalue, err := state.valueType.Cast(value)
if err != nil {
return false, err
}
inrange, err1 := state.IsValueInRange(cvalue)
allowed, err2 := state.IsValueAllowed(cvalue)
if err1 != nil || err2 != nil {
if err1 != nil {
err = err1
} else {
err = err2
}
}
return inrange && allowed, err
}
func (state *StateVariable) HasDescription() bool {
return len(state.description) > 0
}
func (state *StateVariable) Description() string {
return state.description
}
func (state *StateVariable) SetDescription(desc string) {
state.description = strings.TrimSpace(desc)
}
func (state *StateVariable) IsConstant() bool {
return !state.modifiable
}
func (state *StateVariable) SetConstant() {
state.modifiable = false
}
func (state *StateVariable) SetModifiable() {
state.modifiable = true
}
func (state *StateVariable) SetStep(step interface{}) error {
// Validation que le step correspond au type de la variable
if _, err := state.valueType.Cast(step); err != nil {
return fmt.Errorf("invalid step type: %v", err)
}
state.step = step
return nil
}
func (state *StateVariable) UnsetStep() {
state.step = nil
}
func (state *StateVariable) HasStep() bool {
return state.step != nil
}
func (state *StateVariable) Step() interface{} {
return state.step
}
func (state *StateVariable) SetAllowedValues(allowed ...interface{}) {
state.ClearAllowedValues()
state.AppendAllowedValues(allowed...)
}
func (state *StateVariable) AppendAllowedValues(allowed ...interface{}) error {
state.allowedValues = slices.Grow(state.allowedValues, len(allowed))
var err error
for _, val := range allowed {
val, err = state.valueType.Cast(val)
if err != nil {
return err
}
state.allowedValues = append(state.allowedValues, val)
}
return nil
}
func (state *StateVariable) ClearAllowedValues() {
state.allowedValues = make([]interface{}, 0)
}
func (state *StateVariable) NewInstance() *StateVarInstance {
return &StateVarInstance{
model: state,
value: state.DefaultValue(),
lastChange: time.Now(),
lastEvent: time.Unix(int64(1718985600), 0).UTC(),
}
}
// ToXMLElement generates the complete XML representation of the state variable
// Returns an etree.Element that can be serialized to XML
func (sv *StateVariable) ToXMLElement() *etree.Element {
// Create root <stateVariable> element
elem := etree.NewElement("stateVariable")
elem.CreateAttr("name", sv.name)
// Add sendEvents attribute (UPnP eventing capability)
if sv.sendEvents {
elem.CreateAttr("sendEvents", "yes") // Enable event notifications
} else {
elem.CreateAttr("sendEvents", "no") // Disable event notifications
}
// Add data type element
dataType := elem.CreateElement("dataType")
dataType.SetText(sv.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 *StateVariable) valueToString(val interface{}) string {
if val == nil {
return "" // Safeguard against nil values
}
// Type-specific formatting for UPnP compliance
switch sv.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)
}

View File

@@ -0,0 +1,17 @@
package statevariables
var TransferStatus = func() *StateVariable {
ts := StateType_String.NewStateValue("TransferStatus")
ts.SetAllowedValues(
"COMPLETED",
"ERROR",
"IN_PROGRESS",
"NONE",
)
ts.SetSendingEvents()
return ts
}()

View File

@@ -0,0 +1,13 @@
package statevariables
var Volume = func() *StateVariable {
vol := StateType_UI2.NewStateValue("Volume")
vol.SetRange(0, 100)
vol.SetStep(1)
vol.SetSendingEvents()
return vol
}()

View File

@@ -0,0 +1,39 @@
package statevariables
// Add performs addition operation on the given parameters 'a' and 'b'. It calls the
// corresponding method from valueType which is assumed to be an interface that
// provides methods for arithmetic operations. This function returns the result of
// the operation or an error if any occurs during computation. Side effects might
// include modifying the state of the system, but this depends on the actual implementation
// of the valueType's Add method. Errors might occur due to invalid input parameters or
// failures in the addition operation itself. The function does not handle edge cases
// and therefore should be used with caution. Here is an example usage:
//
// result, err := sv.Add(5, 3)
// if err != nil {
// // Handle error
// } else {
// // Use result
// }
func (sv StateVariable) Add(a, b interface{}) (interface{}, error) {
return sv.valueType.Add(a, b)
}
// Sub performs subtraction operation on the given parameters 'a' and 'b'. It follows
// similar semantics as in the Add method, but for subtraction instead of addition.
func (sv StateVariable) Sub(a, b interface{}) (interface{}, error) {
return sv.valueType.Sub(a, b)
}
// Mul performs multiplication operation on the given parameters 'a' and 'b'. It follows
// similar semantics as in the Add method, but for multiplication instead of addition.
func (sv StateVariable) Mul(a, b interface{}) (interface{}, error) {
return sv.valueType.Mul(a, b)
}
// Div performs division operation on the given parameters 'a' and 'b'. It follows similar
// semantics as in the Add method, but for division instead of addition. Please note that
// division by zero is undefined and will result in an error being returned from valueType.Div call.
func (sv StateVariable) Div(a, b interface{}) (interface{}, error) {
return sv.valueType.Div(a, b)
}

View File

@@ -0,0 +1,29 @@
package statevariables
// Cast transforms any given value into an interface suitable for use in a StateValue object, using
// the underlying type's specific casting rules. It will return an error if the transformation fails or
// if the provided interface is not supported by the ValueType of the StateValue.
// This function does NOT mutate the original value it receives as input.
//
// The parameter 'val' is an interface that needs to be cast into a form compatible with the internal
// state representation used by the StateValue object.
//
// It returns an interface and an error: if the casting operation was successful, the first return value will
// be the casted version of 'val', and the second one (an error) will be nil. If the casting fails, it will
// return a nil for the first value and an appropriate error for the second one.
//
// This function should not modify its input value. It is idempotent and always returns consistent results given
// the same inputs. However, if the provided interface 'val' does not match with the ValueType of StateValue, it
// will return an error.
//
// Example usage:
//
// state := upnp.NewStateValue(upnp.NewTime())
// castedVal, err := state.Cast("2022-12-31")
// if err != nil {
// log.Println(err)
// return
// }
func (sv *StateVariable) Cast(val interface{}) (interface{}, error) {
return sv.valueType.Cast(val)
}

View File

@@ -0,0 +1,13 @@
package statevariables
func (sv StateVariable) Cmp(a, b interface{}) (int, error) {
return sv.valueType.Cmp(a, b)
}
func (sv StateVariable) Equal(a, b interface{}) (int, error) {
return sv.valueType.Cmp(a, b)
}
func (sv StateVariable) InRange(val interface{}, interval *ValueRange) (bool, error) {
return sv.valueType.InRange(val, interval)
}

View File

@@ -0,0 +1,158 @@
package statevariables
// IsNumeric checks whether a given StateValue model represents a numeric value or
// not. Numeric types are defined as those that can be used to store number-like
// values. The following types are considered numeric: UI1, UI2, UI4, I1, I2,
// I4, Int, R4, R8, Number and Fixed14_4.
//
// t: StateVarType to check if it's a numeric type or not.
//
// Returns true if the given StateValue represents a numeric value; false
// otherwise.
func (sv StateVariable) IsNumeric() bool {
return sv.valueType.IsNumeric()
}
// IsInteger checks if the state variable type is integer or not.
//
// It returns a boolean value indicating whether the provided StateValue
// is an integer type or not.
//
// Returns: bool: If the state variable type is any of the defined integer types
// (UI1, UI2, UI4, I1, I2, I4, Int), it returns true. Otherwise, it returns false.
func (sv StateVariable) IsInteger() bool {
return sv.valueType.IsInteger()
}
// IsSignedInt checks if the state value type is a signed int.
//
// The return value will be a boolean indicating whether the state value type is
// a signed integer (true) or not (false).
func (sv StateVariable) IsSignedInt() bool {
return sv.valueType.IsSignedInt()
}
// IsUnsignedInt checks if the state value type represents an unsigned integer.
// The method returns a boolean indicating whether the state value type is an
// unsigned integer.
func (sv StateVariable) IsUnsignedInt() bool {
return sv.valueType.IsUnsignedInt()
}
// IsFloat returns true if the StateValue's value type represents a floating point
// number; false otherwise.
func (sv StateVariable) IsFloat() bool {
return sv.valueType.IsFloat()
}
// IsBool checks if the state value is of boolean type.
//
// Parameters:
//
// None
//
// Returns:
//
// (bool) : Indicates whether the StateValue is of boolean type or not.
func (sv StateVariable) IsBool() bool {
return sv.valueType.IsBool()
}
// IsString checks if the underlying value type of a StateValue object
// represents a string.
//
// Parameters:
// - None.
//
// Returns:
//
// bool: Indicates whether the underlying value type is a string or not.
//
// Side Effects:
// - This function does not modify any state. It only reads and returns a boolean value.
//
// Errors:
// - This function does not return an error, so you don't have to check for errors.
//
// Edge Cases:
// - If the underlying type of the StateValue is not TypeString,
// this function will return false as expected.
//
// Usage example:
//
// state := upnp.StateValue{valueType: upnp.TypeInt}
// fmt.Println(state.IsString()) // Outputs: false
func (sv StateVariable) IsString() bool {
return sv.valueType.IsString()
}
// IsTime reports whether this state value represents a time instance.
//
// The function returns true if and only if the underlying type of the
// StateValue is TypeTime, else false. This method does not check for other
// types that are convertible to Time as it's assumed that these would be
// handled by the ConvertToType method beforehand.
//
// No side effects: this function is pure and doesn't change any state.
//
// Errors: This function does not return an error, so you don't have to check
// for errors. However, note that TypeTime conversion might fail if the
// StateValue isn't convertible to Time; use the ConvertToType method in such
// cases.
//
// Edge cases: If the underlying type of the StateValue is not TypeTime or is a
// non-convertible time, this function will return false as expected.
//
// Usage example:
//
// state := upnp.StateValue{valueType: upnp.TypeInt}
// fmt.Println(state.IsTime())
//
// Outputs: false
func (sv StateVariable) IsTime() bool {
return sv.valueType.IsTime()
}
// IsUUID checks if the state value type is a UUID (Universally Unique
// Identifier).
//
// It returns true if the underlying type of the StateValue object represents a
// UUID; false otherwise.
//
// Returns: bool: Indicates whether the StateValue is of UUID type or not.
func (sv StateVariable) IsUUID() bool {
return sv.valueType.IsUUID()
}
// IsURI checks if the state value type is a URI.
// The method returns a boolean indicating whether the state value type represents
// a Uniform Resource Identifier (URI) or not.
//
// Returns: bool: Indicates whether the StateValue is of URI type or not.
func (sv StateVariable) IsURI() bool {
return sv.valueType.IsURI()
}
// IsBinary checks if the state value type is binary or not.
//
// The function returns a boolean indicating whether the provided StateValue's
// value type is of binary format (bin.base64 or bin.hex).
//
// Returns: bool: If the underlying value type of the StateValue object is
// either TypeBinBase64 or TypeBinHex, this method will return true; otherwise,
// it returns false.
func (sv StateVariable) IsBinary() bool {
return sv.valueType.IsBinary()
}
// IsComparable function checks if a state variable is comparable or not.
//
// It returns false for binary types (bin.base64 and bin.hex)
// as they are non-comparable. For all other types, it returns true indicating
// that these types can be compared.
// //
// Returns: bool: A boolean value indicating whether the given StateValue is
// comparable or not. True means it's comparable, False means it isn't.
func (sv StateVariable) IsComparable() bool {
return sv.valueType.IsComparable()
}

View File

@@ -0,0 +1,194 @@
// package stateVariables provides comprehensive handling of UPnP state variable types.
// It includes type identification, value casting, comparison, and range validation
// for all standard UPnP state variable types.
package statevariables
import (
"strings"
"time"
)
// StateVarType represents UPnP state variable types with corresponding Go type mappings.
type StateVarType int
// Constants defining all supported UPnP state variable types
const (
StateType_Unknown StateVarType = iota
StateType_UI1 // Unsigned 8-bit integer (Go: uint8)
StateType_UI2 // Unsigned 16-bit integer (Go: uint16)
StateType_UI4 // Unsigned 32-bit integer (Go: uint32)
StateType_I1 // Signed 8-bit integer (Go: int8)
StateType_I2 // Signed 16-bit integer (Go: int16)
StateType_I4 // Signed 32-bit integer (Go: int32)
StateType_Int // Synonymous with i4 (Go: int32)
StateType_R4 // 32-bit floating point (Go: float32)
StateType_R8 // 64-bit floating point (Go: float64)
StateType_Number // Synonymous with r8 (Go: float64)
StateType_Fixed14_4 // Fixed-point decimal (Go: float64)
StateType_Char // Single Unicode character (Go: rune)
StateType_String // Character string (Go: string)
StateType_Boolean // Boolean value (Go: bool)
StateType_BinBase64 // Base64-encoded binary (Go: []byte)
StateType_BinHex // Hex-encoded binary (Go: []byte)
StateType_Date // Date (YYYY-MM-DD) (Go: time.Time)
StateType_DateTime // DateTime without timezone (Go: time.Time)
StateType_DateTimeTZ // DateTime with timezone (Go: time.Time)
StateType_Time // Time without timezone (Go: time.Time)
StateType_TimeTZ // Time with timezone (Go: time.Time)
StateType_UUID // Universally unique identifier (Go: uuid.UUID)
StateType_URI // Uniform Resource Identifier (Go: *url.URL)
)
// typeNames maps UPnP XML type names to StateVarType constants
var typeNames = map[string]StateVarType{
"ui1": StateType_UI1,
"ui2": StateType_UI2,
"ui4": StateType_UI4,
"i1": StateType_I1,
"i2": StateType_I2,
"i4": StateType_I4,
"int": StateType_Int,
"r4": StateType_R4,
"r8": StateType_R8,
"number": StateType_Number,
"fixed.14.4": StateType_Fixed14_4,
"char": StateType_Char,
"string": StateType_String,
"boolean": StateType_Boolean,
"bin.base64": StateType_BinBase64,
"bin.hex": StateType_BinHex,
"date": StateType_Date,
"dateTime": StateType_DateTime,
"dateTime.tz": StateType_DateTimeTZ,
"time": StateType_Time,
"time.tz": StateType_TimeTZ,
"uuid": StateType_UUID,
"uri": StateType_URI,
}
// typeStrings provides string representations for StateVarType constants
var typeStrings = [...]string{
"unknown",
"ui1",
"ui2",
"ui4",
"i1",
"i2",
"i4",
"int",
"r4",
"r8",
"number",
"fixed.14.4",
"char",
"string",
"boolean",
"bin.base64",
"bin.hex",
"date",
"dateTime",
"dateTime.tz",
"time",
"time.tz",
"uuid",
"uri",
}
// StateVarTypeFactory takes a string and attempts to build from it a valid
// StateVarType. The input string is cleaned before processing -
// leading/trailing spaces are trimmed, the case is lowered for comparison with
// known types, and if no match is found 'StateType_Unknown' is returned.
func StateVarTypeFactory(s string) StateVarType {
s = strings.ToLower(strings.TrimSpace(s))
if val, ok := typeNames[s]; ok {
return val
}
return StateType_Unknown
}
// String returns a string representation of the StateVarType. It defaults to
// "unknown" if the type is not recognized.
func (t StateVarType) String() string {
if int(t) >= 0 && int(t) < len(typeStrings) {
return typeStrings[t]
}
return "unknown"
}
// BitSize returns the bit size of the StateVarType value, or -1 if not numeric.
// The possible return values are 8 for StateTypes I1, UI1, and 64 for others.
func (t StateVarType) BitSize() int {
// If t isn't numeric, return -1
if !t.IsNumeric() {
return -1
}
// Check the value of t and return the appropriate bit size
switch t {
case StateType_I1, StateType_UI1:
return 8
case StateType_I2, StateType_UI2:
return 16
case StateType_I4, StateType_UI4, StateType_Int, StateType_R4:
return 32
case StateType_R8, StateType_Number, StateType_Fixed14_4:
return 64
default:
return 64
}
}
// NewStateValue creates and returns a new StateValue struct instance with the given name
// and the receiver's state variable type. The created StateValue is initialized with an
// empty map for event conditions. If name is an empty string, it will cause panic in later
// usage. Name is typically used to identify or represent a specific value or condition
// associated with the variable type 't'.
func (t StateVarType) NewStateValue(name string) *StateVariable {
return &StateVariable{
name: name,
valueType: t,
eventConditions: make(map[string]StateConditionFunc),
}
}
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
}

View File

@@ -0,0 +1,73 @@
package statevariables
// Add performs addition operation on two interfaces if both are of numeric
// type, otherwise it returns an error. If the types are not numeric, it checks
// and converts them into float64 before performing the addition. The function
// then casts the result back to its original type using Cast method from
// StateVarType t and returns this value or any encountered error.
//
// Parameters:
//
// a (interface{}): First operand for addition operation. Can be of any type.
// b (interface{}): Second operand for addition operation. Can be of any type.
//
// Returns:
//
// interface{}: Result of the addition, casted back to its original type using StateVarType t if no error encountered.
// error: Encountered error in case any conversion or casting fails. This includes non-numeric types for this operation.
func (t StateVarType) Add(a, b interface{}) (interface{}, error) {
af, bf, err := valuesToNumericOperands(t, a, b)
if err != nil {
return nil, err
}
return t.Cast(af + bf)
}
// Sub subtracts 'b' from 'a'. It converts both values to numeric types
// and then performs a subtraction operation, casting the result back to its
// original type. If either conversion fails or an unsupported type is used,
// it returns an error.
func (t StateVarType) Sub(a, b interface{}) (interface{}, error) {
af, bf, err := valuesToNumericOperands(t, a, b)
if err != nil {
return nil, err
}
return t.Cast(af - bf)
}
// Mul takes in two interface types 'a' and 'b', multiplies them together
// and returns the result along with any error encountered during this
// process. If either of the inputs is not compatible with numeric values, an
// error will be returned. The multiplication operation is performed between two
// numbers represented as 'float64' types (since Go does not support generic
// types on its own). The resulting value will be cast to the type represented
// by the receiver of this method 't'. If a casting error occurs, it will also
// be returned along with nil for the result.
func (t StateVarType) Mul(a, b interface{}) (interface{}, error) {
af, bf, err := valuesToNumericOperands(t, a, b)
if err != nil {
return nil, err
}
return t.Cast(af * bf)
}
// Div performs division between the provided arguments 'a' and 'b'. The function casts both operands to their numeric equivalents using valuesToNumericOperands() before performing the division.
//
// Parameters:
// - a: first operand of type interface{}, can be of any type, will be converted if necessary
// - b: second operand of type interface{}, can be of any type, will be converted if necessary
//
// Returns:
// - result: the division result in numeric form after casting it with function t.Cast()
// - err: error that might occur during the conversion or division operation
func (t StateVarType) Div(a, b interface{}) (interface{}, error) {
af, bf, err := valuesToNumericOperands(t, a, b)
if err != nil {
return nil, err
}
return t.Cast(af / bf)
}

View File

@@ -0,0 +1,153 @@
package statevariables
import (
"fmt"
"net/url"
"strings"
"time"
"github.com/google/uuid"
)
// Cast converts a value to the Go type corresponding to the UPnP type.
// Supports conversion from various primitive types and strings.
// Returns an error for unsupported conversions or invalid values.
//
// Examples:
// - StateType_UI2.Cast(42) // uint16(42), nil
// - StateType_Boolean.Cast("true") // true, nil
// - StateType_UI1.Cast(300) // nil, error (overflow)
func (t StateVarType) Cast(val interface{}) (interface{}, error) {
switch t {
case StateType_UI1:
v, err := toUint(val, 8)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to UI1", val, val)
}
return uint8(v), nil
case StateType_UI2:
v, err := toUint(val, 16)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to UI2", val, val)
}
return uint16(v), nil
case StateType_UI4:
v, err := toUint(val, 32)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to UI4", val, val)
}
return uint32(v), nil
case StateType_I1:
v, err := toInt(val, 8)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to I1", val, val)
}
return int8(v), nil
case StateType_I2:
v, err := toInt(val, 16)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to I2", val, val)
}
return int16(v), nil
case StateType_I4, StateType_Int:
v, err := toInt(val, 32)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to I4", val, val)
}
return int32(v), nil
case StateType_R4:
v, err := toFloat(val, 32)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to R4", val, val)
}
return float32(v), nil
case StateType_R8, StateType_Number, StateType_Fixed14_4:
v, err := toFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to R8", val, val)
}
return v, nil
case StateType_Boolean:
b, err := toBool(val)
if err != nil {
return nil, fmt.Errorf("cannot cast %v (%T) to Boolean", val, val)
}
return b, nil
case StateType_Char:
switch s := val.(type) {
case string:
if len(s) != 1 {
return nil, fmt.Errorf("invalid Char: string too long %q", s)
}
return rune(s[0]), nil
case rune:
return s, nil
default:
return nil, fmt.Errorf("cannot cast %v (%T) to Char", val, val)
}
case StateType_String:
return fmt.Sprint(val), nil
case StateType_UUID:
switch val := val.(type) {
case uuid.UUID:
return val, nil
case string:
u, err := uuid.Parse(strings.TrimSpace(val))
if err != nil {
return nil, fmt.Errorf("invalid UUID %v: %v", val, err)
}
return u, nil
default:
return nil, fmt.Errorf("cannot cast %v (%T) to UUID", val, val)
}
case StateType_URI:
switch val := val.(type) {
case *url.URL:
return val, nil
case string:
u, err := url.Parse(strings.TrimSpace(val))
if err != nil {
return nil, fmt.Errorf("invalid URI %v: %v", val, err)
}
return u, nil
default:
return nil, fmt.Errorf("cannot cast %v (%T) to URI", val, val)
}
case StateType_BinBase64, StateType_BinHex:
switch v := val.(type) {
case []byte:
return v, nil
case string:
return decodeBinary(t, v)
default:
return nil, fmt.Errorf("cannot cast %v (%T) to binary", val, val)
}
case StateType_Date, StateType_DateTime, StateType_DateTimeTZ,
StateType_Time, StateType_TimeTZ:
switch v := val.(type) {
case time.Time:
return v, nil
case string:
return parseUPnPTime(t, v)
default:
return nil, fmt.Errorf("cannot cast %v (%T) to time", val, val)
}
default:
return nil, fmt.Errorf("unsupported type: %v", t)
}
}

View File

@@ -0,0 +1,205 @@
package statevariables
import (
"bytes"
"fmt"
"log"
"strings"
)
// Cmp compares two values of a given type 'StateVarType'. The comparison is
// made based on the specific type of 'a' and 'b'. It returns an integer
// indicating whether 'a' is less than, equal to, or greater than 'b'. If 'a' is
// less than 'b', it returns -1. If they are equal, it returns 0. If 'a' is
// greater than 'b', it returns 1. It also returns an error if any of the values
// can't be cast to the required type or if comparison isn't supported for the
// given type 'StateVarType'.
//
// - t: StateVarType representing the specific type to use for comparison.
// - a, b: The values to compare. They should be of type interface{} as they could be
// any valid Go type.
//
// Returns: An integer indicating the result of the comparison (-1 if 'a' is
// less than 'b', 0 if they are equal, and 1 if 'a' is greater than 'b'). It
// also returns an error if any values can't be cast or if comparison isn't
// supported for the given type.
//
// Example:
//
// result, err := t.Cmp(int32(5), int32(7)) // Returns -1 and nil error as 5 is less than 7.
func (t StateVarType) Cmp(a, b interface{}) (int, error) {
a, err1 := t.Cast(a)
b, err2 := t.Cast(b)
if err1 != nil || err2 != nil {
log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", a, b, err1, err2)
}
switch {
case t.IsInteger():
ai, err := toInt(a, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid int value for a: %w", err)
}
bi, err := toInt(b, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid int value for b: %w", err)
}
return cmpInt(ai, bi), nil
case t.IsUnsignedInt():
ai, err := toUint(a, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid uint value for a: %w", err)
}
bi, err := toUint(b, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid uint value for b: %w", err)
}
return cmpUint(ai, bi), nil
case t.IsFloat():
af, err := toFloat(a, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid float value for a: %w", err)
}
bf, err := toFloat(b, t.BitSize())
if err != nil {
return 0, fmt.Errorf("invalid float value for b: %w", err)
}
return cmpFloat64(af, bf), nil
case t == StateType_Boolean:
ab, err := toBool(a)
if err != nil {
return 0, fmt.Errorf("invalid bool value for a: %w", err)
}
bb, err := toBool(b)
if err != nil {
return 0, fmt.Errorf("invalid bool value for b: %w", err)
}
return cmpBool(ab, bb), nil
case t == StateType_String || t == StateType_Char:
as, err := toString(a)
if err != nil {
return 0, fmt.Errorf("invalid string value for a")
}
bs, err := toString(b)
if err != nil {
return 0, fmt.Errorf("invalid string value for b")
}
return strings.Compare(as, bs), nil
case t.IsTime():
at, err := toTime(a)
if err != nil {
return 0, fmt.Errorf("invalid time value for a")
}
bt, err := toTime(b)
if err != nil {
return 0, fmt.Errorf("invalid time value for b")
}
return cmpTime(at, bt), nil
default:
return 0, fmt.Errorf("comparison not supported for type %v", t)
}
}
func (t StateVarType) Equal(a, b interface{}) (bool, error) {
switch {
case t.IsInteger():
ai, err1 := toInt(a, 64)
bi, err2 := toInt(b, 64)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid integer value for type %s", t.String())
}
return ai == bi, nil
case t.IsFloat():
af, err := toFloat(a, 64)
if err != nil {
return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err)
}
bf, err := toFloat(b, 64)
if err != nil {
return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err)
}
return af == bf, nil
case t.IsString():
as, ok1 := a.(string)
bs, ok2 := b.(string)
if !ok1 || !ok2 {
return false, fmt.Errorf("invalid string value for type %s", t.String())
}
return as == bs, nil
case t.IsBool():
ab, err1 := toBool(a)
bb, err2 := toBool(b)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid boolean value for type %s", t.String())
}
return ab == bb, nil
case t.IsTime():
at, err1 := toTime(a)
bt, err2 := toTime(b)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid time.Time value for type %s", t.String())
}
return at.Equal(bt), nil
case t.IsUUID():
au, err1 := toUUID(a)
bu, err2 := toUUID(b)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid uuid.UUID value for type %s", t.String())
}
return au == bu, nil
case t.IsURI():
au, err1 := toURI(a)
bu, err2 := toURI(b)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid *url.URL value for type %s", t.String())
}
return au.String() == bu.String(), nil
case t.IsBinary():
ab, err1 := toBinary(a)
bb, err2 := toBinary(b)
if err1 != nil || err2 != nil {
return false, fmt.Errorf("invalid []byte value for type %s", t.String())
}
return bytes.Equal(ab, bb), nil
default:
return false, fmt.Errorf("equality not supported for type %s", t.String())
}
}
// InRange checks if a value falls within an inclusive range [min, max].
// Uses the type's comparison logic. Returns true if val is between min and max (inclusive).
//
// Example:
//
// range := ValueRange{min: uint16(10), max: uint16(100)}
// StateType_UI2.InRange(uint16(50), range) // true
func (t StateVarType) InRange(val interface{}, interval *ValueRange) (bool, error) {
if interval == nil {
return true, nil
}
cmp1, err1 := t.Cmp(val, interval.min)
cmp2, err2 := t.Cmp(val, interval.max)
if err1 != nil || err2 != nil {
err := err1
if err == nil {
err = err2
}
return false, err
}
return cmp1 >= 0 && cmp2 <= 0, nil
}

View File

@@ -0,0 +1,171 @@
package statevariables
// IsNumeric checks whether a given StateVarType represents a numeric type or
// not. Numeric types are defined as those that can be used to store number-like
// values. The following types are considered numeric: UI1, UI2, UI4, I1, I2,
// I4, Int, R4, R8, Number and Fixed14_4.
//
// t: StateVarType to check if it's a numeric type or not.
//
// Returns true if the given StateVarType represents a numeric type; false
// otherwise.
func (t StateVarType) IsNumeric() bool {
switch t {
case StateType_UI1, StateType_UI2, StateType_UI4,
StateType_I1, StateType_I2, StateType_I4,
StateType_Int,
StateType_R4, StateType_R8,
StateType_Number,
StateType_Fixed14_4:
return true
default:
return false
}
}
// IsInteger checks if the state variable type is integer or not.
//
// It returns a boolean value indicating whether the provided StateVarType (t)
// is an integer type or not. The function takes one parameter, t of type
// StateVarType, which represents the state variable type to be checked.
//
// Parameters: - t (StateVarType): The StateVarType to check for comparability.
//
// Returns: bool: If the state variable type is any of the defined integer types
// (StateType_UI1, StateType_UI2, StateType_UI4, StateType_I1, StateType_I2,
// StateType_I4, StateType_Int), it returns true. Otherwise, it returns false.
func (t StateVarType) IsInteger() bool {
switch t {
case StateType_UI1, StateType_UI2, StateType_UI4,
StateType_I1, StateType_I2, StateType_I4,
StateType_Int:
return true
default:
return false
}
}
// IsSignedInt checks if the state variable type is a signed integer type.
// The function returns true for StateType_I1, StateType_I2, StateType_I4, and
// StateType_Int, otherwise it will return false. This method is part of the
// StateVarType enumeration in statevaluetype package. It takes no parameters
// but operates on the receiver 't' of type StateVarType.
//
// The returned value is a boolean.
func (t StateVarType) IsSignedInt() bool {
switch t {
case StateType_I1, StateType_I2, StateType_I4, StateType_Int:
return true
default:
return false
}
}
// IsUnsignedInt checks if the state variable type is an unsigned integer. It
// returns a boolean indicating whether or not the current StateVarType
// represents an unsigned integer type, namely: StateType_UI1, StateType_UI2,
// and StateType_UI4.
func (t StateVarType) IsUnsignedInt() bool {
switch t {
case StateType_UI1, StateType_UI2, StateType_UI4:
return true
default:
return false
}
}
// IsFloat returns a boolean indicating whether the given state variable type
// represents a float number. If the state variable type is one of R4, R8, Number or
// Fixed14_4 it returns true; otherwise, it returns false.
func (t StateVarType) IsFloat() bool {
switch t {
case StateType_R4, StateType_R8, StateType_Number, StateType_Fixed14_4:
return true
default:
return false
}
}
// IsBool checks if a StateVarType is of type Boolean. It returns true if the
// StateVarType equals to StateType_Boolean, false otherwise.
func (t StateVarType) IsBool() bool {
return t == StateType_Boolean
}
// IsString reports whether or not the state variable type represents a string
// value.
//
// This method returns true if the StateVarType is either StateType_String or
// StateType_Char, otherwise it returns false.
func (t StateVarType) IsString() bool {
switch t {
case StateType_String, StateType_Char:
return true
default:
return false
}
}
// IsTime checks whether a given StateVarType is of time type or not. It accepts
// a StateVarType parameter 't' and returns a boolean value based on the check.
//
// The possible values for 't' are: StateType_Date, StateType_DateTime,
// StateType_DateTimeTZ, StateType_Time, StateType_TimeTZ. If 't' is any of
// these types, the function returns true; otherwise, it returns false.
func (t StateVarType) IsTime() bool {
switch t {
case StateType_Date, StateType_DateTime, StateType_DateTimeTZ,
StateType_Time, StateType_TimeTZ:
return true
default:
return false
}
}
// IsUUID reports whether the receiver represents a UUID (Universally Unique
// Identifier). The StateVarType should be of type StateType_UUID to return
// true. Otherwise, it returns false.
func (t StateVarType) IsUUID() bool {
return t == StateType_UUID
}
// IsURI checks if the given state variable type is a URI.
//
// This function returns true if and only if the receiver (StateVarType t)
// equals StateType_URI, which represents URIs in UPnP protocol. Otherwise, it
// returns false.
func (t StateVarType) IsURI() bool {
return t == StateType_URI
}
// IsBinary checks if the given StateVarType is binary type or not. It returns
// true for types StateType_BinBase64 and StateType_BinHex, otherwise it returns
// false.
func (t StateVarType) IsBinary() bool {
switch t {
case StateType_BinBase64, StateType_BinHex:
return true
default:
return false
}
}
// IsComparable function checks if a StateVarType is comparable or not.
//
// It returns false for binary types (StateType_BinBase64 and StateType_BinHex)
// as they are non-comparable. For all other types, it returns true indicating
// that these types can be compared.
//
// Parameters: - t (StateVarType): The StateVarType to check for comparability.
//
// Returns: bool: A boolean value indicating whether the given StateVarType is
// comparable or not. True means it's comparable, False means it isn't.
func (t StateVarType) IsComparable() bool {
// Tous les types sauf les binaires sont comparables
switch t {
case StateType_BinBase64, StateType_BinHex:
return false
default:
return true
}
}

View File

@@ -0,0 +1,62 @@
package statevariables
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
// toBinary tries to convert v into a []byte.
// - if v is []byte, returns it directly
// - if v is string, attempts base64 decode, then hex decode
func toBinary(v interface{}) ([]byte, error) {
switch val := v.(type) {
case []byte:
return val, nil
case string:
// Try base64 first
if data, err := base64.StdEncoding.DecodeString(val); err == nil {
return data, nil
}
// Try hex next
if data, err := hex.DecodeString(val); err == nil {
return data, nil
}
return nil, fmt.Errorf("cannot parse string as base64 or hex: %q", val)
default:
return nil, fmt.Errorf("cannot convert type %T to binary", v)
}
}
// decodeBinary decodes Base64 or Hex-encoded binary strings to byte slices
func decodeBinary(t StateVarType, val string) ([]byte, error) {
switch t {
case StateType_BinBase64:
data, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return nil, fmt.Errorf("invalid base64: %v", err)
}
return data, nil
case StateType_BinHex:
// Accept even-length hex string
val = strings.TrimSpace(val)
if len(val)%2 != 0 {
return nil, fmt.Errorf("invalid hex: odd-length string")
}
data := make([]byte, len(val)/2)
_, err := hex.Decode(data, []byte(val))
if err != nil {
return nil, fmt.Errorf("invalid hex: %v", err)
}
return data, nil
default:
return nil, fmt.Errorf("decodeBinary: unsupported binary type %v", t)
}
}

View File

@@ -0,0 +1,61 @@
package statevariables
import (
"errors"
"strings"
)
// parseUPnPBoolean parses a string like "true", "false", "1", "0" into a boolean.
func parseUPnPBoolean(s string) (bool, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "1", "true":
return true, nil
case "0", "false":
return false, nil
default:
return false, errors.New("invalid string for UPnP boolean")
}
}
// toBool converts various types to boolean following UPnP rules:
// true: 1, "true"; false: 0, "false"
func toBool(val interface{}) (bool, error) {
if val == nil {
return false, errors.New("cannot convert nil to bool")
}
switch v := val.(type) {
case bool:
return v, nil
case string:
return parseUPnPBoolean(v)
default:
// try to convert numerics to float
f, err := toFloat(v, 64)
if err != nil {
return false, err
}
if f == 1.0 {
return true, nil
}
if f == 0.0 {
return false, nil
}
return false, errors.New("numeric value cannot be converted to bool unless 0 or 1")
}
}
// cmpBool compares two boolean values 'a' and 'b'. If they are equal it returns 0, if 'a' is false and 'b' is true it returns -1 else it returns 1.
func cmpBool(a, b bool) int {
if a == b { // if both booleans are the same, return 0
return 0
}
if !a && b { // if 'a' is false and 'b' is true, return -1
return -1
}
// otherwise, return 1
return 1 // if 'a' is true or 'a' and 'b' are not the same
}

View File

@@ -0,0 +1,95 @@
package statevariables
import (
"fmt"
"math"
"strconv"
)
// maxFloat returns the maximum value for floating point numbers given number of bits.
// If bits is neither 32 nor 64, it defaults to returning the maximum float64 value.
func maxFloat(bits int) float64 {
switch bits {
case 32:
return float64(math.MaxFloat32)
case 64:
return math.MaxFloat64
default:
return math.MaxFloat64 // fallback
}
}
// minFloat returns the minimum float value for the given number of bits.
// If bits are not recognized as either 32 or 64, it will default to the maximum
// possible float64 value.
func minFloat(bits int) float64 {
switch bits {
case 32:
return -float64(math.MaxFloat32)
case 64:
return -math.MaxFloat64
default:
return -math.MaxFloat64 // fallback
}
}
// toFloat converts various types to float (32 or 64 bits).
// Checks float32 boundaries when converting to 32-bit float.
// toFloat converts v to a float64, ensuring it fits within the range of the requested float size.
func toFloat(v interface{}, bits int) (float64, error) {
var f float64
switch val := v.(type) {
case float32:
f = float64(val)
case float64:
f = val
case int:
f = float64(val)
case int8:
f = float64(val)
case int16:
f = float64(val)
case int32:
f = float64(val)
case int64:
f = float64(val)
case uint:
f = float64(val)
case uint8:
f = float64(val)
case uint16:
f = float64(val)
case uint32:
f = float64(val)
case uint64:
f = float64(val)
case string:
var err error
f, err = strconv.ParseFloat(val, bits)
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("%T,unsupported type in toFloat", v)
}
if f < minFloat(bits) || f > maxFloat(bits) {
return 0, fmt.Errorf("value %v overflows float%d range", v, bits)
}
return f, nil
}
// cmpFloat64 compares two float64 values and returns an integer indicating their relation.
// If a is less than b, it returns -1; if a is greater than b, it returns 1; otherwise, it returns 0.
func cmpFloat64(a, b float64) int {
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
}

View File

@@ -0,0 +1,143 @@
package statevariables
import (
"errors"
"math"
"reflect"
"strconv"
)
// minInt returns minimum signed integer value for specified bit size
func minInt(bits int) int64 {
switch bits {
case 8:
return math.MinInt8
case 16:
return math.MinInt16
case 32:
return math.MinInt32
case 64:
return math.MinInt64
default:
return math.MinInt64 // fallback
}
}
// maxInt returns maximum signed integer value for specified bit size
func maxInt(bits int) int64 {
switch bits {
case 8:
return math.MaxInt8
case 16:
return math.MaxInt16
case 32:
return math.MaxInt32
case 64:
return math.MaxInt64
default:
return math.MaxInt64 // fallback
}
}
// toInt converts the given interface value into an int64. The function accepts
// a parameter v of any type and an integer bits representing the size of the
// desired integer type (8, 16, 32 or 64). If successful, it returns the
// converted integer and nil for error. Otherwise, it returns zero for int64 and
// an appropriate error message.
//
// The function checks whether v is nil. If true, it returns an error stating
// "cannot convert nil to int".
//
// It then identifies the type of v using a type switch statement. Depending on
// the type, the function performs different actions:
// - For integer types (int, int8, int16, int32 and int64), it uses checkIntBounds() to ensure the value fits within the specified bits range and returns the result.
// - For unsigned types (uint, uint8, uint16, uint32 and uint64), it checks for overflow before converting to an int64 and calls checkIntBounds().
// - For float types (float32 and float64), it converts them to int64 directly.
// - For string type, it attempts to parse the string as a base-10 integer using strconv.ParseInt() with specified bits range.
//
// If no match is found in these cases or v is of unsupported type, it returns
// an error stating "unsupported type for toInt".
func toInt(v interface{}, bits int) (int64, error) {
if v == nil {
return 0, errors.New("cannot convert nil to int")
}
switch val := v.(type) {
case int:
return checkIntBounds(int64(val), bits)
case int8:
return checkIntBounds(int64(val), bits)
case int16:
return checkIntBounds(int64(val), bits)
case int32:
return checkIntBounds(int64(val), bits)
case int64:
return checkIntBounds(val, bits)
case uint, uint8, uint16, uint32, uint64:
u := reflect.ValueOf(val).Uint()
if u > uint64(math.MaxInt64) {
return 0, errors.New("unsigned value overflows int64")
}
return checkIntBounds(int64(u), bits)
case float32:
return checkIntBounds(int64(val), bits)
case float64:
return checkIntBounds(int64(val), bits)
case string:
i, err := strconv.ParseInt(val, 10, bits)
if err != nil {
return 0, err
}
return checkIntBounds(i, bits)
default:
return 0, errors.New("unsupported type for toInt")
}
}
// CheckIntBounds checks if a given int64 value is within the valid range for a specific number of bits.
// It returns an error and 0 if the value is out of bounds, else it returns the input value and nil.
//
// Parameters:
// - v: The integer value to check.
// - bits: The number of bits that determine the valid range for 'v'.
//
// Returns:
// - int64: The original input value if within bounds, else 0.
// - error: An error object if the value is out of bounds; nil otherwise.
func checkIntBounds(v int64, bits int) (int64, error) {
if v < minInt(bits) || v > maxInt(bits) {
return 0, errors.New("integer value out of bounds")
}
return v, nil
}
// cmpInt compares two integers 'a' and 'b'. If 'a' is less than 'b', it returns -1, if 'a' is greater than 'b' it returns 1, else it returns 0.
func cmpInt(a, b int64) int {
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
}
// cmpUint compares two unsigned integers, a and b.
// It returns -1 if a < b.
// It returns 1 if a > b.
// It returns 0 if a == b.
func cmpUint(a, b uint64) int {
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
}

View File

@@ -0,0 +1,32 @@
package statevariables
import "fmt"
// valuesToNumericOperands takes two interface{} values, casts them to a numeric type based on a given StateVarType, and returns their float64 equivalents.
// If any error occurs during casting or conversion to float64, it is returned along with zero values for the operands.
func valuesToNumericOperands(t StateVarType, a interface{}, b interface{}) (float64, float64, error) {
var err error
if !t.IsNumeric() {
return 0, 0, fmt.Errorf("type %v is not numeric", t)
}
a, err = t.Cast(a)
if err != nil {
return 0, 0, err
}
b, err = t.Cast(b)
if err != nil {
return 0, 0, err
}
af, err := toFloat(a, 64)
if err != nil {
return 0, 0, err
}
bf, err := toFloat(b, 64)
if err != nil {
return 0, 0, err
}
return af, bf, nil
}

View File

@@ -0,0 +1,37 @@
package statevariables
import (
"fmt"
)
// toString converts v to string if possible.
func toString(v interface{}) (string, error) {
switch val := v.(type) {
case string:
return val, nil
case []byte:
return string(val), nil
case fmt.Stringer:
return val.String(), nil
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", val), nil
case uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", val), nil
case float32, float64:
return fmt.Sprintf("%v", val), nil
case bool:
if val {
return "true", nil
}
return "false", nil
default:
return "", fmt.Errorf("cannot convert type %T to string", v)
}
}

View File

@@ -0,0 +1,103 @@
package statevariables
import (
"fmt"
"strings"
"time"
)
// toTime converts the given value to a time.Time type if possible, otherwise it returns an error.
// The supported types for conversion are: time.Time, string, int64 and float64.
func toTime(v interface{}) (time.Time, error) {
switch val := v.(type) {
case time.Time:
return val, nil
case string:
// Liste de layouts communs à UPnP / ISO-8601 / RFC
layouts := []string{
time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00
time.RFC3339, // 2006-01-02T15:04:05Z07:00
"2006-01-02", // date only
"15:04:05", // time only
"15:04:05Z07:00", // time with TZ
"2006-01-02T15:04:05", // no TZ
"2006-01-02T15:04:05Z", // UTC
"2006-01-02 15:04:05", // space-separated
}
for _, layout := range layouts {
if t, err := time.Parse(layout, val); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("cannot parse time string: %q", val)
case int64:
return time.Unix(val, 0), nil
case float64:
sec := int64(val)
nsec := int64((val - float64(sec)) * 1e9)
return time.Unix(sec, nsec), nil
default:
return time.Time{}, fmt.Errorf("unsupported type for time conversion: %T", v)
}
}
// parseUPnPTime parses time values using UPnP-specific formats:
// - Date: "2006-01-02"
// - Time: "15:04:05"
// - DateTime: "2006-01-02T15:04:05"
// - TimeTZ: "15:04:05-07:00"
// - DateTimeTZ: "2006-01-02T15:04:05-07:00"
func parseUPnPTime(t StateVarType, s string) (time.Time, error) {
s = strings.TrimSpace(s)
layouts := []string{}
switch t {
case StateType_Date:
layouts = []string{"2006-01-02"}
case StateType_Time:
layouts = []string{"15:04:05"} // HH:MM:SS
case StateType_TimeTZ:
layouts = []string{"15:04:05Z07:00"} // HH:MM:SS+TZ
case StateType_DateTime:
layouts = []string{"2006-01-02T15:04:05"} // ISO8601 sans TZ
case StateType_DateTimeTZ:
layouts = []string{
"2006-01-02T15:04:05Z07:00", // full
"2006-01-02T15:04:05-0700", // fallback no colon
"2006-01-02T15:04:05Z", // Zulu
}
default:
return time.Time{}, fmt.Errorf("unsupported date/time type: %v", t)
}
for _, layout := range layouts {
if ts, err := time.Parse(layout, s); err == nil {
return ts, nil
}
}
return time.Time{}, fmt.Errorf("invalid %v value: %q", t, s)
}
// cmpTime compares two time.Times, returning -1 if the first is before the second,
// 1 if the first is after the second, and 0 if they're equal.
func cmpTime(a, b time.Time) int {
switch {
case a.Before(b):
return -1
case a.After(b):
return 1
default:
return 0
}
}

View File

@@ -0,0 +1,100 @@
package statevariables
import (
"errors"
"math"
"reflect"
"strconv"
)
// maxUint returns maximum unsigned integer value for specified bit size
func maxUint(bits int) uint64 {
switch bits {
case 8:
return math.MaxUint8
case 16:
return math.MaxUint16
case 32:
return math.MaxUint32
case 64:
return math.MaxUint64
default:
return math.MaxUint64 // fallback
}
}
// toUint converts various types of numeric values into a uint64 type. It
// supports conversions from signed and unsigned integers, floating-point
// numbers, and string representations of integers.
//
// Parameters:
// - v interface{}: input value that can be converted to uint64. The
// function will return an error if the input is not one of these types.
// - bits int: number of bits that should fit within the returned uint64.
// An error will be returned if the conversion would exceed this limit.
//
// Returns:
// - uint64: converted value from input 'v'. If the input 'v' is a string,
// it must represent an integer in base 10 and can fit into an uint64
// type with given number of bits.
// - error: if the conversion fails due to unsupported input type, overflow
// or underflow conditions, or invalid string representation, this will
// contain an appropriate error message. If 'v' is nil,
// it returns an "cannot convert nil to uint" error.
func toUint(v interface{}, bits int) (uint64, error) {
if v == nil {
return 0, errors.New("cannot convert nil to uint")
}
switch val := v.(type) {
case uint:
return checkUintBounds(uint64(val), bits)
case uint8:
return checkUintBounds(uint64(val), bits)
case uint16:
return checkUintBounds(uint64(val), bits)
case uint32:
return checkUintBounds(uint64(val), bits)
case uint64:
return checkUintBounds(val, bits)
case int, int8, int16, int32, int64:
i := reflect.ValueOf(val).Int()
if i < 0 {
return 0, errors.New("negative value cannot be converted to uint")
}
return checkUintBounds(uint64(i), bits)
case float32:
if val < 0 {
return 0, errors.New("negative float cannot be converted to uint")
}
return checkUintBounds(uint64(val), bits)
case float64:
if val < 0 {
return 0, errors.New("negative float cannot be converted to uint")
}
return checkUintBounds(uint64(val), bits)
case string:
u, err := strconv.ParseUint(val, 10, bits)
if err != nil {
return 0, err
}
return checkUintBounds(u, bits)
default:
return 0, errors.New("unsupported type for toUint")
}
}
// checkUintBounds checks whether the given unsigned integer 'v' is within the
// acceptable range defined by the number of bits 'bits'. If 'v' is out of
// bounds, it returns an error with a message "unsigned integer value out of
// bounds". Otherwise, it returns 'v' and nil.
func checkUintBounds(v uint64, bits int) (uint64, error) {
if v > maxUint(bits) {
return 0, errors.New("unsigned integer value out of bounds")
}
return v, nil
}

View File

@@ -0,0 +1,25 @@
package statevariables
import (
"fmt"
"net/url"
)
// toURI converts a value to *url.URL if possible.
func toURI(v interface{}) (*url.URL, error) {
switch val := v.(type) {
case *url.URL:
// Already a URL
return val, nil
case string:
u, err := url.Parse(val)
if err != nil {
return nil, fmt.Errorf("invalid URI string %q: %v", val, err)
}
return u, nil
default:
return nil, fmt.Errorf("cannot convert type %T to URI", v)
}
}

View File

@@ -0,0 +1,32 @@
package statevariables
import (
"fmt"
"github.com/google/uuid"
)
// toUUID converts a value to uuid.UUID if possible.
func toUUID(v interface{}) (uuid.UUID, error) {
switch val := v.(type) {
case uuid.UUID:
return val, nil
case string:
u, err := uuid.Parse(val)
if err != nil {
return uuid.UUID{}, fmt.Errorf("invalid UUID string %q: %v", val, err)
}
return u, nil
case []byte:
u, err := uuid.FromBytes(val)
if err != nil {
return uuid.UUID{}, fmt.Errorf("invalid UUID bytes: %v", err)
}
return u, nil
default:
return uuid.UUID{}, fmt.Errorf("cannot convert type %T to UUID", v)
}
}

View File

@@ -0,0 +1,61 @@
package statevariables
import "fmt"
// 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{}
}
// 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())
}
if cmp, err := t.Cmp(cmin, cmax); err != nil && cmp > 0 {
cmax, cmin = cmin, cmax
}
return &ValueRange{min: cmin, max: cmax}, nil
}