Add in RGBMatterRelay for dealing with matter lights
authorjweigele <jweigele@local>
Tue, 30 Jan 2024 05:49:50 +0000 (21:49 -0800)
committerjweigele <jweigele@local>
Tue, 30 Jan 2024 05:49:50 +0000 (21:49 -0800)
 * Required refactoring the interfaces pretty heavily, mainly RGBRelay
   -> RGBMQRelay being another layer
 * This also required fixing up the lights.yaml on consumption, because
   yaml unmarshalling requires all the various types to get things at
   different layers
 * There's a little wonkiness when it comes to relaydata, since I'm just
   using RGBRelayData over again and ignoring routing key on matter
 * No error checking besides an exit 1 when the websocket horks up for
   matter (ehhhhhhhhhhhh)

go.mod
lights/main.go
mattersocket/mattersocket.go [new file with mode: 0644]

diff --git a/go.mod b/go.mod
index fe6da22d7a4743a74e9ff30381b26cd74d446859..effe08622b614affaa3284caa3af28967e279881 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.18
 require (
        github.com/PuerkitoBio/goquery v1.8.0
        github.com/go-logr/logr v1.2.3
+       github.com/gorilla/websocket v1.5.1
        github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
        github.com/lucasb-eyer/go-colorful v1.2.0
        github.com/mrflynn/go-aqi v0.0.9
@@ -26,7 +27,7 @@ require (
        github.com/prometheus/common v0.37.0 // indirect
        github.com/prometheus/procfs v0.8.0 // indirect
        github.com/shopspring/decimal v1.2.0 // indirect
-       golang.org/x/net v0.3.0 // indirect
-       golang.org/x/sys v0.3.0 // indirect
+       golang.org/x/net v0.17.0 // indirect
+       golang.org/x/sys v0.13.0 // indirect
        google.golang.org/protobuf v1.28.1 // indirect
 )
index 06ef1254631fe9ec3da6c5f62286c7a8fd59df5d..acda210d2872b6472b12871e91823aea9e19e8f8 100644 (file)
@@ -28,6 +28,7 @@ import (
 
        "github.com/go-logr/logr"
        "unpiege.net/rabbit_go.git/helper"
+       "unpiege.net/rabbit_go.git/mattersocket"
 )
 
 var (
@@ -50,11 +51,12 @@ var (
 
 // config superstructure
 type RGBConfigYaml struct {
-       RGBRelays []RGBRelay `yaml:"RGBRelays"`
-       ESPRelays []ESPRelay `yaml:"ESPRelays"`
-       RGBZigs   []RGBZig   `yaml:"RGBZigs"`
-       RGBPicos  []RGBPico  `yaml:"RGBPicos"`
-       Switches  []Switch   `yaml:"Switches"`
+       RGBMQRelays     []RGBMQRelay     `yaml:"RGBMQRelays"`
+       RGBMatterRelays []RGBMatterRelay `yaml:"RGBMatterRelays"`
+       ESPRelays       []ESPRelay       `yaml:"ESPRelays"`
+       RGBZigs         []RGBZig         `yaml:"RGBZigs"`
+       RGBPicos        []RGBPico        `yaml:"RGBPicos"`
+       Switches        []Switch         `yaml:"Switches"`
 }
 
 type Switch struct {
@@ -177,7 +179,6 @@ func (curSwitch *Switch) parseState(data map[string]interface{}) {
 // interface defs for relays
 
 type IRGBRelay interface {
-       Init(sendChannel chan helper.RabbitSend)
        setLastMotion()
        addSwitch(newSwitch *Switch)
        setPWM(red, green, blue float64)
@@ -247,23 +248,14 @@ type RGBRelay struct {
        Dummy             bool `yaml:"Dummy"`
        NightOnly         bool `yaml:"NightOnly"`
        logger            logr.Logger
-       sendChannel       chan helper.RabbitSend
        switches          []*Switch `yaml:"Switches"`
        relayData         IRelayData
        startupFinishTime time.Time
        startupComplete   bool
 }
 
-/*func NewRGBRelay(Location string, DimmingTimeout int, sendChannel chan helper.RabbitSend) *RGBRelay {
-       retval := RGBRelay{Location: Location, DimmingTimeout: DimmingTimeout}
-       retval.sendChannel = sendChannel
-       retval.UpdateStaleness = 10
-       return &retval
-}*/
-
-func (relay *RGBRelay) Init(sendChannel chan helper.RabbitSend) {
+func (relay *RGBRelay) Init() {
        relay.logger = logger.WithValues("relay", relay)
-       relay.sendChannel = sendChannel
        // keep checking until we exceed this value, then we're ready to change some states
        relay.startupFinishTime = time.Now().UTC().Add(time.Duration(300) * time.Second)
        relay.relayData = RelayData{parent: relay}
@@ -421,30 +413,6 @@ func (relay *RGBRelay) setSwitchesExpired(source *Switch) {
        }
 }
 
-func (relay *RGBRelay) setPWM(red, green, blue float64) {
-       if relay.shouldUpdate() && !relay.Dummy {
-               relay.sendUpdate(red, green, blue)
-       }
-}
-
-func (relay *RGBRelay) sendUpdate(red, green, blue float64) {
-       relay.logger.V(3).Info("Sending update for relay")
-       // now, override for any switch that's active
-       for _, curSwitch := range relay.switches {
-               isActive, _ := curSwitch.getColor()
-               if isActive {
-                       red, green, blue = 1.0, 1.0, 1.0
-                       break
-               }
-       }
-
-       rgbData, routingKey := relay.relayData.getRelayData(red, green, blue)
-       relay.logger.V(2).Info("Sending data to rabbitmq", "Data", rgbData, "routingKey", routingKey)
-
-       sendItem := helper.RabbitSend{Data: rgbData, RoutingKey: routingKey, EmptySource: true}
-       relay.sendChannel <- sendItem
-}
-
 func (relay *RGBRelay) shouldUpdate() bool {
        // start with no update needed
        var retval bool = false
@@ -504,6 +472,84 @@ func (relay *RGBRelay) shouldUpdate() bool {
        return retval
 }
 
+type RGBMatterRelay struct {
+       RGBRelay     `yaml:"RGBRelay"`
+       matterSocket *mattersocket.MatterSocket
+       Serials      []string `yaml:"Serials"`
+}
+
+func (relay *RGBMatterRelay) Init(matterSocket *mattersocket.MatterSocket) {
+       relay.RGBRelay.Init()
+       relay.logger = logger.WithValues("relay", relay)
+       relay.matterSocket = matterSocket
+}
+
+func (relay *RGBMatterRelay) setPWM(red, green, blue float64) {
+       if relay.shouldUpdate() && !relay.Dummy {
+               relay.sendUpdate(red, green, blue)
+       }
+}
+
+func (relay *RGBMatterRelay) getDestination() string {
+       return fmt.Sprintf("Matter: %s", relay.Location)
+}
+
+func (relay *RGBMatterRelay) sendUpdate(red, green, blue float64) {
+       relay.logger.V(3).Info("Sending update for relay")
+       // now, override for any switch that's active
+       for _, curSwitch := range relay.switches {
+               isActive, _ := curSwitch.getColor()
+               if isActive {
+                       red, green, blue = 1.0, 1.0, 1.0
+                       break
+               }
+       }
+
+       // just reuse the same function, and ignore the routing key as the second return
+       rgbData, _ := relay.relayData.getRelayData(red, green, blue)
+
+       relay.logger.V(2).Info("Sending PWM data to matter", "Serials", relay.Serials,
+               "red", rgbData["red"], "green", rgbData["green"], "blue", rgbData["blue"])
+       for _, serial := range relay.Serials {
+               relay.matterSocket.NodePWM(serial, rgbData["red"].(float64), rgbData["green"].(float64), rgbData["blue"].(float64))
+       }
+}
+
+type RGBMQRelay struct {
+       RGBRelay    `yaml:"RGBRelay"`
+       sendChannel chan helper.RabbitSend
+}
+
+func (relay *RGBMQRelay) Init(sendChannel chan helper.RabbitSend) {
+       relay.RGBRelay.Init()
+       relay.logger = logger.WithValues("relay", relay)
+       relay.sendChannel = sendChannel
+}
+
+func (relay *RGBMQRelay) setPWM(red, green, blue float64) {
+       if relay.shouldUpdate() && !relay.Dummy {
+               relay.sendUpdate(red, green, blue)
+       }
+}
+
+func (relay *RGBMQRelay) sendUpdate(red, green, blue float64) {
+       relay.logger.V(3).Info("Sending update for relay")
+       // now, override for any switch that's active
+       for _, curSwitch := range relay.switches {
+               isActive, _ := curSwitch.getColor()
+               if isActive {
+                       red, green, blue = 1.0, 1.0, 1.0
+                       break
+               }
+       }
+
+       rgbData, routingKey := relay.relayData.getRelayData(red, green, blue)
+       relay.logger.V(2).Info("Sending data to rabbitmq", "Data", rgbData, "routingKey", routingKey)
+
+       sendItem := helper.RabbitSend{Data: rgbData, RoutingKey: routingKey, EmptySource: true}
+       relay.sendChannel <- sendItem
+}
+
 // RGBZig Defs
 type ZigRelayData struct {
        parent *RGBZig
@@ -557,15 +603,15 @@ func (relayData ZigRelayData) getRelayData(red, green, blue float64) (map[string
 }
 
 type RGBZig struct {
-       RGBRelay     `yaml:"RGBRelay"`
+       RGBMQRelay   `yaml:"RGBMQRelay"`
        FriendlyName string `yaml:"FriendlyName"`
        Sengled      bool   `yaml:"Sengled"`
 }
 
 func (zig *RGBZig) Init(sendChannel chan helper.RabbitSend) {
-       zig.RGBRelay.Init(sendChannel)
+       zig.RGBMQRelay.Init(sendChannel)
        zig.logger = logger.WithValues("relay", zig)
-       zig.RGBRelay.relayData = ZigRelayData{parent: zig}
+       zig.relayData = ZigRelayData{parent: zig}
 }
 
 // RGBZig added function
@@ -585,14 +631,14 @@ func (zig *RGBZig) getRoutingKey() string {
 
 // ESPRelay defs
 type ESPRelay struct {
-       RGBRelay `yaml:"RGBRelay"`
-       Topic    string `yaml:"Topic"`
+       RGBMQRelay `yaml:"RGBMQRelay"`
+       Topic      string `yaml:"Topic"`
 }
 
 func (relay *ESPRelay) Init(sendChannel chan helper.RabbitSend) {
-       relay.RGBRelay.Init(sendChannel)
+       relay.RGBMQRelay.Init(sendChannel)
        relay.logger = logger.WithValues("relay", relay)
-       relay.RGBRelay.relayData = ESPRelayData{parent: relay}
+       relay.relayData = ESPRelayData{parent: relay}
 }
 
 func (relay *ESPRelay) getRoutingKey() string {
@@ -653,7 +699,7 @@ type RGBPico struct {
 func (pico *RGBPico) Init(sendChannel chan helper.RabbitSend) {
        pico.RGBZig.Init(sendChannel)
        pico.logger = logger.WithValues("relay", pico)
-       pico.RGBZig.RGBRelay.relayData = PicoRelayData{parent: pico}
+       pico.relayData = PicoRelayData{parent: pico}
 }
 
 type RGBManager struct {
@@ -894,6 +940,10 @@ func main() {
                logger.V(1).Info("parsed config as follows", "relayDefs", relayDefs)
        }
 
+       // init mattersocket connection
+       matter := &mattersocket.MatterSocket{}
+       matter.Init()
+
        rabbit, err := helper.SetupRabbit(configFilename, "", "lights") // config file, default routing key, source
        if err != nil {
                logger.Error(err, "unable to setup rabbit")
@@ -907,10 +957,16 @@ func main() {
        manager := RGBManager{}
        manager.init(&rabbit)
 
-       for i, _ := range relayDefs.RGBRelays {
-               relayDefs.RGBRelays[i].Init(channel)
-               logger.V(1).Info("Adding relay to manager", "relay", relayDefs.RGBRelays[i])
-               manager.addRelay(&relayDefs.RGBRelays[i])
+       for i, _ := range relayDefs.RGBMQRelays {
+               relayDefs.RGBMQRelays[i].Init(channel)
+               logger.V(1).Info("Adding relay to manager", "relay", relayDefs.RGBMQRelays[i])
+               manager.addRelay(&relayDefs.RGBMQRelays[i])
+       }
+
+       for i, _ := range relayDefs.RGBMatterRelays {
+               relayDefs.RGBMatterRelays[i].Init(matter)
+               logger.V(1).Info("Adding relay to manager", "relay", relayDefs.RGBMatterRelays[i])
+               manager.addRelay(&relayDefs.RGBMatterRelays[i])
        }
 
        for i, _ := range relayDefs.ESPRelays {
diff --git a/mattersocket/mattersocket.go b/mattersocket/mattersocket.go
new file mode 100644 (file)
index 0000000..63dc817
--- /dev/null
@@ -0,0 +1,253 @@
+// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package mattersocket
+
+import (
+       "encoding/json"
+       "flag"
+       "fmt"
+       "log"
+       "math"
+       "net/url"
+       "os"
+       "os/signal"
+       "strconv"
+       "sync"
+
+       "github.com/go-logr/logr"
+       "github.com/gorilla/websocket"
+       "github.com/lucasb-eyer/go-colorful"
+       "k8s.io/klog/v2/klogr"
+       //logging things
+)
+
+var logger logr.Logger
+var sendMutex sync.Mutex
+
+func init() {
+       // our package logger
+       logger = klogr.New()
+}
+
+type MatterSocket struct {
+       Address       string
+       allNodes      map[string]string
+       writeChan     chan []byte
+       doneChan      chan struct{}
+       interruptChan chan os.Signal
+       conn          *websocket.Conn
+       messageID     int64
+}
+
+func (matter *MatterSocket) Init() {
+       matter.Address = "iotbridge:5580"
+       flag.Parse()
+       log.SetFlags(0)
+
+       matter.interruptChan = make(chan os.Signal, 1)
+       matter.doneChan = make(chan struct{})
+       matter.writeChan = make(chan []byte)
+
+       signal.Notify(matter.interruptChan, os.Interrupt)
+
+       u := url.URL{Scheme: "ws", Host: matter.Address, Path: "/ws"}
+       logger.Info("Connecting to url", "url", u.String())
+
+       conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
+       matter.conn = conn
+       if err != nil {
+               log.Fatal("dial:", err)
+       }
+
+       // get initial serial -> node id mapping data
+       matter.fetchNodes()
+
+       // these will loop until an interrupt is hit, then close and exit
+       go matter.ReadLoop()
+       go matter.WriteLoop()
+
+}
+
+func (matter *MatterSocket) WriteMap(data map[string]interface{}) {
+       // starts at 0, so first message sent out should be "1"
+       matter.messageID += 1
+       data["message_id"] = strconv.FormatInt(matter.messageID, 10)
+
+       jsonBytes, err := json.Marshal(data)
+       if err != nil {
+               logger.Error(err, "error encoding json")
+       }
+       matter.writeChan <- jsonBytes
+}
+
+func (matter *MatterSocket) fetchNodes() {
+       var sendData map[string]interface{} = make(map[string]interface{}, 0)
+       sendData["command"] = "get_nodes"
+       go matter.WriteMap(sendData)
+
+}
+
+func (matter *MatterSocket) ReadLoop() {
+       defer close(matter.doneChan)
+       for {
+               messageType, message, err := matter.conn.ReadMessage()
+               if err != nil {
+                       logger.Error(err, "error on read")
+                       os.Exit(1)
+               }
+               var dataObj map[string]interface{}
+               if messageType == websocket.TextMessage {
+                       err := json.Unmarshal(message, &dataObj)
+                       if err != nil {
+                               log.Printf("unable to parse json text for msg: %s", message)
+                       }
+               }
+               messageID, ok := dataObj["message_id"]
+               if ok {
+                       if messageID == "1" {
+                               matter.allNodes = matter.parseNodeData(dataObj)
+                               log.Printf("all nodes %+v\n", matter.allNodes)
+                       }
+               }
+       }
+}
+
+func (matter *MatterSocket) WriteLoop() {
+       for {
+               select {
+               case <-matter.doneChan:
+                       return
+               case msg := <-matter.writeChan:
+                       err := matter.conn.WriteMessage(websocket.TextMessage, msg)
+                       if err != nil {
+                               logger.Error(err, "write error")
+                               os.Exit(1)
+                       } else {
+                               logger.V(4).Info("wrote to websocket", "msg", msg)
+                       }
+               case <-matter.interruptChan:
+                       log.Println("interrupt")
+
+                       // Cleanly close the connection by sending a close message and then
+                       // waiting (with timeout) for the server to close the connection.
+                       err := matter.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+                       if err != nil {
+                               logger.Error(err, "write close")
+                               os.Exit(1)
+                       }
+                       select {
+                       case <-matter.doneChan:
+                       }
+                       return
+               }
+       }
+}
+
+func (matter *MatterSocket) NodePWM(serial string, red, green, blue float64) error {
+       sendData := make(map[string]interface{}, 0)
+       nodeID, ok := matter.allNodes[serial]
+       if !ok {
+               logger.Error(nil, "Unable to find node!", "serial", serial)
+               return fmt.Errorf("Unable to find node serial %s", serial)
+       }
+
+       // we have a node ID to send our request to!
+       // now to find out if we're turning off, or changing color
+       if red != 0 || green != 0 || blue != 0 {
+               // superfluous "on"
+               powerData := make(map[string]interface{}, 0)
+               powerArgs := make(map[string]interface{}, 0)
+               powerData["command"] = "device_command"
+               powerArgs["endpoint_id"] = 1
+               powerArgs["node_id"] = nodeID
+               powerArgs["cluster_id"] = 6
+               powerArgs["payload"] = make(map[string]string, 0)
+               powerArgs["command_name"] = "On"
+
+               powerData["args"] = powerArgs
+
+               matter.WriteMap(powerData)
+
+               // color conversions
+               c := colorful.Color{R: red, G: green, B: blue}
+               h, s, v := c.Hsv()
+
+               newColor := colorful.Hsv(h, s, v)
+               x, y, z := newColor.Xyz()
+
+               // finally, boil down to the X Y values only
+               xyz_x := x / (x + y + z)
+               xyz_y := y / (x + y + z)
+
+               args := make(map[string]interface{}, 0)
+               payload := make(map[string]interface{}, 0)
+
+               //payload["hue"] = int64(h * 255 / 360)
+               //payload["saturation"] = int64((math.Pow(2, 8)-1)*s - 1)
+               payload["colorX"] = int64(math.Pow(2, 16) * xyz_x)
+               payload["colorY"] = int64(math.Pow(2, 16) * xyz_y)
+               payload["transitionTime"] = 0
+               payload["optionsMask"] = 0
+               payload["optionsOverride"] = 0
+
+               //      args["command_name"] = "MoveToHueAndSaturation"
+               args["command_name"] = "MoveToColor"
+               args["endpoint_id"] = 1
+               args["node_id"] = nodeID
+               args["payload"] = payload
+               args["cluster_id"] = 768
+
+               sendData["command"] = "device_command"
+               sendData["args"] = args
+
+               logger.V(3).Info("Final PWM data", "sendData", sendData)
+               matter.WriteMap(sendData)
+
+       } else {
+               logger.V(2).Info(" need to turn off")
+               // superfluous "on"
+               powerData := make(map[string]interface{}, 0)
+               powerArgs := make(map[string]interface{}, 0)
+               powerData["command"] = "device_command"
+               powerArgs["endpoint_id"] = 1
+               powerArgs["node_id"] = nodeID
+               powerArgs["cluster_id"] = 6
+               powerArgs["payload"] = make(map[string]string, 0)
+               powerArgs["command_name"] = "Off"
+
+               powerData["args"] = powerArgs
+
+               logger.V(3).Info("Final power data", "powerData", powerData)
+               matter.WriteMap(powerData)
+       }
+
+       return nil
+}
+
+func (matter *MatterSocket) parseNodeData(data map[string]interface{}) map[string]string {
+       var retval map[string]string = make(map[string]string, 0)
+       result, resultOK := data["result"]
+       if resultOK {
+               resultArray, _ := result.([]interface{})
+               for _, node := range resultArray {
+                       nodeMap, _ := node.(map[string]interface{})
+                       nodeID, nodeOK := nodeMap["node_id"]
+                       if nodeOK {
+                               nodeFloat, _ := nodeID.(float64)
+                               attributes, attrOK := nodeMap["attributes"]
+                               if attrOK {
+                                       attribMap, _ := attributes.(map[string]interface{})
+                                       serial, serialOK := attribMap["0/40/15"]
+                                       if serialOK {
+                                               logger.V(2).Info("Found a node", "nodeID", nodeID, "serial", serial)
+                                               retval[serial.(string)] = strconv.FormatInt(int64(nodeFloat), 10)
+                                       }
+                               }
+                       }
+               }
+       }
+       return retval
+
+}