diff --git a/garden-app/.golangci.yaml b/garden-app/.golangci.yaml index 246eda52..50e5adc7 100644 --- a/garden-app/.golangci.yaml +++ b/garden-app/.golangci.yaml @@ -2,8 +2,6 @@ run: timeout: 2m issues-exit-code: 1 tests: true - skip-files: - - cmd/completion.go issues: exclude: @@ -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 diff --git a/garden-app/pkg/notifications/fake/fake.go b/garden-app/pkg/notifications/fake/fake.go index cad23281..1c6aa7f7 100644 --- a/garden-app/pkg/notifications/fake/fake.go +++ b/garden-app/pkg/notifications/fake/fake.go @@ -2,6 +2,7 @@ package fake import ( "errors" + "sync" "github.com/mitchellh/mapstructure" ) @@ -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{} @@ -30,9 +42,18 @@ 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} return nil } + +func LastMessage() Message { + lastMessageMtx.Lock() + result := lastMessage + lastMessageMtx.Unlock() + return result +} diff --git a/garden-app/worker/scheduler.go b/garden-app/worker/scheduler.go index 686e9dd1..d3709268 100644 --- a/garden-app/worker/scheduler.go +++ b/garden-app/worker/scheduler.go @@ -403,6 +403,13 @@ func (w *Worker) scheduleAdhocLightAction(g *pkg.Garden) error { if err != nil { actionLogger.Error("error executing scheduled adhoc LightAction", "error", err) } + + err = w.sendNotifications(g, a.State) + if err != nil { + actionLogger.Error("error sending notifications", "error", err) + schedulerErrors.WithLabelValues(gardenLabels(g)...).Inc() + } + actionLogger.Debug("removing AdhocOnTime") // Now set AdhocOnTime to nil and save g.LightSchedule.AdhocOnTime = nil @@ -465,6 +472,12 @@ func (w *Worker) executeLightActionInScheduledJob(g *pkg.Garden, input *action.L actionLogger.Error("error executing scheduled LightAction", "error", err) schedulerErrors.WithLabelValues(gardenLabels(g)...).Inc() } + + err = w.sendNotifications(g, input.State) + if err != nil { + actionLogger.Error("error sending notifications", "error", err) + schedulerErrors.WithLabelValues(gardenLabels(g)...).Inc() + } } func timeAtDate(date *time.Time, startTime time.Time) time.Time { @@ -484,3 +497,27 @@ func timeAtDate(date *time.Time, startTime time.Time) time.Time { startTime.Location(), ) } + +func (w *Worker) sendNotifications(g *pkg.Garden, state pkg.LightState) error { + // 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 { + return fmt.Errorf("error getting all notification clients: %w", err) + } + + title := fmt.Sprintf("%s: Light %s", g.Name, state.String()) + + for _, nc := range notificationClients { + ncLogger := w.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") + } + + return nil +} diff --git a/garden-app/worker/scheduler_test.go b/garden-app/worker/scheduler_test.go index 3bd594b4..6718fc2e 100644 --- a/garden-app/worker/scheduler_test.go +++ b/garden-app/worker/scheduler_test.go @@ -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" @@ -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) @@ -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", @@ -403,6 +407,45 @@ func TestScheduleLightActions(t *testing.T) { nextOnTime := worker.GetNextLightTime(g, pkg.LightStateOn) assert.Equal(t, expected, *nextOnTime) }) + + t.Run("ScheduledLightActionCreatesNotification", func(t *testing.T) { + 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(), ¬ifications.Client{ + ID: babyapi.NewID(), + Name: "TestClient", + Type: "fake", + Options: map[string]any{}, + }) + 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, "test-garden: Light ON", fake.LastMessage().Title) + + time.Sleep(1 * time.Second) + assert.Equal(t, "test-garden: Light OFF", fake.LastMessage().Title) + }) } func TestScheduleLightDelay(t *testing.T) {