Fully implemented lights package and further cleaned up logging
authorjweigele <jweigele@local>
Tue, 13 Dec 2022 21:43:53 +0000 (13:43 -0800)
committerjweigele <jweigele@local>
Tue, 13 Dec 2022 21:43:53 +0000 (13:43 -0800)
 * Lights now has working subimplementations for pico and zig
 * Fixed a bug with switch expiration infinite looping
 * Interfaces are used to get the specific relay data... probably very
   messy but it's the smallest amount of function changes I could do
 * Logging is now bound per relay, to allow more specific output when
   we're debugging (i.e. friendlyname for those that support it, etc)
 * Prometheus is basic with no metrics expiration, and a simple one
   second update loop... but that's how it was before in python SO

Dockerfile.lights [new file with mode: 0644]
go.mod
helper/helper.go
lights/main.go
subbuilds

diff --git a/Dockerfile.lights b/Dockerfile.lights
new file mode 100644 (file)
index 0000000..1801d33
--- /dev/null
@@ -0,0 +1,21 @@
+# STEP 1 build executable binary
+FROM golang:alpine as builder
+# Install SSL ca certificates
+RUN apk update && apk add git && apk add ca-certificates
+# Create appuser
+RUN adduser -D -g '' appuser
+COPY . $GOPATH/src/mypackage/myapp/
+WORKDIR $GOPATH/src/mypackage/myapp/
+COPY ./go.mod ./
+#COPY ./go.sum ./
+RUN go mod download
+WORKDIR $GOPATH/src/mypackage/myapp/lights
+
+#get dependancies
+#RUN go get -d -v
+#build the binary
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/lights
+
+FROM alpine:edge
+COPY --from=builder /go/bin/lights /bin/lights
+ENTRYPOINT ["/bin/lights", "-config", "/conf/config-docker.json"]
diff --git a/go.mod b/go.mod
index 4b6fe7c473b7d179f6024211e6eaf009c33a7e72..1bbd00c5151d7ced4df73ed3c366b0cc4c207e9c 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
        github.com/lucasb-eyer/go-colorful v1.2.0
        github.com/prometheus/client_golang v1.14.0
        github.com/rabbitmq/amqp091-go v1.5.0
+       golang.org/x/exp v0.0.0-20221212164502-fae10dda9338
        gopkg.in/yaml.v2 v2.4.0
        k8s.io/klog/v2 v2.80.1
 )
index aa625c08a42c95412f045b234bc9e862f5620d2e..24527b443e4ef6377209cb0d1dd8852797d96438 100644 (file)
@@ -142,7 +142,7 @@ func Bind(routingKey string, rabbit *RabbitConfig) error {
                if err != nil {
                        return err
                }
-               logger.Info("new queue declared", "queue", rabbit.Queue)
+               logger.V(2).Info("new queue declared", "queue", rabbit.Queue)
        }
 
        // last two are block on binding and args
@@ -150,7 +150,7 @@ func Bind(routingKey string, rabbit *RabbitConfig) error {
        if err != nil {
                return err
        }
-       logger.Info("Bound queue to exchange", "queue", rabbit.Queue, "routingKey", routingKey)
+       logger.V(2).Info("Bound queue to exchange", "queue", rabbit.Queue, "routingKey", routingKey)
        return nil
 
 }
@@ -170,7 +170,7 @@ func StartConsuming(rabbit RabbitConfig) (<-chan amqp.Delivery, error) {
                false, // immediate (wait and block please)
                nil,   // args
        )
-       logger.Info("consuming on queue", "queue", rabbit.Queue)
+       logger.V(2).Info("consuming on queue", "queue", rabbit.Queue)
        return deliveries, err
 }
 
@@ -252,9 +252,10 @@ func SendData(rabbitData RabbitSend, rabbit RabbitConfig, verboseSend bool) erro
        if err != nil {
                return err
        }
-
        if verboseSend {
                logger.Info("Sent data to rabbitmq", "data", rabbitData.Data, "routingKey", routingKey)
+       } else {
+               logger.V(2).Info("Sent data to rabbitmq", "data", rabbitData.Data, "routingKey", routingKey)
        }
        return nil
 
index 308c588ee2d7a461a3c4a4f82fed12f3f73d17b6..8014a5df3de5491d00d4786dbfea0fcc32c9196f 100644 (file)
@@ -1,19 +1,29 @@
 package main
 
 import (
+       "encoding/json"
        "flag"
        "fmt"
        "io/ioutil"
        "math"
        "os"
+       "reflect"
+       "sync"
        "time"
 
+       // prometheus imports
+       "net/http"
+
+       "github.com/prometheus/client_golang/prometheus"
+       "github.com/prometheus/client_golang/prometheus/promauto"
+       "github.com/prometheus/client_golang/prometheus/promhttp"
+
        // color correction
        "github.com/lucasb-eyer/go-colorful"
+       "golang.org/x/exp/slices"
        "gopkg.in/yaml.v2"
 
        "github.com/go-logr/logr"
-       "github.com/rabbitmq/amqp091-go"
        "unpiege.net/rabbit_go.git/helper"
 )
 
@@ -21,6 +31,13 @@ var (
        configFilename     string
        yamlConfigFilename string
        logger             logr.Logger
+       lightGauge         = promauto.NewGaugeVec(
+               prometheus.GaugeOpts{
+                       Name: "light",
+                       Help: "Used to measure light status in the real world",
+               },
+               []string{"location"},
+       )
 )
 
 // structs
@@ -29,26 +46,121 @@ var (
 type RGBConfigYaml struct {
        RGBRelays []RGBRelay `yaml:"RGBRelays"`
        RGBZigs   []RGBZig   `yaml:"RGBZigs"`
+       RGBPicos  []RGBPico  `yaml:"RGBPicos"`
+       Switches  []Switch   `yaml:"Switches"`
 }
 
 type Switch struct {
-       state        bool
-       overrideTime time.Time
-       expiresAt    time.Time
-       parentRelay  *RGBRelay
+       State           bool
+       Location        string `yaml:"Location"`
+       FriendlyName    string `yaml:"FriendlyName"`
+       OverrideSeconds int    `yaml:"OverrideSeconds"`
+       ExpiresAt       time.Time
+       parentRelay     *RGBRelay
+}
+
+// Switch definitions
+func (curSwitch *Switch) getLocation() string {
+       return curSwitch.Location
 }
 
 func (curSwitch *Switch) isActive() bool {
-       return false
+       curTime := time.Now().UTC()
+       // this will default true when initialized
+       // thus, switch will start inactive like we want
+       if curTime.After(curSwitch.ExpiresAt) {
+               logger.V(5).Info("Switch is not active", "switch", curSwitch)
+               return false
+       }
+       logger.V(3).Info("Switch is active!", "switch", curSwitch)
+       return true
+}
+
+func (curSwitch *Switch) setExpired(bubbleUp bool) {
+       logger.Info("Override removed for switch", "switch", curSwitch)
+       curTime := time.Now().UTC()
+       curSwitch.ExpiresAt = curTime
+       // set every other switch (not including us to avoid the loop) at this location to expired
+       if curSwitch.parentRelay != nil && bubbleUp {
+               curSwitch.parentRelay.setSwitchesExpired(curSwitch)
+       }
+}
+
+func (curSwitch *Switch) setParent(relay *RGBRelay) {
+       curSwitch.parentRelay = relay
+}
+
+func (curSwitch *Switch) getRoutingKey() string {
+       // this is just how zigbee switches are setup now
+       return fmt.Sprintf("zigbee2mqtt.%s", curSwitch.FriendlyName)
+}
+
+func (curSwitch *Switch) setState(state bool) {
+       curTime := time.Now().UTC()
+       curSwitch.State = state
+       curSwitch.ExpiresAt = curTime.Add(time.Duration(curSwitch.OverrideSeconds) * time.Second)
+       logger.Info("Setting state for switch", "state", state, "switch", curSwitch)
+}
+
+func (curSwitch *Switch) parseState(data map[string]interface{}) {
+       logger.V(3).Info("Got parseState on switch", "switch", curSwitch, "data", data)
+       switchAction := data["action"]
+       // switch on switch, lol
+       switch switchAction {
+       case "on_press_release", "on_press":
+               curSwitch.setState(true)
+       case "off_press_release", "off_press":
+               curSwitch.setState(false)
+       case "down_press", "down_press_release":
+               curSwitch.setExpired(true)
+       default:
+               logger.Error(nil, "Unsupported switch state/data!", "data", data)
+       }
 }
 
+// interface defs for relays
+
 type IRGBRelay interface {
        Init(sendChannel chan helper.RabbitSend)
        setLastMotion()
-       addSwitch(newSwitch Switch)
+       addSwitch(newSwitch *Switch)
        setPWM(red, green, blue float64)
        getLocation() string
+       getMetricValue() float64
+}
+
+type IRGBFriendly interface {
+       IRGBRelay
+       getFriendlyName() string
+}
+
+type IRelayData interface {
+       getRelayData(red, green, blue float64) (map[string]interface{}, string)
+}
+
+type RelayData struct {
+       parent *RGBRelay
+}
+
+func (relayData RelayData) getRelayData(red, green, blue float64) (map[string]interface{}, string) {
+       relayData.parent.logger.V(3).Info("Entering getRelayData for relay")
+       retval := make(map[string]interface{}, 0)
+       if relayData.parent.LastState {
+               retval["red"] = red
+               retval["green"] = green
+               retval["blue"] = blue
+       } else {
+               retval["red"] = 0.0
+               retval["green"] = 0.0
+               retval["blue"] = 0.0
+       }
+       retval["location"] = relayData.parent.Location
+       relayData.parent.logger.V(2).Info("Final relay getRelayData output", "retval", retval)
+
+       return retval, "rgb_relay"
+
 }
+
 type RGBRelay struct {
        UpdateStaleness int    `yaml:"UpdateStaleness"`
        DimmingTimeout  int    `yaml:"DimmingTimeout"`
@@ -56,8 +168,10 @@ type RGBRelay struct {
        LastUpdate      time.Time
        LastMotion      time.Time
        LastState       bool
+       logger          logr.Logger
        sendChannel     chan helper.RabbitSend
-       switches        []Switch `yaml:"Switches"`
+       switches        []*Switch `yaml:"Switches"`
+       relayData       IRelayData
 }
 
 /*func NewRGBRelay(Location string, DimmingTimeout int, sendChannel chan helper.RabbitSend) *RGBRelay {
@@ -68,7 +182,9 @@ type RGBRelay struct {
 }*/
 
 func (relay *RGBRelay) Init(sendChannel chan helper.RabbitSend) {
+       relay.logger = logger.WithValues("relay", relay)
        relay.sendChannel = sendChannel
+       relay.relayData = RelayData{parent: relay}
        // initialize defaults, if they weren't set previously
        if relay.DimmingTimeout == 0 {
                relay.DimmingTimeout = 300
@@ -91,7 +207,7 @@ func (relay *RGBRelay) shouldOverride() bool {
 }
 
 // getMetricValue is used to set prometheus lights on/off state
-func (relay *RGBRelay) getMetricValue() int {
+func (relay *RGBRelay) getMetricValue() float64 {
        if relay.LastState {
                return 1
        } else {
@@ -140,56 +256,42 @@ func (relay *RGBRelay) getRoutingKey() string {
        return "rgb_relay"
 }
 
-func (relay *RGBRelay) getSetMap(red, green, blue float64) map[string]interface{} {
-       logger.Info("Entering getSetMap regular for relay", "relay", relay)
-       retval := make(map[string]interface{}, 0)
-       if relay.LastState {
-               retval["red"] = red
-               retval["green"] = green
-               retval["blue"] = blue
-       } else {
-               retval["red"] = 0.0
-               retval["green"] = 0.0
-               retval["blue"] = 0.0
+func (relay *RGBRelay) isSwitchActive() bool {
+       var retval bool
+       for _, curSwitch := range relay.switches {
+               if curSwitch.isActive() {
+                       retval = true
+               }
        }
-       retval["location"] = relay.Location
        return retval
 }
 
-func (relay *RGBRelay) isSwitchActive() bool {
-       return false
-}
-
 func (relay *RGBRelay) getSwitchOverride() bool {
        var override bool
        for _, curSwitch := range relay.switches {
                if curSwitch.isActive() {
-                       override = curSwitch.state
+                       override = curSwitch.State
                }
        }
        return override
 }
-func (relay *RGBRelay) addSwitch(newSwitch Switch) {
-       switchPresent := false
-       for _, curSwitch := range relay.switches {
-               if curSwitch == newSwitch {
-                       logger.Info("Tried to add a switch twice", "curSwitch", curSwitch, "newSwitch", newSwitch)
-                       switchPresent = true
-                       break
-               }
-
-       }
-       if !switchPresent {
-               logger.Info("Adding new switch to relay", "relay", relay, "switch", newSwitch)
+func (relay *RGBRelay) addSwitch(newSwitch *Switch) {
+       if slices.Contains(relay.switches, newSwitch) {
+               relay.logger.Error(nil, "Tried to add a switch twice", "switch", newSwitch)
+       } else {
+               relay.logger.V(1).Info("Adding new switch to relay", "switch", newSwitch)
                relay.switches = append(relay.switches, newSwitch)
+               newSwitch.setParent(relay)
        }
 }
 
-func (relay *RGBRelay) setSwitchesExpired(source Switch) {
+func (relay *RGBRelay) setSwitchesExpired(source *Switch) {
        // for every switch except the one that triggered the expiration..
+       relay.logger.V(2).Info("triggered expire switches", "source", source, "relay.switches", relay.switches)
        for _, curSwitch := range relay.switches {
+               // expire it
                if curSwitch != source {
-                       // TODO expire the switch without calling the parent
+                       curSwitch.setExpired(false)
                }
        }
 }
@@ -201,9 +303,9 @@ func (relay *RGBRelay) setPWM(red, green, blue float64) {
 }
 
 func (relay *RGBRelay) sendUpdate(red, green, blue float64) {
-       logger.V(3).Info("Sending update for relay", "relay", relay)
-       rgbData := relay.getSetMap(red, green, blue)
-       sendItem := helper.RabbitSend{Data: rgbData, RoutingKey: relay.getRoutingKey()}
+       relay.logger.V(3).Info("Sending update for relay")
+       rgbData, routingKey := relay.relayData.getRelayData(red, green, blue)
+       sendItem := helper.RabbitSend{Data: rgbData, RoutingKey: routingKey}
        relay.sendChannel <- sendItem
 }
 
@@ -223,27 +325,27 @@ func (relay *RGBRelay) shouldUpdate() bool {
        if LastState == true && desiredState == true {
                // last state on, desired on (aka already on)
                if relay.LastUpdateTooOld() {
-                       logger.Info("Last update too old, refreshing (on)", "relay", relay)
+                       relay.logger.V(2).Info("Last update too old, refreshing (on)")
                        relay.setLastUpdate()
                        retval = true
                }
                // otherwise nothing to change
        } else if LastState == false && desiredState == true {
                // last off, desired on (aka turn on)
-               logger.Info("Turning relay on", "relay", relay)
+               relay.logger.Info("Turning relay on")
                relay.LastState = true
                relay.setLastUpdate()
                retval = true
        } else if LastState == true && desiredState == false {
                // last state on, desired off (aka turn off)
-               logger.Info("Turning relay off", "relay", relay)
+               relay.logger.Info("Turning relay off")
                relay.LastState = false
                relay.setLastUpdate()
                retval = true
        } else if LastState == false && desiredState == false {
                // last state off, desired off (aka already off)
                if relay.LastUpdateTooOld() {
-                       logger.Info("Last update too old, refreshing (off)", "relay", relay)
+                       relay.logger.V(2).Info("Last update too old, refreshing (off)")
                        relay.setLastUpdate()
                        retval = true
                }
@@ -259,61 +361,33 @@ func (relay *RGBRelay) shouldUpdate() bool {
 }
 
 // RGBZig Defs
-type RGBZig struct {
-       RGBRelay     `yaml:"RGBRelay"`
-       FriendlyName string `yaml:"FriendlyName"`
-       Sengled      bool   `yaml:"Sengled"`
-}
-
-/*func NewRGBZig(Location string, DimmingTimeout int, sendChannel chan helper.RabbitSend, FriendlyName string, Sengled bool) *RGBZig {
-       retval := RGBZig{}
-       retval.RGBRelay = NewRGBRelay(Location, DimmingTimeout, sendChannel)
-       retval.FriendlyName = FriendlyName
-       retval.Sengled = Sengled
-       return &retval
-}*/
-
-// RGBZig overrides
-func (zig *RGBZig) getRoutingKey() string {
-       return fmt.Sprintf("zigbee2mqtt.%s.set", zig.FriendlyName)
-}
-
-func (zig *RGBZig) setPWM(red, green, blue float64) {
-       if zig.shouldUpdate() {
-               zig.sendUpdate(red, green, blue)
-       }
+type ZigRelayData struct {
+       parent *RGBZig
 }
 
-func (zig *RGBZig) sendUpdate(red, green, blue float64) {
-       logger.V(3).Info("Sending update for zig", "zig", zig)
-       rgbData := zig.getSetMap(red, green, blue)
-       sendItem := helper.RabbitSend{Data: rgbData, RoutingKey: zig.getRoutingKey()}
-       zig.sendChannel <- sendItem
-}
-
-func (zig *RGBZig) getSetMap(red, green, blue float64) map[string]interface{} {
-       logger.Info("Entering getSetMap regular for zig", "zig", zig)
+func (relayData ZigRelayData) getRelayData(red, green, blue float64) (map[string]interface{}, string) {
+       relayData.parent.logger.V(3).Info("Entering getRelayData for zig")
        retval := make(map[string]interface{}, 0)
        // we should be turned on
-       if zig.LastState {
+       if relayData.parent.LastState {
                // correct for different sengled colors
-               if zig.Sengled {
+               if relayData.parent.Sengled {
                        red *= 1.5
                        blue *= 0.75
                        green *= 3.0
                }
                // gamma correct intermediate values
-               red = zig.gammaCorrect(red)
-               green = zig.gammaCorrect(green)
-               blue = zig.gammaCorrect(blue)
+               red = relayData.parent.gammaCorrect(red)
+               green = relayData.parent.gammaCorrect(green)
+               blue = relayData.parent.gammaCorrect(blue)
 
                // color conversions
                c := colorful.Color{R: red, G: green, B: blue}
                h, s, v := c.Hsv()
 
                // again, sengled changes
-               if zig.Sengled {
-                       logger.Info("Boosting saturation", "pre", s, "post", s+0.2)
+               if relayData.parent.Sengled {
+                       logger.V(4).Info("Boosting saturation", "pre", s, "post", s+0.2)
                        s += 0.2
                }
                newColor := colorful.Hsv(h, s, v)
@@ -331,58 +405,243 @@ func (zig *RGBZig) getSetMap(red, green, blue float64) map[string]interface{} {
                // we should be turned off
                retval["state"] = "OFF"
        }
-       logger.Info("Final zig getSetMap output", "retval", retval)
-       return retval
+       relayData.parent.logger.V(2).Info("Final zig getRelayData output", "retval", retval)
+       return retval, relayData.parent.getRoutingKey()
+
+}
+
+type RGBZig struct {
+       RGBRelay     `yaml:"RGBRelay"`
+       FriendlyName string `yaml:"FriendlyName"`
+       Sengled      bool   `yaml:"Sengled"`
+}
+
+func (zig *RGBZig) Init(sendChannel chan helper.RabbitSend) {
+       zig.RGBRelay.Init(sendChannel)
+       zig.logger = logger.WithValues("relay", zig)
+       zig.RGBRelay.relayData = ZigRelayData{parent: zig}
+}
+
+// RGBZig added function
+func (zig *RGBZig) getFriendlyName() string {
+       return zig.FriendlyName
+}
+
+// RGBZig overrides
+func (zig *RGBZig) getRoutingKey() string {
+       return fmt.Sprintf("zigbee2mqtt.%s.set", zig.FriendlyName)
+}
+
+// RGBPico defs
+type PicoRelayData struct {
+       parent *RGBPico
+}
+
+func (relayData PicoRelayData) getRelayData(red, green, blue float64) (map[string]interface{}, string) {
+       relayData.parent.logger.V(3).Info("Entering getRelayData for pico")
+       // need to do this in two stages so the json encodes properly
+       actionMap := make(map[string]interface{}, 0)
+       retval := make(map[string]interface{}, 0)
+
+       // turned on last
+       if relayData.parent.LastState {
+               // need to do this in two stages so the json encodes properly
+               actionMap["red"] = red
+               actionMap["green"] = green
+               actionMap["blue"] = blue
+               actionMap["red"] = 0.0
+               actionMap["green"] = 0.0
+               actionMap["blue"] = 0.0
+       }
+       var actionData []byte
+       actionData, err := json.Marshal(actionMap)
+       if err != nil {
+               logger.Error(err, "error found when trying to encode pico action data, just ignoring")
+       }
+       retval["action"] = string(actionData)
+       // should be fully constructed for final encoding by rabbit helper
+       relayData.parent.logger.V(2).Info("Final pico getRelayData output", "retval", retval)
+       return retval, relayData.parent.getRoutingKey()
+
+}
+
+type RGBPico struct {
+       RGBZig    `yaml:"RGBZig"`
+       relayData PicoRelayData
+}
+
+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}
 }
 
 type RGBManager struct {
-       relays []IRGBRelay
+       relays             []IRGBRelay
+       switches           []*Switch
+       friendlyToLocation map[string][]string
+       friendlyMutex      sync.Mutex
+       rabbitHelper       *helper.RabbitConfig
 }
 
 func (manager *RGBManager) addRelay(relay IRGBRelay) {
        manager.relays = append(manager.relays, relay)
+       logger.V(2).Info("total relays", "manager.relays", manager.relays)
+       manager.friendlyMutex.Lock()
+       defer manager.friendlyMutex.Unlock()
+       logger.V(4).Info("(re)building friendly to location map now")
+
+       manager.friendlyToLocation = make(map[string][]string, 0)
+       // build friendlyToLocation map, in order to update linked locations later
+       for _, relay := range manager.relays {
+               // check if we implement the interface giving friendlynames (which means we're an RGBZig or subclass)
+               curZig, ok := relay.(IRGBFriendly)
+               if ok {
+                       logger.V(4).Info("Found an RGBZig relay", "relay", curZig)
+                       _, isPresent := manager.friendlyToLocation[curZig.getFriendlyName()]
+                       if !isPresent {
+                               manager.friendlyToLocation[curZig.getFriendlyName()] = make([]string, 0)
+                       }
+                       if !slices.Contains(manager.friendlyToLocation[curZig.getFriendlyName()], curZig.getLocation()) {
+                               manager.friendlyToLocation[curZig.getFriendlyName()] = append(manager.friendlyToLocation[curZig.getFriendlyName()], curZig.getLocation())
+                       }
+               }
+       }
+
+       logger.V(4).Info("Final friendlyToLocation map", "map", manager.friendlyToLocation)
+
 }
 
-func (manager *RGBManager) init(rabbit helper.RabbitConfig) {
+func (manager *RGBManager) getLinkedLocations(sourceLocation string) []string {
+       // using a map as a set just to build all the friendly names we care about
+       uniqueFriendlyNames := make(map[string]int, 0)
+
+       for _, relay := range manager.relays {
+               // did we get a friendly name capable relay?
+               curZig, ok := relay.(IRGBFriendly)
+               if ok {
+                       if curZig.getLocation() == sourceLocation {
+                               uniqueFriendlyNames[curZig.getFriendlyName()] = 1
+                       }
+               }
+       }
+
+       // same thing here, for the retval
+       var retval []string
+       // all the keys stored above, aka all the unique friendly names
+       for friendlyName, _ := range uniqueFriendlyNames {
+               // all the values in the map, aka all locations for every unique friendly name (may overlap!)
+               for _, linkedLocation := range manager.friendlyToLocation[friendlyName] {
+                       if !slices.Contains(retval, linkedLocation) {
+                               retval = append(retval, linkedLocation)
+                       }
+               }
+       }
+
+       logger.V(3).Info("Linked locations (by friendly)", "sourceLocation", sourceLocation, "linkedLocations", retval)
+       return retval
+}
+
+func (manager *RGBManager) init(rabbit *helper.RabbitConfig) {
+       manager.rabbitHelper = rabbit
+
        // these are hardcoded mostly, the two places we know we'll get data
        bindTopics := []string{"timecolorshift.py", "motion"}
        for _, bindTopic := range bindTopics {
-               err := helper.Bind(bindTopic, &rabbit)
+               err := helper.Bind(bindTopic, manager.rabbitHelper)
                if err != nil {
                        logger.Error(err, "unable to bind successfully to exchange", "routingKey", "leds")
                        os.Exit(1)
                }
        }
 
-       deliveries, err := helper.StartConsuming(rabbit)
+}
+
+func (manager *RGBManager) addSwitch(curSwitch *Switch) {
+       // first, bind state updates for this switch so we see them later
+       err := helper.Bind(curSwitch.getRoutingKey(), manager.rabbitHelper)
+       logger.V(2).Info("Bound rabbit to routingKey", "routingKey", curSwitch.getRoutingKey())
        if err != nil {
-               logger.Error(err, "unable to start consuming data from rabbit")
+               logger.Error(err, "unable to bind successfully to exchange", "routingKey", curSwitch.getRoutingKey())
                os.Exit(1)
        }
-       go manager.readLoop(deliveries)
+       // next, add to a list of all switches we monitor, so that we know where to route those state updates
+       manager.switches = append(manager.switches, curSwitch)
+
+       // finally, for all relays matching the location make sure to add the relevant switches
+       // huge caveat, needs to be done _after_ all relays are added to the manager for proper matching
+       for _, curRelay := range manager.relays {
+               if curRelay.getLocation() == curSwitch.getLocation() {
+                       curRelay.addSwitch(curSwitch)
+               }
+       }
+}
 
+func (manager *RGBManager) handleSwitch(data map[string]interface{}) {
+       logger.Info("Got switch data", "data", data)
 }
 
 func (manager *RGBManager) handleMotion(data map[string]interface{}) {
        logger.V(3).Info("Got motion data", "data", data)
-       for _, relay := range manager.relays {
-               if data["location"] == relay.getLocation() {
-                       logger.Info("Matching location for relay", "relay", relay, "data", data)
-                       relay.setLastMotion()
+       // only care to go further if it's moving, otherwise we'll just age out
+       if data["motion_detected"].(bool) {
+               linkedLocations := manager.getLinkedLocations(data["location"].(string))
+               for _, relay := range manager.relays {
+                       // matches one of the linked locations we got above
+                       if slices.Contains(linkedLocations, relay.getLocation()) {
+                               logger.V(3).Info("Matching location for relay", "relay", fmt.Sprintf("%v", reflect.ValueOf(relay)), "data", data)
+                               relay.setLastMotion()
+                       }
                }
        }
 }
 
 func (manager *RGBManager) handlePWM(data map[string]interface{}) {
        logger.V(2).Info("Got PWM data", "data", data)
+       var uniqueFriendlyNames = make(map[string]IRGBFriendly, 0)
        for _, relay := range manager.relays {
-               // float64 triplets should always hold as long as we control the sender
+               // did we get a friendly name capable relay?
+               curZig, ok := relay.(IRGBFriendly)
+               if ok {
+                       // set (or overwrite) the friendly name reference to this relay
+                       // essentially, get _only_ one relay per friendly name that we'll action
+                       uniqueFriendlyNames[curZig.getFriendlyName()] = curZig
+               } else {
+                       // immediately set the PWM for non-friendly name enabled relays (since we know there's no dupes)
+                       relay.setPWM(data["red"].(float64), data["green"].(float64), data["blue"].(float64))
+               }
+
+       }
+       // here, we don't care about the key, just the fact that we only get one relay out per
+       for _, relay := range uniqueFriendlyNames {
                relay.setPWM(data["red"].(float64), data["green"].(float64), data["blue"].(float64))
        }
 
 }
 
-func (manager *RGBManager) readLoop(deliveries <-chan amqp091.Delivery) {
+func (manager *RGBManager) updateMetrics() {
+       for {
+               // sleep for 1 second then update
+               time.Sleep(time.Duration(1) * time.Second)
+               for _, relay := range manager.relays {
+                       // did we get a friendly name capable relay?
+                       curZig, ok := relay.(IRGBFriendly)
+                       // this should give us a fairly unique value, location is friendlyname where enabled, otherwise location is location
+                       if ok {
+                               lightGauge.With(prometheus.Labels{"location": curZig.getFriendlyName()}).Set(curZig.getMetricValue())
+                       } else {
+                               lightGauge.With(prometheus.Labels{"location": relay.getLocation()}).Set(relay.getMetricValue())
+                       }
+               }
+       }
+}
+func (manager *RGBManager) readLoop() {
+       deliveries, err := helper.StartConsuming(*manager.rabbitHelper)
+       if err != nil {
+               logger.Error(err, "unable to start consuming data from rabbit")
+               os.Exit(1)
+       }
+
        //var counter int
        for delivery := range deliveries {
                logger.V(3).Info("got a delivery", "delivery", delivery)
@@ -393,12 +652,26 @@ func (manager *RGBManager) readLoop(deliveries <-chan amqp091.Delivery) {
                        continue
                }
                switch delivery.RoutingKey {
+               // first, large bucket categories
                case "motion":
                        manager.handleMotion(item)
                case "timecolorshift.py":
                        manager.handlePWM(item)
+               // now, individual routing keys (entirely switches if they match)
                default:
-                       logger.Error(nil, "no routing key match on delivery!", "routingKey", delivery.RoutingKey)
+                       var matches bool
+                       for _, curSwitch := range manager.switches {
+                               if delivery.RoutingKey == curSwitch.getRoutingKey() {
+                                       matches = true
+                                       logger.V(2).Info("Matching routing key for switch, handling", "routingKey", delivery.RoutingKey, "switch", curSwitch)
+                                       curSwitch.parseState(item)
+                               }
+
+                       }
+                       if !matches {
+                               logger.Error(nil, "no routing key match on delivery!", "routingKey", delivery.RoutingKey)
+                       }
+
                }
 
        }
@@ -408,10 +681,17 @@ func (manager *RGBManager) readLoop(deliveries <-chan amqp091.Delivery) {
 func sendLoop(channel chan helper.RabbitSend, rabbit helper.RabbitConfig) {
        for sendItem := range channel {
                //helper.SendData(sendItem, rabbit, true)
+               logger.V(3).Info("Sending item", "item", sendItem)
                helper.SendData(sendItem, rabbit, false)
        }
 }
 
+func promSetup() {
+       // prometheus setup
+       http.Handle("/metrics", promhttp.Handler())
+       http.ListenAndServe(":9098", nil)
+}
+
 func main() {
        // logging and flag initialization
        flag.StringVar(&configFilename, "config", "", "the config filename")
@@ -441,7 +721,7 @@ func main() {
                        logger.Error(err, "error parsing time config yaml file")
                        os.Exit(1)
                }
-               logger.Info("parsed config as follows", "relayDefs", relayDefs)
+               logger.V(1).Info("parsed config as follows", "relayDefs", relayDefs)
        }
 
        rabbit, err := helper.SetupRabbit(configFilename, "", "lights") // config file, default routing key, source
@@ -449,24 +729,43 @@ func main() {
                logger.Error(err, "unable to setup rabbit")
                os.Exit(1)
        }
-       logger.Info("rabbit", "rabbit", rabbit)
 
        channel := make(chan helper.RabbitSend)
        go sendLoop(channel, rabbit)
 
        manager := RGBManager{}
-       manager.init(rabbit)
+       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 _, relay := range relayDefs.RGBRelays {
-               relay.Init(channel)
-               manager.addRelay(&relay)
+       // we want to use the index value, because we're adding by pointer rather than value
+       for i, _ := range relayDefs.RGBZigs {
+               relayDefs.RGBZigs[i].Init(channel)
+               logger.V(1).Info("Adding relay to manager", "relay", relayDefs.RGBZigs[i])
+               manager.addRelay(&relayDefs.RGBZigs[i])
        }
 
-       for _, relay := range relayDefs.RGBZigs {
-               relay.Init(channel)
-               manager.addRelay(&relay)
+       for i, _ := range relayDefs.RGBPicos {
+               relayDefs.RGBPicos[i].Init(channel)
+               logger.V(1).Info("Adding relay to manager", "relay", relayDefs.RGBPicos[i])
+               manager.addRelay(&relayDefs.RGBPicos[i])
        }
 
+       for i, _ := range relayDefs.Switches {
+               //relayDefs.Switches[i].Init(channel)
+               logger.V(1).Info("Adding switch to manager", "switch", relayDefs.Switches[i])
+               manager.addSwitch(&relayDefs.Switches[i])
+       }
+
+       go manager.readLoop()
+       go promSetup()
+
+       go manager.updateMetrics()
+
        for {
                time.Sleep(time.Duration(5) * time.Second)
        }
index a259dfb0e40a9a68ef09b9840311ab5acf3ed9ec..ddc52972148c2f68950316225d7e5d1aeed21bfb 100644 (file)
--- a/subbuilds
+++ b/subbuilds
@@ -1 +1 @@
-export BUILDS="timecolorshift wunder reprocess"
+export BUILDS="lights timecolorshift wunder reprocess"