Browse Source

Pass values (1, 2, 3) to IFTTT Maker Webhook

Douglas Thrift 2 days ago
parent
commit
007ed7d9d3

+ 1 - 1
.github/workflows/release.yml

@@ -20,6 +20,6 @@ jobs:
           go-version: stable
       - uses: goreleaser/goreleaser-action@v6
         with:
-          args: release --rm-dist
+          args: release --clean
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 12 - 1
.goreleaser.yaml

@@ -15,9 +15,20 @@ builds:
       - arm
       - arm64
       - riscv64
+    targets: # work around skipping FreeBSD RISC-V target
+      - freebsd_386_sse2
+      - freebsd_amd64_v1
+      - freebsd_arm_6
+      - freebsd_arm64_v8.0
+      - freebsd_riscv64_rva20u64
+      - linux_386_sse2
+      - linux_amd64_v1
+      - linux_arm_6
+      - linux_arm64_v8.0
+      - linux_riscv64_rva20u64
     main: ./cmd/presence
 archives:
-  - format: tar.xz
+  - formats: [tar.xz]
     wrap_in_directory: true
 checksum:
   name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.sha256"

+ 20 - 2
cmd/presence/detect.go

@@ -32,7 +32,16 @@ func (d *Detect) Run(cli *CLI) error {
 		log.Fatal(ctx, err, log.KV{K: "msg", V: "error finding dependencies"})
 	}
 
-	client, err := ifttt.NewClient(http.DefaultClient, config.IFTTT.BaseURL, config.IFTTT.Key, config.IFTTT.Events.Present, config.IFTTT.Events.Absent, cli.Debug)
+	client, err := ifttt.NewClient(http.DefaultClient, config.IFTTT.BaseURL, config.IFTTT.Key,
+		config.IFTTT.Events.Present.Event, config.IFTTT.Events.Absent.Event, ifttt.Values{
+			Value1: config.IFTTT.Events.Present.Value1,
+			Value2: config.IFTTT.Events.Present.Value2,
+			Value3: config.IFTTT.Events.Present.Value3,
+		}, ifttt.Values{
+			Value1: config.IFTTT.Events.Absent.Value1,
+			Value2: config.IFTTT.Events.Absent.Value2,
+			Value3: config.IFTTT.Events.Absent.Value3,
+		}, cli.Debug)
 	if err != nil {
 		log.Fatal(ctx, err, log.KV{K: "msg", V: "error creating IFTTT client"})
 	}
@@ -86,7 +95,16 @@ func (d *Detect) Run(cli *CLI) error {
 			config, err = presence.ParseConfigWithContext(ctx, cli.Config, wNet)
 			if err != nil {
 				log.Error(ctx, err, log.KV{K: "msg", V: "error parsing config"}, log.KV{K: "config", V: cli.Config})
-			} else if client, err = ifttt.NewClient(http.DefaultClient, config.IFTTT.BaseURL, config.IFTTT.Key, config.IFTTT.Events.Present, config.IFTTT.Events.Absent, cli.Debug); err != nil {
+			} else if client, err = ifttt.NewClient(http.DefaultClient, config.IFTTT.BaseURL, config.IFTTT.Key,
+				config.IFTTT.Events.Present.Event, config.IFTTT.Events.Absent.Event, ifttt.Values{
+					Value1: config.IFTTT.Events.Present.Value1,
+					Value2: config.IFTTT.Events.Present.Value2,
+					Value3: config.IFTTT.Events.Present.Value3,
+				}, ifttt.Values{
+					Value1: config.IFTTT.Events.Absent.Value1,
+					Value2: config.IFTTT.Events.Absent.Value2,
+					Value3: config.IFTTT.Events.Absent.Value3,
+				}, cli.Debug); err != nil {
 				log.Error(ctx, err, log.KV{K: "msg", V: "error creating IFTTT client"})
 			} else {
 				arp.Count(config.PingCount)

+ 10 - 0
cmd/presence/main.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"runtime"
+	"runtime/debug"
 
 	"github.com/alecthomas/kong"
 	"goa.design/clue/log"
@@ -29,6 +30,15 @@ var (
 	wNet    = wrap.NewNet()
 )
 
+func init() {
+	if version == "dev" {
+		info, ok := debug.ReadBuildInfo()
+		if ok {
+			version = info.Main.Version
+		}
+	}
+}
+
 func main() {
 	cli := &CLI{}
 	ctx := kong.Parse(

+ 27 - 14
config.go

@@ -32,8 +32,15 @@ type (
 	}
 
 	Events struct {
-		Present string `yaml:"present"`
-		Absent  string `yaml:"absent"`
+		Present Event `yaml:"present"`
+		Absent  Event `yaml:"absent"`
+	}
+
+	Event struct {
+		Event  string `yaml:"event"`
+		Value1 string `yaml:"value1"`
+		Value2 string `yaml:"value2"`
+		Value3 string `yaml:"value3"`
 	}
 )
 
@@ -56,7 +63,7 @@ func ParseConfigWithContext(ctx context.Context, name string, wNet wrap.Net) (*C
 	if err != nil {
 		return nil, err
 	}
-	defer f.Close()
+	defer func() { _ = f.Close() }()
 
 	d := yaml.NewDecoder(f)
 	d.KnownFields(true)
@@ -130,19 +137,25 @@ func ParseConfigWithContext(ctx context.Context, name string, wNet wrap.Net) (*C
 	}
 	log.Print(ctx, log.KV{K: "msg", V: "IFTTT key"}, log.KV{K: "value", V: strings.Repeat("*", len(c.IFTTT.Key))})
 
-	if c.IFTTT.Events.Present == "" {
-		c.IFTTT.Events.Present = defaultPresentEvent
-	} else if !eventName.MatchString(c.IFTTT.Events.Present) {
-		return nil, fmt.Errorf("invalid IFTTT present event name: %#v", c.IFTTT.Events.Present)
+	if c.IFTTT.Events.Present.Event == "" {
+		c.IFTTT.Events.Present.Event = defaultPresentEvent
+	} else if !eventName.MatchString(c.IFTTT.Events.Present.Event) {
+		return nil, fmt.Errorf("invalid IFTTT present event name: %#v", c.IFTTT.Events.Present.Event)
 	}
-	log.Print(ctx, log.KV{K: "msg", V: "IFTTT present event"}, log.KV{K: "value", V: c.IFTTT.Events.Present})
-
-	if c.IFTTT.Events.Absent == "" {
-		c.IFTTT.Events.Absent = defaultAbsentEvent
-	} else if !eventName.MatchString(c.IFTTT.Events.Absent) {
-		return nil, fmt.Errorf("invalid IFTTT absent event name: %#v", c.IFTTT.Events.Absent)
+	log.Print(ctx, log.KV{K: "msg", V: "IFTTT present event"}, log.KV{K: "value", V: c.IFTTT.Events.Present},
+		log.KV{K: "value1", V: c.IFTTT.Events.Present.Value1},
+		log.KV{K: "value2", V: c.IFTTT.Events.Present.Value2},
+		log.KV{K: "value3", V: c.IFTTT.Events.Present.Value3})
+
+	if c.IFTTT.Events.Absent.Event == "" {
+		c.IFTTT.Events.Absent.Event = defaultAbsentEvent
+	} else if !eventName.MatchString(c.IFTTT.Events.Absent.Event) {
+		return nil, fmt.Errorf("invalid IFTTT absent event name: %#v", c.IFTTT.Events.Absent.Event)
 	}
-	log.Print(ctx, log.KV{K: "msg", V: "IFTTT absent event"}, log.KV{K: "value", V: c.IFTTT.Events.Absent})
+	log.Print(ctx, log.KV{K: "msg", V: "IFTTT absent event"}, log.KV{K: "value", V: c.IFTTT.Events.Absent},
+		log.KV{K: "value1", V: c.IFTTT.Events.Absent.Value1},
+		log.KV{K: "value2", V: c.IFTTT.Events.Absent.Value2},
+		log.KV{K: "value3", V: c.IFTTT.Events.Absent.Value3})
 
 	return c, nil
 }

+ 14 - 4
config_test.go

@@ -41,8 +41,18 @@ func TestParseConfig(t *testing.T) {
 					BaseURL: "https://example.com",
 					Key:     "abcdef123456",
 					Events: Events{
-						Present: "event_presence_detected",
-						Absent:  "event_absence_detected",
+						Present: Event{
+							Event:  "event_presence_detected",
+							Value1: "event_presence_detected_value1",
+							Value2: "event_presence_detected_value2",
+							Value3: "event_presence_detected_value3",
+						},
+						Absent: Event{
+							Event:  "event_absence_detected",
+							Value1: "event_absence_detected_value1",
+							Value2: "event_absence_detected_value2",
+							Value3: "event_absence_detected_value3",
+						},
 					},
 				},
 			},
@@ -64,8 +74,8 @@ func TestParseConfig(t *testing.T) {
 					BaseURL: defaultBaseURL,
 					Key:     "xyz7890!@#",
 					Events: Events{
-						Present: defaultPresentEvent,
-						Absent:  defaultAbsentEvent,
+						Present: Event{Event: defaultPresentEvent},
+						Absent:  Event{Event: defaultAbsentEvent},
 					},
 				},
 			},

+ 5 - 2
detector.go

@@ -51,12 +51,15 @@ func (d *detector) Detect(ctx context.Context) error {
 
 	log.Print(ctx, log.KV{K: "msg", V: "detected presence"}, log.KV{K: "present", V: d.state.Present()}, log.KV{K: "changed", V: d.state.Changed()})
 	if d.state.Changed() {
-		event, err := d.client.Trigger(ctx, d.state.Present())
+		event, values, err := d.client.Trigger(ctx, d.state.Present())
 		if err != nil {
 			d.state.Reset()
 			return err
 		}
-		log.Print(ctx, log.KV{K: "msg", V: "triggered IFTTT"}, log.KV{K: "event", V: event})
+		log.Print(ctx, log.KV{K: "msg", V: "triggered IFTTT"}, log.KV{K: "event", V: event},
+			log.KV{K: "value1", V: values.Value1},
+			log.KV{K: "value2", V: values.Value2},
+			log.KV{K: "value3", V: values.Value3})
 	}
 
 	return nil

+ 7 - 2
go.mod

@@ -9,7 +9,7 @@ require (
 	github.com/magefile/mage v1.15.0
 	github.com/stretchr/testify v1.10.0
 	goa.design/clue v1.1.1
-	goa.design/goa/v3 v3.20.0
+	goa.design/goa/v3 v3.20.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -24,12 +24,17 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.opentelemetry.io/otel v1.35.0 // indirect
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	golang.org/x/mod v0.24.0 // indirect
 	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/term v0.30.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
+	golang.org/x/tools v0.31.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
 	google.golang.org/grpc v1.71.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
 )
+
+tool goa.design/clue/mock/cmd/cmg

+ 10 - 4
go.sum

@@ -52,22 +52,28 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
 go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 goa.design/clue v1.1.1 h1:u/d+Fkxl1k/c1WqnZmnKvQip7KiKVk6VvMS9DU7RhRI=
 goa.design/clue v1.1.1/go.mod h1:9dhc4P/+Xmg6k+y+fZMdrg6OcqguN6XpTAb1Ci+bqDw=
-goa.design/goa/v3 v3.20.0 h1:mYYNqCBg9SSxe2jxvPJFOPmJqqKkSAUSU84jpczky3s=
-goa.design/goa/v3 v3.20.0/go.mod h1:g8sT4ioTaRt8BZKwZ1YOQe7UgWqkZMx+q6NWgQfzLUU=
+goa.design/goa/v3 v3.20.1 h1:NpGdgRjaXUMOvb4dIfkUwAJOrLf1Jz0K0ggYj4Q7DFM=
+goa.design/goa/v3 v3.20.1/go.mod h1:cLX3Y1JvnCabMWDAZxmfnjxM1f1l9g7Zf0C9CD9GIAQ=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
 golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
 golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
 google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 51 - 27
ifttt/client.go

@@ -1,7 +1,9 @@
 package ifttt
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -12,49 +14,73 @@ import (
 
 type (
 	Client interface {
-		Trigger(ctx context.Context, present bool) (event string, err error)
+		Trigger(ctx context.Context, present bool) (event string, values *Values, err error)
 	}
 
 	client struct {
-		c                         *http.Client
-		presentEvent, absentEvent string
-		presentURL, absentURL     *url.URL
-		debug                     bool
+		c                                                *http.Client
+		presentEvent, presentURL, absentEvent, absentURL string
+		presentValues, absentValues                      *Values
+		debug                                            bool
+	}
+
+	Values struct {
+		Value1 string `json:"value1,omitempty"`
+		Value2 string `json:"value2,omitempty"`
+		Value3 string `json:"value3,omitempty"`
 	}
 )
 
-func NewClient(c *http.Client, baseURL, key, presentEvent, absentEvent string, debug bool) (Client, error) {
-	u, err := url.Parse(baseURL)
+func NewClient(c *http.Client, baseURL, key, presentEvent, absentEvent string, presentValues, absentValues Values, debug bool) (Client, error) {
+	presentURL, err := url.JoinPath(baseURL, "trigger", presentEvent, "with/key", key)
+	if err != nil {
+		return nil, err
+	}
+
+	absentURL, err := url.JoinPath(baseURL, "trigger", absentEvent, "with/key", key)
 	if err != nil {
 		return nil, err
 	}
-	presentURL, absentURL := *u, *u
-	presentURL.Path = "/trigger/" + presentEvent + "/with/key/" + key
-	absentURL.Path = "/trigger/" + absentEvent + "/with/key/" + key
 
 	return &client{
-		c:            c,
-		presentEvent: presentEvent,
-		absentEvent:  absentEvent,
-		presentURL:   &presentURL,
-		absentURL:    &absentURL,
-		debug:        debug,
+		c:             c,
+		presentEvent:  presentEvent,
+		presentURL:    presentURL,
+		presentValues: &presentValues,
+		absentEvent:   absentEvent,
+		absentURL:     absentURL,
+		absentValues:  &absentValues,
+		debug:         debug,
 	}, nil
 }
 
-func (c *client) Trigger(ctx context.Context, present bool) (event string, err error) {
-	var u *url.URL
+func (c *client) Trigger(ctx context.Context, present bool) (string, *Values, error) {
+	var (
+		event, u string
+		values   *Values
+	)
 	if present {
 		event = c.presentEvent
 		u = c.presentURL
+		values = c.presentValues
 	} else {
 		event = c.absentEvent
 		u = c.absentURL
+		values = c.absentValues
+	}
+
+	var (
+		b = &bytes.Buffer{}
+		e = json.NewEncoder(b)
+	)
+	e.SetEscapeHTML(false)
+	if err := e.Encode(values); err != nil {
+		return "", nil, err
 	}
 
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil)
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, b)
 	if err != nil {
-		return
+		return "", nil, err
 	}
 
 	doer := goahttp.Doer(c.c)
@@ -64,23 +90,21 @@ func (c *client) Trigger(ctx context.Context, present bool) (event string, err e
 
 	resp, err := doer.Do(req)
 	if err != nil {
-		return
+		return "", nil, err
 	}
-	defer resp.Body.Close()
+	defer func() { _ = resp.Body.Close() }()
 
 	if resp.StatusCode != http.StatusOK {
 		var b []byte
 		b, err = io.ReadAll(resp.Body)
 		if err != nil {
-			err = fmt.Errorf("%v: <failed to read body: %w>", resp.Status, err)
-			return
+			return "", nil, fmt.Errorf("%v: <failed to read body: %w>", resp.Status, err)
 		} else if len(b) == 0 {
 			b = []byte("<empty body>")
 		}
 
-		err = fmt.Errorf("%v: %s", resp.Status, b)
-		return
+		return "", nil, fmt.Errorf("%v: %s", resp.Status, b)
 	}
 
-	return
+	return event, values, nil
 }

+ 45 - 9
ifttt/client_test.go

@@ -2,8 +2,10 @@ package ifttt
 
 import (
 	"context"
+	"io"
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -11,13 +13,27 @@ import (
 )
 
 const (
+	baseURL      = "https://maker.ifttt.com"
 	presentEvent = "presence_detected"
 	absentEvent  = "absence_detected"
 )
 
+var (
+	presentValues = Values{
+		Value1: "presence_detected_value1",
+		Value2: "presence_detected_value2",
+		Value3: "presence_detected_value3",
+	}
+	absentValues = Values{
+		Value1: "absence_detected_value1",
+		Value2: "absence_detected_value2",
+		Value3: "absence_detected_value3",
+	}
+)
+
 func TestNewClient(t *testing.T) {
 	t.Run("invalid base URL", func(t *testing.T) {
-		_, err := NewClient(http.DefaultClient, "%", "key", presentEvent, absentEvent, false)
+		_, err := NewClient(http.DefaultClient, "%", "key", presentEvent, absentEvent, presentValues, absentValues, false)
 		assert.ErrorContains(t, err, `parse "%": invalid URL escape "%"`)
 	})
 }
@@ -29,19 +45,29 @@ func TestClient_Trigger(t *testing.T) {
 		name, key, event, err string
 		ctx                   context.Context
 		present, noDebug      bool
+		values                Values
 		handler               func(t *testing.T) http.HandlerFunc
 	}{
 		{
-			name:    "preset",
+			name:    "present",
 			key:     "key",
 			ctx:     ctx,
 			present: true,
+			values:  presentValues,
 			handler: func(t *testing.T) http.HandlerFunc {
 				return func(w http.ResponseWriter, r *http.Request) {
 					assert := assert.New(t)
 
 					assert.Equal(http.MethodPost, r.Method)
 					assert.Equal("/trigger/"+presentEvent+"/with/key/key", r.URL.Path)
+
+					body, err := io.ReadAll(r.Body)
+					assert.NoError(err)
+					assert.JSONEq(`{
+						"value1": "presence_detected_value1",
+						"value2": "presence_detected_value2",
+						"value3": "presence_detected_value3"
+					}`, string(body))
 				}
 			},
 			event: presentEvent,
@@ -51,12 +77,21 @@ func TestClient_Trigger(t *testing.T) {
 			key:     "key",
 			ctx:     ctx,
 			present: false,
+			values:  absentValues,
 			handler: func(t *testing.T) http.HandlerFunc {
 				return func(w http.ResponseWriter, r *http.Request) {
 					assert := assert.New(t)
 
 					assert.Equal(http.MethodPost, r.Method)
 					assert.Equal("/trigger/"+absentEvent+"/with/key/key", r.URL.Path)
+
+					body, err := io.ReadAll(r.Body)
+					assert.NoError(err)
+					assert.JSONEq(`{
+						"value1": "absence_detected_value1",
+						"value2": "absence_detected_value2",
+						"value3": "absence_detected_value3"
+					}`, string(body))
 				}
 			},
 			event: absentEvent,
@@ -88,10 +123,10 @@ func TestClient_Trigger(t *testing.T) {
 						assert.FailNow("error hijacking")
 					}
 
-					conn.Close()
+					assert.NoError(conn.Close())
 				}
 			},
-			err: "EOF",
+			err: `Post "` + baseURL + `/trigger/` + absentEvent + `/with/key/key": EOF`,
 		},
 		{
 			name: "unauthorized",
@@ -142,15 +177,16 @@ func TestClient_Trigger(t *testing.T) {
 			ts := httptest.NewTLSServer(tc.handler(t))
 			defer ts.Close()
 
-			c, err := NewClient(ts.Client(), ts.URL, tc.key, presentEvent, absentEvent, !tc.noDebug)
+			c, err := NewClient(ts.Client(), ts.URL, tc.key, presentEvent, absentEvent, presentValues, absentValues, !tc.noDebug)
 			assert.NoError(err)
 
-			event, err := c.Trigger(tc.ctx, tc.present)
+			event, values, err := c.Trigger(tc.ctx, tc.present)
 			if tc.err != "" {
-				assert.ErrorContains(err, tc.err)
-			} else {
-				assert.NoError(err)
+				tc.err = strings.ReplaceAll(tc.err, baseURL, ts.URL)
+				assert.EqualError(err, tc.err)
+			} else if assert.NoError(err) {
 				assert.Equal(tc.event, event)
+				assert.Equal(&tc.values, values)
 			}
 		})
 	}

+ 4 - 4
ifttt/mocks/client.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence/ifttt
@@ -20,7 +20,7 @@ type (
 		t *testing.T
 	}
 
-	ClientTriggerFunc func(ctx context.Context, present bool) (event string, err error)
+	ClientTriggerFunc func(ctx context.Context, present bool) (event string, values *ifttt.Values, err error)
 )
 
 func NewClient(t *testing.T) *Client {
@@ -39,13 +39,13 @@ func (m *Client) SetTrigger(f ClientTriggerFunc) {
 	m.m.Set("Trigger", f)
 }
 
-func (m *Client) Trigger(ctx context.Context, present bool) (event string, err error) {
+func (m *Client) Trigger(ctx context.Context, present bool) (event string, values *ifttt.Values, err error) {
 	if f := m.m.Next("Trigger"); f != nil {
 		return f.(ClientTriggerFunc)(ctx, present)
 	}
 	m.t.Helper()
 	m.t.Error("unexpected Trigger call")
-	return "", nil
+	return "", nil, nil
 }
 
 func (m *Client) HasMore() bool {

+ 19 - 5
magefiles/magefile.go

@@ -1,24 +1,38 @@
 package main
 
 import (
+	"runtime"
+	"strconv"
+
 	"github.com/magefile/mage/sh"
 )
 
 var (
-	Default = Build // nolint: deadcode
+	Default = Build
 )
 
 // Generate generates mock implementations of interfaces.
-func Generate() (err error) { // nolint: deadcode
-	return sh.RunV("cmg", "gen", "./...")
+func Generate() (err error) {
+	return sh.RunV("go", "tool", "cmg", "gen", "./...")
 }
 
 // Build builds the binaries.
-func Build() error { // nolint: deadcode
+func Build() error {
 	return sh.RunV("go", "build", "./cmd/presence")
 }
 
+// Lint runs the lint suite.
+func Lint() error {
+	return sh.RunV("golangci-lint", "run", "./...")
+}
+
 // Test runs the test suite.
-func Test() error { // nolint: deadcode
+func Test() error {
 	return sh.RunV("go", "test", "-cover", "-race", "./...")
 }
+
+// Snapshot runs the release snapshot.
+func Snapshot() error {
+	nc := runtime.NumCPU()
+	return sh.RunV("goreleaser", "release", "--clean", "--parallelism", strconv.Itoa(nc), "--snapshot")
+}

+ 1 - 1
mocks/detector.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence

+ 1 - 1
neighbors/mocks/arp.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence/neighbors

+ 1 - 1
neighbors/mocks/arping.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence/neighbors

+ 1 - 1
neighbors/mocks/state.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence/neighbors

+ 2 - 1
tests/invalid_ifttt_absent_event_name.yml

@@ -4,4 +4,5 @@ mac_addresses:
 ifttt:
   key: abcdef123456
   events:
-    absent: ^
+    absent:
+      event: ^

+ 2 - 1
tests/invalid_ifttt_present_event_name.yml

@@ -4,4 +4,5 @@ mac_addresses:
 ifttt:
   key: abcdef123456
   events:
-    present: $
+    present:
+      event: $

+ 10 - 2
tests/success.yml

@@ -8,5 +8,13 @@ ifttt:
   base_url: https://example.com
   key: abcdef123456
   events:
-    present: event_presence_detected
-    absent: event_absence_detected
+    present:
+      event: event_presence_detected
+      value1: event_presence_detected_value1
+      value2: event_presence_detected_value2
+      value3: event_presence_detected_value3
+    absent:
+      event: event_absence_detected
+      value1: event_absence_detected_value1
+      value2: event_absence_detected_value2
+      value3: event_absence_detected_value3

+ 1 - 1
wrap/mocks/net.go

@@ -1,4 +1,4 @@
-// Code generated by Clue Mock Generator v0.17.0, DO NOT EDIT.
+// Code generated by Clue Mock Generator v1.1.1, DO NOT EDIT.
 //
 // Command:
 // $ cmg gen douglasthrift.net/presence/wrap

+ 2 - 2
wrap/net.go

@@ -17,10 +17,10 @@ func NewNet() Net {
 	return &netImpl{}
 }
 
-func (_ *netImpl) InterfaceByName(name string) (*net.Interface, error) {
+func (*netImpl) InterfaceByName(name string) (*net.Interface, error) {
 	return net.InterfaceByName(name)
 }
 
-func (_ *netImpl) Interfaces() ([]net.Interface, error) {
+func (*netImpl) Interfaces() ([]net.Interface, error) {
 	return net.Interfaces()
 }