github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/homekitwatcher/homekit.go (about)

     1  // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package homekitwatcher
     6  
     7  import (
     8  	"context"
     9  	"crypto/md5"
    10  	"fmt"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/brutella/hc"
    17  	"github.com/brutella/hc/accessory"
    18  	"github.com/choria-io/go-choria/aagent/model"
    19  	"github.com/choria-io/go-choria/aagent/util"
    20  	"github.com/choria-io/go-choria/aagent/watchers/event"
    21  	"github.com/choria-io/go-choria/aagent/watchers/watcher"
    22  	"golang.org/x/text/cases"
    23  	"golang.org/x/text/language"
    24  )
    25  
    26  type State int
    27  
    28  const (
    29  	Unknown State = iota
    30  	On
    31  	Off
    32  	// used to indicate that an external event - rpc or other watcher - initiated a transition
    33  	OnNoTransition
    34  	OffNoTransition
    35  
    36  	wtype   = "homekit"
    37  	version = "v1"
    38  )
    39  
    40  var stateNames = map[State]string{
    41  	Unknown: "unknown",
    42  	On:      "on",
    43  	Off:     "off",
    44  }
    45  
    46  type transport interface {
    47  	Stop() <-chan struct{}
    48  	Start()
    49  }
    50  
    51  type properties struct {
    52  	SerialNumber  string `mapstructure:"serial_number"`
    53  	Model         string
    54  	Pin           string
    55  	SetupId       string   `mapstructure:"setup_id"`
    56  	ShouldOn      []string `mapstructure:"on_when"`
    57  	ShouldOff     []string `mapstructure:"off_when"`
    58  	ShouldDisable []string `mapstructure:"disable_when"`
    59  	InitialState  State    `mapstructure:"-"`
    60  	Path          string   `mapstructure:"-"`
    61  	Initial       bool
    62  }
    63  
    64  type Watcher struct {
    65  	*watcher.Watcher
    66  
    67  	name        string
    68  	machine     model.Machine
    69  	previous    State
    70  	interval    time.Duration
    71  	hkt         transport
    72  	ac          *accessory.Switch
    73  	buttonPress chan State
    74  	started     bool
    75  	properties  *properties
    76  	mu          *sync.Mutex
    77  }
    78  
    79  func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, rawprop map[string]any) (any, error) {
    80  	var err error
    81  
    82  	hkw := &Watcher{
    83  		name:        name,
    84  		machine:     machine,
    85  		interval:    5 * time.Second,
    86  		buttonPress: make(chan State, 1),
    87  		mu:          &sync.Mutex{},
    88  		properties: &properties{
    89  			Model:         "Autonomous Agent",
    90  			ShouldOn:      []string{},
    91  			ShouldOff:     []string{},
    92  			ShouldDisable: []string{},
    93  			Path:          filepath.Join(machine.Directory(), wtype, fmt.Sprintf("%x", md5.Sum([]byte(name)))),
    94  		},
    95  	}
    96  
    97  	hkw.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	err = hkw.setProperties(rawprop)
   103  	if err != nil {
   104  		return nil, fmt.Errorf("could not set properties: %s", err)
   105  	}
   106  
   107  	return hkw, err
   108  }
   109  
   110  func (w *Watcher) Delete() {
   111  	w.mu.Lock()
   112  	defer w.mu.Unlock()
   113  
   114  	if w.hkt != nil {
   115  		<-w.hkt.Stop()
   116  	}
   117  }
   118  
   119  func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) {
   120  	defer wg.Done()
   121  
   122  	if w.ShouldWatch() {
   123  		w.ensureStarted()
   124  
   125  		w.Infof("homekit watcher for %s starting in state %s", w.name, stateNames[w.properties.InitialState])
   126  		switch w.properties.InitialState {
   127  		case On:
   128  			w.buttonPress <- On
   129  		case Off:
   130  			w.buttonPress <- Off
   131  		default:
   132  			w.Watcher.StateChangeC() <- struct{}{}
   133  		}
   134  	}
   135  
   136  	for {
   137  		select {
   138  		// button call backs
   139  		case e := <-w.buttonPress:
   140  			err := w.handleStateChange(e)
   141  			if err != nil {
   142  				w.Errorf("Could not handle button %s press: %v", stateNames[e], err)
   143  			}
   144  
   145  		// rpc initiated state changes would trigger this
   146  		case <-w.Watcher.StateChangeC():
   147  			mstate := w.machine.State()
   148  
   149  			switch {
   150  			case w.shouldBeOn(mstate):
   151  				w.ensureStarted()
   152  				w.buttonPress <- OnNoTransition
   153  
   154  			case w.shouldBeOff(mstate):
   155  				w.ensureStarted()
   156  				w.buttonPress <- OffNoTransition
   157  
   158  			case w.shouldBeDisabled(mstate):
   159  				w.ensureStopped()
   160  			}
   161  
   162  		case <-ctx.Done():
   163  			w.Infof("Stopping on context interrupt")
   164  			w.ensureStopped()
   165  			return
   166  		}
   167  	}
   168  }
   169  
   170  func (w *Watcher) ensureStopped() {
   171  	w.mu.Lock()
   172  	defer w.mu.Unlock()
   173  
   174  	if !w.started {
   175  		return
   176  	}
   177  
   178  	w.Infof("Stopping homekit integration")
   179  	<-w.hkt.Stop()
   180  	w.Infof("Homekit integration stopped")
   181  
   182  	w.started = false
   183  }
   184  
   185  func (w *Watcher) ensureStarted() {
   186  	w.mu.Lock()
   187  	defer w.mu.Unlock()
   188  
   189  	if w.started {
   190  		return
   191  	}
   192  
   193  	w.Infof("Starting homekit integration")
   194  
   195  	// kind of want to just hk.Start() here but stop kills a context that
   196  	// start does not recreate so we have to go back to start
   197  	err := w.startAccessoryUnlocked()
   198  	if err != nil {
   199  		w.Errorf("Could not start homekit service: %s", err)
   200  		return
   201  	}
   202  
   203  	go w.hkt.Start()
   204  
   205  	w.started = true
   206  }
   207  
   208  func (w *Watcher) shouldBeOff(s string) bool {
   209  	for _, state := range w.properties.ShouldOff {
   210  		if state == s {
   211  			return true
   212  		}
   213  	}
   214  
   215  	return false
   216  }
   217  
   218  func (w *Watcher) shouldBeOn(s string) bool {
   219  	for _, state := range w.properties.ShouldOn {
   220  		if state == s {
   221  			return true
   222  		}
   223  	}
   224  
   225  	return false
   226  }
   227  
   228  func (w *Watcher) shouldBeDisabled(s string) bool {
   229  	for _, state := range w.properties.ShouldDisable {
   230  		if state == s {
   231  			return true
   232  		}
   233  	}
   234  
   235  	return false
   236  }
   237  
   238  func (w *Watcher) handleStateChange(s State) error {
   239  	if !w.ShouldWatch() {
   240  		return nil
   241  	}
   242  
   243  	switch s {
   244  	case On:
   245  		w.setPreviousState(s)
   246  		w.NotifyWatcherState(w.CurrentState())
   247  		return w.SuccessTransition()
   248  
   249  	case OnNoTransition:
   250  		w.setPreviousState(On)
   251  		w.ac.Switch.On.SetValue(true)
   252  		w.NotifyWatcherState(w.CurrentState())
   253  		return nil
   254  
   255  	case Off:
   256  		w.setPreviousState(s)
   257  		w.NotifyWatcherState(w.CurrentState())
   258  		return w.FailureTransition()
   259  
   260  	case OffNoTransition:
   261  		w.setPreviousState(Off)
   262  		w.ac.Switch.On.SetValue(false)
   263  		w.machine.NotifyWatcherState(w.name, w.CurrentState())
   264  		return nil
   265  	}
   266  
   267  	return fmt.Errorf("invalid state change event: %s", stateNames[s])
   268  }
   269  
   270  func (w *Watcher) CurrentState() any {
   271  	w.mu.Lock()
   272  	defer w.mu.Unlock()
   273  
   274  	s := &StateNotification{
   275  		Event:           event.New(w.name, wtype, version, w.machine),
   276  		Path:            w.properties.Path,
   277  		PreviousOutcome: stateNames[w.previous],
   278  	}
   279  
   280  	return s
   281  }
   282  
   283  func (w *Watcher) setPreviousState(s State) {
   284  	w.mu.Lock()
   285  	defer w.mu.Unlock()
   286  
   287  	w.previous = s
   288  }
   289  
   290  func (w *Watcher) startAccessoryUnlocked() error {
   291  	info := accessory.Info{
   292  		Name:             cases.Title(language.AmericanEnglish).String(strings.Replace(w.name, "_", " ", -1)),
   293  		SerialNumber:     w.properties.SerialNumber,
   294  		Manufacturer:     "Choria",
   295  		Model:            w.properties.Model,
   296  		FirmwareRevision: w.machine.Version(),
   297  	}
   298  	w.ac = accessory.NewSwitch(info)
   299  
   300  	t, err := hc.NewIPTransport(hc.Config{Pin: w.properties.Pin, SetupId: w.properties.SetupId, StoragePath: w.properties.Path}, w.ac.Accessory)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	hc.OnTermination(func() {
   306  		<-t.Stop()
   307  	})
   308  
   309  	w.ac.Switch.On.OnValueRemoteUpdate(func(new bool) {
   310  		w.mu.Lock()
   311  		defer w.mu.Unlock()
   312  
   313  		w.Infof("Handling app button press: %v", new)
   314  
   315  		if !w.ShouldWatch() {
   316  			w.Infof("Ignoring event while in %s state", w.machine.State())
   317  			// undo the button press
   318  			w.ac.Switch.On.SetValue(!new)
   319  			return
   320  		}
   321  
   322  		if new {
   323  			w.Infof("Setting state to On")
   324  			w.buttonPress <- On
   325  		} else {
   326  			w.Infof("Setting state to Off")
   327  			w.buttonPress <- Off
   328  		}
   329  	})
   330  
   331  	w.ac.Switch.On.SetValue(w.previous == On)
   332  
   333  	w.hkt = t
   334  
   335  	return nil
   336  }
   337  
   338  func (w *Watcher) validate() error {
   339  	if len(w.properties.Pin) > 0 && len(w.properties.Pin) != 8 {
   340  		return fmt.Errorf("pin should be 8 characters long")
   341  	}
   342  
   343  	if len(w.properties.SetupId) > 0 && len(w.properties.SetupId) != 4 {
   344  		return fmt.Errorf("setup_id should be 4 characters long")
   345  	}
   346  
   347  	if w.properties.Path == "" {
   348  		return fmt.Errorf("machine path could not be determined")
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  func (w *Watcher) setProperties(props map[string]any) error {
   355  	if w.properties == nil {
   356  		w.properties = &properties{
   357  			ShouldDisable: []string{},
   358  			ShouldOff:     []string{},
   359  			ShouldOn:      []string{},
   360  		}
   361  	}
   362  
   363  	err := util.ParseMapStructure(props, w.properties)
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	_, set := props["initial"]
   369  	switch {
   370  	case !set:
   371  		w.properties.InitialState = Unknown
   372  	case w.properties.Initial:
   373  		w.properties.InitialState = On
   374  	default:
   375  		w.properties.InitialState = Off
   376  
   377  	}
   378  
   379  	return w.validate()
   380  }