summaryrefslogtreecommitdiff
path: root/vendor/xengineering.eu/homematic-go/homematic
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2025-12-10 21:08:39 +0100
committerxengineering <me@xengineering.eu>2025-12-10 21:08:39 +0100
commit1f4c3308be296d95163e3e0ea54761410f4da140 (patch)
tree54640f0f423c02aaae07d99a6754beb47a83f79c /vendor/xengineering.eu/homematic-go/homematic
parent83277c420525e06f9d0c234e018f481b4579d6cd (diff)
downloadsia-server-1f4c3308be296d95163e3e0ea54761410f4da140.tar
sia-server-1f4c3308be296d95163e3e0ea54761410f4da140.tar.zst
sia-server-1f4c3308be296d95163e3e0ea54761410f4da140.zip
Add homematic-go v0.1.0
This is the minimal viable product (MVP) of this library suitable to build the MVP of the sia-server.
Diffstat (limited to 'vendor/xengineering.eu/homematic-go/homematic')
-rw-r--r--vendor/xengineering.eu/homematic-go/homematic/device.go92
-rw-r--r--vendor/xengineering.eu/homematic-go/homematic/meson.build4
-rw-r--r--vendor/xengineering.eu/homematic-go/homematic/xmlrpc.go147
3 files changed, 243 insertions, 0 deletions
diff --git a/vendor/xengineering.eu/homematic-go/homematic/device.go b/vendor/xengineering.eu/homematic-go/homematic/device.go
new file mode 100644
index 0000000..fc7f383
--- /dev/null
+++ b/vendor/xengineering.eu/homematic-go/homematic/device.go
@@ -0,0 +1,92 @@
+package homematic
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/beevik/etree"
+)
+
+type Device struct {
+ Type string
+ Subtype string
+ Address string
+ Parent string
+ Version int
+}
+
+func (d Device) String() string {
+ return fmt.Sprintf(
+ "Homematic device\n"+
+ "TYPE: %s\n"+
+ "SUBTYPE: %s\n"+
+ "ADDRESS: %s\n"+
+ "PARENT: %s\n"+
+ "VERSION: %d",
+ d.Type,
+ d.Subtype,
+ d.Address,
+ d.Parent,
+ d.Version,
+ )
+}
+
+func (d *Device) LoadXML(doc *etree.Document) error {
+ types := doc.FindElements("/struct/member[name='TYPE']/value")
+ if len(types) != 1 {
+ return fmt.Errorf("Expected one type field but got %d.", len(types))
+ }
+
+ subtypes := doc.FindElements("/struct/member[name='SUBTYPE']/value")
+ if len(subtypes) != 1 {
+ return fmt.Errorf("Expected one subtype field but got %d.", len(subtypes))
+ }
+
+ addresses := doc.FindElements("/struct/member[name='ADDRESS']/value")
+ if len(addresses) != 1 {
+ return fmt.Errorf("Expected one address field but got %d.", len(addresses))
+ }
+
+ parents := doc.FindElements("/struct/member[name='PARENT']/value")
+ if len(parents) != 1 {
+ return fmt.Errorf("Expected one parent field but got %d.", len(parents))
+ }
+
+ versions := doc.FindElements("/struct/member[name='VERSION']/value/i4")
+ if len(versions) != 1 {
+ return fmt.Errorf("Expected one version field but got %d.", len(versions))
+ }
+
+ version, err := strconv.Atoi(versions[0].Text())
+ if err != nil {
+ return fmt.Errorf(
+ "Cannot convert version value '%s' to an integer: %w",
+ versions[0].Text(),
+ err,
+ )
+ }
+
+ d.Type = types[0].Text()
+ d.Subtype = subtypes[0].Text()
+ d.Address = addresses[0].Text()
+ d.Parent = parents[0].Text()
+ d.Version = version
+
+ return nil
+}
+
+type Devices []Device
+
+func (devices Devices) String() string {
+ value := ""
+
+ for index, device := range devices {
+ if index > 0 {
+ value = value + "\n"
+ }
+
+ value = value + fmt.Sprintf("%v\n", device)
+ }
+
+ return value
+}
diff --git a/vendor/xengineering.eu/homematic-go/homematic/meson.build b/vendor/xengineering.eu/homematic-go/homematic/meson.build
new file mode 100644
index 0000000..5a35437
--- /dev/null
+++ b/vendor/xengineering.eu/homematic-go/homematic/meson.build
@@ -0,0 +1,4 @@
+homematic = [
+ meson.current_source_dir() / 'xmlrpc.go',
+ meson.current_source_dir() / 'device.go',
+]
diff --git a/vendor/xengineering.eu/homematic-go/homematic/xmlrpc.go b/vendor/xengineering.eu/homematic-go/homematic/xmlrpc.go
new file mode 100644
index 0000000..e6d44af
--- /dev/null
+++ b/vendor/xengineering.eu/homematic-go/homematic/xmlrpc.go
@@ -0,0 +1,147 @@
+package homematic
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "strconv"
+
+ "github.com/beevik/etree"
+)
+
+type Requester struct {
+ addr string
+}
+
+func NewRequester(addr string) Requester {
+ return Requester{
+ addr: addr,
+ }
+}
+
+func (req Requester) Request(request etree.Document) (*etree.Document, error) {
+ r, w := io.Pipe()
+
+ go func() {
+ request.Indent(2)
+ _, err := request.WriteTo(w)
+ _ = w.CloseWithError(err)
+ }()
+
+ response, err := http.Post(req.addr, "application/xml", r)
+ if err != nil {
+ return nil, fmt.Errorf("HTTP POST failed: %w", err)
+ }
+
+ doc := etree.NewDocument()
+ _, err = doc.ReadFrom(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to decode response: %w", err)
+ }
+
+ return doc, nil
+}
+
+func (req Requester) ListDevices() (Devices, error) {
+ var devices Devices
+
+ request := etree.NewDocument()
+ request.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
+ call := request.CreateElement("methodCall")
+ call.CreateElement("methodName").CreateText("listDevices")
+ call.CreateElement("params")
+
+ response, err := req.Request(*request)
+ if err != nil {
+ return devices, fmt.Errorf("Could not request listDevices: %w", err)
+ }
+
+ for _, dev := range response.FindElements("/methodResponse/params/param/value/array/data/value/struct") {
+ doc := etree.NewDocumentWithRoot(dev.Copy())
+
+ device := Device{}
+ device.LoadXML(doc)
+
+ devices = append(devices, device)
+ }
+
+ return devices, nil
+}
+
+func (req Requester) GetValue(address string) (bool, error) {
+ request := etree.NewDocument()
+ request.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
+ methodCall := request.CreateElement("methodCall")
+ methodName := methodCall.CreateElement("methodName")
+ methodName.CreateText("getValue")
+ params := methodCall.CreateElement("params")
+ params.CreateElement("param").CreateElement("value").CreateElement("string").CreateText(address)
+ params.CreateElement("param").CreateElement("value").CreateElement("string").CreateText("STATE")
+
+ response, err := req.Request(*request)
+ if err != nil {
+ return false, fmt.Errorf("Could not request getValue: %w", err)
+ }
+
+ value := response.FindElement("/methodResponse/params/param/value/i4")
+ if value == nil {
+ return false, fmt.Errorf("Could not get value from response")
+ }
+
+ integer, err := strconv.Atoi(value.Text())
+ if err != nil {
+ return false, fmt.Errorf("Could not convert state value to int: %w", err)
+ }
+
+ switch integer {
+ case 0:
+ return false, nil
+ case 1:
+ return true, nil
+ default:
+ return false, fmt.Errorf("Cannot cast integer %d to bool", integer)
+ }
+}
+
+type Responder struct {
+ ip net.IP
+ port int
+ handler func(http.ResponseWriter, *http.Request)
+}
+
+func NewResponder(ip net.IP, port int, handler func(http.ResponseWriter, *http.Request)) Responder {
+ return Responder{
+ ip: ip,
+ port: port,
+ handler: handler,
+ }
+}
+
+func (resp Responder) Start() (context.CancelFunc, int, error) {
+ listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", resp.ip, resp.port))
+ if err != nil {
+ return nil, 0, fmt.Errorf("Could not listen: %w", err)
+ }
+
+ port := listener.Addr().(*net.TCPAddr).Port
+
+ m := http.NewServeMux()
+ m.HandleFunc("/", resp.handler)
+
+ s := http.Server{
+ Handler: m,
+ }
+ go s.Serve(listener)
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ go func() {
+ <-ctx.Done()
+ s.Close()
+ listener.Close()
+ }()
+
+ return cancel, port, nil
+}