github.com/martinohmann/rfoutlet@v1.2.1-0.20220707195255-8a66aa411105/internal/controller/controller.go (about)

     1  // Package controller provides a controller that handles all outlet commands
     2  // sequentially.
     3  package controller
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  
     9  	"github.com/martinohmann/rfoutlet/internal/command"
    10  	"github.com/martinohmann/rfoutlet/internal/outlet"
    11  	"github.com/sirupsen/logrus"
    12  )
    13  
    14  var log = logrus.WithField("component", "controller")
    15  
    16  // Broadcaster can broadcast messages to all connected clients.
    17  type Broadcaster interface {
    18  	// Broadcast broadcasts msg to all connected clients.
    19  	Broadcast(msg []byte)
    20  }
    21  
    22  // Controller controls the outlets registered to the registry.
    23  type Controller struct {
    24  	// Registry contains all known outlets and outlet groups.
    25  	Registry *outlet.Registry
    26  	// Switcher switches outlets on or off based on commands from the
    27  	// CommandQueue.
    28  	Switcher outlet.Switcher
    29  	// Broadcaster broadcasts state updates to all connected clients.
    30  	Broadcaster Broadcaster
    31  	// CommandQueue is consumed sequentially by the controller. The commands
    32  	// may cause outlet and group state changes which are communicated back to
    33  	// one or more connected clients.
    34  	CommandQueue <-chan command.Command
    35  }
    36  
    37  // Run runs the main control loop until stopCh is closed.
    38  func (c *Controller) Run(stopCh <-chan struct{}) {
    39  	for {
    40  		select {
    41  		case cmd, ok := <-c.CommandQueue:
    42  			if !ok {
    43  				log.Error("command queue was closed unexpectedly, shutting down controller")
    44  				return
    45  			}
    46  
    47  			err := c.handleCommand(cmd)
    48  			if err != nil {
    49  				log.WithField("command", fmt.Sprintf("%T", cmd)).
    50  					Errorf("error handling command: %v", err)
    51  			}
    52  		case <-stopCh:
    53  			log.Info("shutting down controller")
    54  			return
    55  		}
    56  	}
    57  }
    58  
    59  // commandContext creates a new command.Context.
    60  func (c *Controller) commandContext() command.Context {
    61  	return command.Context{
    62  		Registry: c.Registry,
    63  		Switcher: c.Switcher,
    64  	}
    65  }
    66  
    67  // handleCommand executes cmd and may trigger broadcasts of state changes back
    68  // to the connected clients.
    69  func (c *Controller) handleCommand(cmd command.Command) error {
    70  	log.WithField("command", fmt.Sprintf("%T", cmd)).
    71  		Debug("handling command")
    72  
    73  	ctx := c.commandContext()
    74  
    75  	broadcast, err := cmd.Execute(ctx)
    76  	if err != nil || !broadcast {
    77  		return err
    78  	}
    79  
    80  	return c.broadcastState()
    81  }
    82  
    83  // broadcastState broadcasts the current outlet group state back to connected
    84  // clients.
    85  func (c *Controller) broadcastState() error {
    86  	msg, err := json.Marshal(c.Registry.GetGroups())
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	c.Broadcaster.Broadcast(msg)
    92  
    93  	return nil
    94  }