Skip to content

Commit

Permalink
Merge pull request #159 from calvinmclean/feature/notifications
Browse files Browse the repository at this point in the history
Add Watering Notifications
  • Loading branch information
calvinmclean authored Apr 28, 2024
2 parents aeec7d0 + b0a8ab0 commit 252ca8e
Show file tree
Hide file tree
Showing 15 changed files with 814 additions and 18 deletions.
9 changes: 1 addition & 8 deletions garden-app/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,9 @@ func NewController(cfg Config) (*Controller, error) {
})
}

// Create default handler and mqttClient, then connect
defaultHandler := paho.MessageHandler(func(_ paho.Client, msg paho.Message) {
controller.logger.With(
"topic", msg.Topic(),
"message", string(msg.Payload()),
).Info("default handler called with message")
})
// Override configured ClientID with the TopicPrefix from command flags
controller.MQTTConfig.ClientID = fmt.Sprintf(controller.TopicPrefix)
controller.mqttClient, err = mqtt.NewClient(controller.MQTTConfig, defaultHandler, handlers...)
controller.mqttClient, err = mqtt.NewClient(controller.MQTTConfig, mqtt.DefaultHandler(controller.logger), handlers...)
if err != nil {
return nil, fmt.Errorf("unable to initialize MQTT client: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions garden-app/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/go-chi/render v1.0.3
github.com/go-co-op/gocron v1.35.2
github.com/gregdel/pushover v1.3.0
github.com/influxdata/influxdb-client-go/v2 v2.12.3
github.com/madflojo/hord v0.2.2
github.com/mitchellh/mapstructure v1.5.0
Expand Down
2 changes: 2 additions & 0 deletions garden-app/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw=
github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
Expand Down
10 changes: 10 additions & 0 deletions garden-app/pkg/mqtt/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"html/template"
"log/slog"
"sync"

mqtt "github.com/eclipse/paho.mqtt.golang"
Expand Down Expand Up @@ -141,3 +142,12 @@ func (c *Config) executeTopicTemplate(templateString string, topicPrefix string)
err := t.Execute(&result, data)
return result.String(), err
}

func DefaultHandler(logger *slog.Logger) mqtt.MessageHandler {
return func(_ mqtt.Client, msg mqtt.Message) {
logger.With(
"topic", msg.Topic(),
"message", string(msg.Payload()),
).Info("default handler called with message")
}
}
117 changes: 117 additions & 0 deletions garden-app/pkg/notifications/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package notifications

import (
"errors"
"fmt"
"net/http"
"time"

"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications/fake"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications/pushover"
"github.com/calvinmclean/babyapi"
)

// Client is used to interact with an external notification API. It has generic options to allow multiple Client implementations
type Client struct {
ID babyapi.ID `json:"id" yaml:"id"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Options map[string]any `json:"options" yaml:"options"`
}

// TestCreate will call the Client implementation's initialization function to make sure it is valid and able to connect
func (nc *Client) TestCreate() error {
_, err := newClient(nc)
return err
}

func (nc *Client) GetID() string {
return nc.ID.String()
}

func (nc *Client) Render(_ http.ResponseWriter, _ *http.Request) error {
return nil
}

func (nc *Client) Bind(r *http.Request) error {
if nc == nil {
return errors.New("missing required NotificationClient fields")
}

err := nc.ID.Bind(r)
if err != nil {
return err
}

switch r.Method {
case http.MethodPut, http.MethodPost:
if nc.Name == "" {
return errors.New("missing required name field")
}
if nc.Type == "" {
return errors.New("missing required type field")
}
if nc.Options == nil {
return errors.New("missing required options field")
}
}

return nil
}

// Patch allows modifying an existing Config with fields from a new one
func (nc *Client) Patch(newConfig *Client) *babyapi.ErrResponse {
if newConfig.Name != "" {
nc.Name = newConfig.Name
}
if newConfig.Type != "" {
nc.Type = newConfig.Type
}

if nc.Options == nil && newConfig.Options != nil {
nc.Options = map[string]any{}
}
for k, v := range newConfig.Options {
nc.Options[k] = v
}

return nil
}

// EndDated allows this to satisfy an interface even though the resources does not have end-dates
func (*Client) EndDated() bool {
return false
}

func (*Client) SetEndDate(_ time.Time) {}

// SendMessage will send a notification using the client created from this config
func (nc *Client) SendMessage(title, message string) error {
client, err := newClient(nc)
if err != nil {
return fmt.Errorf("error initializing client: %w", err)
}

return client.SendMessage(title, message)
}

// client is an interface defining the possible methods used to interact with the notification APIs
type client interface {
SendMessage(title, message string) error
}

// newClient will use the config to create and return the correct type of notification client
func newClient(c *Client) (client, error) {
var client client
var err error
switch c.Type {
case "pushover":
client, err = pushover.NewClient(c.Options)
case "fake":
client, err = fake.NewClient(c.Options)
default:
err = fmt.Errorf("invalid type '%s'", c.Type)
}

return client, err
}
49 changes: 49 additions & 0 deletions garden-app/pkg/notifications/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package notifications

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConfigPatch(t *testing.T) {
tests := []struct {
name string
newConfig *Client
}{
{
"PatchType",
&Client{Type: "other_type"},
},
{
"PatchName",
&Client{Name: "NewName"},
},
{
"PatchOptions",
&Client{Options: map[string]interface{}{
"key": "value",
}},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{}
err := c.Patch(tt.newConfig)
require.Nil(t, err)
assert.Equal(t, tt.newConfig, c)
})
}
}

func TestNewClientInvalidType(t *testing.T) {
_, err := newClient(&Client{Type: "DNE"})
assert.Error(t, err)
assert.Equal(t, "invalid type 'DNE'", err.Error())
}

func TestEndDated(t *testing.T) {
assert.False(t, (&Client{}).EndDated())
}
38 changes: 38 additions & 0 deletions garden-app/pkg/notifications/fake/fake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fake

import (
"errors"

"github.com/mitchellh/mapstructure"
)

type Config struct {
CreateError string `mapstructure:"create_error"`
SendMessageError string `mapstructure:"send_message_error"`
}

type Client struct {
*Config
}

func NewClient(options map[string]interface{}) (*Client, error) {
client := &Client{}

err := mapstructure.Decode(options, &client.Config)
if err != nil {
return nil, err
}

if client.Config.CreateError != "" {
return nil, errors.New(client.CreateError)
}

return client, nil
}

func (c *Client) SendMessage(string, string) error {
if c.SendMessageError != "" {
return errors.New(c.SendMessageError)
}
return nil
}
46 changes: 46 additions & 0 deletions garden-app/pkg/notifications/pushover/pushover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pushover

import (
"errors"

"github.com/gregdel/pushover"
"github.com/mitchellh/mapstructure"
)

type Config struct {
AppToken string `json:"app_token,omitempty" yaml:"app_token,omitempty" mapstructure:"app_token,omitempty"`
RecipientToken string `json:"recipient_token,omitempty" yaml:"recipient_token,omitempty" mapstructure:"recipient_token,omitempty"`
}

type Client struct {
*Config
app *pushover.Pushover
recipient *pushover.Recipient
}

func NewClient(options map[string]interface{}) (*Client, error) {
client := &Client{}

err := mapstructure.Decode(options, &client.Config)
if err != nil {
return nil, err
}

if client.AppToken == "" {
return nil, errors.New("missing required app_token")
}
if client.RecipientToken == "" {
return nil, errors.New("missing required recipient_token")
}

client.app = pushover.New(client.AppToken)
client.recipient = pushover.NewRecipient(client.RecipientToken)

return client, nil
}

func (c *Client) SendMessage(title, message string) error {
msg := pushover.NewMessageWithTitle(message, title)
_, err := c.app.SendMessage(msg, c.recipient)
return err
}
19 changes: 11 additions & 8 deletions garden-app/pkg/storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications"
"github.com/calvinmclean/automated-garden/garden-app/pkg/weather"

"github.com/calvinmclean/babyapi"
Expand All @@ -21,10 +22,11 @@ type Config struct {
}

type Client struct {
Gardens babyapi.Storage[*pkg.Garden]
Zones babyapi.Storage[*pkg.Zone]
WaterSchedules babyapi.Storage[*pkg.WaterSchedule]
WeatherClientConfigs babyapi.Storage[*weather.Config]
Gardens babyapi.Storage[*pkg.Garden]
Zones babyapi.Storage[*pkg.Zone]
WaterSchedules babyapi.Storage[*pkg.WaterSchedule]
WeatherClientConfigs babyapi.Storage[*weather.Config]
NotificationClientConfigs babyapi.Storage[*notifications.Client]
}

func NewClient(config Config) (*Client, error) {
Expand All @@ -34,10 +36,11 @@ func NewClient(config Config) (*Client, error) {
}

return &Client{
Gardens: kv.NewClient[*pkg.Garden](db, "Garden"),
Zones: kv.NewClient[*pkg.Zone](db, "Zone"),
WaterSchedules: kv.NewClient[*pkg.WaterSchedule](db, "WaterSchedule"),
WeatherClientConfigs: kv.NewClient[*weather.Config](db, "WeatherClient"),
Gardens: kv.NewClient[*pkg.Garden](db, "Garden"),
Zones: kv.NewClient[*pkg.Zone](db, "Zone"),
WaterSchedules: kv.NewClient[*pkg.WaterSchedule](db, "WaterSchedule"),
WeatherClientConfigs: kv.NewClient[*weather.Config](db, "WeatherClient"),
NotificationClientConfigs: kv.NewClient[*notifications.Client](db, "NotificationClient"),
}, nil
}

Expand Down
Loading

0 comments on commit 252ca8e

Please sign in to comment.