--- /dev/null
+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)
+ }
+}