From 2abb4b10241057398e74491578ea5185c126424c Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 2 Oct 2021 14:43:15 +0200 Subject: [PATCH] add time-of-day limitation to events. --- app/services/events.go | 3 + cmd/lucy/command.go | 41 ++++-- cmd/lucy/handlercmd.go | 11 +- cmd/lucy/help.go | 1 + cmd/lucy/main.go | 2 +- internal/mysql/eventhandlerrepo.go | 19 ++- models/eventhandler.go | 26 +++- models/timeofday.go | 138 ++++++++++++++++++ .../20211002131800_eventhandler_from_to.sql | 13 ++ 9 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 models/timeofday.go create mode 100644 scripts/20211002131800_eventhandler_from_to.sql diff --git a/app/services/events.go b/app/services/events.go index eb81679..2c3b99f 100644 --- a/app/services/events.go +++ b/app/services/events.go @@ -93,6 +93,9 @@ func handleEvent(event models.Event) (responses []models.Event) { if handler.EventName != event.Name { continue } + if !handler.IsWithinTimeRange() { + continue + } devices, err := config.DeviceRepository().FetchByReference(ctx, handler.TargetKind, handler.TargetValue) if err != nil { diff --git a/cmd/lucy/command.go b/cmd/lucy/command.go index 4c9b033..2648711 100644 --- a/cmd/lucy/command.go +++ b/cmd/lucy/command.go @@ -22,6 +22,28 @@ func (p *Param) String() *string { return &p.Value } +func (p *Param) TimeOfDay() *models.TimeOfDay { + if p == nil { + return nil + } + + tod, err := models.ParseTimeOfDay(p.Value) + if err != nil { + return nil + } + + return &tod +} + +func (p *Param) TimeOfDayOr(fallback models.TimeOfDay) models.TimeOfDay { + tod := p.TimeOfDay() + if tod == nil { + return fallback + } + + return *tod +} + func (p *Param) StringOr(fallback string) string { if p == nil { return fallback @@ -190,6 +212,16 @@ func (p Params) EventConditions() map[string]models.EventCondition { return ecMap } +func (p Params) DeviceState(prefix string) models.NewDeviceState { + return models.NewDeviceState{ + Power: p.Get(prefix + "power").Bool(), + Color: p.Get(prefix + "color").String(), + Intensity: p.Get(prefix + "intensity").Float(), + Temperature: p.Get(prefix + "temperature").Int(), + } +} + + type Command struct { Name string Params Params @@ -231,12 +263,3 @@ func parseCommand(args []string) Command { return cmd } - -func (cmd Command) NewDeviceState(prefix string) models.NewDeviceState { - return models.NewDeviceState{ - Power: cmd.Params.Get(prefix + "power").Bool(), - Color: cmd.Params.Get(prefix + "color").String(), - Intensity: cmd.Params.Get(prefix + "intensity").Float(), - Temperature: cmd.Params.Get(prefix + "temperature").Int(), - } -} diff --git a/cmd/lucy/handlercmd.go b/cmd/lucy/handlercmd.go index 1081f7e..7d39695 100644 --- a/cmd/lucy/handlercmd.go +++ b/cmd/lucy/handlercmd.go @@ -39,6 +39,13 @@ func handlerCmd( } model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr) + // From / To + model.From = cmd.Params.Get("from").TimeOfDayOr(models.Never) + model.To = cmd.Params.Get("to").TimeOfDayOr(models.Never) + if model.From.IsNever() != model.To.IsNever() { + log.Fatalln("from and to must be specified together!") + } + // One shot model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true" @@ -49,7 +56,7 @@ func handlerCmd( model.Conditions = cmd.Params.Subset("conditions").EventConditions() // Power, Color, Intensity, Temperature - nds := cmd.NewDeviceState("set-") + nds := cmd.Params.DeviceState("set-") model.Actions.SetPower = nds.Power model.Actions.SetColor = nds.Color model.Actions.SetIntensity = nds.Intensity @@ -64,7 +71,7 @@ func handlerCmd( firePayloadParams := fireParams.Subset("payload") model.Actions.FireEvent = &models.Event{ - Name: *fireName, + Name: *fireName, Payload: make(map[string]string, len(firePayloadParams)), } diff --git a/cmd/lucy/help.go b/cmd/lucy/help.go index e94f138..3bfa856 100644 --- a/cmd/lucy/help.go +++ b/cmd/lucy/help.go @@ -18,6 +18,7 @@ EVENT HANDLER COMMANDS handler add \ \ \ + \ |>]*> \ \ \ diff --git a/cmd/lucy/main.go b/cmd/lucy/main.go index 887acac..adc8fa8 100644 --- a/cmd/lucy/main.go +++ b/cmd/lucy/main.go @@ -45,7 +45,7 @@ func main() { } case "set": { - devices, err := c.PutDeviceState(ctx, cmd.Params.Get(0).StringOr("all"), cmd.NewDeviceState("")) + devices, err := c.PutDeviceState(ctx, cmd.Params.Get(0).StringOr("all"), cmd.Params.DeviceState("")) if err != nil { log.Fatalln(err) } diff --git a/internal/mysql/eventhandlerrepo.go b/internal/mysql/eventhandlerrepo.go index 3536a42..7a17b88 100644 --- a/internal/mysql/eventhandlerrepo.go +++ b/internal/mysql/eventhandlerrepo.go @@ -14,6 +14,8 @@ type eventHandlerRecord struct { Priority int `db:"priority"` TargetKind string `db:"target_kind"` TargetValue string `db:"target_value"` + From *string `db:"from_tod"` + To *string `db:"to_tod"` ConditionsJSON json.RawMessage `db:"conditions"` ActionsJSON json.RawMessage `db:"actions"` } @@ -61,12 +63,14 @@ func (r *EventHandlerRepo) Save(ctx context.Context, handler *models.EventHandle TargetValue: handler.TargetValue, ConditionsJSON: conditionsJson, ActionsJSON: actionsJson, + From: handler.From.StringPtr(), + To: handler.To.StringPtr(), } if record.ID == 0 { res, err := r.DBX.NamedExecContext(ctx, ` - INSERT INTO event_handler (event_name, one_shot, priority, target_kind, target_value, conditions, actions) - VALUES (:event_name, :one_shot, :priority, :target_kind, :target_value, :conditions, :actions); + INSERT INTO event_handler (event_name, one_shot, priority, target_kind, target_value, conditions, from_tod, to_tod, actions) + VALUES (:event_name, :one_shot, :priority, :target_kind, :target_value, :conditions, :from_tod, :to_tod, :actions); `, record) if err != nil { return dbErr(err) @@ -87,6 +91,8 @@ func (r *EventHandlerRepo) Save(ctx context.Context, handler *models.EventHandle target_kind = :target_kind, target_value = :target_value, conditions = :conditions, + from_tod = :from_tod, + to_tod = :to_tod, actions = :actions WHERE id = :id `, record) @@ -139,6 +145,15 @@ func (r *EventHandlerRepo) populate(records []eventHandlerRecord) ([]models.Even return nil, dbErr(err) } + handler.From, err = models.ParseTimeOfDayPtr(record.From) + if err != nil { + return nil, dbErr(err) + } + handler.To, err = models.ParseTimeOfDayPtr(record.To) + if err != nil { + return nil, dbErr(err) + } + res = append(res, handler) } diff --git a/models/eventhandler.go b/models/eventhandler.go index 1651d4b..81866e9 100644 --- a/models/eventhandler.go +++ b/models/eventhandler.go @@ -16,6 +16,8 @@ type EventHandler struct { TargetKind ReferenceKind `json:"targetKind"` TargetValue string `json:"targetValue"` Actions EventAction `json:"actions"` + From TimeOfDay `json:"from"` + To TimeOfDay `json:"to"` } type EventHandlerRepository interface { @@ -42,6 +44,8 @@ type EventHandlerUpdate struct { SetConditions map[string]EventCondition `json:"setConditions"` PatchConditions map[string]*EventCondition `json:"patchConditions"` SetActions *EventAction `json:"setActions"` + SetFrom *TimeOfDay `json:"setFrom"` + SetTo *TimeOfDay `json:"setTo"` } func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) { @@ -63,6 +67,12 @@ func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) { if update.SetOneShot != nil { h.OneShot = *update.SetOneShot } + if update.SetFrom != nil { + h.From = *update.SetFrom + } + if update.SetFrom != nil { + h.To = *update.SetFrom + } if update.SetConditions != nil { h.Conditions = update.SetConditions @@ -100,6 +110,14 @@ func (h *EventHandler) MatchesEvent(event Event, targets []Device) bool { return true } +func (h *EventHandler) IsWithinTimeRange() bool { + if h.From.IsNever() || h.To.IsNever() { + return true + } + + return CurrentTimeOfDay().IsBetween(h.From, h.To) +} + func (c *EventCondition) check(key, value string, targets []Device) bool { any := strings.HasPrefix(key, "any.") all := strings.HasPrefix(key, "all.") @@ -225,19 +243,19 @@ func (action *EventAction) Apply(other EventAction) { func (c EventCondition) String() string { str := make([]string, 0, 5) if len(c.LT) > 0 { - str = append(str, "lt:" + c.LT) + str = append(str, "lt:"+c.LT) } if len(c.LTE) > 0 { - str = append(str, "lte:" + c.LTE) + str = append(str, "lte:"+c.LTE) } if len(c.EQ) > 0 { str = append(str, c.EQ) } if len(c.GTE) > 0 { - str = append(str, "gte:" + c.GTE) + str = append(str, "gte:"+c.GTE) } if len(c.GT) > 0 { - str = append(str, "gte:" + c.GT) + str = append(str, "gte:"+c.GT) } return strings.Join(str, ";") diff --git a/models/timeofday.go b/models/timeofday.go new file mode 100644 index 0000000..780d067 --- /dev/null +++ b/models/timeofday.go @@ -0,0 +1,138 @@ +package models + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" +) + +type TimeOfDay int + +func (t *TimeOfDay) UnmarshalJSON(v []byte) error { + if bytes.Equal(v, []byte("null")) { + *t = -1 + } + + var str string + err := json.Unmarshal(v, &str) + if err != nil { + var n int + err = json.Unmarshal(v, &n) + if err != nil { + return err + } + if n < -1 && n >= 86400 { + return fmt.Errorf("value outside range 0..86400: %d", n) + } + } + + + t2, err := ParseTimeOfDay(str) + if err != nil { + return err + } + + *t = t2 + + return nil +} + +func (t TimeOfDay) MarshalJSON() ([]byte, error) { + if t.IsNever() { + return []byte("null"), nil + } + + return json.Marshal(t.String()) +} + +const Never = TimeOfDay(-1) + +func (t *TimeOfDay) String() string { + if *t < 0 { + return "n/a" + } + + return fmt.Sprintf("%02d:%02d:%02d", *t/3600, (*t/60)%60, *t%60) +} + +func (t *TimeOfDay) StringPtr() *string { + if *t < 0 { + return nil + } + + s := t.String() + return &s +} + +func (t TimeOfDay) IsNever() bool { + return t == Never +} + +func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool { + if from == to { + return t == from + } else if from > to { + return t >= to || t <= from + } else { + return t >= from && t <= to + } +} + +func CurrentTimeOfDay() TimeOfDay { + return TimeOfDayFromDate(time.Now()) +} + +func TimeOfDayFromDate(date time.Time) TimeOfDay { + return NewTimeOfDay(date.Hour(), date.Minute(), date.Second()) +} + +func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay { + return TimeOfDay(((hours % 24) * 3600) + ((minutes % 60) * 60) + seconds) +} + +func ParseTimeOfDayPtr(str *string) (TimeOfDay, error) { + if str == nil { + return Never, nil + } + + return ParseTimeOfDay(*str) +} + +func ParseTimeOfDay(str string) (TimeOfDay, error) { + if str == "" || str == "n/a" || str == "N/A" { + return -1, nil + } + + split := strings.SplitN(str, ":", 4) + res := 0 + + n, err := strconv.Atoi(split[0]) + if err != nil { + return Never, err + } + res += n * 3600 + + if len(split) >= 2 { + n, err := strconv.Atoi(split[1]) + if err != nil { + return Never, err + } + res += n * 60 + } + if len(split) >= 3 { + n, err := strconv.Atoi(split[2]) + if err != nil { + return Never, err + } + res += n + } + + if res >= 86400 || res < 0 { + return Never, fmt.Errorf("invalid time of day string %s (=%d)", str, res) + } + + return TimeOfDay(res), nil +} diff --git a/scripts/20211002131800_eventhandler_from_to.sql b/scripts/20211002131800_eventhandler_from_to.sql new file mode 100644 index 0000000..39b16bb --- /dev/null +++ b/scripts/20211002131800_eventhandler_from_to.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE event_handler + ADD COLUMN from_tod VARCHAR(255), + ADD COLUMN to_tod VARCHAR(255); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE event_handler + DROP COLUMN IF EXISTS from_tod, + DROP COLUMN IF EXISTS to_tod; +-- +goose StatementEnd