Browse Source

Keep track of individual hardware presence state as it changes

Douglas Thrift 1 year ago
parent
commit
90dc32ea8c
10 changed files with 239 additions and 12 deletions
  1. 1 0
      .gitignore
  2. 2 1
      README.md
  3. 8 4
      cmd/presence/main.go
  4. 8 0
      go.mod
  5. 11 0
      go.sum
  6. 6 1
      neighbors/arp.go
  7. 16 3
      neighbors/arp_freebsd.go
  8. 16 3
      neighbors/arp_linux.go
  9. 36 0
      state.go
  10. 135 0
      state_test.go

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 # Binaries for programs and plugins
+/presence
 *.exe
 *.exe~
 *.dll

+ 2 - 1
README.md

@@ -1,2 +1,3 @@
-# presence
+# Presence
+
 Home network presence detection daemon for IFTTT

+ 8 - 4
cmd/presence/main.go

@@ -5,14 +5,15 @@ import (
 	"log"
 	"os"
 
+	"douglasthrift.net/presence"
 	"douglasthrift.net/presence/neighbors"
 )
 
 func main() {
-	ifs := map[string]bool{os.Args[1]: true}
-	hws := make(map[string]bool, len(os.Args[2:]))
+	ifs := neighbors.Interfaces{os.Args[1]: true}
+	hws := make(neighbors.HardwareAddrStates, len(os.Args[2:]))
 	for _, hw := range os.Args[2:] {
-		hws[hw] = true
+		hws[hw] = presence.NewState()
 	}
 
 	ctx := context.Background()
@@ -25,5 +26,8 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
-	log.Println(ok)
+	log.Printf("present=%v", ok)
+	for hw, state := range hws {
+		log.Printf("%v present=%v changed=%v", hw, state.Present(), state.Changed())
+	}
 }

+ 8 - 0
go.mod

@@ -1,3 +1,11 @@
 module douglasthrift.net/presence
 
 go 1.17
+
+require github.com/stretchr/testify v1.7.2
+
+require (
+	github.com/davecgh/go-spew v1.1.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 11 - 0
go.sum

@@ -0,0 +1,11 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 6 - 1
neighbors/arp.go

@@ -2,10 +2,15 @@ package neighbors
 
 import (
 	"context"
+
+	"douglasthrift.net/presence"
 )
 
 type (
+	Interfaces         map[string]bool
+	HardwareAddrStates map[string]presence.State
+
 	ARP interface {
-		Present(ctx context.Context, ifs map[string]bool, addrs map[string]bool) (bool, error)
+		Present(ctx context.Context, ifs Interfaces, addrs HardwareAddrStates) (bool, error)
 	}
 )

+ 16 - 3
neighbors/arp_freebsd.go

@@ -46,7 +46,12 @@ func NewARP(count uint) (ARP, error) {
 	return &arp{cmd: cmd, arping: arping}, nil
 }
 
-func (a *arp) Present(ctx context.Context, ifs map[string]bool, hws map[string]bool) (ok bool, err error) {
+func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrStates) (present bool, err error) {
+	as := make(map[string]bool, len(hws))
+	for hw := range hws {
+		as[hw] = false
+	}
+
 	cmd := exec.CommandContext(ctx, a.cmd, "--libxo=json", "-an")
 	b, err := cmd.Output()
 	if err != nil {
@@ -73,14 +78,22 @@ func (a *arp) Present(ctx context.Context, ifs map[string]bool, hws map[string]b
 			}
 			hw := hwa.String()
 
-			if hws[hw] {
+			if _, ok := as[hw]; ok {
 				ok, err = a.arping.Ping(ctx, e.Interface, hw, e.IPAddress)
-				if ok || err != nil {
+				if err != nil {
 					return
 				}
+				as[hw] = ok
 			}
 		}
 	}
 
+	for hw, ok := range as {
+		hws[hw].Set(ok)
+		if ok {
+			present = true
+		}
+	}
+
 	return
 }

+ 16 - 3
neighbors/arp_linux.go

@@ -34,7 +34,12 @@ func NewARP(count uint) (ARP, error) {
 	return &arp{cmd: cmd, arping: arping}, nil
 }
 
-func (a *arp) Present(ctx context.Context, ifs map[string]bool, hws map[string]bool) (ok bool, err error) {
+func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrStates) (present bool, err error) {
+	as := make(map[string]bool, len(hws))
+	for hw := range hws {
+		as[hw] = false
+	}
+
 	cmd := exec.CommandContext(ctx, a.cmd, "-family", "inet", "-json", "neighbor", "show", "nud", "reachable")
 	b, err := cmd.Output()
 	if err != nil {
@@ -53,14 +58,22 @@ func (a *arp) Present(ctx context.Context, ifs map[string]bool, hws map[string]b
 			}
 			hw := hwa.String()
 
-			if hws[hw] {
+			if _, ok := as[hw]; ok {
 				ok, err = a.arping.Ping(ctx, e.Interface, hw, e.IPAddress)
-				if ok || err != nil {
+				if err != nil {
 					return
 				}
+				as[hw] = ok
 			}
 		}
 	}
 
+	for hw, ok := range as {
+		hws[hw].Set(ok)
+		if ok {
+			present = true
+		}
+	}
+
 	return
 }

+ 36 - 0
state.go

@@ -0,0 +1,36 @@
+package presence
+
+type (
+	State interface {
+		Present() bool
+		Changed() bool
+		Set(present bool)
+	}
+
+	state struct {
+		present, was, initial bool
+	}
+)
+
+func NewState() State {
+	return &state{initial: true}
+}
+
+func (s *state) Present() bool {
+	return s.present
+}
+
+func (s *state) Changed() bool {
+	return s.present != s.was
+}
+
+func (s *state) Set(present bool) {
+	if s.initial {
+		s.was = !present
+		s.present = present
+		s.initial = false
+	} else {
+		s.was = s.present
+		s.present = present
+	}
+}

+ 135 - 0
state_test.go

@@ -0,0 +1,135 @@
+package presence
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewState(t *testing.T) {
+	s := NewState()
+	assert.Equal(t, &state{present: false, was: false, initial: true}, s)
+}
+
+func TestState_Present(t *testing.T) {
+	cases := []struct {
+		name string
+		s    State
+		exp  bool
+	}{
+		{
+			name: "true",
+			s:    &state{present: true},
+			exp:  true,
+		},
+		{
+			name: "false",
+			s:    &state{present: false},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			assert.Equal(t, tc.exp, tc.s.Present())
+		})
+	}
+}
+
+func TestState_Changed(t *testing.T) {
+	cases := []struct {
+		name string
+		s    State
+		exp  bool
+	}{
+		{
+			name: "true to true",
+			s:    &state{present: true, was: true},
+			exp:  false,
+		},
+		{
+			name: "true to false",
+			s:    &state{present: false, was: true},
+			exp:  true,
+		},
+		{
+			name: "false to true",
+			s:    &state{present: true, was: false},
+			exp:  true,
+		},
+		{
+			name: "false to false",
+			s:    &state{present: false, was: false},
+			exp:  false,
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			assert.Equal(t, tc.exp, tc.s.Changed())
+		})
+	}
+}
+
+func TestState_Set(t *testing.T) {
+	cases := []struct {
+		name   string
+		s, exp State
+		p      bool
+	}{
+		{
+			name: "initial to true",
+			s:    &state{initial: true},
+			p:    true,
+			exp:  &state{present: true, was: false, initial: false},
+		},
+		{
+			name: "initial to false",
+			s:    &state{initial: true},
+			p:    false,
+			exp:  &state{present: false, was: true, initial: false},
+		},
+		{
+			name: "true to true",
+			s:    &state{present: true},
+			p:    true,
+			exp:  &state{present: true, was: true},
+		},
+		{
+			name: "true to false",
+			s:    &state{present: true},
+			p:    false,
+			exp:  &state{present: false, was: true},
+		},
+		{
+			name: "false to true",
+			s:    &state{present: false, was: true},
+			p:    true,
+			exp:  &state{present: true, was: false},
+		},
+		{
+			name: "false to false",
+			s:    &state{present: false, was: true},
+			p:    false,
+			exp:  &state{present: false, was: false},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			tc.s.Set(tc.p)
+			assert.Equal(t, tc.exp, tc.s)
+		})
+	}
+}