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 }