Initial testing of lights implementation.
authorjweigele <jweigele@local>
Tue, 13 Dec 2022 03:24:04 +0000 (19:24 -0800)
committerjweigele <jweigele@local>
Tue, 13 Dec 2022 03:24:04 +0000 (19:24 -0800)
 * Only does RGBRelay and RGBZig right now, still needs pico and
   switch overrides
 * Seems to emulate behavior alright, but not much testing done
 * Parsing very rudimentary with a yaml config, but better in the long
   run

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

diff --git a/go.mod b/go.mod
index c5bf4e122fc232879db9b59527daef84cc8d158c..4b6fe7c473b7d179f6024211e6eaf009c33a7e72 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require (
        github.com/PuerkitoBio/goquery v1.8.0
        github.com/go-logr/logr v1.2.3
        github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
+       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-20221211140036-ad323defaf05
        gopkg.in/yaml.v2 v2.4.0
        k8s.io/klog/v2 v2.80.1
 )
diff --git a/lights/main.go b/lights/main.go
new file mode 100644 (file)
index 0000000..308c588
--- /dev/null
@@ -0,0 +1,473 @@
+package main
+
+import (
+       "flag"
+       "fmt"
+       "io/ioutil"
+       "math"
+       "os"
+       "time"
+
+       // color correction
+       "github.com/lucasb-eyer/go-colorful"
+       "gopkg.in/yaml.v2"
+
+       "github.com/go-logr/logr"
+       "github.com/rabbitmq/amqp091-go"
+       "unpiege.net/rabbit_go.git/helper"
+)
+
+var (
+       configFilename     string
+       yamlConfigFilename string
+       logger             logr.Logger
+)
+
+// structs
+
+// config superstructure
+type RGBConfigYaml struct {
+       RGBRelays []RGBRelay `yaml:"RGBRelays"`
+       RGBZigs   []RGBZig   `yaml:"RGBZigs"`
+}
+
+type Switch struct {
+       state        bool
+       overrideTime time.Time
+       expiresAt    time.Time
+       parentRelay  *RGBRelay
+}
+
+func (curSwitch *Switch) isActive() bool {
+       return false
+}
+
+type IRGBRelay interface {
+       Init(sendChannel chan helper.RabbitSend)
+       setLastMotion()
+       addSwitch(newSwitch Switch)
+       setPWM(red, green, blue float64)
+       getLocation() string
+}
+type RGBRelay struct {
+       UpdateStaleness int    `yaml:"UpdateStaleness"`
+       DimmingTimeout  int    `yaml:"DimmingTimeout"`
+       Location        string `yaml:"Location"`
+       LastUpdate      time.Time
+       LastMotion      time.Time
+       LastState       bool
+       sendChannel     chan helper.RabbitSend
+       switches        []Switch `yaml:"Switches"`
+}
+
+/*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) {
+       relay.sendChannel = sendChannel
+       // initialize defaults, if they weren't set previously
+       if relay.DimmingTimeout == 0 {
+               relay.DimmingTimeout = 300
+       }
+       if relay.UpdateStaleness == 0 {
+               relay.UpdateStaleness = 10
+       }
+}
+
+func (relay *RGBRelay) getLocation() string {
+       return relay.Location
+}
+
+func (relay *RGBRelay) getMotionLocs() []string {
+       return []string{}
+}
+
+func (relay *RGBRelay) shouldOverride() bool {
+       return false
+}
+
+// getMetricValue is used to set prometheus lights on/off state
+func (relay *RGBRelay) getMetricValue() int {
+       if relay.LastState {
+               return 1
+       } else {
+               return 0
+       }
+}
+
+func (relay *RGBRelay) setLastUpdate() {
+       relay.LastUpdate = time.Now().UTC()
+       logger.V(3).Info("last update set", "LastUpdate", relay.LastUpdate)
+}
+
+func (relay *RGBRelay) setLastMotion() {
+       relay.LastMotion = time.Now().UTC()
+}
+
+func (relay *RGBRelay) LastUpdateTooOld() bool {
+       curTime := time.Now().UTC()
+       // default LastUpdate is year 1 so this will hold regardless
+       staleDuration := time.Duration(relay.UpdateStaleness) * time.Second
+       if relay.LastUpdate.Add(staleDuration).Before(curTime) {
+               return true
+       }
+       return false
+}
+
+func (relay *RGBRelay) shouldDim() bool {
+       curTime := time.Now().UTC()
+       dimmingDuration := time.Duration(relay.DimmingTimeout) * time.Second
+       if relay.LastMotion.Add(dimmingDuration).Before(curTime) {
+               return true
+       }
+       return false
+}
+
+func (relay *RGBRelay) gammaCorrect(inputVal float64) float64 {
+       if inputVal > 0.04045 {
+               inputVal = math.Pow(((inputVal + 0.055) / (1.0 + 0.055)), 2.4)
+       } else {
+               inputVal = inputVal / 12.92
+       }
+       return inputVal
+}
+
+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
+       }
+       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
+               }
+       }
+       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)
+               relay.switches = append(relay.switches, newSwitch)
+       }
+}
+
+func (relay *RGBRelay) setSwitchesExpired(source Switch) {
+       // for every switch except the one that triggered the expiration..
+       for _, curSwitch := range relay.switches {
+               if curSwitch != source {
+                       // TODO expire the switch without calling the parent
+               }
+       }
+}
+
+func (relay *RGBRelay) setPWM(red, green, blue float64) {
+       if relay.shouldUpdate() {
+               relay.sendUpdate(red, green, blue)
+       }
+}
+
+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.sendChannel <- sendItem
+}
+
+func (relay *RGBRelay) shouldUpdate() bool {
+       // start with no update needed
+       var retval bool = false
+       // get desired state, starting with the should dim value - i.e. was there recent motion
+       var desiredState = !relay.shouldDim()
+       // now, override for any switch that's active
+       if relay.isSwitchActive() {
+               desiredState = relay.getSwitchOverride()
+       }
+       // just for brevity and clarity
+       var LastState = relay.LastState
+
+       // should have the desired state here, now to do some conditionals
+       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.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.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.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.setLastUpdate()
+                       retval = true
+               }
+
+       } else {
+               // should never reach this point
+
+               logger.Error(nil, "Exhausted all light conditions", "LastState", LastState, "desiredState", desiredState)
+               os.Exit(1)
+       }
+       // we should know by now if we need to update or not
+       return retval
+}
+
+// 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)
+       }
+}
+
+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)
+       retval := make(map[string]interface{}, 0)
+       // we should be turned on
+       if zig.LastState {
+               // correct for different sengled colors
+               if zig.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)
+
+               // 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)
+                       s += 0.2
+               }
+               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)
+               colorMap := map[string]float64{"x": xyz_x, "y": xyz_y}
+
+               retval["state"] = "ON"
+               retval["color"] = colorMap
+
+       } else {
+               // we should be turned off
+               retval["state"] = "OFF"
+       }
+       logger.Info("Final zig getSetMap output", "retval", retval)
+       return retval
+}
+
+type RGBManager struct {
+       relays []IRGBRelay
+}
+
+func (manager *RGBManager) addRelay(relay IRGBRelay) {
+       manager.relays = append(manager.relays, relay)
+}
+
+func (manager *RGBManager) init(rabbit helper.RabbitConfig) {
+       // 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)
+               if err != nil {
+                       logger.Error(err, "unable to bind successfully to exchange", "routingKey", "leds")
+                       os.Exit(1)
+               }
+       }
+
+       deliveries, err := helper.StartConsuming(rabbit)
+       if err != nil {
+               logger.Error(err, "unable to start consuming data from rabbit")
+               os.Exit(1)
+       }
+       go manager.readLoop(deliveries)
+
+}
+
+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()
+               }
+       }
+}
+
+func (manager *RGBManager) handlePWM(data map[string]interface{}) {
+       logger.V(2).Info("Got PWM data", "data", data)
+       for _, relay := range manager.relays {
+               // float64 triplets should always hold as long as we control the sender
+               relay.setPWM(data["red"].(float64), data["green"].(float64), data["blue"].(float64))
+       }
+
+}
+
+func (manager *RGBManager) readLoop(deliveries <-chan amqp091.Delivery) {
+       //var counter int
+       for delivery := range deliveries {
+               logger.V(3).Info("got a delivery", "delivery", delivery)
+               item, err := helper.DecodeDelivery(delivery)
+               // it's just one delivery so we're going to yell and then continue along unabated
+               if err != nil {
+                       logger.Error(err, "unable to decode delivery", "delivery", delivery)
+                       continue
+               }
+               switch delivery.RoutingKey {
+               case "motion":
+                       manager.handleMotion(item)
+               case "timecolorshift.py":
+                       manager.handlePWM(item)
+               default:
+                       logger.Error(nil, "no routing key match on delivery!", "routingKey", delivery.RoutingKey)
+               }
+
+       }
+
+}
+
+func sendLoop(channel chan helper.RabbitSend, rabbit helper.RabbitConfig) {
+       for sendItem := range channel {
+               //helper.SendData(sendItem, rabbit, true)
+               helper.SendData(sendItem, rabbit, false)
+       }
+}
+
+func main() {
+       // logging and flag initialization
+       flag.StringVar(&configFilename, "config", "", "the config filename")
+       flag.StringVar(&yamlConfigFilename, "yamlConfig", "", "yaml config filename (containing relay and switch definitions)")
+
+       logger = helper.NewLogger()
+       flag.Parse()
+
+       if configFilename == "" {
+               logger.Error(fmt.Errorf("need to define config in order to load!"), "invalid config")
+               os.Exit(1)
+       }
+
+       var relayDefs RGBConfigYaml
+       if yamlConfigFilename == "" {
+               logger.Error(fmt.Errorf("need to define yaml config in order to load!"), "invalid config")
+               os.Exit(1)
+       } else {
+               logger.Info("Parsing provided timeConfigFilename", "yamlConfigFilename", yamlConfigFilename)
+               data, err := ioutil.ReadFile(yamlConfigFilename)
+               if err != nil {
+                       logger.Error(err, "error reading time config yaml file")
+                       os.Exit(1)
+               }
+               err = yaml.Unmarshal(data, &relayDefs)
+               if err != nil {
+                       logger.Error(err, "error parsing time config yaml file")
+                       os.Exit(1)
+               }
+               logger.Info("parsed config as follows", "relayDefs", relayDefs)
+       }
+
+       rabbit, err := helper.SetupRabbit(configFilename, "", "lights") // config file, default routing key, source
+       if err != nil {
+               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)
+
+       for _, relay := range relayDefs.RGBRelays {
+               relay.Init(channel)
+               manager.addRelay(&relay)
+       }
+
+       for _, relay := range relayDefs.RGBZigs {
+               relay.Init(channel)
+               manager.addRelay(&relay)
+       }
+
+       for {
+               time.Sleep(time.Duration(5) * time.Second)
+       }
+}