From 4ff389813f6aea719304f51792d34833a6e0ce18 Mon Sep 17 00:00:00 2001 From: jweigele Date: Mon, 29 Jan 2024 21:49:50 -0800 Subject: [PATCH] Add in RGBMatterRelay for dealing with matter lights * 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 | 5 +- lights/main.go | 160 +++++++++++++++------- mattersocket/mattersocket.go | 253 +++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+), 54 deletions(-) create mode 100644 mattersocket/mattersocket.go diff --git a/go.mod b/go.mod index fe6da22..effe086 100644 --- 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 ) diff --git a/lights/main.go b/lights/main.go index 06ef125..acda210 100644 --- a/lights/main.go +++ b/lights/main.go @@ -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 index 0000000..63dc817 --- /dev/null +++ b/mattersocket/mattersocket.go @@ -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 + +} -- 2.30.2