Skip to content

Commit

Permalink
Merge pull request #167 from calvinmclean/feature/light-notification
Browse files Browse the repository at this point in the history
Add notifications for scheduled LightActions
  • Loading branch information
calvinmclean committed Jun 29, 2024
2 parents abf8da6 + 7b99710 commit 146c77b
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 5 deletions.
4 changes: 0 additions & 4 deletions garden-app/.golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ run:
timeout: 2m
issues-exit-code: 1
tests: true
skip-files:
- cmd/completion.go

issues:
exclude:
Expand All @@ -17,8 +15,6 @@ issues:
- revive

output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
Expand Down
30 changes: 29 additions & 1 deletion garden-app/pkg/notifications/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fake

import (
"errors"
"sync"

"github.com/mitchellh/mapstructure"
)
Expand All @@ -15,6 +16,17 @@ type Client struct {
*Config
}

type Message struct {
Title string
Message string
}

var (
// lastMessage allows checking the last message that was sent
lastMessage = Message{}
lastMessageMtx = sync.Mutex{}
)

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

Expand All @@ -30,9 +42,25 @@ func NewClient(options map[string]interface{}) (*Client, error) {
return client, nil
}

func (c *Client) SendMessage(string, string) error {
func (c *Client) SendMessage(title, message string) error {
if c.SendMessageError != "" {
return errors.New(c.SendMessageError)
}
lastMessageMtx.Lock()
lastMessage = Message{title, message}
lastMessageMtx.Unlock()
return nil
}

func LastMessage() Message {
lastMessageMtx.Lock()
result := lastMessage
lastMessageMtx.Unlock()
return result
}

func ResetLastMessage() {
lastMessageMtx.Lock()
lastMessage = Message{}
lastMessageMtx.Unlock()
}
28 changes: 28 additions & 0 deletions garden-app/worker/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@ func (w *Worker) scheduleAdhocLightAction(g *pkg.Garden) error {
if err != nil {
actionLogger.Error("error executing scheduled adhoc LightAction", "error", err)
}

w.sendNotifications(g, a.State, actionLogger)

actionLogger.Debug("removing AdhocOnTime")
// Now set AdhocOnTime to nil and save
g.LightSchedule.AdhocOnTime = nil
Expand Down Expand Up @@ -465,6 +468,8 @@ func (w *Worker) executeLightActionInScheduledJob(g *pkg.Garden, input *action.L
actionLogger.Error("error executing scheduled LightAction", "error", err)
schedulerErrors.WithLabelValues(gardenLabels(g)...).Inc()
}

w.sendNotifications(g, input.State, actionLogger)
}

func timeAtDate(date *time.Time, startTime time.Time) time.Time {
Expand All @@ -484,3 +489,26 @@ func timeAtDate(date *time.Time, startTime time.Time) time.Time {
startTime.Location(),
)
}

func (w *Worker) sendNotifications(g *pkg.Garden, state pkg.LightState, logger *slog.Logger) {
// TODO: this might end up getting client from garden or zone config instead of using all
notificationClients, err := w.storageClient.NotificationClientConfigs.GetAll(context.Background(), nil)
if err != nil {
logger.Error("error getting all notification clients", "error", err)
schedulerErrors.WithLabelValues(gardenLabels(g)...).Inc()
}

title := fmt.Sprintf("%s: Light %s", g.Name, state.String())

for _, nc := range notificationClients {
ncLogger := logger.With("notification_client_id", nc.GetID())

err = nc.SendMessage(title, "")
if err != nil {
ncLogger.Error("error sending message", "error", err)
continue
}

ncLogger.Info("successfully send notification")
}
}
81 changes: 81 additions & 0 deletions garden-app/worker/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/calvinmclean/automated-garden/garden-app/pkg/action"
"github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb"
"github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications/fake"
"github.com/calvinmclean/automated-garden/garden-app/pkg/storage"
"github.com/calvinmclean/automated-garden/garden-app/pkg/weather"
"github.com/calvinmclean/babyapi"
Expand Down Expand Up @@ -252,6 +254,7 @@ func TestGetNextWaterTime(t *testing.T) {

ws := createExampleWaterSchedule()
ws.StartTime = pkg.NewStartTime(tt.startTime)
ws.StartDate = &now
ws.Interval = &pkg.Duration{Duration: tt.interval}

err = worker.ScheduleWaterAction(ws)
Expand Down Expand Up @@ -364,6 +367,7 @@ func TestScheduleLightActions(t *testing.T) {
nextOnTime := worker.GetNextLightTime(g, pkg.LightStateOn)
assert.Equal(t, later, *nextOnTime)
})

t.Run("AdhocOnTimeInPastIsNotUsed", func(t *testing.T) {
storageClient, err := storage.NewClient(storage.Config{
Driver: "hashmap",
Expand Down Expand Up @@ -403,6 +407,83 @@ func TestScheduleLightActions(t *testing.T) {
nextOnTime := worker.GetNextLightTime(g, pkg.LightStateOn)
assert.Equal(t, expected, *nextOnTime)
})

t.Run("ScheduledLightActionCreatesNotification", func(t *testing.T) {
tests := []struct {
name string
opts map[string]any
off bool
expectedOnMessage string
expectedOffMessage string
}{
{
"SuccessfulOnAndOff",
map[string]any{},
true,
"test-garden: Light ON",
"test-garden: Light OFF",
},
{
"ErrorCreatingClient",
map[string]any{"create_error": "error"},
false,
"",
"",
},
{
"ErrorSendingMessage",
map[string]any{"send_message_error": "error"},
false,
"",
"",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake.ResetLastMessage()

storageClient, err := storage.NewClient(storage.Config{
Driver: "hashmap",
})
assert.NoError(t, err)

mqttClient := new(mqtt.MockClient)
mqttClient.On("LightTopic", mock.Anything).Return("test-garden/action/light", nil)
mqttClient.On("Publish", "test-garden/action/light", mock.Anything).Return(nil)
mqttClient.On("Disconnect", uint(100)).Return()

err = storageClient.NotificationClientConfigs.Set(context.Background(), &notifications.Client{
ID: babyapi.NewID(),
Name: "TestClient",
Type: "fake",
Options: tt.opts,
})
assert.NoError(t, err)

worker := NewWorker(storageClient, nil, mqttClient, slog.Default())
worker.StartAsync()
defer worker.Stop()

// Create new LightSchedule that turns on in 1 second for only 1 second
now := time.Now().UTC()
later := now.Add(1 * time.Second).Truncate(time.Second)
g := createExampleGarden()
g.LightSchedule.StartTime = pkg.NewStartTime(later)
g.LightSchedule.Duration = &pkg.Duration{Duration: time.Second}
err = worker.ScheduleLightActions(g)
assert.NoError(t, err)

time.Sleep(1 * time.Second)
assert.Equal(t, tt.expectedOnMessage, fake.LastMessage().Title)

if tt.off {
time.Sleep(1 * time.Second)
assert.Equal(t, tt.expectedOffMessage, fake.LastMessage().Title)
}
})
}
})
}

func TestScheduleLightDelay(t *testing.T) {
Expand Down

0 comments on commit 146c77b

Please sign in to comment.