From c7d7e264cab67302f22cd71342e9010dc30689f1 Mon Sep 17 00:00:00 2001 From: Stian Fredrik Aune Date: Tue, 28 Sep 2021 21:54:03 +0200 Subject: [PATCH 1/6] Lucy handler add --- app/api/devices.go | 22 ++---------- app/client/handler.go | 12 ++++++- app/services/events.go | 4 +++ cmd/lucy/command.go | 77 +++++++++++++++++++++++++++++++++++++----- cmd/lucy/handlercmd.go | 58 ++++++++++++++++++++++++++++++- cmd/lucy/help.go | 17 +++++++--- cmd/lucy/main.go | 14 ++++---- cmd/lucy/tables.go | 3 ++ models/eventhandler.go | 11 +++--- models/shared.go | 26 ++++++++++++++ 10 files changed, 197 insertions(+), 47 deletions(-) diff --git a/app/api/devices.go b/app/api/devices.go index 4555772..f77dced 100644 --- a/app/api/devices.go +++ b/app/api/devices.go @@ -6,29 +6,11 @@ import ( "git.aiterp.net/lucifer/new-server/models" "github.com/gin-gonic/gin" "log" - "strconv" - "strings" ) func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) { - if strings.HasPrefix(fetchStr, "tag:") { - return config.DeviceRepository().FetchByReference(ctx, models.RKTag, fetchStr[4:]) - } else if strings.HasPrefix(fetchStr, "bridge:") { - return config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, fetchStr[7:]) - } else if strings.HasPrefix(fetchStr, "id:") { - return config.DeviceRepository().FetchByReference(ctx, models.RKDeviceID, fetchStr[7:]) - } else if strings.HasPrefix(fetchStr, "name:") { - return config.DeviceRepository().FetchByReference(ctx, models.RKName, fetchStr[7:]) - }else if fetchStr == "all" { - return config.DeviceRepository().FetchByReference(ctx, models.RKAll, "") - } else { - _, err := strconv.Atoi(fetchStr) - if err != nil { - return config.DeviceRepository().FetchByReference(ctx, models.RKName, fetchStr) - } - - return config.DeviceRepository().FetchByReference(ctx, models.RKDeviceID, fetchStr) - } + kind, value := models.ParseFetchString(fetchStr) + return config.DeviceRepository().FetchByReference(ctx, kind, value) } func Devices(r gin.IRoutes) { diff --git a/app/client/handler.go b/app/client/handler.go index 89200c0..b0b4fdd 100644 --- a/app/client/handler.go +++ b/app/client/handler.go @@ -6,7 +6,7 @@ import ( "git.aiterp.net/lucifer/new-server/models" ) -func (client *Client) GetHandlers(ctx context.Context, fetchStr string) ([]models.EventHandler, error) { +func (client *Client) GetHandlers(ctx context.Context) ([]models.EventHandler, error) { handlers := make([]models.EventHandler, 0, 16) err := client.Fetch(ctx, "GET", "/api/event-handlers", &handlers, nil) if err != nil { @@ -16,6 +16,16 @@ func (client *Client) GetHandlers(ctx context.Context, fetchStr string) ([]model return handlers, nil } +func (client *Client) AddHandler(ctx context.Context, handler models.EventHandler) (*models.EventHandler, error) { + var result models.EventHandler + err := client.Fetch(ctx, "POST", "/api/event-handlers", &result, handler) + if err != nil { + return nil, err + } + + return &result, nil +} + func (client *Client) DeleteHandler(ctx context.Context, id int) (*models.EventHandler, error) { var handler models.EventHandler err := client.Fetch(ctx, "DELETE", fmt.Sprintf("/api/event-handlers/%d", id), &handler, nil) diff --git a/app/services/events.go b/app/services/events.go index c1bac4d..eb81679 100644 --- a/app/services/events.go +++ b/app/services/events.go @@ -144,6 +144,10 @@ func handleEvent(event models.Event) (responses []models.Event) { newState.Intensity = &newIntensity } + if action.SetTemperature != nil { + newState.Temperature = action.SetTemperature + } + err = allDevices[i].SetState(newState) if err != nil { log.Println("Error updating state for device", device.ID, "err:", err) diff --git a/cmd/lucy/command.go b/cmd/lucy/command.go index 9e1af72..4c9b033 100644 --- a/cmd/lucy/command.go +++ b/cmd/lucy/command.go @@ -1,15 +1,17 @@ package main import ( + "git.aiterp.net/lucifer/new-server/models" "log" "strconv" "strings" ) type Param struct { - Index int - Key string - Value string + Index int + Key string + Value string + Operator string } func (p *Param) String() *string { @@ -28,7 +30,6 @@ func (p *Param) StringOr(fallback string) string { return p.Value } - func (p *Param) Int() *int { if p == nil { return nil @@ -42,6 +43,15 @@ func (p *Param) Int() *int { return &n } +func (p *Param) IntOr(other int) int { + val := p.Int() + if val == nil { + return other + } + + return *val +} + func (p *Param) Float() *float64 { if p == nil { return nil @@ -108,7 +118,7 @@ func (p Params) Subset(prefix string) Params { for _, param := range p { if param.Index == -1 && strings.HasPrefix(param.Key, prefix) { - res = append(res, Param{Index: -1, Key: param.Key[len(prefix):], Value: param.Value}) + res = append(res, Param{Index: -1, Key: param.Key[len(prefix):], Value: param.Value, Operator: param.Operator}) } } @@ -152,6 +162,34 @@ func (p Params) Strings(minIndex int) []string { return res } +func (p Params) EventConditions() map[string]models.EventCondition { + ecMap := make(map[string]models.EventCondition, len(p)) + + for _, param := range p { + element, ok := ecMap[param.Key] + if !ok { + element = models.EventCondition{} + } + + switch param.Operator { + case "<": + element.LT = param.Value + case "<=": + element.LTE = param.Value + case ">=": + element.GTE = param.Value + case ">": + element.GT = param.Value + default: + element.EQ = param.Value + } + + ecMap[param.Key] = element + } + + return ecMap +} + type Command struct { Name string Params Params @@ -169,10 +207,22 @@ func parseCommand(args []string) Command { nextIndex := 0 for _, arg := range args[1:] { - kv := strings.SplitN(arg, "=", 2) - - if len(kv) == 2 { - cmd.Params = append(cmd.Params, Param{Index: -1, Key: kv[0], Value: kv[1]}) + kvle := strings.SplitN(arg, "<=", 2) + kvge := strings.SplitN(arg, ">=", 2) + kvl := strings.SplitN(arg, "<", 2) + kvg := strings.SplitN(arg, ">", 2) + kve := strings.SplitN(arg, "=", 2) + + if len(kvle) == 2 { + cmd.Params = append(cmd.Params, Param{Index: -1, Key: kvle[0], Value: kvle[1], Operator: "<="}) + } else if len(kvge) == 2 { + cmd.Params = append(cmd.Params, Param{Index: -1, Key: kvge[0], Value: kvge[1], Operator: ">="}) + } else if len(kvl) == 2 { + cmd.Params = append(cmd.Params, Param{Index: -1, Key: kvl[0], Value: kvl[1], Operator: "<"}) + } else if len(kvg) == 2 { + cmd.Params = append(cmd.Params, Param{Index: -1, Key: kvg[0], Value: kvg[1], Operator: ">"}) + } else if len(kve) == 2 { + cmd.Params = append(cmd.Params, Param{Index: -1, Key: kve[0], Value: kve[1], Operator: "="}) } else { cmd.Params = append(cmd.Params, Param{Index: nextIndex, Value: arg}) nextIndex += 1 @@ -181,3 +231,12 @@ 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 1f84dcd..1081f7e 100644 --- a/cmd/lucy/handlercmd.go +++ b/cmd/lucy/handlercmd.go @@ -17,12 +17,68 @@ func handlerCmd( switch cmd.Name { case "list": - handlers, err := c.GetHandlers(ctx, cmd.Params.Get(0).StringOr("all")) + handlers, err := c.GetHandlers(ctx) if err != nil { log.Fatalln(err) } WriteHandlerInfoTable(os.Stdout, handlers) + case "add": + model := models.EventHandler{} + + // Event name + model.EventName = cmd.Params.Get(0).StringOr("") + if len(model.EventName) == 0 { + log.Fatalln("Event name needed!") + } + + // Search + fetchStr := cmd.Params.Get(1).String() + if fetchStr == nil { + log.Fatalln("Search filter needed!") + } + model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr) + + // One shot + model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true" + + // Priority + model.Priority = cmd.Params.Get("priority").IntOr(100) + + // Conditions + model.Conditions = cmd.Params.Subset("conditions").EventConditions() + + // Power, Color, Intensity, Temperature + nds := cmd.NewDeviceState("set-") + model.Actions.SetPower = nds.Power + model.Actions.SetColor = nds.Color + model.Actions.SetIntensity = nds.Intensity + + // Add intensity + model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float() + + // Fire + fireParams := cmd.Params.Subset("fire") + fireName := fireParams.Get("name").String() + if fireName != nil { + firePayloadParams := fireParams.Subset("payload") + + model.Actions.FireEvent = &models.Event{ + Name: *fireName, + Payload: make(map[string]string, len(firePayloadParams)), + } + + for key, value := range firePayloadParams.StringMap() { + model.Actions.FireEvent.Payload[key] = value + } + } + + newHandler, err := c.AddHandler(ctx, model) + if err != nil { + log.Fatalln(err) + } + + WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler}) case "delete": id := cmd.Params.Get(0).Int() if id == nil { diff --git a/cmd/lucy/help.go b/cmd/lucy/help.go index 4d77a2f..e94f138 100644 --- a/cmd/lucy/help.go +++ b/cmd/lucy/help.go @@ -7,16 +7,25 @@ EXAMPLES lucy set tag:Hexagon color=hs:35,1 intensity=0.3 lucy run SetProfile name=evening -EVENT HANDLER COMMANDS - handler list - handler delete - DEVICE COMMANDS list set tag <[+/-]tag-name> update +EVENT HANDLER COMMANDS + handler list + handler add \ + \ + \ + |>]*> \ + \ + \ + + handler delete + EVENT COMMANDS run <*=S> + +NOTE: You may have to surround arguments including < or > with quotation marks! ` diff --git a/cmd/lucy/main.go b/cmd/lucy/main.go index fb2baa1..887acac 100644 --- a/cmd/lucy/main.go +++ b/cmd/lucy/main.go @@ -37,16 +37,15 @@ func main() { log.Fatalln(err) } - WriteDeviceStateTable(os.Stdout, devices) + if cmd.Params.Get("info").StringOr("false") == "true" { + WriteDeviceInfoTable(os.Stdout, devices) + } else { + WriteDeviceStateTable(os.Stdout, devices) + } } case "set": { - devices, err := c.PutDeviceState(ctx, cmd.Params.Get(0).StringOr("all"), models.NewDeviceState{ - Power: cmd.Params.Get("power").Bool(), - Color: cmd.Params.Get("color").String(), - Intensity: cmd.Params.Get("intensity").Float(), - Temperature: cmd.Params.Get("temperature").Int(), - }) + devices, err := c.PutDeviceState(ctx, cmd.Params.Get(0).StringOr("all"), cmd.NewDeviceState("")) if err != nil { log.Fatalln(err) } @@ -124,3 +123,4 @@ func main() { _, _ = fmt.Fprintln(os.Stderr, helpString[1:]) } } + diff --git a/cmd/lucy/tables.go b/cmd/lucy/tables.go index 327dae0..cb3a15b 100644 --- a/cmd/lucy/tables.go +++ b/cmd/lucy/tables.go @@ -95,6 +95,9 @@ func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) { if h.Actions.SetIntensity != nil { actionStr += fmt.Sprintf("setIntensity=%.02f ", *h.Actions.SetIntensity) } + if h.Actions.SetTemperature != nil { + actionStr += fmt.Sprintf("setTemperature=%d ", *h.Actions.SetTemperature) + } if h.Actions.AddIntensity != nil { actionStr += fmt.Sprintf("addIntensity=%.02f ", *h.Actions.AddIntensity) } diff --git a/models/eventhandler.go b/models/eventhandler.go index 840ff50..1651d4b 100644 --- a/models/eventhandler.go +++ b/models/eventhandler.go @@ -196,11 +196,12 @@ func (c *EventCondition) matches(value string) bool { } type EventAction struct { - SetPower *bool `json:"setPower"` - SetColor *string `json:"setColor"` - SetIntensity *float64 `json:"setIntensity"` - AddIntensity *float64 `json:"addIntensity"` - FireEvent *Event `json:"fireEvent"` + SetPower *bool `json:"setPower"` + SetColor *string `json:"setColor"` + SetIntensity *float64 `json:"setIntensity"` + SetTemperature *int `json:"setTemperature"` + AddIntensity *float64 `json:"addIntensity"` + FireEvent *Event `json:"fireEvent"` } func (action *EventAction) Apply(other EventAction) { diff --git a/models/shared.go b/models/shared.go index f92a20f..3917b58 100644 --- a/models/shared.go +++ b/models/shared.go @@ -1,5 +1,10 @@ package models +import ( + "strconv" + "strings" +) + type ReferenceKind string var ( @@ -9,3 +14,24 @@ var ( RKAll ReferenceKind = "All" RKName ReferenceKind = "Name" ) + +func ParseFetchString(fetchStr string) (ReferenceKind, string) { + if strings.HasPrefix(fetchStr, "tag:") { + return RKTag, fetchStr[4:] + } else if strings.HasPrefix(fetchStr, "bridge:") { + return RKBridgeID, fetchStr[7:] + } else if strings.HasPrefix(fetchStr, "id:") { + return RKDeviceID, fetchStr[7:] + } else if strings.HasPrefix(fetchStr, "name:") { + return RKName, fetchStr[7:] + } else if fetchStr == "all" { + return RKAll, "" + } else { + _, err := strconv.Atoi(fetchStr) + if err != nil { + return RKName, fetchStr + } + + return RKDeviceID, fetchStr + } +} From 6fe0d8b864c0965a033dea0bfb3405eee1fea320 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Thu, 30 Sep 2021 23:07:11 +0200 Subject: [PATCH 2/6] adapt Nanoleaf driver to Canvas. --- internal/drivers/nanoleaf/bridge.go | 16 +++++++++++++--- internal/drivers/nanoleaf/data.go | 12 ++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/internal/drivers/nanoleaf/bridge.go b/internal/drivers/nanoleaf/bridge.go index 864eff3..c371b59 100644 --- a/internal/drivers/nanoleaf/bridge.go +++ b/internal/drivers/nanoleaf/bridge.go @@ -37,12 +37,22 @@ func (b *bridge) Devices() []models.Device { hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv() + shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType] + if !shapeTypeOK { + shapeType = "NanoLeaf" + } + + shapeIcon, shapeIconOK := shapeIconMap[panel.ShapeType] + if !shapeIconOK { + shapeIcon = "lightbulb" + } + results = append(results, models.Device{ ID: -1, BridgeID: b.externalID, InternalID: strconv.Itoa(int(panel.ID)), - Icon: "hexagon", - Name: fmt.Sprintf("Hexagon %d", i), + Icon: shapeIcon, + Name: fmt.Sprintf("%s %d", shapeType, i), Capabilities: []models.DeviceCapability{ models.DCPower, models.DCColorHS, @@ -83,7 +93,7 @@ func (b *bridge) Refresh(ctx context.Context) error { b.mu.Lock() PanelLoop: for _, panelInfo := range overview.PanelLayout.Data.PositionData { - if panelInfo.PanelID == 0 { + if shapeTypeMap[panelInfo.ShapeType] == "Shapes Controller" { continue } diff --git a/internal/drivers/nanoleaf/data.go b/internal/drivers/nanoleaf/data.go index 97ba26f..79c6399 100644 --- a/internal/drivers/nanoleaf/data.go +++ b/internal/drivers/nanoleaf/data.go @@ -135,6 +135,18 @@ var shapeTypeMap = map[int]string{ 12: "Shapes Controller", } +var shapeIconMap = map[int]string{ + 0: "triangle", + 1: "rhythm", + 2: "Square", + 3: "square", + 4: "square", + 7: "hexagon", + 8: "triangle", + 9: "triangle-small", + 12: "hexagon", +} + var shapeWidthMap = map[int]int{ 0: 150, 1: -1, From b83673b2cc68cd0365f60f90d47ffc8d5fb95fe8 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Thu, 30 Sep 2021 23:08:33 +0200 Subject: [PATCH 3/6] change nanoleaf default names to start at 1 instead of 0. --- internal/drivers/nanoleaf/bridge.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/drivers/nanoleaf/bridge.go b/internal/drivers/nanoleaf/bridge.go index c371b59..2c91727 100644 --- a/internal/drivers/nanoleaf/bridge.go +++ b/internal/drivers/nanoleaf/bridge.go @@ -52,7 +52,7 @@ func (b *bridge) Devices() []models.Device { BridgeID: b.externalID, InternalID: strconv.Itoa(int(panel.ID)), Icon: shapeIcon, - Name: fmt.Sprintf("%s %d", shapeType, i), + Name: fmt.Sprintf("%s %d", shapeType, i+1), Capabilities: []models.DeviceCapability{ models.DCPower, models.DCColorHS, From 2abb4b10241057398e74491578ea5185c126424c Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 2 Oct 2021 14:43:15 +0200 Subject: [PATCH 4/6] 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 From ad2f912a6f52df52f9659f5d07ea1abe8f18556b Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 2 Oct 2021 17:34:44 +0200 Subject: [PATCH 5/6] change presence events, fix non-eq conditions. --- internal/drivers/hue/bridge.go | 7 +++--- internal/drivers/hue/state.go | 38 ++++++++++++++++++++------------ models/eventhandler.go | 6 ++--- models/timeofday.go | 2 +- models/timeofday_test.go | 40 ++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 models/timeofday_test.go diff --git a/internal/drivers/hue/bridge.go b/internal/drivers/hue/bridge.go index cff00ec..b8b64a2 100644 --- a/internal/drivers/hue/bridge.go +++ b/internal/drivers/hue/bridge.go @@ -76,9 +76,10 @@ func (b *Bridge) Refresh(ctx context.Context) error { if state == nil { state = &hueSensorState{ - index: index, - uniqueID: sensor.UniqueID, - externalID: -1, + index: index, + uniqueID: sensor.UniqueID, + externalID: -1, + presenceCooldown: -2, } b.sensorStates = append(b.sensorStates, state) diff --git a/internal/drivers/hue/state.go b/internal/drivers/hue/state.go index 2883b81..288eaa0 100644 --- a/internal/drivers/hue/state.go +++ b/internal/drivers/hue/state.go @@ -92,7 +92,7 @@ type hueSensorState struct { uniqueID string prevData *SensorData prevTime time.Time - presenceCooldown bool + presenceCooldown int } func (state *hueSensorState) Update(newData SensorData) *models.Event { @@ -153,26 +153,36 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event { case "ZLLPresence": { if state.prevData != nil && state.prevData.State.Presence != newData.State.Presence { - name := models.ENSensorPresenceStarted - if !newData.State.Presence { - name = models.ENSensorPresenceEnding - state.presenceCooldown = true + if newData.State.Presence { + state.presenceCooldown = -1 + + return &models.Event{ + Name: models.ENSensorPresenceStarted, + Payload: map[string]string{ + "deviceId": strconv.Itoa(state.externalID), + "deviceInternalId": newData.UniqueID, + }, + } + } else { + state.presenceCooldown = 0 } + } + + if state.presenceCooldown == -2 { + state.presenceCooldown = int(time.Since(stateTime) / time.Minute) + } + + nextEventWait := time.Minute * time.Duration(state.presenceCooldown) + if state.presenceCooldown != -1 && !newData.State.Presence && time.Since(stateTime) > nextEventWait { + state.presenceCooldown += 1 - return &models.Event{ - Name: name, - Payload: map[string]string{ - "deviceId": strconv.Itoa(state.externalID), - "deviceInternalId": newData.UniqueID, - }, - } - } else if state.presenceCooldown && !newData.State.Presence && time.Since(stateTime) > time.Minute { - state.presenceCooldown = false return &models.Event{ Name: models.ENSensorPresenceEnded, Payload: map[string]string{ "deviceId": strconv.Itoa(state.externalID), "deviceInternalId": newData.UniqueID, + "minutesElapsed": strconv.Itoa(state.presenceCooldown - 1), + "secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60), }, } } diff --git a/models/eventhandler.go b/models/eventhandler.go index 81866e9..31a2b7e 100644 --- a/models/eventhandler.go +++ b/models/eventhandler.go @@ -173,11 +173,11 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s } } -var numRegex = regexp.MustCompile("^{-[0-9].}+$") +var numRegex = regexp.MustCompile("^-*[0-9]+(.[0-9]+)*$") func (c *EventCondition) matches(value string) bool { if numRegex.MatchString(value) { - numValue, _ := strconv.ParseFloat(c.LT, 64) + numValue, _ := strconv.ParseFloat(value, 64) stillAlive := true if c.LT != "" { @@ -197,7 +197,7 @@ func (c *EventCondition) matches(value string) bool { if stillAlive && c.GTE != "" { gte, _ := strconv.ParseFloat(c.GTE, 64) - stillAlive = numValue == gte + stillAlive = numValue >= gte } if stillAlive && c.GT != "" { diff --git a/models/timeofday.go b/models/timeofday.go index 780d067..58b26fd 100644 --- a/models/timeofday.go +++ b/models/timeofday.go @@ -75,7 +75,7 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool { if from == to { return t == from } else if from > to { - return t >= to || t <= from + return t >= from || t <= to } else { return t >= from && t <= to } diff --git a/models/timeofday_test.go b/models/timeofday_test.go new file mode 100644 index 0000000..db0b644 --- /dev/null +++ b/models/timeofday_test.go @@ -0,0 +1,40 @@ +package models + +import ( + "fmt" + "testing" +) + +func tod(s string) TimeOfDay { + tod, err := ParseTimeOfDay(s) + if err != nil { + panic(err) + } + + return tod +} + +func TestTimeOfDay_IsBetween(t *testing.T) { + table := []struct{ + Value TimeOfDay + From TimeOfDay + To TimeOfDay + Expected bool + }{ + { tod("16:13:11"), tod("07"), tod("21:30"), true }, + { tod("16:13:31"), tod("21:30"), tod("07"), false }, + { tod("23:15:32"), tod("21:30"), tod("07"), true }, + { tod("04:13:57"), tod("21:30"), tod("07"), true }, + { tod("16:14:43"), tod("15:30"), tod("16:00"), false }, + { tod("16:14:43"), tod("15:30"), tod("16:15"), true }, + } + + for i, row := range table { + t.Run(fmt.Sprintf("row_%d", i), func (t *testing.T) { + if row.Value.IsBetween(row.From, row.To) != row.Expected { + t.Log(row.Value, row.From, row.To) + t.Fail() + } + }) + } +} From ade49589a921c56f37d008a2798767a0f44e4611 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 3 Oct 2021 18:35:04 +0200 Subject: [PATCH 6/6] add another test and fix local. --- models/timeofday.go | 2 +- models/timeofday_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/models/timeofday.go b/models/timeofday.go index 58b26fd..1c5f1b2 100644 --- a/models/timeofday.go +++ b/models/timeofday.go @@ -82,7 +82,7 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool { } func CurrentTimeOfDay() TimeOfDay { - return TimeOfDayFromDate(time.Now()) + return TimeOfDayFromDate(time.Now().Local()) } func TimeOfDayFromDate(date time.Time) TimeOfDay { diff --git a/models/timeofday_test.go b/models/timeofday_test.go index db0b644..d341b1a 100644 --- a/models/timeofday_test.go +++ b/models/timeofday_test.go @@ -27,6 +27,8 @@ func TestTimeOfDay_IsBetween(t *testing.T) { { tod("04:13:57"), tod("21:30"), tod("07"), true }, { tod("16:14:43"), tod("15:30"), tod("16:00"), false }, { tod("16:14:43"), tod("15:30"), tod("16:15"), true }, + { tod("08:45:43"), tod("07:00:00"), tod("21:30:00"), true }, + { tod("07:00:00"), tod("07:00:00"), tod("21:30:00"), true }, } for i, row := range table {