On renome le module upnp pmoupnp pour homogénéiser le tout
This commit is contained in:
35
pmoupnp/debug_index.go
Normal file
35
pmoupnp/debug_index.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (s *Server) ServeDebugIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>UPnP Debug Interface</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2em; }
|
||||
h1 { border-bottom: 1px solid #ccc; }
|
||||
pre { background: #f5f5f5; padding: 1em; overflow-x: auto; }
|
||||
a { color: #007bff; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Host %s </h1>
|
||||
<h2>address: %s</h2>`,
|
||||
s.Name(),
|
||||
html.EscapeString(html.EscapeString(s.BaseURL())))
|
||||
|
||||
fmt.Fprint(w, `
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
36
pmoupnp/dev_set.go
Normal file
36
pmoupnp/dev_set.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type DeviceInstanceSet objectstore.ObjectSet[*DeviceInstance]
|
||||
|
||||
func (m *DeviceInstanceSet) Insert(obj *DeviceInstance) error {
|
||||
return (*objectstore.ObjectSet[*DeviceInstance])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *DeviceInstanceSet) InsertOrReplace(obj *DeviceInstance) {
|
||||
(*objectstore.ObjectSet[*DeviceInstance])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (set *DeviceInstanceSet) Contains(obj *DeviceInstance) bool {
|
||||
return (*objectstore.ObjectSet[*DeviceInstance])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *DeviceInstanceSet) All() iter.Seq[*DeviceInstance] {
|
||||
return (*objectstore.ObjectSet[*DeviceInstance])(m).All()
|
||||
}
|
||||
|
||||
func (m *DeviceInstanceSet) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("DeviceList")
|
||||
|
||||
for sv := range m.All() {
|
||||
elem.AddChild(sv.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
160
pmoupnp/device.go
Normal file
160
pmoupnp/device.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type DeviceType string
|
||||
|
||||
const (
|
||||
MediaServer DeviceType = "MediaServer"
|
||||
MediaRenderer DeviceType = "MediaRenderer"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
name string
|
||||
devtype DeviceType
|
||||
version int
|
||||
|
||||
friendlyName string
|
||||
manufacturer string
|
||||
manufacturerURL string
|
||||
modelDescription string
|
||||
modelName string
|
||||
modelNumber string
|
||||
modelURL string
|
||||
serialNumber string
|
||||
specVersion string
|
||||
|
||||
services ServiceSet
|
||||
}
|
||||
|
||||
// NewDevice creates a new UPnP Device with the given name and type.
|
||||
// It populates a minimal set of device attributes such as
|
||||
// FriendlyName, Manufacturer, ModelName and a default version.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// name – the human‑readable name of the device.
|
||||
// devtype – a unique string identifying the device type
|
||||
// (used as the Device Identifier as well).
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// *Device – a pointer to the freshly allocated Device instance.
|
||||
// The caller owns the reference and may further
|
||||
// customise the device by setting additional fields
|
||||
// or services.
|
||||
//
|
||||
// Side effects:
|
||||
//
|
||||
// No I/O or external calls are performed. The function
|
||||
// simply constructs a struct in memory; it is safe to
|
||||
// call from multiple goroutines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a speaker device and register it with a server.
|
||||
// dev := upnp.NewDevice("LivingRoomSpeaker", "AudioDevice")
|
||||
// server.RegisterDevice(dev.Name(), dev)
|
||||
func NewDevice(name string, devtype DeviceType) *Device {
|
||||
switch devtype {
|
||||
case MediaServer, MediaRenderer:
|
||||
dev := &Device{
|
||||
name: name,
|
||||
devtype: devtype,
|
||||
friendlyName: "PMOMusic - " + name,
|
||||
manufacturer: "Petit Maison Orange",
|
||||
modelName: "PMOMusic - " + name,
|
||||
version: 1,
|
||||
services: make(ServiceSet),
|
||||
}
|
||||
return dev
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *Device) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d *Device) SetName(name string) {
|
||||
d.name = name
|
||||
}
|
||||
|
||||
func (d *Device) SetFriendlyName(name string) {
|
||||
d.friendlyName = name
|
||||
}
|
||||
|
||||
func (d *Device) SetModelName(name string) {
|
||||
d.modelName = name
|
||||
}
|
||||
|
||||
func (d *Device) TypeID() string {
|
||||
return "Device"
|
||||
}
|
||||
|
||||
func (d *Device) DeviceType() DeviceType {
|
||||
return d.devtype
|
||||
}
|
||||
|
||||
func (d *Device) SetVersion(version int) error {
|
||||
if version < 1 {
|
||||
return fmt.Errorf("%s", "version must be greater than or equal to 1")
|
||||
}
|
||||
d.version = version
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) Version() int {
|
||||
return d.version
|
||||
}
|
||||
|
||||
func (d *Device) Manufacturer() string {
|
||||
return d.manufacturer
|
||||
}
|
||||
|
||||
func (d *Device) SetManufacturer(manufacturer string) {
|
||||
d.manufacturer = manufacturer
|
||||
}
|
||||
|
||||
func (d *Device) ModelName() string {
|
||||
return d.modelName
|
||||
}
|
||||
|
||||
func (d *Device) AddService(srv *Service) error {
|
||||
err := d.services.Insert(srv)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) NewInstance(server *Server, udn string) *DeviceInstance {
|
||||
di := &DeviceInstance{
|
||||
name: d.name,
|
||||
devtype: d.devtype,
|
||||
version: d.version,
|
||||
udn: udn,
|
||||
server: server,
|
||||
friendlyName: d.friendlyName,
|
||||
manufacturer: d.manufacturer,
|
||||
manufacturerURL: d.manufacturerURL,
|
||||
modelDescription: d.modelDescription,
|
||||
modelName: d.modelName,
|
||||
modelNumber: d.modelNumber,
|
||||
modelURL: d.modelURL,
|
||||
serialNumber: d.serialNumber,
|
||||
specVersion: d.specVersion,
|
||||
devices: make(DeviceInstanceSet), // vide initialement
|
||||
services: make(ServiceInstanceSet),
|
||||
}
|
||||
|
||||
for svc := range d.services.All() {
|
||||
i := svc.NewInstance()
|
||||
i.device = di
|
||||
di.services.Insert(i)
|
||||
}
|
||||
|
||||
return di
|
||||
}
|
||||
179
pmoupnp/deviceinstance.go
Normal file
179
pmoupnp/deviceinstance.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/ssdp"
|
||||
"github.com/beevik/etree"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DeviceInstance struct {
|
||||
// Identification spécifique à l’instance
|
||||
name string
|
||||
devtype DeviceType
|
||||
version int
|
||||
udn string
|
||||
server *Server
|
||||
|
||||
// Copie figée des infos du Device
|
||||
friendlyName string
|
||||
manufacturer string
|
||||
manufacturerURL string
|
||||
modelDescription string
|
||||
modelName string
|
||||
modelNumber string
|
||||
modelURL string
|
||||
serialNumber string
|
||||
specVersion string
|
||||
|
||||
// Sous-devices si le device en contient
|
||||
devices DeviceInstanceSet
|
||||
services ServiceInstanceSet
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) Name() string {
|
||||
return di.name
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) TypeID() string {
|
||||
return "DeviceInstance"
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) DeviceType() DeviceType {
|
||||
return di.devtype
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) UDN() string {
|
||||
return di.udn
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) ServiceType() string {
|
||||
return fmt.Sprintf("urn:schemas-upnp-org:device:%s:%d", di.devtype, di.version)
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) FriendlyName() string {
|
||||
return di.friendlyName
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) Manufacturer() string {
|
||||
return di.manufacturer
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) ModelName() string {
|
||||
return di.modelName
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) BaseRoute() string {
|
||||
return fmt.Sprintf("/device/%s/%s", di.DeviceType(), di.UDN())
|
||||
}
|
||||
func (di *DeviceInstance) DescriptionURL() string {
|
||||
return fmt.Sprintf("%s/desc.xml", di.BaseRoute())
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) RegisterURLs() error {
|
||||
|
||||
mux, ok := di.server.httpSrv.Handler.(*http.ServeMux)
|
||||
|
||||
if mux == nil || !ok {
|
||||
return fmt.Errorf("❌ Device %s the server handler is not correctly defined", di.Name())
|
||||
}
|
||||
|
||||
mux.HandleFunc(
|
||||
di.DescriptionURL(),
|
||||
di.server.ServeXML(di.ToXMLElement),
|
||||
)
|
||||
|
||||
log.Infof(
|
||||
"✅ Device description for %s available at : %s%s",
|
||||
di.Name(),
|
||||
di.server.BaseURL(),
|
||||
di.DescriptionURL(),
|
||||
)
|
||||
|
||||
for svc := range di.services.All() {
|
||||
err := svc.RegisterURLs()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"❌ Service %s:%s URL error: %v",
|
||||
di.Name(),
|
||||
svc.Name(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) NT() string {
|
||||
return fmt.Sprintf("uuid:%s::urn:%s", di.UDN(), di.ServiceType())
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) RegisterSSPD() {
|
||||
|
||||
osName := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
dev := &ssdp.Device{
|
||||
UUID: di.UDN(),
|
||||
DeviceType: di.ServiceType(),
|
||||
Location: fmt.Sprintf("%s%s", di.server.BaseURL(), di.DescriptionURL()),
|
||||
Server: fmt.Sprintf(
|
||||
"%s/%s UPnP/1.1 PMOMusic/1.0",
|
||||
osName, arch,
|
||||
),
|
||||
NTs: make([]string, 0, 2+len(di.services)),
|
||||
}
|
||||
|
||||
dev.NTs = append(
|
||||
dev.NTs,
|
||||
"upnp:rootdevice",
|
||||
di.ServiceType(),
|
||||
)
|
||||
|
||||
for s := range di.services.All() {
|
||||
dev.NTs = append(dev.NTs, s.ServiceType())
|
||||
}
|
||||
|
||||
di.server.sspd.AddDevice(dev)
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) UnregisterSSPD() {
|
||||
di.server.sspd.RemoveDevice(di.UDN())
|
||||
}
|
||||
|
||||
func (di *DeviceInstance) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("root")
|
||||
elem.CreateAttr("xmlns", "urn:schemas-upnp-org:device-1-0")
|
||||
|
||||
spec := elem.CreateElement("specVersion")
|
||||
spec.CreateElement("major").SetText("1")
|
||||
spec.CreateElement("minor").SetText("0")
|
||||
|
||||
device := elem.CreateElement("device")
|
||||
device.CreateElement("deviceType").SetText(di.ServiceType())
|
||||
device.CreateElement("friendlyName").SetText(di.FriendlyName())
|
||||
device.CreateElement("manufacturer").SetText(di.Manufacturer())
|
||||
device.CreateElement("modelName").SetText(di.ModelName())
|
||||
device.CreateElement("UDN").SetText("uuid:" + di.UDN())
|
||||
|
||||
if len(di.services) > 0 {
|
||||
device.AddChild(di.services.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
|
||||
// // NewMediaRendererDescription génère la device description complète
|
||||
// // comme *etree.Element (racine <root>).
|
||||
// func NewMediaRendererDescription(udn string, friendlyName string) *etree.Element {
|
||||
|
||||
// // Inject serviceList
|
||||
// device.AddChild(NewMediaRendererServiceList(udn))
|
||||
|
||||
// return root
|
||||
// }
|
||||
23
pmoupnp/devices/mediarenderer/AVTransport/ac_Play.go
Normal file
23
pmoupnp/devices/mediarenderer/AVTransport/ac_Play.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package avtransport
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var Play = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("Play")
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"InstanceID",
|
||||
A_ARG_TYPE_InstanceID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"Speed",
|
||||
TransportPlaySpeed,
|
||||
),
|
||||
)
|
||||
|
||||
return ac
|
||||
}()
|
||||
@@ -0,0 +1,30 @@
|
||||
package avtransport
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var SetAVTransportURI = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("SetAVTransportURI")
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"InstanceID",
|
||||
A_ARG_TYPE_InstanceID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"CurrentURI",
|
||||
AVTransportURI,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"CurrentURIMetaData",
|
||||
AVTransportURIMetaData,
|
||||
),
|
||||
)
|
||||
|
||||
return ac
|
||||
}()
|
||||
16
pmoupnp/devices/mediarenderer/AVTransport/ac_Stop.go
Normal file
16
pmoupnp/devices/mediarenderer/AVTransport/ac_Stop.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package avtransport
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var Stop = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("Stop")
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"InstanceID",
|
||||
A_ARG_TYPE_InstanceID,
|
||||
),
|
||||
)
|
||||
|
||||
return ac
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var AVTransportURI = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("AVTransportURI")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,25 @@
|
||||
package avtransport
|
||||
|
||||
import (
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/didl"
|
||||
sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func _AVTransportURIMetaDataParser(value string) (interface{}, error) {
|
||||
log.Debug("[avtransport] Parsing AVTransport)")
|
||||
didl, err := didl.Parse(value)
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
return didl, nil
|
||||
}
|
||||
|
||||
var AVTransportURIMetaData = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("AVTransportURIMetaData")
|
||||
ts.SetValueParser(_AVTransportURIMetaDataParser)
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_InstanceID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_UI4.NewStateValue("A_ARG_TYPE_InstanceID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_PlaySpeed = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("A_ARG_TYPE_PlaySpeed")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var CurrentTrackDuration = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("CurrentTrackDuration")
|
||||
|
||||
return ts
|
||||
}()
|
||||
10
pmoupnp/devices/mediarenderer/AVTransport/sv_SeekMode.go
Normal file
10
pmoupnp/devices/mediarenderer/AVTransport/sv_SeekMode.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var SeekMode = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("SeekMode")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,11 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var TransportPlaySpeed = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("TransportPlaySpeed")
|
||||
ts.AppendAllowedValue("1")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,22 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var TransportState = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("TransportState")
|
||||
|
||||
ts.SetAllowedValues(
|
||||
"STOPPED",
|
||||
"PLAYING",
|
||||
"RECORDING",
|
||||
"TRANSITIONING",
|
||||
"PAUSED_PLAYBACK",
|
||||
"PAUSED_RECORDING",
|
||||
"NO_MEDIA_PRESENT",
|
||||
)
|
||||
|
||||
ts.SetSendingEvents()
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,17 @@
|
||||
package avtransport
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var TransportStatus = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("TransportStatus")
|
||||
|
||||
ts.SetAllowedValues(
|
||||
"OK",
|
||||
"ERROR_OCCURRED",
|
||||
)
|
||||
|
||||
ts.SetSendingEvents()
|
||||
|
||||
return ts
|
||||
}()
|
||||
23
pmoupnp/devices/mediarenderer/AVTransport/svc_avtransport.go
Normal file
23
pmoupnp/devices/mediarenderer/AVTransport/svc_avtransport.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package avtransport
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp"
|
||||
|
||||
var AVTransport = func() *upnp.Service {
|
||||
svc := upnp.NewService("AVTransport")
|
||||
|
||||
svc.AddAction(SetAVTransportURI)
|
||||
svc.AddAction(Play)
|
||||
svc.AddAction(Stop)
|
||||
|
||||
svc.AddVariable(A_ARG_TYPE_InstanceID)
|
||||
svc.AddVariable(A_ARG_TYPE_PlaySpeed)
|
||||
svc.AddVariable(AVTransportURI)
|
||||
svc.AddVariable(AVTransportURIMetaData)
|
||||
svc.AddVariable(CurrentTrackDuration)
|
||||
svc.AddVariable(SeekMode)
|
||||
svc.AddVariable(TransportPlaySpeed)
|
||||
svc.AddVariable(TransportState)
|
||||
svc.AddVariable(TransportStatus)
|
||||
|
||||
return svc
|
||||
}()
|
||||
@@ -0,0 +1,16 @@
|
||||
package connectionmanager
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var GetCurrentConnectionIDs = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("GetCurrentConnectionIDs")
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"ConnectionIDs",
|
||||
CurrentConnectionIDs,
|
||||
),
|
||||
)
|
||||
|
||||
return ac
|
||||
}()
|
||||
@@ -0,0 +1,64 @@
|
||||
package connectionmanager
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var GetCurrentConnectionInfo = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("GetCurrentConnectionInfo")
|
||||
ac.AddArgument(
|
||||
actions.NewInArgument(
|
||||
"ConnectionIDs",
|
||||
A_ARG_TYPE_ConnectionID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"RcsID",
|
||||
A_ARG_TYPE_RcsID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"AVTransportID",
|
||||
A_ARG_TYPE_AVTransportID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"ProtocolInfo",
|
||||
A_ARG_TYPE_ProtocolInfo,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"PeerConnectionManager",
|
||||
A_ARG_TYPE_ConnectionManager,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"PeerConnectionID",
|
||||
A_ARG_TYPE_ConnectionID,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"Direction",
|
||||
A_ARG_TYPE_Direction,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"Status",
|
||||
A_ARG_TYPE_ConnectionStatus,
|
||||
),
|
||||
)
|
||||
return ac
|
||||
}()
|
||||
@@ -0,0 +1,23 @@
|
||||
package connectionmanager
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
|
||||
var GetProtocolInfo = func() *actions.Action {
|
||||
|
||||
ac := actions.NewAction("GetProtocolInfo")
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"Source",
|
||||
SourceProtocolInfo,
|
||||
),
|
||||
)
|
||||
|
||||
ac.AddArgument(
|
||||
actions.NewOutArgument(
|
||||
"Sink",
|
||||
SinkProtocolInfo,
|
||||
),
|
||||
)
|
||||
|
||||
return ac
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var AVTransportID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("AVTransportID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_AVTransportID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("A_ARG_TYPE_AVTransportID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_ConnectionID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("A_ARG_TYPE_ConnectionID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_ConnectionManager = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("A_ARG_TYPE_ConnectionManager")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,17 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_ConnectionStatus = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("A_ARG_TYPE_ConnectionStatus")
|
||||
|
||||
ts.AppendAllowedValue(
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
"InsufficientBandwidth",
|
||||
"UnreliableChannel",
|
||||
"Unknown",
|
||||
)
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,11 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_Direction = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("A_ARG_TYPE_Direction")
|
||||
ts.AppendAllowedValue("Input", "Output")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_ProtocolInfo = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("A_ARG_TYPE_ProtocolInfo")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var A_ARG_TYPE_RcsID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("A_ARG_TYPE_RcsID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var ConnectionID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("ConnectionID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var CurrentConnectionIDs = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("CurrentConnectionIDs")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,11 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var Direction = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("Direction")
|
||||
ts.AppendAllowedValue("Input", "Ouput")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var PeerConnectionID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("PeerConnectionID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var PeerConnectionManager = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("PeerConnectionManager")
|
||||
|
||||
return ts
|
||||
}()
|
||||
10
pmoupnp/devices/mediarenderer/ConnectionManager/sv_RcsID.go
Normal file
10
pmoupnp/devices/mediarenderer/ConnectionManager/sv_RcsID.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var RcsID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("RcsID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var SinkProtocolInfo = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("SinkProtocolInfo")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var SourceProtocolInfo = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("SourceProtocolInfo")
|
||||
|
||||
return ts
|
||||
}()
|
||||
17
pmoupnp/devices/mediarenderer/ConnectionManager/sv_Status.go
Normal file
17
pmoupnp/devices/mediarenderer/ConnectionManager/sv_Status.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package connectionmanager
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var Status = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("Status")
|
||||
ts.AppendAllowedValue(
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
"InsufficientBandwidth",
|
||||
"UnreliableChannel",
|
||||
"Unknown",
|
||||
)
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,32 @@
|
||||
package connectionmanager
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp"
|
||||
|
||||
var ConnectionManager = func() *upnp.Service {
|
||||
svc := upnp.NewService("ConnectionManager")
|
||||
|
||||
svc.AddVariable(AVTransportID)
|
||||
svc.AddVariable(ConnectionID)
|
||||
svc.AddVariable(CurrentConnectionIDs)
|
||||
svc.AddVariable(Direction)
|
||||
svc.AddVariable(PeerConnectionID)
|
||||
svc.AddVariable(PeerConnectionManager)
|
||||
svc.AddVariable(RcsID)
|
||||
svc.AddVariable(SinkProtocolInfo)
|
||||
svc.AddVariable(SourceProtocolInfo)
|
||||
svc.AddVariable(Status)
|
||||
|
||||
svc.AddVariable(A_ARG_TYPE_AVTransportID)
|
||||
svc.AddVariable(A_ARG_TYPE_ConnectionID)
|
||||
svc.AddVariable(A_ARG_TYPE_ConnectionManager)
|
||||
svc.AddVariable(A_ARG_TYPE_ConnectionStatus)
|
||||
svc.AddVariable(A_ARG_TYPE_Direction)
|
||||
svc.AddVariable(A_ARG_TYPE_ProtocolInfo)
|
||||
svc.AddVariable(A_ARG_TYPE_RcsID)
|
||||
|
||||
svc.AddAction(GetCurrentConnectionIDs)
|
||||
svc.AddAction(GetCurrentConnectionInfo)
|
||||
svc.AddAction(GetProtocolInfo)
|
||||
|
||||
return svc
|
||||
}()
|
||||
11
pmoupnp/devices/mediarenderer/RenderingControl/sv_Channel.go
Normal file
11
pmoupnp/devices/mediarenderer/RenderingControl/sv_Channel.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package renderingcontrol
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var Channel = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_String.NewStateValue("Channel")
|
||||
ts.AppendAllowedValue("Master", "LF", "RF")
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1,10 @@
|
||||
package renderingcontrol
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var InstanceID = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_I4.NewStateValue("InstanceID")
|
||||
|
||||
return ts
|
||||
}()
|
||||
12
pmoupnp/devices/mediarenderer/RenderingControl/sv_Mute.go
Normal file
12
pmoupnp/devices/mediarenderer/RenderingControl/sv_Mute.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package renderingcontrol
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var Mute = func() *sv.StateVariable {
|
||||
|
||||
ts := sv.StateType_Boolean.NewStateValue("Mute")
|
||||
ts.SetSendingEvents()
|
||||
ts.SetDefault(false)
|
||||
|
||||
return ts
|
||||
}()
|
||||
@@ -0,0 +1 @@
|
||||
package renderingcontrol
|
||||
15
pmoupnp/devices/mediarenderer/RenderingControl/sv_Volume.go
Normal file
15
pmoupnp/devices/mediarenderer/RenderingControl/sv_Volume.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package renderingcontrol
|
||||
|
||||
import sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
|
||||
var Volume = func() *sv.StateVariable {
|
||||
|
||||
vol := sv.StateType_UI2.NewStateValue("Volume")
|
||||
|
||||
vol.SetRange(0, 100)
|
||||
vol.SetStep(1)
|
||||
|
||||
vol.SetSendingEvents()
|
||||
|
||||
return vol
|
||||
}()
|
||||
@@ -0,0 +1,14 @@
|
||||
package renderingcontrol
|
||||
|
||||
import "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp"
|
||||
|
||||
var RenderingControl = func() *upnp.Service {
|
||||
svc := upnp.NewService("RenderingControl")
|
||||
|
||||
svc.AddVariable(InstanceID)
|
||||
svc.AddVariable(Channel)
|
||||
svc.AddVariable(Mute)
|
||||
svc.AddVariable(Volume)
|
||||
|
||||
return svc
|
||||
}()
|
||||
18
pmoupnp/devices/mediarenderer/mr_fake_renderer.go
Normal file
18
pmoupnp/devices/mediarenderer/mr_fake_renderer.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package mediarenderer
|
||||
|
||||
import (
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp"
|
||||
avtransport "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/mediarenderer/AVTransport"
|
||||
connectionmanager "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/mediarenderer/ConnectionManager"
|
||||
renderingcontrol "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/mediarenderer/RenderingControl"
|
||||
)
|
||||
|
||||
var FakeRenderer = func() *upnp.Device {
|
||||
|
||||
renderer := upnp.NewDevice("FakeRenderer", "MediaRenderer")
|
||||
|
||||
renderer.AddService(avtransport.AVTransport)
|
||||
renderer.AddService(connectionmanager.ConnectionManager)
|
||||
renderer.AddService(renderingcontrol.RenderingControl)
|
||||
return renderer
|
||||
}()
|
||||
38
pmoupnp/devices/services/actions/action.go
Normal file
38
pmoupnp/devices/services/actions/action.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package actions
|
||||
|
||||
import "maps"
|
||||
|
||||
type Action struct {
|
||||
name string
|
||||
|
||||
arguments ArgumentSet
|
||||
}
|
||||
|
||||
func NewAction(name string) *Action {
|
||||
ac := &Action{
|
||||
name: name,
|
||||
arguments: make(ArgumentSet),
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func (a *Action) Name() string {
|
||||
return a.name
|
||||
}
|
||||
|
||||
func (a *Action) TypeID() string {
|
||||
return "Action"
|
||||
}
|
||||
|
||||
func (a *Action) AddArgument(arg *Argument) {
|
||||
a.arguments.Insert(arg)
|
||||
}
|
||||
|
||||
func (a *Action) NewInstance() *ActionInstance {
|
||||
ac := &ActionInstance{
|
||||
model: a,
|
||||
arguments: maps.Clone(a.arguments),
|
||||
}
|
||||
return ac
|
||||
}
|
||||
32
pmoupnp/devices/services/actions/actioninstance.go
Normal file
32
pmoupnp/devices/services/actions/actioninstance.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package actions
|
||||
|
||||
import "github.com/beevik/etree"
|
||||
|
||||
type ActionInstance struct {
|
||||
model *Action
|
||||
|
||||
arguments ArgumentSet
|
||||
}
|
||||
|
||||
func (a *ActionInstance) Name() string {
|
||||
return a.model.Name()
|
||||
}
|
||||
|
||||
func (a *ActionInstance) TypeID() string {
|
||||
return "ActionInstance"
|
||||
}
|
||||
|
||||
func (a *ActionInstance) Arguments(name string) (*Argument, bool) {
|
||||
arg, ok := a.arguments[name]
|
||||
return arg, ok
|
||||
}
|
||||
|
||||
func (a *ActionInstance) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("action")
|
||||
|
||||
name := elem.CreateElement("name")
|
||||
name.SetText(a.Name())
|
||||
|
||||
elem.AddChild(a.arguments.ToXMLElement())
|
||||
return elem
|
||||
}
|
||||
32
pmoupnp/devices/services/actions/actioninstanceset.go
Normal file
32
pmoupnp/devices/services/actions/actioninstanceset.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type ActionInstanceSet objectstore.ObjectSet[*ActionInstance]
|
||||
|
||||
func (m *ActionInstanceSet) Insert(obj *ActionInstance) {
|
||||
(*objectstore.ObjectSet[*ActionInstance])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (set *ActionInstanceSet) Contains(obj *ActionInstance) bool {
|
||||
return (*objectstore.ObjectSet[*ActionInstance])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *ActionInstanceSet) All() iter.Seq[*ActionInstance] {
|
||||
return (*objectstore.ObjectSet[*ActionInstance])(m).All()
|
||||
}
|
||||
|
||||
func (m *ActionInstanceSet) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("actionList")
|
||||
|
||||
for sv := range m.All() {
|
||||
elem.AddChild(sv.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
25
pmoupnp/devices/services/actions/actionset.go
Normal file
25
pmoupnp/devices/services/actions/actionset.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
)
|
||||
|
||||
type ActionSet objectstore.ObjectSet[*Action]
|
||||
|
||||
func (m *ActionSet) Insert(obj *Action) error {
|
||||
return (*objectstore.ObjectSet[*Action])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *ActionSet) InsertOrReplace(obj *Action) {
|
||||
(*objectstore.ObjectSet[*Action])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (set *ActionSet) Contains(obj *Action) bool {
|
||||
return (*objectstore.ObjectSet[*Action])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *ActionSet) All() iter.Seq[*Action] {
|
||||
return (*objectstore.ObjectSet[*Action])(m).All()
|
||||
}
|
||||
100
pmoupnp/devices/services/actions/argument.go
Normal file
100
pmoupnp/devices/services/actions/argument.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type Argument struct {
|
||||
name string
|
||||
statevariables *statevariables.StateVariable
|
||||
in bool
|
||||
out bool
|
||||
}
|
||||
|
||||
func newArgument(name string, statevariable *statevariables.StateVariable) *Argument {
|
||||
arg := &Argument{
|
||||
name: name,
|
||||
statevariables: statevariable,
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
func NewInArgument(name string, statevariable *statevariables.StateVariable) *Argument {
|
||||
arg := newArgument(name, statevariable)
|
||||
arg.in = true
|
||||
return arg
|
||||
}
|
||||
|
||||
func NewOutArgument(name string, statevariable *statevariables.StateVariable) *Argument {
|
||||
arg := newArgument(name, statevariable)
|
||||
arg.out = true
|
||||
return arg
|
||||
}
|
||||
|
||||
func NewInOutArgument(name string, statevariable *statevariables.StateVariable) *Argument {
|
||||
arg := newArgument(name, statevariable)
|
||||
arg.in = true
|
||||
arg.out = true
|
||||
return arg
|
||||
}
|
||||
|
||||
func (a *Argument) Name() string {
|
||||
return a.name
|
||||
}
|
||||
|
||||
func (sv Argument) TypeID() string {
|
||||
return "Argument"
|
||||
}
|
||||
|
||||
func (a *Argument) StateVariable() *statevariables.StateVariable {
|
||||
return a.statevariables
|
||||
}
|
||||
|
||||
func (a *Argument) IsIn() bool {
|
||||
return a.in
|
||||
}
|
||||
|
||||
func (a *Argument) IsOut() bool {
|
||||
return a.out
|
||||
}
|
||||
|
||||
func (a *Argument) ToXMLElement() *etree.Element {
|
||||
var elem, arg *etree.Element
|
||||
|
||||
if a.IsIn() && a.IsOut() {
|
||||
elem = etree.NewElement("")
|
||||
arg = elem.CreateElement("argument")
|
||||
} else {
|
||||
elem = etree.NewElement("argument")
|
||||
arg = elem
|
||||
}
|
||||
|
||||
if a.IsIn() {
|
||||
name := arg.CreateElement("name")
|
||||
name.SetText(a.Name())
|
||||
|
||||
direction := arg.CreateElement("direction")
|
||||
direction.SetText("in")
|
||||
|
||||
relatedStateVariable := arg.CreateElement("relatedStateVariable")
|
||||
relatedStateVariable.SetText(a.StateVariable().Name())
|
||||
}
|
||||
|
||||
if a.IsIn() && a.IsOut() {
|
||||
arg = elem.CreateElement("argument")
|
||||
}
|
||||
|
||||
if a.IsOut() {
|
||||
name := arg.CreateElement("name")
|
||||
name.SetText(a.Name())
|
||||
|
||||
direction := arg.CreateElement("direction")
|
||||
direction.SetText("out")
|
||||
|
||||
relatedStateVariable := arg.CreateElement("relatedStateVariable")
|
||||
relatedStateVariable.SetText(a.StateVariable().Name())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
32
pmoupnp/devices/services/actions/argumentset.go
Normal file
32
pmoupnp/devices/services/actions/argumentset.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type ArgumentSet objectstore.ObjectSet[*Argument]
|
||||
|
||||
func (m *ArgumentSet) Insert(obj *Argument) {
|
||||
(*objectstore.ObjectSet[*Argument])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (set *ArgumentSet) Contains(obj *Argument) bool {
|
||||
return (*objectstore.ObjectSet[*Argument])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *ArgumentSet) All() iter.Seq[*Argument] {
|
||||
return (*objectstore.ObjectSet[*Argument])(m).All()
|
||||
}
|
||||
|
||||
func (m *ArgumentSet) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("argumentList")
|
||||
|
||||
for sv := range m.All() {
|
||||
elem.AddChild(sv.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
402
pmoupnp/devices/services/statevariables/statevalueinstance.go
Normal file
402
pmoupnp/devices/services/statevariables/statevalueinstance.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package statevariables
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type notifiable interface {
|
||||
Name() string
|
||||
EventToBeSent(name string, value interface{})
|
||||
}
|
||||
|
||||
type StateVarInstance struct {
|
||||
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
|
||||
service notifiable
|
||||
value interface{}
|
||||
previousValue interface{}
|
||||
lastChange time.Time
|
||||
lastEvent time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (instance *StateVarInstance) Cast(val interface{}) (interface{}, error) {
|
||||
return instance.model.Cast(val)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (instance *StateVarInstance) Model() *StateVariable {
|
||||
return instance.model
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if ok, err := instance.IsValidValue(cval); !ok || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Not valid value %v for variable %s", cval, instance.Name())
|
||||
}
|
||||
|
||||
instance.mu.Lock()
|
||||
defer instance.mu.Unlock()
|
||||
instance.previousValue = instance.value
|
||||
instance.value = cval
|
||||
|
||||
if instance.ShouldTriggerEvent() {
|
||||
instance.service.EventToBeSent(instance.Name(), instance.Value())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (instance *StateVarInstance) Incr() error {
|
||||
if instance.HasStep() {
|
||||
value, err := instance.model.valueType.Add(instance.Value(), instance.Step())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return instance.SetValue(value)
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"no step for variable %s:%s",
|
||||
instance.service.Name(),
|
||||
instance.Name(),
|
||||
)
|
||||
}
|
||||
|
||||
func (instance *StateVarInstance) Decr() error {
|
||||
if instance.HasStep() {
|
||||
value, err := instance.model.valueType.Sub(instance.Value(), instance.Step())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return instance.SetValue(value)
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"no step for variable %s:%s",
|
||||
instance.service.Name(),
|
||||
instance.Name(),
|
||||
)
|
||||
}
|
||||
|
||||
// ShouldTriggerEvent vérifie toutes les conditions
|
||||
func (instance *StateVarInstance) ShouldTriggerEvent() bool {
|
||||
if instance.IsSendingEvents() {
|
||||
for name, condition := range instance.model.eventConditions {
|
||||
if !condition(instance) {
|
||||
log.Debugf(
|
||||
"State variable %s:%s event condition %s not true",
|
||||
instance.service.Name(),
|
||||
instance.Name(),
|
||||
name,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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.valueToString(sv.Value()))
|
||||
|
||||
return propSet
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package statevariables
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type StateVarInstanceSet objectstore.ObjectSet[*StateVarInstance]
|
||||
|
||||
func (m *StateVarInstanceSet) Insert(obj *StateVarInstance) error {
|
||||
return (*objectstore.ObjectSet[*StateVarInstance])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *StateVarInstanceSet) InsertOrReplace(obj *StateVarInstance) {
|
||||
(*objectstore.ObjectSet[*StateVarInstance])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (m *StateVarInstanceSet) Contains(obj *StateVarInstance) bool {
|
||||
return (*objectstore.ObjectSet[*StateVarInstance])(m).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *StateVarInstanceSet) All() iter.Seq[*StateVarInstance] {
|
||||
return (*objectstore.ObjectSet[*StateVarInstance])(m).All()
|
||||
}
|
||||
|
||||
func (m *StateVarInstanceSet) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("serviceStateTable")
|
||||
|
||||
for sv := range m.All() {
|
||||
elem.AddChild(sv.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
423
pmoupnp/devices/services/statevariables/statevariable.go
Normal file
423
pmoupnp/devices/services/statevariables/statevariable.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package statevariables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (sv StateVariable) TypeID() string {
|
||||
return "StateVariable"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (state *StateVariable) SetValueParser(parser StringValueParser) {
|
||||
state.parse = parser
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// bool: True if within range or no range defined
|
||||
func (state *StateVariable) IsValueInRange(value interface{}) (bool, error) {
|
||||
return state.valueType.InRange(value, state.valueRange)
|
||||
}
|
||||
|
||||
func (state *StateVariable) IsValueAllowed(value interface{}) (bool, error) {
|
||||
if !state.HasAllowedValues() {
|
||||
return true, nil // No list = any value valid
|
||||
}
|
||||
cvalue, err := state.Cast(value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, allowed := range state.allowedValues {
|
||||
if reflect.DeepEqual(cvalue, allowed) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (state *StateVariable) IsValidValue(value interface{}) (bool, error) {
|
||||
cvalue, err := state.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) NewInstance() *StateVarInstance {
|
||||
instance := &StateVarInstance{
|
||||
model: state,
|
||||
name: state.name,
|
||||
modifiable: state.modifiable,
|
||||
description: state.description,
|
||||
step: state.step,
|
||||
defaultValue: state.defaultValue,
|
||||
|
||||
eventConditions: maps.Clone(state.eventConditions),
|
||||
allowedValues: slices.Clone(state.allowedValues),
|
||||
sendEvents: state.sendEvents,
|
||||
parse: state.parse,
|
||||
marshal: state.marshal,
|
||||
|
||||
value: state.DefaultValue(),
|
||||
lastChange: time.Now(),
|
||||
lastEvent: time.Unix(int64(1718985600), 0).UTC(),
|
||||
}
|
||||
|
||||
if state.HasRange() {
|
||||
instance.valueRange = &ValueRange{
|
||||
min: state.valueRange.min,
|
||||
max: state.valueRange.max,
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
@@ -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
|
||||
}()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
13
pmoupnp/devices/services/statevariables/statevariable_cmp.go
Normal file
13
pmoupnp/devices/services/statevariables/statevariable_cmp.go
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
25
pmoupnp/devices/services/statevariables/statevariableset.go
Normal file
25
pmoupnp/devices/services/statevariables/statevariableset.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package statevariables
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
)
|
||||
|
||||
type StateVariableSet objectstore.ObjectSet[*StateVariable]
|
||||
|
||||
func (m *StateVariableSet) Insert(obj *StateVariable) error {
|
||||
return (*objectstore.ObjectSet[*StateVariable])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *StateVariableSet) InsertOrReplace(obj *StateVariable) {
|
||||
(*objectstore.ObjectSet[*StateVariable])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (set *StateVariableSet) Contains(obj *StateVariable) bool {
|
||||
return (*objectstore.ObjectSet[*StateVariable])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *StateVariableSet) All() iter.Seq[*StateVariable] {
|
||||
return (*objectstore.ObjectSet[*StateVariable])(m).All()
|
||||
}
|
||||
194
pmoupnp/devices/services/statevariables/statevartype.go
Normal file
194
pmoupnp/devices/services/statevariables/statevartype.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
153
pmoupnp/devices/services/statevariables/statevartype_cast.go
Normal file
153
pmoupnp/devices/services/statevariables/statevartype_cast.go
Normal 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)
|
||||
}
|
||||
}
|
||||
205
pmoupnp/devices/services/statevariables/statevartype_cmp.go
Normal file
205
pmoupnp/devices/services/statevariables/statevartype_cmp.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
62
pmoupnp/devices/services/statevariables/utils_binary.go
Normal file
62
pmoupnp/devices/services/statevariables/utils_binary.go
Normal 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)
|
||||
}
|
||||
}
|
||||
61
pmoupnp/devices/services/statevariables/utils_bool.go
Normal file
61
pmoupnp/devices/services/statevariables/utils_bool.go
Normal 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
|
||||
}
|
||||
95
pmoupnp/devices/services/statevariables/utils_float.go
Normal file
95
pmoupnp/devices/services/statevariables/utils_float.go
Normal 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
|
||||
}
|
||||
}
|
||||
143
pmoupnp/devices/services/statevariables/utils_int.go
Normal file
143
pmoupnp/devices/services/statevariables/utils_int.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
37
pmoupnp/devices/services/statevariables/utils_string.go
Normal file
37
pmoupnp/devices/services/statevariables/utils_string.go
Normal 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)
|
||||
}
|
||||
}
|
||||
104
pmoupnp/devices/services/statevariables/utils_time.go
Normal file
104
pmoupnp/devices/services/statevariables/utils_time.go
Normal file
@@ -0,0 +1,104 @@
|
||||
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)
|
||||
|
||||
var layouts []string = nil
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
100
pmoupnp/devices/services/statevariables/utils_uint.go
Normal file
100
pmoupnp/devices/services/statevariables/utils_uint.go
Normal 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
|
||||
}
|
||||
25
pmoupnp/devices/services/statevariables/utils_uri.go
Normal file
25
pmoupnp/devices/services/statevariables/utils_uri.go
Normal 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)
|
||||
}
|
||||
}
|
||||
32
pmoupnp/devices/services/statevariables/utils_uuid.go
Normal file
32
pmoupnp/devices/services/statevariables/utils_uuid.go
Normal 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)
|
||||
}
|
||||
}
|
||||
61
pmoupnp/devices/services/statevariables/valuerange.go
Normal file
61
pmoupnp/devices/services/statevariables/valuerange.go
Normal 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
|
||||
}
|
||||
5
pmoupnp/events.go
Normal file
5
pmoupnp/events.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package upnp
|
||||
|
||||
import "github.com/beevik/etree"
|
||||
|
||||
type UpnpEvent *etree.Element
|
||||
5
pmoupnp/interface.go
Normal file
5
pmoupnp/interface.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package upnp
|
||||
|
||||
type Markdownable interface {
|
||||
ToMarkdown() string
|
||||
}
|
||||
69
pmoupnp/objectstore/object.go
Normal file
69
pmoupnp/objectstore/object.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package objectstore
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ObjectIdSet map[uuid.UUID]struct{}
|
||||
|
||||
func (set ObjectIdSet) Contains(id uuid.UUID) bool {
|
||||
_, ok := set[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (set ObjectIdSet) Add(id uuid.UUID) {
|
||||
set[id] = struct{}{}
|
||||
}
|
||||
|
||||
func (set ObjectIdSet) Remove(id uuid.UUID) {
|
||||
delete(set, id)
|
||||
}
|
||||
|
||||
func (set ObjectIdSet) Clear() {
|
||||
for k := range set {
|
||||
delete(set, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (set ObjectIdSet) Len() int {
|
||||
return len(set)
|
||||
}
|
||||
|
||||
func (set ObjectIdSet) All() iter.Seq[uuid.UUID] {
|
||||
return func(yield func(uuid.UUID) bool) {
|
||||
for uuid := range set {
|
||||
if !yield(uuid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var objectstore = make(map[uuid.UUID]Object)
|
||||
|
||||
func RegisterObject(o Object) uuid.UUID {
|
||||
id := uuid.New()
|
||||
objectstore[id] = o
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func GetObject(id uuid.UUID) (Object, bool) {
|
||||
o, ok := objectstore[id]
|
||||
return o, ok
|
||||
}
|
||||
|
||||
func RemoveObject(id uuid.UUID) {
|
||||
if _, ok := objectstore[id]; !ok {
|
||||
delete(objectstore, id)
|
||||
return
|
||||
}
|
||||
log.Warnf("Object %s not found in objectstore", id)
|
||||
}
|
||||
|
||||
func CountOfStoredObjects() int {
|
||||
return len(objectstore)
|
||||
}
|
||||
40
pmoupnp/objectstore/objectset.go
Normal file
40
pmoupnp/objectstore/objectset.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package objectstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
)
|
||||
|
||||
type Object interface {
|
||||
Name() string
|
||||
TypeID() string
|
||||
}
|
||||
|
||||
type ObjectSet[T Object] map[string]T
|
||||
|
||||
func (m *ObjectSet[T]) Insert(obj T) error {
|
||||
if m.Contains(obj) {
|
||||
return fmt.Errorf("object %s already present in set", obj.Name())
|
||||
}
|
||||
(*m)[obj.Name()] = obj
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ObjectSet[T]) InsertOrReplace(obj T) {
|
||||
(*m)[obj.Name()] = obj
|
||||
}
|
||||
|
||||
func (set *ObjectSet[T]) Contains(obj T) bool {
|
||||
_, ok := (*set)[obj.Name()]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *ObjectSet[T]) All() iter.Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
for _, sv := range *m {
|
||||
if !yield(sv) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
pmoupnp/server.go
Normal file
204
pmoupnp/server.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/netutils"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmoapp"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmoconfig"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmocover"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmolog"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/ssdp"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
name string
|
||||
HTTPPort int
|
||||
baseURL string
|
||||
|
||||
Logger *log.Logger
|
||||
httpSrv *http.Server
|
||||
sspd *ssdp.SSDPServer
|
||||
|
||||
devices DeviceInstanceSet
|
||||
mu sync.RWMutex
|
||||
startOnce sync.Once
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewServer(name string, opts ...ServerOption) *Server {
|
||||
config := pmoconfig.GetConfig()
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
httpPort := config.GetHTTPPort()
|
||||
|
||||
if baseURL == "" {
|
||||
ip, err := netutils.GuessLocalIP()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to determine local IP: %w", err))
|
||||
}
|
||||
baseURL = fmt.Sprintf("http://%s:%d", ip, httpPort)
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
name: name,
|
||||
HTTPPort: httpPort,
|
||||
baseURL: baseURL,
|
||||
Logger: log.New(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Name() string { return s.name }
|
||||
func (s *Server) TypeID() string { return "Server" }
|
||||
|
||||
type ServerOption func(*Server)
|
||||
|
||||
func WithLogger(l *log.Logger) ServerOption {
|
||||
return func(s *Server) {
|
||||
s.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.startOnce.Do(func() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
s.mu.RLock()
|
||||
|
||||
cover_cache, err := pmocover.GetCoverCache()
|
||||
if err != nil {
|
||||
log.Panicf("❌ Cannot initialize the Cover Cache")
|
||||
}
|
||||
|
||||
log.Info("✅ Cover cache activated")
|
||||
|
||||
cover_cache.ServeMux(mux)
|
||||
pmoapp.Handler(mux)
|
||||
|
||||
s.httpSrv = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.HTTPPort),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
for device := range s.devices.All() {
|
||||
err := device.RegisterURLs()
|
||||
|
||||
if err != nil {
|
||||
log.Panicf("❌ Cannot register URLs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RUnlock()
|
||||
|
||||
go func() {
|
||||
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.Logger.Printf("❌ server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("✅ UPnP server started on %s", s.baseURL)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
var err error
|
||||
s.stopOnce.Do(func() {
|
||||
if s.httpSrv != nil {
|
||||
s.Logger.Println("✅ Shutting down UPNP server...")
|
||||
err = s.httpSrv.Shutdown(ctx)
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
if err := s.Start(); err != nil {
|
||||
return fmt.Errorf("❌ failed to start server: %w", err)
|
||||
}
|
||||
|
||||
pmolog.LoggerWeb(ctx, s.httpSrv.Handler.(*http.ServeMux))
|
||||
|
||||
s.sspd = ssdp.NewSSDPServer()
|
||||
if err := s.sspd.Start(ctx); err != nil {
|
||||
return fmt.Errorf("❌ failed to start SSDP server: %w", err)
|
||||
}
|
||||
|
||||
for d := range s.devices.All() {
|
||||
d.RegisterSSPD()
|
||||
|
||||
for svc := range d.services.All() {
|
||||
svc.StartNotifier(ctx, 1*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// attente d’annulation du contexte
|
||||
<-ctx.Done()
|
||||
|
||||
// arrêt avec le même ctx ou un nouveau ctx avec timeout
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return s.Stop(shutdownCtx)
|
||||
}
|
||||
|
||||
func (s *Server) BaseURL() string { return s.baseURL }
|
||||
|
||||
// ServeXML prend un générateur de XML (*etree.Element)
|
||||
// et renvoie la string XML avec header.
|
||||
func (s *Server) XML(gen func() *etree.Element) (string, error) {
|
||||
root := gen()
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(root)
|
||||
|
||||
doc.Indent(2)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := doc.WriteTo(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ajoute le header XML
|
||||
return `<?xml version="1.0" encoding="utf-8"?>` + "\n" + buf.String(), nil
|
||||
}
|
||||
|
||||
func (s *Server) ServeXML(gen func() *etree.Element) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
xmlStr, err := s.XML(gen)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to generate XML", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
osName := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
w.Header().Set("Server", fmt.Sprintf(
|
||||
"%s/%s UPnP/1.1 PMOMusic/1.0",
|
||||
osName, arch,
|
||||
))
|
||||
w.Header().Set("Connection", "close")
|
||||
w.Header().Set("Cache-Control", "max-age=1800")
|
||||
w.Header().Set("EXT", "")
|
||||
w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(xmlStr))
|
||||
}
|
||||
}
|
||||
99
pmoupnp/service.go
Normal file
99
pmoupnp/service.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
sv "gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
name string
|
||||
identifier string
|
||||
version int
|
||||
|
||||
actions actions.ActionSet
|
||||
stateTable sv.StateVariableSet
|
||||
}
|
||||
|
||||
func NewService(name string) *Service {
|
||||
svc := &Service{
|
||||
name: name,
|
||||
identifier: name,
|
||||
version: 1,
|
||||
stateTable: make(sv.StateVariableSet),
|
||||
actions: make(actions.ActionSet),
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc *Service) Name() string {
|
||||
return svc.name
|
||||
}
|
||||
|
||||
func (svc *Service) TypeID() string {
|
||||
return "Service"
|
||||
}
|
||||
|
||||
func (svc *Service) Identifier() string {
|
||||
return svc.identifier
|
||||
}
|
||||
|
||||
func (svc *Service) SetIdentifier(id string) {
|
||||
svc.identifier = id
|
||||
}
|
||||
|
||||
func (svc *Service) SetVersion(version int) error {
|
||||
if version < 1 {
|
||||
return fmt.Errorf("%s", "version must be greater than or equal to 1")
|
||||
}
|
||||
svc.version = version
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) Version() int {
|
||||
return svc.version
|
||||
}
|
||||
|
||||
func (svc *Service) AddVariable(sv *sv.StateVariable) error {
|
||||
return svc.stateTable.Insert(sv)
|
||||
}
|
||||
|
||||
func (svc *Service) ContaintsVariable(sv *sv.StateVariable) bool {
|
||||
return svc.stateTable.Contains(sv)
|
||||
}
|
||||
|
||||
func (svc *Service) Variables() iter.Seq[*sv.StateVariable] {
|
||||
return svc.stateTable.All()
|
||||
}
|
||||
|
||||
func (svc *Service) AddAction(ac *actions.Action) error {
|
||||
return svc.actions.Insert(ac)
|
||||
}
|
||||
|
||||
func (svc *Service) NewInstance() *ServiceInstance {
|
||||
instance := &ServiceInstance{
|
||||
name: svc.Name(),
|
||||
identifier: svc.Identifier(),
|
||||
version: svc.Version(),
|
||||
|
||||
statevariables: make(sv.StateVarInstanceSet),
|
||||
actions: make(actions.ActionInstanceSet),
|
||||
|
||||
subscribers: make(map[string]string),
|
||||
changedBuffer: make(map[string]interface{}),
|
||||
seqid: make(map[string]uint32),
|
||||
}
|
||||
|
||||
for v := range svc.stateTable.All() {
|
||||
instance.statevariables.Insert(v.NewInstance())
|
||||
}
|
||||
|
||||
for a := range svc.actions.All() {
|
||||
instance.actions.Insert(a.NewInstance())
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
460
pmoupnp/serviceinstance.go
Normal file
460
pmoupnp/serviceinstance.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmolog"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/soap"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/actions"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/devices/services/statevariables"
|
||||
"github.com/beevik/etree"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ServiceInstance struct {
|
||||
name string
|
||||
identifier string
|
||||
version int
|
||||
|
||||
device *DeviceInstance
|
||||
statevariables statevariables.StateVarInstanceSet
|
||||
actions actions.ActionInstanceSet
|
||||
subscribers map[string]string // SID → Callback URL
|
||||
changedBuffer map[string]interface{}
|
||||
seqid map[string]uint32
|
||||
cbmu sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
const (
|
||||
MethodSubscribe = "SUBSCRIBE"
|
||||
MethodUnsubscribe = "UNSUBSCRIBE"
|
||||
)
|
||||
|
||||
func (si *ServiceInstance) Name() string {
|
||||
return si.name
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) TypeID() string {
|
||||
return "ServiceInstance"
|
||||
}
|
||||
|
||||
func (si *ServiceInstance) Identifier() string {
|
||||
return si.identifier
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) ServiceType() string {
|
||||
return fmt.Sprintf("urn:schemas-upnp-org:service:%s:%d", svc.name, svc.version)
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) ServiceId() string {
|
||||
return fmt.Sprintf("urn:upnp-org:serviceId:%s", svc.identifier)
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) BaseRoute() string {
|
||||
return fmt.Sprintf("%s/service/%s", svc.device.BaseRoute(), svc.Name())
|
||||
}
|
||||
func (svc *ServiceInstance) ControlURL() string {
|
||||
return fmt.Sprintf("%s/control", svc.BaseRoute())
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) EventSubURL() string {
|
||||
return fmt.Sprintf("%s/event", svc.BaseRoute())
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) SCPDURL() string {
|
||||
return fmt.Sprintf("%s/desc.xml", svc.BaseRoute())
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) RegisterURLs() error {
|
||||
|
||||
mux, ok := svc.device.server.httpSrv.Handler.(*http.ServeMux)
|
||||
|
||||
if mux == nil || !ok {
|
||||
return fmt.Errorf("❌ Device %s the server handler is not correctly defined", svc.Name())
|
||||
}
|
||||
|
||||
mux.HandleFunc(
|
||||
svc.SCPDURL(),
|
||||
svc.device.server.ServeXML(svc.SPCDElement),
|
||||
)
|
||||
|
||||
mux.HandleFunc(
|
||||
svc.ControlURL(),
|
||||
svc.ControlHandler(),
|
||||
)
|
||||
|
||||
mux.HandleFunc(
|
||||
svc.EventSubURL(),
|
||||
svc.EventSubHandler(),
|
||||
)
|
||||
|
||||
log.Infof(
|
||||
"✅ Service description for %s:%s available at : %s%s",
|
||||
svc.device.Name(),
|
||||
svc.Name(),
|
||||
svc.device.server.BaseURL(),
|
||||
svc.SCPDURL(),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) USN() string {
|
||||
return fmt.Sprintf("uuid:%s::urn:%s", svc.device.UDN(), svc.ServiceType())
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) SPCDElement() *etree.Element {
|
||||
elem := etree.NewElement("scpd")
|
||||
|
||||
elem.CreateAttr("xmlns", "urn:schemas-upnp-org:service-1-0")
|
||||
|
||||
spec := elem.CreateElement("specVersion")
|
||||
spec.CreateElement("major").SetText("1")
|
||||
spec.CreateElement("minor").SetText("0")
|
||||
|
||||
if len(svc.actions) > 0 {
|
||||
elem.AddChild(svc.actions.ToXMLElement())
|
||||
}
|
||||
|
||||
if len(svc.statevariables) > 0 {
|
||||
elem.AddChild(svc.statevariables.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) AddSubscriber(sid, callback string) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
if svc.subscribers == nil {
|
||||
svc.subscribers = make(map[string]string)
|
||||
}
|
||||
svc.subscribers[sid] = callback
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) RenewSubscriber(sid, timeout string) {
|
||||
// Pour l'instant juste log, on peut étendre avec expiration
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
log.Infof("Renewed SID %s for timeout %s", sid, timeout)
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) RemoveSubscriber(sid string) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
delete(svc.subscribers, sid)
|
||||
}
|
||||
|
||||
// Envoi d'un événement initial (optionnel)
|
||||
func (svc *ServiceInstance) SendInitialEvent(sid string) {
|
||||
svc.mu.Lock()
|
||||
callback := svc.subscribers[sid]
|
||||
svc.mu.Unlock()
|
||||
if callback == "" {
|
||||
return
|
||||
}
|
||||
|
||||
changed := make(map[string]interface{})
|
||||
for name, sv := range svc.statevariables {
|
||||
if sv.IsSendingEvents() { // sendEvents="yes"
|
||||
changed[name] = sv.Value()
|
||||
}
|
||||
}
|
||||
|
||||
if len(changed) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
callback = strings.TrimSpace(callback)
|
||||
callback = strings.Trim(callback, "<>")
|
||||
|
||||
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">`
|
||||
for name, val := range changed {
|
||||
body += fmt.Sprintf("<e:property><%s>%v</%s></e:property>", name, val, name)
|
||||
}
|
||||
body += "</e:propertyset>"
|
||||
|
||||
req, _ := http.NewRequest("NOTIFY", callback, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||
req.Header.Set("NT", "upnp:event")
|
||||
req.Header.Set("NTS", "upnp:propchange")
|
||||
req.Header.Set("SID", sid)
|
||||
req.Header.Set("SEQ", "0") // initial event
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to send initial event to %s: %v", callback, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
log.Infof(
|
||||
"✅ Initial event sent to %s, status=%s\n<details>\n\n```xml\n%s\n```\n</details>\n",
|
||||
callback,
|
||||
resp.Status,
|
||||
pmolog.PrettyPrintXML(body),
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("service")
|
||||
|
||||
st := elem.CreateElement("serviceType")
|
||||
st.SetText(svc.ServiceType())
|
||||
|
||||
sid := elem.CreateElement("serviceId")
|
||||
sid.SetText(svc.ServiceId())
|
||||
|
||||
spcd := elem.CreateElement("SCPDURL")
|
||||
spcd.SetText(svc.SCPDURL())
|
||||
|
||||
ctrl := elem.CreateElement("controlURL")
|
||||
ctrl.SetText(svc.ControlURL())
|
||||
|
||||
event := elem.CreateElement("eventSubURL")
|
||||
event.SetText(svc.EventSubURL())
|
||||
|
||||
return elem
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) EventToBeSent(name string, value interface{}) {
|
||||
svc.cbmu.Lock()
|
||||
defer svc.cbmu.Unlock()
|
||||
|
||||
svc.changedBuffer[name] = value
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) nextSeq(sid string) string {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
svc.seqid[sid]++
|
||||
return fmt.Sprintf("%d", svc.seqid[sid])
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) NotifySubscribers() {
|
||||
svc.cbmu.Lock()
|
||||
if len(svc.subscribers) == 0 || len(svc.changedBuffer) == 0 {
|
||||
svc.cbmu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Copier et réinitialiser le buffer
|
||||
changed := svc.changedBuffer
|
||||
svc.changedBuffer = make(map[string]interface{})
|
||||
svc.cbmu.Unlock()
|
||||
|
||||
for sid, callback := range svc.subscribers {
|
||||
go func(sid, callback string, changed map[string]interface{}) {
|
||||
callback = strings.TrimSpace(callback)
|
||||
callback = strings.Trim(callback, "<>")
|
||||
|
||||
u, err := url.Parse(callback)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid callback URL %s: %v", callback, err)
|
||||
return
|
||||
}
|
||||
|
||||
body := `<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">`
|
||||
for name, val := range changed {
|
||||
body += fmt.Sprintf("<e:property><%s>%v</%s></e:property>", name, val, name)
|
||||
}
|
||||
body += "</e:propertyset>"
|
||||
|
||||
req, err := http.NewRequest("NOTIFY", u.String(), strings.NewReader(body))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create NOTIFY request to %s: %v", callback, err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||
req.Header.Set("NT", "upnp:event")
|
||||
req.Header.Set("NTS", "upnp:propchange")
|
||||
req.Header.Set("SID", sid)
|
||||
req.Header.Set("SEQ", svc.nextSeq(sid))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to notify subscriber %s: %v", callback, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
log.Infof("✅ Notified subscriber %s of changes: %v", callback, changed)
|
||||
}(sid, callback, changed)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) StartNotifier(ctx context.Context, interval time.Duration) {
|
||||
log.Infof("✅ Starting notifier for %s:%s every %.2f s", svc.device.Name(), svc.Name(), interval.Seconds())
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Infof("✅ Notifier stopped for %s:%s", svc.device.Name(), svc.Name())
|
||||
return
|
||||
case <-ticker.C:
|
||||
svc.NotifySubscribers()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) EventSubHandler() func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("📡 Event Subscription request for %s:%s", svc.device.Name(), svc.Name())
|
||||
|
||||
sid := r.Header.Get("SID")
|
||||
timeout := r.Header.Get("Timeout")
|
||||
callback := r.Header.Get("Callback")
|
||||
|
||||
switch r.Method {
|
||||
case MethodSubscribe:
|
||||
if sid == "" {
|
||||
// Nouvelle subscription
|
||||
sid = fmt.Sprintf("uuid:%s", uuid.New().String())
|
||||
if callback != "" {
|
||||
svc.AddSubscriber(sid, callback)
|
||||
}
|
||||
if timeout == "" {
|
||||
timeout = "Second-1800"
|
||||
}
|
||||
log.Infof("🔔 New subscription: SID=%s, Callback=%s, Timeout=%s", sid, callback, timeout)
|
||||
go svc.SendInitialEvent(sid) // envoyer l’état initial
|
||||
} else {
|
||||
// Renouvellement
|
||||
svc.RenewSubscriber(sid, timeout)
|
||||
log.Infof("♻️ Renew subscription: SID=%s, Timeout=%s", sid, timeout)
|
||||
}
|
||||
|
||||
w.Header().Set("SID", sid)
|
||||
w.Header().Set("Timeout", timeout)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
case MethodUnsubscribe:
|
||||
if sid != "" {
|
||||
svc.RemoveSubscriber(sid)
|
||||
log.Infof("❌ Unsubscribe SID=%s", sid)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
default:
|
||||
log.Warnf("Unsupported EventSub method: %s", r.Method)
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// Debug log headers
|
||||
for k, v := range r.Header {
|
||||
log.Debugf("Header: %s=%v", k, v)
|
||||
}
|
||||
|
||||
// Lire le body au besoin
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err == nil && len(body) > 0 {
|
||||
log.Debugf("Body: %s", string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *ServiceInstance) ControlHandler() func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("📡 Control request for %s:%s", svc.device.Name(), svc.Name())
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Errorf("❌ Failed to read body: %v", err)
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Callback générique pour décoder chaque paramètre
|
||||
decoder := func(action, param, value string) (string, interface{}, error) {
|
||||
// 1️⃣ Chercher si le param correspond à une StateVarInstance
|
||||
|
||||
sv, ok := svc.statevariables[param]
|
||||
log.Debugf("Look for a variable named : %s -> %v", param, ok)
|
||||
|
||||
if ok {
|
||||
if sv.HasParser() {
|
||||
v, err := sv.ParseValue(value)
|
||||
if err != nil {
|
||||
return param, value, err
|
||||
}
|
||||
return param, v, err
|
||||
}
|
||||
v, err := sv.Cast(value)
|
||||
if err != nil {
|
||||
return param, value, err
|
||||
}
|
||||
return param, v, nil
|
||||
}
|
||||
|
||||
// 2️⃣ Chercher si le param correspond à une ActionInstance et appliquer un parseur associé
|
||||
act, ok := svc.actions[action]
|
||||
log.Debugf("Look for an action named : %s -> %v", action, ok)
|
||||
|
||||
if ok {
|
||||
if argument, ok := act.Arguments(param); ok {
|
||||
sv_name := argument.StateVariable().Name()
|
||||
sv := svc.statevariables[sv_name]
|
||||
v, err := sv.ParseValue(value)
|
||||
|
||||
log.Debugf("It corresponds to variable : %s with a parser %v", sv_name, sv.HasParser())
|
||||
|
||||
if err != nil {
|
||||
return param, value, err
|
||||
}
|
||||
return param, v, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ Sinon, valeur brute
|
||||
return param, value, nil
|
||||
}
|
||||
|
||||
env, err := soap.ParseSOAPEnvelope(body)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("❌ Failed to parse SOAP enveloppe: %v", err)
|
||||
soapResp, _ := soap.BuildSOAPFault("s:Client", "Invalid Args", err.Error())
|
||||
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(soapResp)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := soap.ParseUPnPAction(env, decoder)
|
||||
if err != nil {
|
||||
log.Errorf("❌ Failed to parse SOAP Action: %v", err)
|
||||
soapResp, _ := soap.BuildSOAPFault("s:Client", "Invalid Args", err.Error())
|
||||
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(soapResp)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info(req.ToMarkdown())
|
||||
|
||||
// Ici tu peux appeler l'action correspondante sur svc.actions[req.Name] et récupérer le résultat
|
||||
// Exemple de réponse minimale :
|
||||
resp, _ := soap.BuildUPnPResponse(svc.ServiceType(), req.Name, map[string]string{})
|
||||
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(resp)
|
||||
}
|
||||
}
|
||||
36
pmoupnp/serviceinstanceset.go
Normal file
36
pmoupnp/serviceinstanceset.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type ServiceInstanceSet objectstore.ObjectSet[*ServiceInstance]
|
||||
|
||||
func (m *ServiceInstanceSet) Insert(obj *ServiceInstance) error {
|
||||
return (*objectstore.ObjectSet[*ServiceInstance])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *ServiceInstanceSet) InsertOrReplace(obj *ServiceInstance) {
|
||||
(*objectstore.ObjectSet[*ServiceInstance])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (set *ServiceInstanceSet) Contains(obj *ServiceInstance) bool {
|
||||
return (*objectstore.ObjectSet[*ServiceInstance])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *ServiceInstanceSet) All() iter.Seq[*ServiceInstance] {
|
||||
return (*objectstore.ObjectSet[*ServiceInstance])(m).All()
|
||||
}
|
||||
|
||||
func (m *ServiceInstanceSet) ToXMLElement() *etree.Element {
|
||||
elem := etree.NewElement("serviceList")
|
||||
|
||||
for sv := range m.All() {
|
||||
elem.AddChild(sv.ToXMLElement())
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
25
pmoupnp/serviceset.go
Normal file
25
pmoupnp/serviceset.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/upnp/objectstore"
|
||||
)
|
||||
|
||||
type ServiceSet objectstore.ObjectSet[*Service]
|
||||
|
||||
func (m *ServiceSet) Insert(obj *Service) error {
|
||||
return (*objectstore.ObjectSet[*Service])(m).Insert(obj)
|
||||
}
|
||||
|
||||
func (m *ServiceSet) InsertOrReplace(obj *Service) {
|
||||
(*objectstore.ObjectSet[*Service])(m).InsertOrReplace(obj)
|
||||
}
|
||||
|
||||
func (set *ServiceSet) Contains(obj *Service) bool {
|
||||
return (*objectstore.ObjectSet[*Service])(set).Contains(obj)
|
||||
}
|
||||
|
||||
func (m *ServiceSet) All() iter.Seq[*Service] {
|
||||
return (*objectstore.ObjectSet[*Service])(m).All()
|
||||
}
|
||||
44
pmoupnp/srv_reg_devices.go
Normal file
44
pmoupnp/srv_reg_devices.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmoconfig"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterDevice(name string, d *Device) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.devices == nil {
|
||||
s.devices = make(DeviceInstanceSet)
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
name = d.Name()
|
||||
}
|
||||
|
||||
config := pmoconfig.GetConfig()
|
||||
udn := config.GetDeviceUDN(string(d.DeviceType()), name)
|
||||
|
||||
instance := d.NewInstance(s, udn)
|
||||
|
||||
log.Infof("✅ Registering device %s", name)
|
||||
|
||||
err := s.devices.Insert(instance)
|
||||
|
||||
if err != nil {
|
||||
log.Panicf("❌ Device %s is already registered", instance.Name())
|
||||
}
|
||||
|
||||
log.Infof("✅ New device %s get UDN : %s", instance.Name(), instance.UDN())
|
||||
|
||||
// s.devices[name] = d
|
||||
// d.mu.Lock()
|
||||
// defer d.mu.Unlock()
|
||||
// d.UDN = s.UDN + "-" + name
|
||||
// d.Name = name
|
||||
|
||||
// for _, service := range d.Services.All() {
|
||||
// service.DeviceName = name
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user