github.com/martinohmann/rfoutlet@v1.2.1-0.20220707195255-8a66aa411105/cmd/serve.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "os" 8 "os/signal" 9 "syscall" 10 "time" 11 12 "github.com/gin-contrib/cors" 13 "github.com/gin-gonic/gin" 14 "github.com/gobuffalo/packr" 15 "github.com/imdario/mergo" 16 "github.com/martinohmann/rfoutlet/internal/command" 17 "github.com/martinohmann/rfoutlet/internal/config" 18 "github.com/martinohmann/rfoutlet/internal/controller" 19 "github.com/martinohmann/rfoutlet/internal/outlet" 20 "github.com/martinohmann/rfoutlet/internal/statedrift" 21 "github.com/martinohmann/rfoutlet/internal/timeswitch" 22 "github.com/martinohmann/rfoutlet/internal/websocket" 23 "github.com/martinohmann/rfoutlet/pkg/gpio" 24 log "github.com/sirupsen/logrus" 25 "github.com/spf13/cobra" 26 ) 27 28 const webDir = "../web/build" 29 30 func NewServeCommand() *cobra.Command { 31 options := &ServeOptions{ 32 ConfigFilename: "/etc/rfoutlet/config.yml", 33 } 34 35 cmd := &cobra.Command{ 36 Use: "serve", 37 Short: "Serve the frontend for controlling outlets", 38 Long: "The serve command starts a server which serves the frontend and connects clients through websockets for controlling outlets via web interface.", 39 RunE: func(cmd *cobra.Command, _ []string) error { 40 return options.Run(cmd) 41 }, 42 } 43 44 options.AddFlags(cmd) 45 46 return cmd 47 } 48 49 type ServeOptions struct { 50 config.Config 51 ConfigFilename string 52 } 53 54 func (o *ServeOptions) AddFlags(cmd *cobra.Command) { 55 cmd.Flags().StringVar(&o.ConfigFilename, "config", o.ConfigFilename, "path to the outlet config file") 56 cmd.Flags().StringVar(&o.StateFile, "state-file", o.StateFile, "path to the file where outlet state and schedule should be stored") 57 cmd.Flags().StringVar(&o.ListenAddress, "listen-address", o.ListenAddress, "address to serve the web app on") 58 cmd.Flags().BoolVar(&o.DetectStateDrift, "detect-state-drift", o.DetectStateDrift, "detect state drift (e.g. if an outlet was switched via the phyical remote instead of rfoutlet)") 59 cmd.Flags().UintVar(&o.GPIO.TransmitPin, "transmit-pin", o.GPIO.TransmitPin, "gpio pin to transmit rf codes on") 60 cmd.Flags().UintVar(&o.GPIO.ReceivePin, "receive-pin", o.GPIO.ReceivePin, "gpio pin to receive rf codes on (this is used by the state drift detector)") 61 cmd.Flags().IntVar(&o.GPIO.TransmissionCount, "transmission-count", o.GPIO.TransmissionCount, "number of times a code should be transmitted in a row. The higher the value, the more likely it is that an outlet actually received the code") 62 } 63 64 func (o *ServeOptions) Run(cmd *cobra.Command) error { 65 cfg, err := config.LoadWithDefaults(o.ConfigFilename) 66 if err != nil { 67 return fmt.Errorf("failed to load config: %v", err) 68 } 69 70 err = mergo.Merge(cfg, o.Config, mergo.WithOverride) 71 if err != nil { 72 return fmt.Errorf("failed to merge config values: %v", err) 73 } 74 75 log.Debugf("merged config values: %#v", cfg) 76 77 registry := outlet.NewRegistry() 78 79 err = registry.RegisterGroups(cfg.BuildOutletGroups()...) 80 if err != nil { 81 return fmt.Errorf("failed to register outlet groups: %v", err) 82 } 83 84 device, err := openGPIODevice(cmd) 85 if err != nil { 86 return err 87 } 88 defer device.Close() 89 90 transmitter, err := gpio.NewTransmitter(device.Chip, int(cfg.GPIO.TransmitPin), gpio.TransmissionCount(cfg.GPIO.TransmissionCount)) 91 if err != nil { 92 return fmt.Errorf("failed to create gpio transmitter: %v", err) 93 } 94 defer transmitter.Close() 95 96 if cfg.StateFile != "" { 97 log := log.WithField("stateFile", cfg.StateFile) 98 99 stateFile := outlet.NewStateFile(cfg.StateFile) 100 101 log.Debug("loading outlet states") 102 103 err := stateFile.ReadBack(registry.GetOutlets()) 104 if err != nil && !os.IsNotExist(err) { 105 return fmt.Errorf("failed to load outlet states: %v", err) 106 } 107 108 defer func() { 109 log.Info("saving outlet states") 110 111 err := stateFile.WriteOut(registry.GetOutlets()) 112 if err != nil { 113 log.Errorf("failed to save state: %v", err) 114 } 115 }() 116 } 117 118 ctx, cancel := context.WithCancel(context.Background()) 119 defer cancel() 120 121 stopCh := ctx.Done() 122 commandQueue := make(chan command.Command) 123 124 if cfg.DetectStateDrift { 125 receiver, err := gpio.NewReceiver(device.Chip, int(cfg.GPIO.ReceivePin)) 126 if err != nil { 127 return fmt.Errorf("failed to create gpio receiver: %v", err) 128 } 129 defer receiver.Close() 130 131 detector := statedrift.NewDetector(registry, receiver, commandQueue) 132 133 go detector.Run(stopCh) 134 } 135 136 hub := websocket.NewHub() 137 138 controller := controller.Controller{ 139 Registry: registry, 140 Switcher: outlet.NewSwitch(transmitter), 141 Broadcaster: hub, 142 CommandQueue: commandQueue, 143 } 144 145 timeSwitch := timeswitch.New(registry, commandQueue) 146 147 go handleSignals(cancel) 148 go controller.Run(stopCh) 149 go timeSwitch.Run(stopCh) 150 go hub.Run(stopCh) 151 152 router := setupRouter(hub, commandQueue) 153 154 return listenAndServe(stopCh, router, cfg.ListenAddress) 155 } 156 157 func setupRouter(hub *websocket.Hub, commandQueue chan<- command.Command) http.Handler { 158 r := gin.New() 159 r.Use(gin.Recovery(), gin.Logger(), cors.Default()) 160 r.GET("/", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/app") }) 161 r.GET("/ws", websocket.Handler(hub, commandQueue)) 162 r.GET("/healthz", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) 163 r.StaticFS("/app", packr.NewBox(webDir)) 164 165 return r 166 } 167 168 func listenAndServe(stopCh <-chan struct{}, handler http.Handler, addr string) error { 169 srv := &http.Server{ 170 Addr: addr, 171 Handler: handler, 172 } 173 174 go func() { 175 log.Infof("listening on %s", addr) 176 177 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 178 log.Fatalf("Listen: %s\n", err) 179 } 180 }() 181 182 <-stopCh 183 184 log.Info("shutting down server...") 185 186 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 187 defer cancel() 188 189 return srv.Shutdown(ctx) 190 } 191 192 func handleSignals(cancel func()) { 193 quit := make(chan os.Signal, 1) 194 signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) 195 sig := <-quit 196 log.WithField("signal", sig).Info("received signal, shutting down...") 197 cancel() 198 }