12 Commits
Author SHA1 Message Date
Stian Fredrik Aune d91bd472dd Merge branch 'asmodeus' of git.aiterp.net:lucifer/new-server into asmodeus
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
continuous-integration/drone/tag Build is passing
2021-10-03 19:10:06 +02:00
Stian Fredrik Aune 251687b85d Edit. 2021-10-03 19:10:02 +02:00
gisle ade49589a9 add another test and fix local.
continuous-integration/drone/push Build is failing
2021-10-03 18:35:04 +02:00
gisle ad2f912a6f change presence events, fix non-eq conditions.
continuous-integration/drone/push Build is failing
2021-10-02 17:34:44 +02:00
gisle 2abb4b1024 add time-of-day limitation to events.
continuous-integration/drone/push Build is failing
2021-10-02 14:43:15 +02:00
gisle b83673b2cc change nanoleaf default names to start at 1 instead of 0.
continuous-integration/drone/push Build is failing
2021-09-30 23:08:33 +02:00
gisle 6fe0d8b864 adapt Nanoleaf driver to Canvas.
continuous-integration/drone/push Build is failing
2021-09-30 23:07:11 +02:00
Stian Fredrik Aune c7d7e264ca Lucy handler add 2021-09-28 21:54:03 +02:00
gisle 0cd325e8db add SaveMode for device which can be 0 (All) or a bitmask for what to save. It did not improve performance
continuous-integration/drone/push Build is passing
2021-09-26 10:50:54 +02:00
Stian Fredrik Aune b1fa52e53f lucy handler delete
continuous-integration/drone/push Build is passing
2021-09-25 19:03:11 +02:00
Stian Fredrik Aune b28496fd6b less focus on oneshot
continuous-integration/drone/push Build is passing
2021-09-25 17:27:56 +02:00
Stian Fredrik Aune 35c3a387c9 lucy handler list
continuous-integration/drone/push Build is passing
2021-09-25 17:26:27 +02:00
24 changed files with 862 additions and 127 deletions
+6 -24
View File
@@ -6,29 +6,11 @@ import (
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log" "log"
"strconv"
"strings"
) )
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) { func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
if strings.HasPrefix(fetchStr, "tag:") { kind, value := models.ParseFetchString(fetchStr)
return config.DeviceRepository().FetchByReference(ctx, models.RKTag, fetchStr[4:]) return config.DeviceRepository().FetchByReference(ctx, kind, value)
} 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)
}
} }
func Devices(r gin.IRoutes) { func Devices(r gin.IRoutes) {
@@ -80,7 +62,7 @@ func Devices(r gin.IRoutes) {
go func() { go func() {
for _, device := range changed { for _, device := range changed {
err := config.DeviceRepository().Save(context.Background(), &device) err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
if err != nil { if err != nil {
log.Println("Failed to save device for state:", err) log.Println("Failed to save device for state:", err)
continue continue
@@ -109,7 +91,7 @@ func Devices(r gin.IRoutes) {
for i := range devices { for i := range devices {
devices[i].ApplyUpdate(update) devices[i].ApplyUpdate(update)
err := config.DeviceRepository().Save(context.Background(), &devices[i]) err := config.DeviceRepository().Save(context.Background(), &devices[i], models.SMProperties)
if err != nil { if err != nil {
log.Println("Failed to save device for state:", err) log.Println("Failed to save device for state:", err)
continue continue
@@ -145,7 +127,7 @@ func Devices(r gin.IRoutes) {
go func() { go func() {
for _, device := range devices { for _, device := range devices {
err := config.DeviceRepository().Save(context.Background(), &device) err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
if err != nil { if err != nil {
log.Println("Failed to save device for state:", err) log.Println("Failed to save device for state:", err)
continue continue
@@ -204,7 +186,7 @@ func Devices(r gin.IRoutes) {
device.Tags = append(device.Tags[:index], device.Tags[index+1:]...) device.Tags = append(device.Tags[:index], device.Tags[index+1:]...)
} }
err = config.DeviceRepository().Save(ctxOf(c), device) err = config.DeviceRepository().Save(ctxOf(c), device, models.SMTags)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+16 -5
View File
@@ -35,17 +35,28 @@ func EventHandlers(r gin.IRoutes) {
})) }))
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) { r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
var body models.EventHandlerUpdate
err := parseBody(c, &body)
if err != nil {
return nil, err
}
handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id")) handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if c.Query("hard") == "true" {
var body models.EventHandler
err := parseBody(c, &body)
if err != nil || body.ID != intParam(c, "id") {
return nil, err
}
handler = &body
} else {
var body models.EventHandlerUpdate
err := parseBody(c, &body)
if err != nil {
return nil, err
}
handler.ApplyUpdate(body) handler.ApplyUpdate(body)
}
err = config.EventHandlerRepository().Save(ctxOf(c), handler) err = config.EventHandlerRepository().Save(ctxOf(c), handler)
if err != nil { if err != nil {
+57
View File
@@ -0,0 +1,57 @@
package client
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
)
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 {
return nil, err
}
return handlers, nil
}
func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHandler, error) {
var handler models.EventHandler
err := client.Fetch(ctx, "GET", fmt.Sprintf("/api/event-handlers/%d", id), &handler, nil)
if err != nil {
return nil, err
}
return &handler, nil
}
func (client *Client) PutHandler(ctx context.Context, handler *models.EventHandler) (*models.EventHandler, error) {
var response models.EventHandler
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d", handler.ID), &response, handler)
if err != nil {
return nil, err
}
return &response, 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)
if err != nil {
return nil, err
}
return &handler, nil
}
+8 -1
View File
@@ -93,6 +93,9 @@ func handleEvent(event models.Event) (responses []models.Event) {
if handler.EventName != event.Name { if handler.EventName != event.Name {
continue continue
} }
if !handler.IsWithinTimeRange() {
continue
}
devices, err := config.DeviceRepository().FetchByReference(ctx, handler.TargetKind, handler.TargetValue) devices, err := config.DeviceRepository().FetchByReference(ctx, handler.TargetKind, handler.TargetValue)
if err != nil { if err != nil {
@@ -144,6 +147,10 @@ func handleEvent(event models.Event) (responses []models.Event) {
newState.Intensity = &newIntensity newState.Intensity = &newIntensity
} }
if action.SetTemperature != nil {
newState.Temperature = action.SetTemperature
}
err = allDevices[i].SetState(newState) err = allDevices[i].SetState(newState)
if err != nil { if err != nil {
log.Println("Error updating state for device", device.ID, "err:", err) log.Println("Error updating state for device", device.ID, "err:", err)
@@ -157,7 +164,7 @@ func handleEvent(event models.Event) (responses []models.Event) {
wg.Add(1) wg.Add(1)
go func(device models.Device) { go func(device models.Device) {
err := config.DeviceRepository().Save(context.Background(), &device) err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
if err != nil { if err != nil {
log.Println("Failed to save device for state:", err) log.Println("Failed to save device for state:", err)
} }
+1 -1
View File
@@ -66,7 +66,7 @@ func checkNewDevices() error {
log.Println("Saving new device", driverDevice.InternalID) log.Println("Saving new device", driverDevice.InternalID)
err := config.DeviceRepository().Save(ctx, &driverDevice) err := config.DeviceRepository().Save(ctx, &driverDevice, models.SMState|models.SMProperties|models.SMTags)
if err != nil { if err != nil {
log.Println("Failed to save device:", err) log.Println("Failed to save device:", err)
continue continue
+87 -5
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"git.aiterp.net/lucifer/new-server/models"
"log" "log"
"strconv" "strconv"
"strings" "strings"
@@ -10,6 +11,7 @@ type Param struct {
Index int Index int
Key string Key string
Value string Value string
Operator string
} }
func (p *Param) String() *string { func (p *Param) String() *string {
@@ -20,6 +22,28 @@ func (p *Param) String() *string {
return &p.Value 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 { func (p *Param) StringOr(fallback string) string {
if p == nil { if p == nil {
return fallback return fallback
@@ -28,7 +52,6 @@ func (p *Param) StringOr(fallback string) string {
return p.Value return p.Value
} }
func (p *Param) Int() *int { func (p *Param) Int() *int {
if p == nil { if p == nil {
return nil return nil
@@ -42,6 +65,15 @@ func (p *Param) Int() *int {
return &n return &n
} }
func (p *Param) IntOr(other int) int {
val := p.Int()
if val == nil {
return other
}
return *val
}
func (p *Param) Float() *float64 { func (p *Param) Float() *float64 {
if p == nil { if p == nil {
return nil return nil
@@ -108,7 +140,7 @@ func (p Params) Subset(prefix string) Params {
for _, param := range p { for _, param := range p {
if param.Index == -1 && strings.HasPrefix(param.Key, prefix) { 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 +184,44 @@ func (p Params) Strings(minIndex int) []string {
return res 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
}
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 { type Command struct {
Name string Name string
Params Params Params Params
@@ -169,10 +239,22 @@ func parseCommand(args []string) Command {
nextIndex := 0 nextIndex := 0
for _, arg := range args[1:] { for _, arg := range args[1:] {
kv := strings.SplitN(arg, "=", 2) 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(kv) == 2 { if len(kvle) == 2 {
cmd.Params = append(cmd.Params, Param{Index: -1, Key: kv[0], Value: kv[1]}) 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 { } else {
cmd.Params = append(cmd.Params, Param{Index: nextIndex, Value: arg}) cmd.Params = append(cmd.Params, Param{Index: nextIndex, Value: arg})
nextIndex += 1 nextIndex += 1
+177
View File
@@ -0,0 +1,177 @@
package main
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/client"
"git.aiterp.net/lucifer/new-server/models"
"log"
"os"
"strings"
)
func handlerCmd(
ctx context.Context,
c client.Client,
) {
cmd := parseCommand(os.Args[2:])
switch cmd.Name {
case "list":
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)
model = applyCmdToHandler(model, cmd)
newHandler, err := c.AddHandler(ctx, model)
if err != nil {
log.Fatalln(err)
}
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler})
case "edit":
id := cmd.Params.Get(0).IntOr(0)
if id == 0 {
log.Fatalln("ID missing")
}
model, err := c.GetHandler(ctx, id)
if err != nil {
log.Fatalln(err.Error())
}
eventStr := cmd.Params.Get("event").String()
if eventStr != nil {
model.EventName = *eventStr
}
fetchStr := cmd.Params.Get("search").String()
if fetchStr != nil {
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
}
modified := applyCmdToHandler(*model, cmd)
returned, err := c.PutHandler(ctx, &modified)
if err != nil {
log.Fatalln(err.Error())
}
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
case "delete":
id := cmd.Params.Get(0).Int()
if id == nil {
log.Fatalln("ID missing")
}
handler, err := c.DeleteHandler(ctx, *id)
if err != nil {
log.Fatalln(err)
}
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
default:
if cmd.Name != "help" {
log.Println("Unknown command:", cmd.Name)
}
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
}
}
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
// Remove keys
for _, elem := range cmd.Params.Strings(1) {
if elem[0] != '-' {
continue
}
keyToRemove := elem[1:]
if strings.HasPrefix(keyToRemove, "conditions.") {
condKeyToRemove := keyToRemove[11:]
delete(model.Conditions, condKeyToRemove)
} else if strings.HasPrefix(keyToRemove, "fire.payload.") && model.Actions.FireEvent != nil {
firePayloadKeyToRemove := keyToRemove[13:]
delete(model.Actions.FireEvent.Payload, firePayloadKeyToRemove)
} else {
switch keyToRemove {
case "set-power":
model.Actions.SetPower = nil
case "set-color":
model.Actions.SetColor = nil
case "set-intensity":
model.Actions.SetIntensity = nil
case "add-intensity":
model.Actions.AddIntensity = nil
case "fire":
fallthrough
case "fire.name":
model.Actions.FireEvent = nil
case "from":
model.From = models.Never
case "to":
model.To = models.Never
}
}
}
// 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"
// Priority
model.Priority = cmd.Params.Get("priority").IntOr(100)
// Conditions
model.Conditions = cmd.Params.Subset("conditions").EventConditions()
// Power, Color, Intensity, Temperature
nds := cmd.Params.DeviceState("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
}
}
return model
}
+21
View File
@@ -13,6 +13,27 @@ DEVICE COMMANDS
tag <search> <[+/-]tag-name> tag <search> <[+/-]tag-name>
update <search> <name=S> <icon=S> <prop.*=S/NULL> update <search> <name=S> <icon=S> <prop.*=S/NULL>
EVENT HANDLER COMMANDS
handler list
handler add <event> <search> \
<one-shot=B> \
<priority=N> \
<from=TOD to=TOD> \
<conditions.[any.*|all.*|*][<|<=|=|=>|>]*> \
<set-power=B> <set-color=C> <set-intensity=F> <set-temperature=N> \
<add-intensity=F> \
<fire.name=S> <fire.payload.*=S>
handler edit <id> \
(...all arguments from 'add', appending conditions and fire payload matchers...) \
<event=S> <search=S>
<-from> <-to> \
<-set-power> <-set-color> <-set-intensity> <-set-temperature> \
<-conditions.*> \
<-fire> <-fire.payload.*>
handler delete <id>
EVENT COMMANDS EVENT COMMANDS
run <event name> <*=S> run <event name> <*=S>
NOTE: You may have to surround arguments including < or > with quotation marks!
` `
+15 -10
View File
@@ -17,6 +17,8 @@ func main() {
cfg = &Config{Remote: "http://localhost:8000"} cfg = &Config{Remote: "http://localhost:8000"}
} }
ctx := context.Background()
c := client.Client{APIRoot: cfg.Remote} c := client.Client{APIRoot: cfg.Remote}
cmd := parseCommand(os.Args[1:]) cmd := parseCommand(os.Args[1:])
@@ -30,21 +32,20 @@ func main() {
fetchStr = &s fetchStr = &s
} }
devices, err := c.GetDevices(context.Background(), *fetchStr) devices, err := c.GetDevices(ctx, *fetchStr)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
if cmd.Params.Get("info").StringOr("false") == "true" {
WriteDeviceInfoTable(os.Stdout, devices)
} else {
WriteDeviceStateTable(os.Stdout, devices) WriteDeviceStateTable(os.Stdout, devices)
} }
}
case "set": case "set":
{ {
devices, err := c.PutDeviceState(context.Background(), cmd.Params.Get(0).StringOr("all"), models.NewDeviceState{ devices, err := c.PutDeviceState(ctx, cmd.Params.Get(0).StringOr("all"), cmd.Params.DeviceState(""))
Power: cmd.Params.Get("power").Bool(),
Color: cmd.Params.Get("color").String(),
Intensity: cmd.Params.Get("intensity").Float(),
Temperature: cmd.Params.Get("temperature").Int(),
})
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
@@ -67,7 +68,7 @@ func main() {
} }
devices, err := c.PutDeviceTags( devices, err := c.PutDeviceTags(
context.Background(), ctx,
cmd.Params.Get(0).StringOr("all"), cmd.Params.Get(0).StringOr("all"),
addTags, addTags,
removeTags, removeTags,
@@ -80,7 +81,7 @@ func main() {
} }
case "update": case "update":
{ {
devices, err := c.PutDevice(context.Background(), cmd.Params.Get(0).StringOr("all"), models.DeviceUpdate{ devices, err := c.PutDevice(ctx, cmd.Params.Get(0).StringOr("all"), models.DeviceUpdate{
Name: cmd.Params.Get("name").String(), Name: cmd.Params.Get("name").String(),
Icon: cmd.Params.Get("icon").String(), Icon: cmd.Params.Get("icon").String(),
UserProperties: cmd.Params.Subset("prop.").StringPtrMap(), UserProperties: cmd.Params.Subset("prop.").StringPtrMap(),
@@ -92,6 +93,9 @@ func main() {
WriteDeviceInfoTable(os.Stdout, devices) WriteDeviceInfoTable(os.Stdout, devices)
} }
case "handler":
handlerCmd(ctx, c)
// EVENT // EVENT
case "run": case "run":
{ {
@@ -105,7 +109,7 @@ func main() {
Payload: cmd.Params.StringMap(), Payload: cmd.Params.StringMap(),
} }
err := c.FireEvent(context.Background(), event) err := c.FireEvent(ctx, event)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
@@ -119,3 +123,4 @@ func main() {
_, _ = fmt.Fprintln(os.Stderr, helpString[1:]) _, _ = fmt.Fprintln(os.Stderr, helpString[1:])
} }
} }
+50
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"io" "io"
@@ -72,3 +73,52 @@ func WriteDeviceInfoTable(w io.Writer, devices []models.Device) {
table.Render() table.Render()
} }
func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
table := tablewriter.NewWriter(w)
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
table.SetReflowDuringAutoWrap(false)
for _, h := range handlers {
condStr := ""
for key, value := range h.Conditions {
condStr += fmt.Sprintf("%s=%s ", key, value)
}
actionStr := ""
if h.Actions.SetPower != nil {
actionStr += fmt.Sprintf("setPower=%t ", *h.Actions.SetPower)
}
if h.Actions.SetColor != nil {
actionStr += fmt.Sprintf("setColor=%s ", *h.Actions.SetColor)
}
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)
}
if h.Actions.FireEvent != nil {
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
}
eventName := h.EventName
if h.OneShot {
eventName += " (one shot)"
}
table.Append([]string{
strconv.Itoa(h.ID),
eventName,
strconv.Itoa(h.Priority),
condStr,
fmt.Sprintf("%s:%s", h.TargetKind, h.TargetValue),
actionStr,
})
}
table.Render()
}
+6
View File
@@ -20,6 +20,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
@@ -55,6 +56,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ= github.com/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ=
github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8= github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -69,6 +71,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -78,10 +81,13 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
printf "Building lucy...\n"
go mod download || exit 1
go build -ldflags "-w -s" -o lucy ./cmd/lucy || exit 1
printf "Removing old lucy...\n"
sudo rm /usr/local/bin/lucy > /dev/null 2>&1
printf "Installing...\n"
sudo mv ./lucy /usr/local/bin/lucy
+1
View File
@@ -79,6 +79,7 @@ func (b *Bridge) Refresh(ctx context.Context) error {
index: index, index: index,
uniqueID: sensor.UniqueID, uniqueID: sensor.UniqueID,
externalID: -1, externalID: -1,
presenceCooldown: -2,
} }
b.sensorStates = append(b.sensorStates, state) b.sensorStates = append(b.sensorStates, state)
+19 -9
View File
@@ -92,7 +92,7 @@ type hueSensorState struct {
uniqueID string uniqueID string
prevData *SensorData prevData *SensorData
prevTime time.Time prevTime time.Time
presenceCooldown bool presenceCooldown int
} }
func (state *hueSensorState) Update(newData SensorData) *models.Event { func (state *hueSensorState) Update(newData SensorData) *models.Event {
@@ -153,26 +153,36 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
case "ZLLPresence": case "ZLLPresence":
{ {
if state.prevData != nil && state.prevData.State.Presence != newData.State.Presence { if state.prevData != nil && state.prevData.State.Presence != newData.State.Presence {
name := models.ENSensorPresenceStarted if newData.State.Presence {
if !newData.State.Presence { state.presenceCooldown = -1
name = models.ENSensorPresenceEnding
state.presenceCooldown = true
}
return &models.Event{ return &models.Event{
Name: name, Name: models.ENSensorPresenceStarted,
Payload: map[string]string{ Payload: map[string]string{
"deviceId": strconv.Itoa(state.externalID), "deviceId": strconv.Itoa(state.externalID),
"deviceInternalId": newData.UniqueID, "deviceInternalId": newData.UniqueID,
}, },
} }
} else if state.presenceCooldown && !newData.State.Presence && time.Since(stateTime) > time.Minute { } else {
state.presenceCooldown = false 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{ return &models.Event{
Name: models.ENSensorPresenceEnded, Name: models.ENSensorPresenceEnded,
Payload: map[string]string{ Payload: map[string]string{
"deviceId": strconv.Itoa(state.externalID), "deviceId": strconv.Itoa(state.externalID),
"deviceInternalId": newData.UniqueID, "deviceInternalId": newData.UniqueID,
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
}, },
} }
} }
+13 -3
View File
@@ -37,12 +37,22 @@ func (b *bridge) Devices() []models.Device {
hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv() 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{ results = append(results, models.Device{
ID: -1, ID: -1,
BridgeID: b.externalID, BridgeID: b.externalID,
InternalID: strconv.Itoa(int(panel.ID)), InternalID: strconv.Itoa(int(panel.ID)),
Icon: "hexagon", Icon: shapeIcon,
Name: fmt.Sprintf("Hexagon %d", i), Name: fmt.Sprintf("%s %d", shapeType, i+1),
Capabilities: []models.DeviceCapability{ Capabilities: []models.DeviceCapability{
models.DCPower, models.DCPower,
models.DCColorHS, models.DCColorHS,
@@ -83,7 +93,7 @@ func (b *bridge) Refresh(ctx context.Context) error {
b.mu.Lock() b.mu.Lock()
PanelLoop: PanelLoop:
for _, panelInfo := range overview.PanelLayout.Data.PositionData { for _, panelInfo := range overview.PanelLayout.Data.PositionData {
if panelInfo.PanelID == 0 { if shapeTypeMap[panelInfo.ShapeType] == "Shapes Controller" {
continue continue
} }
+12
View File
@@ -135,6 +135,18 @@ var shapeTypeMap = map[int]string{
12: "Shapes Controller", 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{ var shapeWidthMap = map[int]int{
0: 150, 0: 150,
1: -1, 1: -1,
+12 -1
View File
@@ -117,7 +117,7 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference
return r.populate(ctx, records) return r.populate(ctx, records)
} }
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error { func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
tx, err := r.DBX.Beginx() tx, err := r.DBX.Beginx()
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
@@ -149,14 +149,19 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
} }
// Let's just be lazy for now, optimize later if need be. // Let's just be lazy for now, optimize later if need be.
if mode == 0 || mode&models.SMTags != 0 {
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID) _, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
} }
}
if mode == 0 || mode&models.SMProperties != 0 {
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID) _, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
} }
}
} else { } else {
res, err := tx.NamedExecContext(ctx, ` res, err := tx.NamedExecContext(ctx, `
INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names) INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names)
@@ -175,13 +180,16 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
device.ID = int(lastID) device.ID = int(lastID)
} }
if mode == 0 || mode&models.SMTags != 0 {
for _, tag := range device.Tags { for _, tag := range device.Tags {
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag) _, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
} }
} }
}
if mode == 0 || mode&models.SMProperties != 0 {
for key, value := range device.UserProperties { for key, value := range device.UserProperties {
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)", _, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
record.ID, key, value, record.ID, key, value,
@@ -206,7 +214,9 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
return dbErr(err) return dbErr(err)
} }
} }
}
if mode == 0 || mode&models.SMState != 0 {
_, err = tx.NamedExecContext(ctx, ` _, err = tx.NamedExecContext(ctx, `
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity) REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity) VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
@@ -221,6 +231,7 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
} }
}
return tx.Commit() return tx.Commit()
} }
+17 -2
View File
@@ -14,6 +14,8 @@ type eventHandlerRecord struct {
Priority int `db:"priority"` Priority int `db:"priority"`
TargetKind string `db:"target_kind"` TargetKind string `db:"target_kind"`
TargetValue string `db:"target_value"` TargetValue string `db:"target_value"`
From *string `db:"from_tod"`
To *string `db:"to_tod"`
ConditionsJSON json.RawMessage `db:"conditions"` ConditionsJSON json.RawMessage `db:"conditions"`
ActionsJSON json.RawMessage `db:"actions"` ActionsJSON json.RawMessage `db:"actions"`
} }
@@ -61,12 +63,14 @@ func (r *EventHandlerRepo) Save(ctx context.Context, handler *models.EventHandle
TargetValue: handler.TargetValue, TargetValue: handler.TargetValue,
ConditionsJSON: conditionsJson, ConditionsJSON: conditionsJson,
ActionsJSON: actionsJson, ActionsJSON: actionsJson,
From: handler.From.StringPtr(),
To: handler.To.StringPtr(),
} }
if record.ID == 0 { if record.ID == 0 {
res, err := r.DBX.NamedExecContext(ctx, ` res, err := r.DBX.NamedExecContext(ctx, `
INSERT INTO event_handler (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, :actions); VALUES (:event_name, :one_shot, :priority, :target_kind, :target_value, :conditions, :from_tod, :to_tod, :actions);
`, record) `, record)
if err != nil { if err != nil {
return dbErr(err) return dbErr(err)
@@ -87,6 +91,8 @@ func (r *EventHandlerRepo) Save(ctx context.Context, handler *models.EventHandle
target_kind = :target_kind, target_kind = :target_kind,
target_value = :target_value, target_value = :target_value,
conditions = :conditions, conditions = :conditions,
from_tod = :from_tod,
to_tod = :to_tod,
actions = :actions actions = :actions
WHERE id = :id WHERE id = :id
`, record) `, record)
@@ -139,6 +145,15 @@ func (r *EventHandlerRepo) populate(records []eventHandlerRecord) ([]models.Even
return nil, dbErr(err) 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) res = append(res, handler)
} }
+9 -1
View File
@@ -46,10 +46,18 @@ type NewDeviceState struct {
type DeviceCapability string type DeviceCapability string
type SaveMode int
const (
SMState SaveMode = 1
SMProperties SaveMode = 2
SMTags SaveMode = 4
)
type DeviceRepository interface { type DeviceRepository interface {
Find(ctx context.Context, id int) (*Device, error) Find(ctx context.Context, id int) (*Device, error)
FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error) FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error)
Save(ctx context.Context, device *Device) error Save(ctx context.Context, device *Device, mode SaveMode) error
Delete(ctx context.Context, device *Device) error Delete(ctx context.Context, device *Device) error
} }
+43 -3
View File
@@ -16,6 +16,8 @@ type EventHandler struct {
TargetKind ReferenceKind `json:"targetKind"` TargetKind ReferenceKind `json:"targetKind"`
TargetValue string `json:"targetValue"` TargetValue string `json:"targetValue"`
Actions EventAction `json:"actions"` Actions EventAction `json:"actions"`
From TimeOfDay `json:"from"`
To TimeOfDay `json:"to"`
} }
type EventHandlerRepository interface { type EventHandlerRepository interface {
@@ -42,6 +44,8 @@ type EventHandlerUpdate struct {
SetConditions map[string]EventCondition `json:"setConditions"` SetConditions map[string]EventCondition `json:"setConditions"`
PatchConditions map[string]*EventCondition `json:"patchConditions"` PatchConditions map[string]*EventCondition `json:"patchConditions"`
SetActions *EventAction `json:"setActions"` SetActions *EventAction `json:"setActions"`
SetFrom *TimeOfDay `json:"setFrom"`
SetTo *TimeOfDay `json:"setTo"`
} }
func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) { func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) {
@@ -63,6 +67,12 @@ func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) {
if update.SetOneShot != nil { if update.SetOneShot != nil {
h.OneShot = *update.SetOneShot h.OneShot = *update.SetOneShot
} }
if update.SetFrom != nil {
h.From = *update.SetFrom
}
if update.SetFrom != nil {
h.To = *update.SetFrom
}
if update.SetConditions != nil { if update.SetConditions != nil {
h.Conditions = update.SetConditions h.Conditions = update.SetConditions
@@ -100,6 +110,14 @@ func (h *EventHandler) MatchesEvent(event Event, targets []Device) bool {
return true 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 { func (c *EventCondition) check(key, value string, targets []Device) bool {
any := strings.HasPrefix(key, "any.") any := strings.HasPrefix(key, "any.")
all := strings.HasPrefix(key, "all.") all := strings.HasPrefix(key, "all.")
@@ -155,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 { func (c *EventCondition) matches(value string) bool {
if numRegex.MatchString(value) { if numRegex.MatchString(value) {
numValue, _ := strconv.ParseFloat(c.LT, 64) numValue, _ := strconv.ParseFloat(value, 64)
stillAlive := true stillAlive := true
if c.LT != "" { if c.LT != "" {
@@ -179,7 +197,7 @@ func (c *EventCondition) matches(value string) bool {
if stillAlive && c.GTE != "" { if stillAlive && c.GTE != "" {
gte, _ := strconv.ParseFloat(c.GTE, 64) gte, _ := strconv.ParseFloat(c.GTE, 64)
stillAlive = numValue == gte stillAlive = numValue >= gte
} }
if stillAlive && c.GT != "" { if stillAlive && c.GT != "" {
@@ -199,6 +217,7 @@ type EventAction struct {
SetPower *bool `json:"setPower"` SetPower *bool `json:"setPower"`
SetColor *string `json:"setColor"` SetColor *string `json:"setColor"`
SetIntensity *float64 `json:"setIntensity"` SetIntensity *float64 `json:"setIntensity"`
SetTemperature *int `json:"setTemperature"`
AddIntensity *float64 `json:"addIntensity"` AddIntensity *float64 `json:"addIntensity"`
FireEvent *Event `json:"fireEvent"` FireEvent *Event `json:"fireEvent"`
} }
@@ -220,3 +239,24 @@ func (action *EventAction) Apply(other EventAction) {
action.FireEvent = other.FireEvent action.FireEvent = other.FireEvent
} }
} }
func (c EventCondition) String() string {
str := make([]string, 0, 5)
if len(c.LT) > 0 {
str = append(str, "lt:"+c.LT)
}
if len(c.LTE) > 0 {
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)
}
if len(c.GT) > 0 {
str = append(str, "gte:"+c.GT)
}
return strings.Join(str, ";")
}
+26
View File
@@ -1,5 +1,10 @@
package models package models
import (
"strconv"
"strings"
)
type ReferenceKind string type ReferenceKind string
var ( var (
@@ -9,3 +14,24 @@ var (
RKAll ReferenceKind = "All" RKAll ReferenceKind = "All"
RKName ReferenceKind = "Name" 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
}
}
+138
View File
@@ -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 >= from || t <= to
} else {
return t >= from && t <= to
}
}
func CurrentTimeOfDay() TimeOfDay {
return TimeOfDayFromDate(time.Now().Local())
}
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
}
+42
View File
@@ -0,0 +1,42 @@
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 },
{ 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 {
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()
}
})
}
}
@@ -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