change a lot
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
/vendor/
|
||||
**/*.log
|
||||
**/*.old
|
||||
xxx
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/netutils"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/ssdp"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/renderer"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
localIP, err := netutils.GuessLocalIP()
|
||||
// Crée le serveur avec baseURL auto-déduite depuis l’IP locale
|
||||
server := upnp.NewServer("PMO Music Server", "PMO Factory", "Fake Server", "", 1400)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Could not guess local IP: %v", err)
|
||||
}
|
||||
|
||||
usn := ssdp.GetUUID()
|
||||
|
||||
desc := upnp.NewDevice(localIP, 1400, usn, "pmomusic Fake Renderer", "pmomusic")
|
||||
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:AVTransport:1")
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:RenderingControl:1")
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:ConnectionManager:1")
|
||||
|
||||
desc.RegisterService("urn:av-openhome-org:service:Product:1")
|
||||
desc.RegisterService("urn:av-openhome-org:service:Playlist:1")
|
||||
desc.RegisterService("urn:av-openhome-org:service:Info:1")
|
||||
|
||||
location := "http://" + localIP + ":1400/description.xml"
|
||||
log.Printf("Server UUID: %s", usn)
|
||||
|
||||
log.Info(desc.GenerateXML())
|
||||
// Démarre la diffusion SSDP
|
||||
go ssdp.AnnounceRenderer(desc)
|
||||
go ssdp.StartSSDPResponder(desc)
|
||||
// Crée le renderer UPnP
|
||||
rendererDevice := renderer.NewMusicRenderer("pmomusic Fake Renderer", "pmomusic", "fake model")
|
||||
server.RegisterDevice("MusicRenderer", rendererDevice)
|
||||
|
||||
// Lance le serveur HTTP
|
||||
log.Println("Starting HTTP server at:", location)
|
||||
upnp.StartHTTPServer(desc)
|
||||
if err := server.Start(); err != nil {
|
||||
log.Fatalf("Failed to start UPnP server: %v", err)
|
||||
}
|
||||
|
||||
// Gère les signaux pour un arrêt propre
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Println("UPnP MusicRenderer is running... Press Ctrl+C to stop.")
|
||||
<-sigs
|
||||
|
||||
log.Println("Shutting down...")
|
||||
|
||||
// Dé-annonce SSDP (optionnel)
|
||||
server.NotifyByeBye()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Stop(ctx); err != nil {
|
||||
log.Printf("Error shutting down UPnP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GuessLocalIP returns the best-guess local IP address usable for UPnP location,
|
||||
// in order of preference: eth0 > en* > wl* > any private, non-loopback IP.
|
||||
func GuessLocalIP() (string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type scoredIP struct {
|
||||
ip net.IP
|
||||
score int
|
||||
}
|
||||
|
||||
var candidates []scoredIP
|
||||
|
||||
for _, iface := range ifaces {
|
||||
// Skip interfaces that are down or loopback
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
// Skip non-IPv4 or non-private addresses
|
||||
if ip == nil || ip.IsLoopback() || ip.To4() == nil || !isPrivateIPv4(ip) {
|
||||
continue
|
||||
}
|
||||
|
||||
score := scoreInterfaceName(iface.Name)
|
||||
candidates = append(candidates, scoredIP{ip: ip, score: score})
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return "", errors.New("no suitable local IP found")
|
||||
}
|
||||
|
||||
// Prefer interfaces with higher score
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
return candidates[i].score > candidates[j].score
|
||||
})
|
||||
|
||||
return candidates[0].ip.String(), nil
|
||||
}
|
||||
|
||||
func isPrivateIPv4(ip net.IP) bool {
|
||||
private := []string{
|
||||
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.2", "192.168.",
|
||||
}
|
||||
ipStr := ip.String()
|
||||
for _, p := range private {
|
||||
if strings.HasPrefix(ipStr, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scoreInterfaceName(name string) int {
|
||||
switch {
|
||||
case name == "eth0":
|
||||
return 100
|
||||
case strings.HasPrefix(name, "en"):
|
||||
return 80
|
||||
case name == "wlan0" || strings.HasPrefix(name, "wl"):
|
||||
return 60
|
||||
default:
|
||||
return 10
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// ListAllIPs returns a map of interface names to their associated IPv4 addresses.
|
||||
func ListAllIPs() map[string][]string {
|
||||
result := make(map[string][]string)
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
result["error"] = []string{err.Error()}
|
||||
return result
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue // Ignore down interfaces
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
if ip == nil || ip.To4() == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
result[iface.Name] = ips
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package renderer
|
||||
@@ -1,69 +0,0 @@
|
||||
package soap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HandleSOAP(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Println("Failed to read SOAP request:", err)
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
log.Println("Received SOAP request:")
|
||||
log.Println(string(body))
|
||||
|
||||
// Extract action name from the SOAP body
|
||||
action := extractSOAPAction(body)
|
||||
log.Printf("SOAP Action: %s\n", action)
|
||||
|
||||
// Send dummy response
|
||||
w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(dummySOAPResponse(action)))
|
||||
}
|
||||
|
||||
func extractSOAPAction(body []byte) string {
|
||||
type Envelope struct {
|
||||
Body struct {
|
||||
XMLName xml.Name
|
||||
} `xml:"Body"`
|
||||
}
|
||||
|
||||
var env Envelope
|
||||
if err := xml.Unmarshal(body, &env); err != nil {
|
||||
log.Println("SOAP parse error:", err)
|
||||
return "UnknownAction"
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(bytes.NewReader(body))
|
||||
for {
|
||||
tok, err := decoder.Token()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if se, ok := tok.(xml.StartElement); ok && se.Name.Local != "Envelope" && se.Name.Local != "Body" {
|
||||
return se.Name.Local
|
||||
}
|
||||
}
|
||||
return "UnknownAction"
|
||||
}
|
||||
|
||||
func dummySOAPResponse(action string) string {
|
||||
// You can specialize per action if needed.
|
||||
return `<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body>
|
||||
<u:` + action + `Response xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
||||
</u:` + action + `Response>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||
)
|
||||
|
||||
const (
|
||||
ssdpAddr = "239.255.255.250:1900"
|
||||
serviceType = "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
)
|
||||
|
||||
func fromUDPAddress(src *net.UDPAddr) string {
|
||||
from := src.String()
|
||||
|
||||
hostnames, err := net.LookupAddr(src.IP.String())
|
||||
if err == nil {
|
||||
from = strings.TrimSuffix(hostnames[0], ".") + ":" + strconv.Itoa(src.Port)
|
||||
}
|
||||
|
||||
return from
|
||||
}
|
||||
|
||||
// StartSSDPResponder listens for M-SEARCH requests and responds if they match
|
||||
func StartSSDPResponder(device *upnp.DeviceDescription) {
|
||||
addr, err := net.ResolveUDPAddr("udp4", ssdpAddr)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to resolve SSDP address:", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenMulticastUDP("udp4", nil, addr)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to listen for SSDP:", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetReadBuffer(2048)
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
log.Println("🔍 Listening for M-SEARCH...")
|
||||
|
||||
for {
|
||||
n, src, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Println("ReadFromUDP error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
data := string(buf[:n])
|
||||
if strings.HasPrefix(data, "M-SEARCH * HTTP/1.1") {
|
||||
st := extractHeader(data, "ST")
|
||||
|
||||
log.Printf("🔎 M-SEARCH from %s, ST=%s\n", fromUDPAddress(src), st)
|
||||
|
||||
if st == "ssdp:all" ||
|
||||
st == serviceType ||
|
||||
strings.HasPrefix(st, "urn:schemas-upnp-org:service:") ||
|
||||
strings.HasPrefix(st, "urn:av-openhome-org:service:") ||
|
||||
strings.HasPrefix(st, "urn:bubblesoftapps-com:service:") {
|
||||
|
||||
go sendSSDPResponse(src, device, st)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendSSDPResponse(dst *net.UDPAddr, device *upnp.DeviceDescription, st string) {
|
||||
resp := fmt.Sprintf(
|
||||
"HTTP/1.1 200 OK\r\n"+
|
||||
"CACHE-CONTROL: max-age=1800\r\n"+
|
||||
"DATE: %s\r\n"+
|
||||
"EXT:\r\n"+
|
||||
"LOCATION: %s:%d\r\n"+
|
||||
"SERVER: pmomusic/1.0 UPnP/1.1 DLNARenderer/1.0\r\n"+
|
||||
"ST: %s\r\n"+
|
||||
"USN: %s::%s\r\n"+
|
||||
"\r\n",
|
||||
time.Now().Format(time.RFC1123),
|
||||
device.IP, device.Port,
|
||||
st,
|
||||
device.USN,
|
||||
st,
|
||||
)
|
||||
|
||||
// envoie UDP
|
||||
conn, err := net.DialUDP("udp4", nil, dst)
|
||||
if err != nil {
|
||||
log.Println("Failed to dial UDP to respond:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte(resp))
|
||||
if err != nil {
|
||||
log.Println("Failed to send SSDP response:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func extractHeader(data string, key string) string {
|
||||
lines := strings.Split(data, "\r\n")
|
||||
key = strings.ToLower(key)
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 && strings.ToLower(strings.TrimSpace(parts[0])) == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||
"github.com/koron/go-ssdp"
|
||||
)
|
||||
|
||||
func AnnounceRenderer(device *upnp.DeviceDescription) {
|
||||
st := "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
server := "pmomusic/1.0 UPnP/1.1 DLNARenderer/1.0"
|
||||
maxAge := 1800
|
||||
|
||||
_, err := ssdp.Advertise(st, device.USN+"::"+st, device.IP+":"+strconv.Itoa(int(device.Port)), server, maxAge)
|
||||
if err != nil {
|
||||
log.Println("SSDP advertise error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("✅ SSDP advertisement started")
|
||||
|
||||
// Ce `Advertiser` fait automatiquement le NOTIFY loop.
|
||||
// Il est censé continuer à diffuser les `alive` tous les MaxAge / 2.
|
||||
|
||||
// Exemple : on attend indéfiniment
|
||||
for {
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
|
||||
// Un jour on voudra faire ça à l'arrêt :
|
||||
// adv.Close() // envoie le ssdp:byebye
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var __UUID = ""
|
||||
|
||||
func loadOrCreateUUID() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := filepath.Join(configDir, "pmomusic", "uuid.txt")
|
||||
os.MkdirAll(filepath.Dir(filePath), 0755)
|
||||
|
||||
if data, err := os.ReadFile(filePath); err == nil && len(data) > 0 {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
id := "uuid:pmo-" + uuid.New().String()
|
||||
if err := os.WriteFile(filePath, []byte(id), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func GetUUID() string {
|
||||
if __UUID == "" {
|
||||
u, err := loadOrCreateUUID()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
__UUID = u
|
||||
}
|
||||
return __UUID
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateDeviceDescription(device *DeviceDescription) string {
|
||||
return fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>http://%s:%d/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
||||
<friendlyName>pmomusic Fake Renderer</friendlyName>
|
||||
<manufacturer>GoDLNA</manufacturer>
|
||||
<manufacturerURL>http://example.com</manufacturerURL>
|
||||
<modelDescription>Fake DLNA Renderer</modelDescription>
|
||||
<modelName>pmomusic</modelName>
|
||||
<modelNumber>1.0</modelNumber>
|
||||
<modelURL>http://example.com/model</modelURL>
|
||||
<UDN>%s</UDN>
|
||||
<presentationURL>http://%s:%d</presentationURL>
|
||||
<serviceList>
|
||||
<service>
|
||||
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
||||
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
|
||||
<controlURL>/upnp/control/AVTransport</controlURL>
|
||||
<eventSubURL>/upnp/event/AVTransport</eventSubURL>
|
||||
<SCPDURL>/scpd/AVTransport.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
|
||||
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
|
||||
<controlURL>/upnp/control/RenderingControl</controlURL>
|
||||
<eventSubURL>/upnp/event/RenderingControl</eventSubURL>
|
||||
<SCPDURL>/scpd/RenderingControl.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
|
||||
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
|
||||
<controlURL>/upnp/control/ConnectionManager</controlURL>
|
||||
<eventSubURL>/upnp/event/ConnectionManager</eventSubURL>
|
||||
<SCPDURL>/scpd/ConnectionManager.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Product:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Product</serviceId>
|
||||
<controlURL>/upnp/control/Product</controlURL>
|
||||
<eventSubURL>/upnp/event/Product</eventSubURL>
|
||||
<SCPDURL>/scpd/Product.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Playlist:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Playlist</serviceId>
|
||||
<controlURL>/upnp/control/Playlist</controlURL>
|
||||
<eventSubURL>/upnp/event/Playlist</eventSubURL>
|
||||
<SCPDURL>/scpd/Playlist.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Info:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Info</serviceId>
|
||||
<controlURL>/upnp/control/Info</controlURL>
|
||||
<eventSubURL>/upnp/event/Info</eventSubURL>
|
||||
<SCPDURL>/scpd/Info.xml</SCPDURL>
|
||||
</service>
|
||||
</serviceList>
|
||||
</device>
|
||||
</root>`, device.IP, device.Port, device.USN, device.IP, device.Port)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DeviceDescription struct {
|
||||
IP string
|
||||
Port uint
|
||||
USN string
|
||||
Friendly string
|
||||
ModelName string
|
||||
Services *ServiceList
|
||||
}
|
||||
|
||||
const deviceTemplate = `<?xml version="1.0"?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>http://{{.IP}}:{{.Port}}/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
||||
<friendlyName>{{.Friendly}}</friendlyName>
|
||||
<manufacturer>GoDLNA</manufacturer>
|
||||
<manufacturerURL>http://example.com</manufacturerURL>
|
||||
<modelDescription>Fake DLNA Renderer</modelDescription>
|
||||
<modelName>{{.ModelName}}</modelName>
|
||||
<modelNumber>1.0</modelNumber>
|
||||
<modelURL>http://example.com/model</modelURL>
|
||||
<UDN>{{.USN}}</UDN>
|
||||
<presentationURL>http://{{.IP}}:{{.Port}}/</presentationURL>
|
||||
{{.ServicesXML}}
|
||||
</device>
|
||||
</root>`
|
||||
|
||||
func NewDevice(ip string, port uint, usn string, friendly string, modelname string) *DeviceDescription {
|
||||
services := NewServiceList()
|
||||
return &DeviceDescription{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
USN: usn,
|
||||
Friendly: friendly,
|
||||
ModelName: modelname,
|
||||
Services: services,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeviceDescription) RegisterService(service string) {
|
||||
d.Services.Append(service)
|
||||
}
|
||||
|
||||
func (d *DeviceDescription) GenerateXML() (string, error) {
|
||||
servicesXML, err := d.Services.GenerateServiceListXML()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to generate service list XML: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpl, err := template.New("device").Parse(deviceTemplate)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to parse device description template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
IP string
|
||||
Port uint
|
||||
USN string
|
||||
Friendly string
|
||||
ModelName string
|
||||
ServicesXML string // trusted raw XML
|
||||
}{
|
||||
IP: d.IP,
|
||||
Port: d.Port,
|
||||
USN: d.USN,
|
||||
Friendly: d.Friendly,
|
||||
ModelName: d.ModelName,
|
||||
ServicesXML: servicesXML,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
log.Errorf("Failed to execute template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/soap"
|
||||
)
|
||||
|
||||
//go:embed xml/*.xml
|
||||
var embeddedXML embed.FS
|
||||
|
||||
//go:embed html/*.html
|
||||
var embeddedHTML embed.FS
|
||||
|
||||
// ServeStaticXML mounts handlers for SCPD XML files.
|
||||
func ServeStaticXML(mux *http.ServeMux) {
|
||||
subFS, err := fs.Sub(embeddedXML, "xml")
|
||||
if err != nil {
|
||||
panic("failed to create sub FS: " + err.Error())
|
||||
}
|
||||
mux.Handle("/scpd/", http.StripPrefix("/scpd/", http.FileServer(http.FS(subFS))))
|
||||
}
|
||||
|
||||
func ServeStaticHTML(mux *http.ServeMux) {
|
||||
subFS, err := fs.Sub(embeddedHTML, "html")
|
||||
if err != nil {
|
||||
panic("failed to create sub FS: " + err.Error())
|
||||
}
|
||||
mux.Handle("/", http.FileServer(http.FS(subFS)))
|
||||
}
|
||||
|
||||
func StartHTTPServer(device *DeviceDescription) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Device description
|
||||
mux.HandleFunc("/description.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
xml, err := device.GenerateXML()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(xml))
|
||||
})
|
||||
|
||||
// SOAP control endpoints
|
||||
mux.HandleFunc("/upnp/control/AVTransport", soap.HandleSOAP)
|
||||
mux.HandleFunc("/upnp/control/RenderingControl", soap.HandleSOAP)
|
||||
mux.HandleFunc("/upnp/control/ConnectionManager", soap.HandleSOAP)
|
||||
|
||||
// Serve static SCPD XML files
|
||||
ServeStaticXML(mux)
|
||||
ServeStaticHTML(mux)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", device.IP, device.Port)
|
||||
log.Printf("Serving UPnP fake renderer at http://%s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type ServiceGroup string
|
||||
|
||||
const (
|
||||
UPnP ServiceGroup = "upnp"
|
||||
OpenHome ServiceGroup = "openhome"
|
||||
Other ServiceGroup = "other"
|
||||
)
|
||||
|
||||
type ServiceDescription struct {
|
||||
Group ServiceGroup
|
||||
Type string
|
||||
Name string
|
||||
ID string
|
||||
URLPrefix string
|
||||
XML string // auto-rempli temporairement pour le template parent
|
||||
}
|
||||
|
||||
const serviceTemplate = `
|
||||
<service>
|
||||
<serviceType>{{.Type}}</serviceType>
|
||||
<serviceId>{{.ID}}</serviceId>
|
||||
<controlURL>{{.URLPrefix}}/control</controlURL>
|
||||
<eventSubURL>{{.URLPrefix}}/event</eventSubURL>
|
||||
<SCPDURL>{{.URLPrefix}}.xml</SCPDURL>
|
||||
</service>
|
||||
`
|
||||
|
||||
func NewServiceDescription(serviceType string) *ServiceDescription {
|
||||
parts := strings.Split(serviceType, ":")
|
||||
name := parts[len(parts)-2]
|
||||
|
||||
var group ServiceGroup
|
||||
switch {
|
||||
case strings.Contains(serviceType, "schemas-upnp-org"):
|
||||
group = UPnP
|
||||
case strings.Contains(serviceType, "av-openhome-org"):
|
||||
group = OpenHome
|
||||
default:
|
||||
group = Other
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("urn:%s:serviceId:%s", string(group), name)
|
||||
urlPrefix := fmt.Sprintf("/%s/%s", group, name)
|
||||
|
||||
return &ServiceDescription{
|
||||
Group: group,
|
||||
Type: serviceType,
|
||||
Name: name,
|
||||
ID: id,
|
||||
URLPrefix: urlPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (desc *ServiceDescription) GenerateServiceXML() (string, error) {
|
||||
tmpl, err := template.New("service").Parse(strings.TrimSpace(serviceTemplate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, desc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
type ServiceList struct {
|
||||
Services []*ServiceDescription
|
||||
}
|
||||
|
||||
func NewServiceList() *ServiceList {
|
||||
return &ServiceList{
|
||||
Services: make([]*ServiceDescription, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *ServiceList) Append(serviceType string) {
|
||||
svc := NewServiceDescription(serviceType)
|
||||
sl.Services = append(sl.Services, svc)
|
||||
}
|
||||
|
||||
func (sl *ServiceList) GenerateServiceListXML() (string, error) {
|
||||
const listTemplate = `
|
||||
<serviceList>
|
||||
{{range .Services}}{{.XML}}
|
||||
{{end}}</serviceList>`
|
||||
|
||||
// Génère tous les XML individuellement
|
||||
for _, svc := range sl.Services {
|
||||
xml, err := svc.GenerateServiceXML()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
svc.XML = xml
|
||||
}
|
||||
|
||||
// Template principal
|
||||
tmpl, err := template.New("list").Parse(strings.TrimSpace(listTemplate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, sl); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import "time"
|
||||
|
||||
type StateValueInstance struct {
|
||||
model *StateValue
|
||||
value interface{}
|
||||
previousValue interface{}
|
||||
lastChange time.Time
|
||||
lastEvent time.Time
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Model() *StateValue {
|
||||
return instance.model
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Value() interface{} {
|
||||
return instance.value
|
||||
}
|
||||
|
||||
// ShouldTriggerEvent vérifie toutes les conditions
|
||||
func (instance *StateValueInstance) ShouldTriggerEvent() bool {
|
||||
for _, condition := range instance.model.eventConditions {
|
||||
if !condition(instance) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package upnp
|
||||
|
||||
// ValueRange represents an inclusive range constraint for a state variable value.
|
||||
// It defines the minimum and maximum allowable values for a given UPnP type.
|
||||
//
|
||||
// Usage:
|
||||
// - For numeric types: min/max must be numeric types
|
||||
// - For time types: min/max must be time.Time values
|
||||
// - For strings/UUIDs: min/max must be string values
|
||||
//
|
||||
// Use with StateVarType.InRange() to check if values fall within the range.
|
||||
type ValueRange struct {
|
||||
min interface{}
|
||||
max interface{}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>SetAVTransportURI</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>InstanceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>CurrentURI</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>AVTransportURI</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>CurrentURIMetaData</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>AVTransportURIMetaData</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Play</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>InstanceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Speed</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>TransportPlaySpeed</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Stop</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>InstanceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_InstanceID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>AVTransportURI</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>AVTransportURIMetaData</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>TransportPlaySpeed</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>1</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion><major>1</major><minor>0</minor></specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>GetProtocolInfo</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Source</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SourceProtocolInfo</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Sink</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SinkProtocolInfo</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>SourceProtocolInfo</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>SinkProtocolInfo</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
@@ -1,111 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Track</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Uri</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackUri</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Metadata</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackMetadata</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Details</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Duration</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackDuration</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>BitRate</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackBitRate</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>BitDepth</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackBitDepth</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>SampleRate</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackSampleRate</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Lossless</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackLossless</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>CodecName</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackCodecName</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Metatext</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Metatext</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackUri</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackMetadata</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackDuration</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackBitRate</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackBitDepth</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackSampleRate</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackLossless</name>
|
||||
<dataType>boolean</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackCodecName</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>Metatext</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
@@ -1,89 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Play</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>TransportState</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Pause</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Stop</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Next</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Previous</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Insert</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>AfterId</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>TrackId</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Uri</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Uri</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Metadata</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Metadata</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>NewId</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackId</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TransportState</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>Playing</allowedValue>
|
||||
<allowedValue>Paused</allowedValue>
|
||||
<allowedValue>Stopped</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Uri</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Metadata</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>TrackId</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Manufacturer</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Manufacturer</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Model</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Model</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Product</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Product</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Standby</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Standby</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>SetStandby</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Standby</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Manufacturer</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Model</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Product</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>Standby</name>
|
||||
<dataType>boolean</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion><major>1</major><minor>0</minor></specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>SetVolume</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>InstanceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Channel</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>DesiredVolume</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Volume</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_InstanceID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Channel</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>Master</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Volume</name>
|
||||
<dataType>ui2</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
5
upnp/events.go
Normal file
5
upnp/events.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package upnp
|
||||
|
||||
import "github.com/beevik/etree"
|
||||
|
||||
type UpnpEvent *etree.Element
|
||||
@@ -1,75 +1,39 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventEnabled EventType = "yes"
|
||||
EventDisabled EventType = "no"
|
||||
EventConditional EventType = "conditional"
|
||||
)
|
||||
|
||||
type StateConditionFunc func(instance *StateValueInstance) bool
|
||||
type StringValueParser func(value string) (interface{}, error)
|
||||
type ValueSerializer func(value interface{}) (string, error)
|
||||
|
||||
type TypeModifier int
|
||||
|
||||
const (
|
||||
ModifierAtomic TypeModifier = iota // Valeur simple (par défaut)
|
||||
ModifierList // Liste d'éléments
|
||||
ModifierMap // Tableau associatif
|
||||
ModifierStruct // Structure nommée
|
||||
)
|
||||
|
||||
// StateValue represents a UPnP state variable with value constraints and eventing capabilities.
|
||||
// It encapsulates:
|
||||
// - Value type (StateVarType)
|
||||
// - Value range constraints (min/max)
|
||||
// - Allowed value list
|
||||
// - Eventing configuration
|
||||
// graph TD
|
||||
// A[StateValue] --> B[baseType: Type sérialisé]
|
||||
// A --> C[modifier: Structure des données]
|
||||
// C --> D[Liste]
|
||||
// C --> E[Map]
|
||||
// C --> F[Structure]
|
||||
// D --> G[elementType: Type des éléments]
|
||||
// E --> H[keyType: Type des clés]
|
||||
// E --> I[elementType: Type des valeurs]
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// state := StateValue{
|
||||
// name: "Volume",
|
||||
// baseType: StateType_UI2,
|
||||
// }
|
||||
// state.SetRange(0, 100) // Set 0-100 range
|
||||
// state.AppendAllowedValue(25, 50, 75) // Add specific allowed values
|
||||
type StateValue struct {
|
||||
name string
|
||||
baseType StateVarType // Renommé pour plus de clarté
|
||||
modifier TypeModifier
|
||||
step interface{}
|
||||
minDelta interface{}
|
||||
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 []StateConditionFunc
|
||||
eventConditions map[string]StateConditionFunc
|
||||
description string
|
||||
defaultValue interface{}
|
||||
valueRange *ValueRange
|
||||
allowedValues []interface{}
|
||||
sendEvents bool
|
||||
elementType StateVarType // Pour listes et valeurs de maps
|
||||
keyType StateVarType // Type des clés (pour les maps)
|
||||
structFields map[string]StateVarType // Champs (pour les structures)
|
||||
|
||||
parse StringValueParser
|
||||
marshal ValueSerializer
|
||||
}
|
||||
|
||||
// Name returns the state variable's name (e.g., "Volume", "Brightness").
|
||||
@@ -79,21 +43,57 @@ func (state *StateValue) Name() string {
|
||||
|
||||
// Type returns the UPnP data type of the state variable.
|
||||
func (state *StateValue) Type() StateVarType {
|
||||
return state.baseType
|
||||
return state.valueType
|
||||
}
|
||||
|
||||
func (state *StateValue) AddEventCondition(condition StateConditionFunc) {
|
||||
state.eventConditions = append(state.eventConditions, condition)
|
||||
func (state *StateValue) AddEventCondition(name string, condition StateConditionFunc) {
|
||||
state.eventConditions[name] = condition
|
||||
}
|
||||
|
||||
func (state *StateValue) 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 *StateValue) ClearEventConditions() {
|
||||
state.eventConditions = nil
|
||||
state.eventConditions = make(map[string]StateConditionFunc)
|
||||
}
|
||||
|
||||
func (sv *StateValue) 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 *StateValue) SetDefault(value interface{}) error {
|
||||
if state.IsValidValue(value) {
|
||||
cvalue, _ := state.baseType.Cast(value)
|
||||
cvalue, _ := state.valueType.Cast(value)
|
||||
state.defaultValue = cvalue
|
||||
log.Debugf("🐞 Setting default value for %v to %v", state.name, cvalue)
|
||||
return nil
|
||||
@@ -107,7 +107,7 @@ func (state *StateValue) HasDefault() bool {
|
||||
|
||||
func (state *StateValue) DefaultValue() interface{} {
|
||||
if !state.HasDefault() {
|
||||
return state.baseType.DefaultValue()
|
||||
return state.valueType.DefaultValue()
|
||||
}
|
||||
|
||||
return state.DefaultValue()
|
||||
@@ -156,7 +156,7 @@ func (state *StateValue) SetRange(min, max interface{}) error {
|
||||
if min == nil || max == nil {
|
||||
return fmt.Errorf("min and max must not be nil")
|
||||
}
|
||||
limits, err := state.baseType.ValueRange(min, max)
|
||||
limits, err := state.valueType.ValueRange(min, max)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting range: %v", err)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (state *StateValue) UpdateMinimalValue(value interface{}) error {
|
||||
if state.valueRange == nil {
|
||||
return fmt.Errorf("no range set for value %v", state.name)
|
||||
}
|
||||
cvalue, err := state.baseType.Cast(value)
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("casting value: %v", err)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func (state *StateValue) UpdateMaximalValue(value interface{}) error {
|
||||
if state.valueRange == nil {
|
||||
return fmt.Errorf("no range set for value %v", state.name)
|
||||
}
|
||||
cvalue, err := state.baseType.Cast(value)
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("casting value: %v", err)
|
||||
}
|
||||
@@ -225,7 +225,7 @@ func (state *StateValue) UpdateMaximalValue(value interface{}) error {
|
||||
//
|
||||
// bool: True if within range or no range defined
|
||||
func (state *StateValue) IsValueInRange(value interface{}) bool {
|
||||
return state.baseType.InRange(value, state.valueRange)
|
||||
return state.valueType.InRange(value, state.valueRange)
|
||||
}
|
||||
|
||||
// IsSendingEvents indicates if state changes trigger UPnP events.
|
||||
@@ -273,7 +273,7 @@ func (state *StateValue) AllowedValues() []interface{} {
|
||||
func (state *StateValue) AppendAllowedValue(value ...interface{}) error {
|
||||
state.allowedValues = slices.Grow(state.allowedValues, len(value))
|
||||
for _, v := range value {
|
||||
cv, err := state.baseType.Cast(v)
|
||||
cv, err := state.valueType.Cast(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("casting allowed value: %v", err)
|
||||
}
|
||||
@@ -299,7 +299,7 @@ func (state *StateValue) IsValueAllowed(value interface{}) bool {
|
||||
return true // No list = any value valid
|
||||
}
|
||||
|
||||
cvalue, err := state.baseType.Cast(value)
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -322,7 +322,7 @@ func (state *StateValue) IsValueAllowed(value interface{}) bool {
|
||||
//
|
||||
// bool: True if value passes all applicable constraints
|
||||
func (state *StateValue) IsValidValue(value interface{}) bool {
|
||||
cvalue, err := state.baseType.Cast(value)
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func (state *StateValue) SetModifiable() {
|
||||
|
||||
func (state *StateValue) SetStep(step interface{}) error {
|
||||
// Validation que le step correspond au type de la variable
|
||||
if _, err := state.baseType.Cast(step); err != nil {
|
||||
if _, err := state.valueType.Cast(step); err != nil {
|
||||
return fmt.Errorf("invalid step type: %v", err)
|
||||
}
|
||||
state.step = step
|
||||
@@ -382,3 +382,133 @@ func (state *StateValue) NewInstance() *StateValueInstance {
|
||||
lastEvent: time.Unix(int64(1718985600), 0).UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToXMLElement generates the complete XML representation of the state variable
|
||||
// Returns an etree.Element that can be serialized to XML
|
||||
func (sv *StateValue) ToXMLElement() *etree.Element {
|
||||
// Create root <stateVariable> element
|
||||
elem := etree.NewElement("stateVariable")
|
||||
elem.CreateAttr("name", sv.name)
|
||||
|
||||
// Add sendEvents attribute (UPnP eventing capability)
|
||||
if sv.sendEvents {
|
||||
elem.CreateAttr("sendEvents", "yes") // Enable event notifications
|
||||
} else {
|
||||
elem.CreateAttr("sendEvents", "no") // Disable event notifications
|
||||
}
|
||||
|
||||
// Add data type element
|
||||
dataType := elem.CreateElement("dataType")
|
||||
dataType.SetText(sv.valueType.String()) // Set UPnP type name (e.g., "ui1", "boolean")
|
||||
|
||||
// Add default value if specified
|
||||
if sv.defaultValue != nil {
|
||||
defaultValue := elem.CreateElement("defaultValue")
|
||||
// Convert value to UPnP-compatible string representation
|
||||
defaultValue.SetText(sv.valueToString(sv.defaultValue))
|
||||
}
|
||||
|
||||
// Add value range constraints if defined
|
||||
if sv.valueRange != nil {
|
||||
rangeElem := elem.CreateElement("allowedValueRange")
|
||||
|
||||
// Minimum boundary value
|
||||
min := rangeElem.CreateElement("minimum")
|
||||
min.SetText(sv.valueToString(sv.valueRange.min))
|
||||
|
||||
// Maximum boundary value
|
||||
max := rangeElem.CreateElement("maximum")
|
||||
max.SetText(sv.valueToString(sv.valueRange.max))
|
||||
|
||||
// Add step value if defined (for incremental controls)
|
||||
if sv.step != nil {
|
||||
step := rangeElem.CreateElement("step")
|
||||
step.SetText(sv.valueToString(sv.step))
|
||||
}
|
||||
}
|
||||
|
||||
// Add allowed value list if defined
|
||||
if len(sv.allowedValues) > 0 {
|
||||
allowedList := elem.CreateElement("allowedValueList")
|
||||
for _, value := range sv.allowedValues {
|
||||
// Create individual <allowedValue> elements
|
||||
allowed := allowedList.CreateElement("allowedValue")
|
||||
allowed.SetText(sv.valueToString(value))
|
||||
}
|
||||
}
|
||||
|
||||
// Add description if available
|
||||
if sv.description != "" {
|
||||
desc := elem.CreateElement("description")
|
||||
desc.SetText(sv.description) // Human-readable description
|
||||
}
|
||||
|
||||
return elem
|
||||
}
|
||||
|
||||
// valueToString converts a value to its UPnP-compatible string representation
|
||||
// Handles type-specific formatting for proper XML serialization
|
||||
func (sv *StateValue) valueToString(val interface{}) string {
|
||||
if val == nil {
|
||||
return "" // Safeguard against nil values
|
||||
}
|
||||
|
||||
// Type-specific formatting for UPnP compliance
|
||||
switch sv.valueType {
|
||||
case StateType_Boolean:
|
||||
// Boolean: "1" for true, "0" for false (UPnP standard)
|
||||
if b, ok := val.(bool); ok && b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
|
||||
case StateType_Date:
|
||||
// Date: YYYY-MM-DD format
|
||||
if t, ok := val.(time.Time); ok {
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
case StateType_DateTime, StateType_DateTimeTZ:
|
||||
// DateTime: ISO 8601 format with timezone
|
||||
if t, ok := val.(time.Time); ok {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
case StateType_Time, StateType_TimeTZ:
|
||||
// Time: HH:MM:SS format
|
||||
if t, ok := val.(time.Time); ok {
|
||||
return t.Format("15:04:05")
|
||||
}
|
||||
|
||||
case StateType_BinBase64:
|
||||
// Binary: Base64 encoding
|
||||
if b, ok := val.([]byte); ok {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
case StateType_BinHex:
|
||||
// Binary: Hex encoding
|
||||
if b, ok := val.([]byte); ok {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
case StateType_URI:
|
||||
// URI: Full URL string
|
||||
if u, ok := val.(*url.URL); ok {
|
||||
return u.String()
|
||||
}
|
||||
|
||||
case StateType_UUID:
|
||||
// UUID: Canonical string representation
|
||||
if u, ok := val.(uuid.UUID); ok {
|
||||
return u.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Default conversion for unsupported types or fallback
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
|
||||
func (sv *StateValue) Cast(val interface{}) (interface{}, error) {
|
||||
return sv.valueType.Cast(val)
|
||||
}
|
||||
75
upnp/statevalueinstance.go
Normal file
75
upnp/statevalueinstance.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
type StateValueInstance struct {
|
||||
model *StateValue
|
||||
value interface{}
|
||||
previousValue interface{}
|
||||
lastChange time.Time
|
||||
lastEvent time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Cast(val interface{}) (interface{}, error) {
|
||||
return instance.model.Cast(val)
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Model() *StateValue {
|
||||
return instance.model
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Value() interface{} {
|
||||
instance.mu.RLock()
|
||||
defer instance.mu.RUnlock()
|
||||
return instance.value
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) SetValue(val interface{}) error {
|
||||
cval, err := instance.Cast(val)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance.mu.Lock()
|
||||
defer instance.mu.Unlock()
|
||||
instance.previousValue = instance.value
|
||||
instance.value = cval
|
||||
return nil
|
||||
}
|
||||
|
||||
func (instance *StateValueInstance) Incr() {
|
||||
instance.mu.Lock()
|
||||
defer instance.mu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
// ShouldTriggerEvent vérifie toutes les conditions
|
||||
func (instance *StateValueInstance) ShouldTriggerEvent() bool {
|
||||
for _, condition := range instance.model.eventConditions {
|
||||
if !condition(instance) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (sv *StateValueInstance) GenerateEvent() *etree.Element {
|
||||
|
||||
// Construire le XML d'événement
|
||||
propSet := etree.NewElement("e:propertyset")
|
||||
propSet.CreateAttr("xmlns:e", "urn:schemas-upnp-org:event-1-0")
|
||||
|
||||
prop := propSet.CreateElement("e:property")
|
||||
elem := prop.CreateElement(sv.model.Name())
|
||||
elem.SetText(sv.model.valueToString(sv.Value()))
|
||||
|
||||
return propSet
|
||||
|
||||
}
|
||||
@@ -9,10 +9,8 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -105,8 +103,8 @@ var typeStrings = [...]string{
|
||||
"uri",
|
||||
}
|
||||
|
||||
// String returns the UPnP XML name of the type.
|
||||
// Returns "unknown" for unrecognized types.
|
||||
// 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]
|
||||
@@ -114,6 +112,122 @@ func (t StateVarType) String() string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ParseStateVarType converts a UPnP type name to its StateVarType constant.
|
||||
// Case-insensitive and trims whitespace. Returns StateType_Unknown for unrecognized types.
|
||||
func ParseStateVarType(s string) StateVarType {
|
||||
@@ -135,64 +249,64 @@ func ParseStateVarType(s string) StateVarType {
|
||||
func (t StateVarType) Cast(val interface{}) (interface{}, error) {
|
||||
switch t {
|
||||
case StateType_UI1:
|
||||
v, ok := toUint(val, 8)
|
||||
if !ok {
|
||||
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, ok := toUint(val, 16)
|
||||
if !ok {
|
||||
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, ok := toUint(val, 32)
|
||||
if !ok {
|
||||
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, ok := toInt(val, 8)
|
||||
if !ok {
|
||||
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, ok := toInt(val, 16)
|
||||
if !ok {
|
||||
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, ok := toInt(val, 32)
|
||||
if !ok {
|
||||
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, ok := toFloat(val, 32)
|
||||
if !ok {
|
||||
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, ok := toFloat(val, 64)
|
||||
if !ok {
|
||||
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, ok := toBool(val)
|
||||
if !ok {
|
||||
b, err := toBool(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot cast %v (%T) to Boolean", val, val)
|
||||
}
|
||||
return b, nil
|
||||
@@ -383,87 +497,12 @@ func (t StateVarType) InRange(val interface{}, interval *ValueRange) bool {
|
||||
return interval == nil || t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0
|
||||
}
|
||||
|
||||
// ValueRange creates a valid value range for the UPnP type.
|
||||
//
|
||||
// This method casts the provided min and max values to the UPnP type and returns
|
||||
// a ValueRange struct suitable for range validation. If either value cannot be
|
||||
// cast to the type, it returns an error.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// min: Minimum value of the range (inclusive)
|
||||
// max: Maximum value of the range (inclusive)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// ValueRange: Valid range structure if cast succeeds
|
||||
// error: If min or max cannot be cast to the type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a range for UI2 (uint16) values
|
||||
// r, err := StateType_UI2.ValueRange(10, 100)
|
||||
// if err != nil { /* handle error */ }
|
||||
// valid := StateType_UI2.InRange(50, r) // true
|
||||
//
|
||||
// Notes:
|
||||
// - The range is inclusive: [min, max]
|
||||
// - Values must be comparable using the type's comparison logic
|
||||
// - For time types, min and max must be valid time values
|
||||
func (t StateVarType) ValueRange(min, max interface{}) (*ValueRange, error) {
|
||||
cmin, error := t.Cast(min)
|
||||
if error != nil {
|
||||
return nil, fmt.Errorf("min value %v is not castable to type %s", min, t.String())
|
||||
}
|
||||
cmax, error := t.Cast(max)
|
||||
if error != nil {
|
||||
return nil, fmt.Errorf("max value %v is not castable to type %s", min, t.String())
|
||||
}
|
||||
|
||||
return &ValueRange{min: cmin, max: cmax}, nil
|
||||
}
|
||||
|
||||
// NewAtomicValue crée une valeur simple
|
||||
func (t StateVarType) NewAtomicValue(name string) *StateValue {
|
||||
return &StateValue{
|
||||
name: name,
|
||||
baseType: t,
|
||||
modifier: ModifierAtomic,
|
||||
}
|
||||
}
|
||||
|
||||
// NewListValue crée une liste
|
||||
func (t StateVarType) NewListValue(name string, elementType StateVarType) *StateValue {
|
||||
return &StateValue{
|
||||
name: name,
|
||||
baseType: t,
|
||||
modifier: ModifierList,
|
||||
elementType: elementType,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMapValue crée une map
|
||||
func (t StateVarType) NewMapValue(
|
||||
name string,
|
||||
keyType StateVarType,
|
||||
valueType StateVarType,
|
||||
) *StateValue {
|
||||
return &StateValue{
|
||||
name: name,
|
||||
baseType: t,
|
||||
modifier: ModifierMap,
|
||||
keyType: keyType,
|
||||
elementType: valueType, // elementType = valeur de la map
|
||||
}
|
||||
}
|
||||
|
||||
// NewStructValue crée une valeur de type struct
|
||||
func (t StateVarType) NewStructValue(name string, fields map[string]StateVarType) *StateValue {
|
||||
return &StateValue{
|
||||
name: name,
|
||||
baseType: t,
|
||||
modifier: ModifierStruct,
|
||||
structFields: fields,
|
||||
name: name,
|
||||
valueType: t,
|
||||
eventConditions: make(map[string]StateConditionFunc),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,376 +548,6 @@ func (t StateVarType) DefaultValue() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// toInt converts various types to signed integer with specified bit size.
|
||||
// Handles overflow/underflow. Returns converted value and success status.
|
||||
func toInt(v interface{}, bits int) (int64, bool) {
|
||||
min := minInt(bits)
|
||||
max := maxInt(bits)
|
||||
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
if int64(val) < min || int64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case int8:
|
||||
if int64(val) < min || int64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case int16:
|
||||
if int64(val) < min || int64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case int32:
|
||||
if int64(val) < min || int64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case int64:
|
||||
if val < min || val > max {
|
||||
return 0, false
|
||||
}
|
||||
return val, true
|
||||
case uint:
|
||||
if uint64(val) > uint64(max) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case uint8:
|
||||
if uint64(val) > uint64(max) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case uint16:
|
||||
if uint64(val) > uint64(max) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case uint32:
|
||||
if uint64(val) > uint64(max) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case uint64:
|
||||
if val > uint64(max) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(val), true
|
||||
case float32:
|
||||
r := int64(math.Round(float64(val)))
|
||||
if r < min || r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
case float64:
|
||||
r := int64(math.Round(val))
|
||||
if r < min || r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
case string:
|
||||
// Try int parse direct
|
||||
if i, err := strconv.ParseInt(val, 10, bits); err == nil {
|
||||
if i < min || i > max {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
// Try float parse then round + bounds check
|
||||
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
r := int64(math.Round(f))
|
||||
if r < min || r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
return 0, false
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// toUint converts various types to unsigned integer with specified bit size.
|
||||
// Handles numeric types and strings. Returns converted value and success status.
|
||||
func toUint(v interface{}, bits int) (uint64, bool) {
|
||||
max := maxUint(bits)
|
||||
|
||||
switch val := v.(type) {
|
||||
case uint:
|
||||
if uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case uint8:
|
||||
if uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case uint16:
|
||||
if uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case uint32:
|
||||
if uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case uint64:
|
||||
if val > max {
|
||||
return 0, false
|
||||
}
|
||||
return val, true
|
||||
|
||||
case int:
|
||||
if val < 0 || uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case int8:
|
||||
if val < 0 || uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case int16:
|
||||
if val < 0 || uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case int32:
|
||||
if val < 0 || uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case int64:
|
||||
if val < 0 || uint64(val) > max {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
|
||||
case float32:
|
||||
r := uint64(math.Round(float64(val)))
|
||||
if r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
case float64:
|
||||
r := uint64(math.Round(val))
|
||||
if r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
|
||||
case string:
|
||||
// Try parse float first (handles int and float strings)
|
||||
f, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil || f < 0 {
|
||||
return 0, false
|
||||
}
|
||||
r := uint64(math.Round(f))
|
||||
if r > max {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// toFloat converts various types to float (32 or 64 bits).
|
||||
// Checks float32 boundaries when converting to 32-bit float.
|
||||
func toFloat(v interface{}, bits int) (float64, bool) {
|
||||
switch val := v.(type) {
|
||||
case float32:
|
||||
f := float64(val)
|
||||
if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
case float64:
|
||||
if bits == 32 && (val > math.MaxFloat32 || val < -math.MaxFloat32) {
|
||||
return 0, false
|
||||
}
|
||||
return val, true
|
||||
case int, int8, int16, int32, int64:
|
||||
f := float64(reflect.ValueOf(val).Int())
|
||||
if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
f := float64(reflect.ValueOf(val).Uint())
|
||||
if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(val, bits)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// toBool converts various types to boolean following UPnP rules:
|
||||
// true: 1, "true"; false: 0, "false"
|
||||
func toBool(val interface{}) (bool, bool) {
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
return v, true
|
||||
|
||||
case int:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case int8:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case int16:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case int32:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case int64:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
|
||||
case uint:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case uint8:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case uint16:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case uint32:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
case uint64:
|
||||
if v == 0 {
|
||||
return false, true
|
||||
} else if v == 1 {
|
||||
return true, true
|
||||
}
|
||||
|
||||
case float32:
|
||||
if v == 0.0 {
|
||||
return false, true
|
||||
} else if v == 1.0 {
|
||||
return true, true
|
||||
}
|
||||
case float64:
|
||||
if v == 0.0 {
|
||||
return false, true
|
||||
} else if v == 1.0 {
|
||||
return true, true
|
||||
}
|
||||
|
||||
case string:
|
||||
return parseUPnPBoolean(v)
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
// parseUPnPBoolean parses boolean from string:
|
||||
// "1"/"true" → true, "0"/"false" → false
|
||||
func parseUPnPBoolean(s string) (bool, bool) {
|
||||
switch strings.TrimSpace(strings.ToLower(s)) {
|
||||
case "1", "true":
|
||||
return true, true
|
||||
case "0", "false":
|
||||
return false, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
// decodeBinary decodes Base64 or Hex-encoded binary strings to byte slices
|
||||
func decodeBinary(t StateVarType, val string) ([]byte, error) {
|
||||
switch t {
|
||||
49
upnp/utils_bool.go
Normal file
49
upnp/utils_bool.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package upnp
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
77
upnp/utils_float.go
Normal file
77
upnp/utils_float.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func maxFloat(bits int) float64 {
|
||||
switch bits {
|
||||
case 32:
|
||||
return float64(math.MaxFloat32)
|
||||
case 64:
|
||||
return math.MaxFloat64
|
||||
default:
|
||||
return math.MaxFloat64 // fallback
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
116
upnp/utils_int.go
Normal file
116
upnp/utils_int.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package upnp
|
||||
|
||||
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
|
||||
}
|
||||
30
upnp/utils_numeric_operandes.go
Normal file
30
upnp/utils_numeric_operandes.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package upnp
|
||||
|
||||
import "fmt"
|
||||
|
||||
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
|
||||
}
|
||||
100
upnp/utils_uint.go
Normal file
100
upnp/utils_uint.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package upnp
|
||||
|
||||
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
|
||||
}
|
||||
61
upnp/valuerange.go
Normal file
61
upnp/valuerange.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package upnp
|
||||
|
||||
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 t.Cmp(cmin, cmax) > 0 {
|
||||
cmax, cmin = cmin, cmax
|
||||
}
|
||||
|
||||
return &ValueRange{min: cmin, max: cmax}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user