From: jweigele Date: Tue, 13 Dec 2022 03:24:04 +0000 (-0800) Subject: Initial testing of lights implementation. X-Git-Url: http://git.hexthepla.net/?a=commitdiff_plain;h=61a79ca6022d3f07be6fb1dc02cc745c0928f4f0;p=rabbit_go Initial testing of lights implementation. * 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 --- diff --git a/go.mod b/go.mod index c5bf4e1..4b6fe7c 100644 --- 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 index 0000000..308c588 --- /dev/null +++ b/lights/main.go @@ -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) + } +}