From 06fa73b9161e6ec91a80b12f51c8665cea3aad23 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Fri, 24 Sep 2021 16:58:24 +0200 Subject: [PATCH] add beginning of CLI app and some backend fixes. --- app/api/devices.go | 47 ++++++++- app/client/client.go | 136 ++++++++++++++++++++++++++ app/services/events.go | 18 ++++ cmd/lucy/command.go | 183 +++++++++++++++++++++++++++++++++++ cmd/lucy/help.go | 18 ++++ cmd/lucy/main.go | 115 ++++++++++++++++++++++ cmd/lucy/tables.go | 74 ++++++++++++++ go.mod | 1 + go.sum | 4 + internal/mysql/devicerepo.go | 9 +- models/colorvalue.go | 4 +- models/device.go | 31 +++++- models/shared.go | 3 +- 13 files changed, 632 insertions(+), 11 deletions(-) create mode 100644 app/client/client.go create mode 100644 cmd/lucy/command.go create mode 100644 cmd/lucy/help.go create mode 100644 cmd/lucy/main.go create mode 100644 cmd/lucy/tables.go diff --git a/app/api/devices.go b/app/api/devices.go index 644d0a5..275961f 100644 --- a/app/api/devices.go +++ b/app/api/devices.go @@ -6,6 +6,7 @@ import ( "git.aiterp.net/lucifer/new-server/models" "github.com/gin-gonic/gin" "log" + "strconv" "strings" ) @@ -14,9 +15,18 @@ func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) 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 fetchStr == "all" { + } 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) } } @@ -30,9 +40,9 @@ func Devices(r gin.IRoutes) { return fetchDevices(ctxOf(c), c.Param("fetch")) })) - r.PUT("/batch", handler(func(c *gin.Context) (interface{}, error) { + r.PUT("", handler(func(c *gin.Context) (interface{}, error) { var body []struct { - Fetch string `json:"fetch"` + Fetch string `json:"fetch"` SetState models.NewDeviceState `json:"setState"` } err := parseBody(c, &body) @@ -81,6 +91,34 @@ func Devices(r gin.IRoutes) { return changed, nil })) + r.PUT("/:fetch", handler(func(c *gin.Context) (interface{}, error) { + update := models.DeviceUpdate{} + err := parseBody(c, &update) + if err != nil { + return nil, err + } + + devices, err := fetchDevices(ctxOf(c), c.Param("fetch")) + if err != nil { + return nil, err + } + if len(devices) == 0 { + return []models.Device{}, nil + } + + for i := range devices { + devices[i].ApplyUpdate(update) + + err := config.DeviceRepository().Save(context.Background(), &devices[i]) + if err != nil { + log.Println("Failed to save device for state:", err) + continue + } + } + + return devices, nil + })) + r.PUT("/:fetch/state", handler(func(c *gin.Context) (interface{}, error) { state := models.NewDeviceState{} err := parseBody(c, &state) @@ -159,6 +197,9 @@ func Devices(r gin.IRoutes) { index = i } } + if index == -1 { + continue + } device.Tags = append(device.Tags[:index], device.Tags[index+1:]...) } diff --git a/app/client/client.go b/app/client/client.go new file mode 100644 index 0000000..45c2f6c --- /dev/null +++ b/app/client/client.go @@ -0,0 +1,136 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "git.aiterp.net/lucifer/new-server/models" + "io" + "net" + "net/http" + "strings" + "time" +) + +type Client struct { + APIRoot string +} + +func (client *Client) GetDevices(ctx context.Context, fetchStr string) ([]models.Device, error) { + devices := make([]models.Device, 0, 16) + err := client.Fetch(ctx, "GET", "/api/devices/"+fetchStr, &devices, nil) + if err != nil { + return nil, err + } + + return devices, nil +} + +func (client *Client) PutDevice(ctx context.Context, fetchStr string, update models.DeviceUpdate) ([]models.Device, error) { + devices := make([]models.Device, 0, 16) + err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr, &devices, update) + if err != nil { + return nil, err + } + + return devices, nil +} + +func (client *Client) PutDeviceState(ctx context.Context, fetchStr string, update models.NewDeviceState) ([]models.Device, error) { + devices := make([]models.Device, 0, 16) + err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/state", &devices, update) + if err != nil { + return nil, err + } + + return devices, nil +} + +func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTags []string, removeTags []string) ([]models.Device, error) { + devices := make([]models.Device, 0, 16) + err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/tags", &devices, map[string][]string{ + "add": addTags, + "remove": removeTags, + }) + if err != nil { + return nil, err + } + + return devices, nil +} + +func (client *Client) FireEvent(ctx context.Context, event models.Event) error { + err := client.Fetch(ctx, "POST", "/api/events", nil, event) + if err != nil { + return err + } + + return nil +} + +func (client *Client) Fetch(ctx context.Context, method string, path string, dst interface{}, body interface{}) error { + var reqBody io.ReadWriter + if body != nil && method != "GET" { + reqBody = bytes.NewBuffer(make([]byte, 0, 512)) + + err := json.NewEncoder(reqBody).Encode(body) + if err != nil { + return err + } + } + + req, err := http.NewRequest(method, client.APIRoot+path, reqBody) + if err != nil { + return err + } + + res, err := httpClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer res.Body.Close() + + if !strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") { + return fmt.Errorf("%s: %s", path, res.Status) + } + + var resJson struct { + Code int `json:"code"` + Message *string `json:"message"` + Data json.RawMessage `json:"data"` + } + err = json.NewDecoder(res.Body).Decode(&resJson) + if err != nil { + return err + } + + if resJson.Code != 200 { + msg := "" + if resJson.Message != nil { + msg = *resJson.Message + } + + return fmt.Errorf("%d: %s", resJson.Code, msg) + } + + if dst == nil { + return nil + } + + return json.Unmarshal(resJson.Data, dst) +} + +var httpClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 16, + MaxIdleConnsPerHost: 16, + IdleConnTimeout: time.Minute, + }, + Timeout: time.Minute, +} diff --git a/app/services/events.go b/app/services/events.go index b655c84..b873924 100644 --- a/app/services/events.go +++ b/app/services/events.go @@ -46,6 +46,8 @@ var loc, _ = time.LoadLocation("Europe/Oslo") var ctx = context.Background() func handleEvent(event models.Event) (responses []models.Event) { + var deadHandlers []models.EventHandler + startTime := time.Now() defer func() { duration := time.Since(startTime) @@ -102,6 +104,10 @@ func handleEvent(event models.Event) (responses []models.Event) { continue } + if handler.OneShot { + deadHandlers = append(deadHandlers, handler) + } + if handler.Priority > highestPriority { highestPriority = handler.Priority prioritizedEvent = handler.Actions.FireEvent @@ -159,6 +165,18 @@ func handleEvent(event models.Event) (responses []models.Event) { wg.Done() }(device) } + for _, handler := range deadHandlers { + wg.Add(1) + + go func(handler models.EventHandler) { + err := config.EventHandlerRepository().Delete(context.Background(), &handler) + if err != nil { + log.Println("Failed to delete spent one-shot event handler:", err) + } + + wg.Done() + }(handler) + } wg.Wait() diff --git a/cmd/lucy/command.go b/cmd/lucy/command.go new file mode 100644 index 0000000..9e1af72 --- /dev/null +++ b/cmd/lucy/command.go @@ -0,0 +1,183 @@ +package main + +import ( + "log" + "strconv" + "strings" +) + +type Param struct { + Index int + Key string + Value string +} + +func (p *Param) String() *string { + if p == nil { + return nil + } + + return &p.Value +} + +func (p *Param) StringOr(fallback string) string { + if p == nil { + return fallback + } + + return p.Value +} + + +func (p *Param) Int() *int { + if p == nil { + return nil + } + + n, err := strconv.Atoi(p.Value) + if err != nil { + return nil + } + + return &n +} + +func (p *Param) Float() *float64 { + if p == nil { + return nil + } + + n, err := strconv.ParseFloat(p.Value, 64) + if err != nil { + return nil + } + + return &n +} + +func (p *Param) Bool() *bool { + if p == nil { + return nil + } + + v := strings.ToLower(p.Value) + if v == "yes" || v == "true" || v == "on" { + r := true + return &r + } else if v == "no" || v == "false" || v == "off" { + r := false + return &r + } + + return nil +} + +type Params []Param + +func (p Params) Get(key interface{}) *Param { + switch key := key.(type) { + case string: + for _, p := range p { + if p.Key == key { + return &p + } + } + case int: + for _, p := range p { + if p.Index == key { + return &p + } + } + default: + log.Panicf("Incorrect key type %T", key) + } + + return nil +} + +func (p Params) Subset(prefix string) Params { + if len(p) == 0 { + return Params{} + } + + if len(prefix) > 0 && !strings.HasSuffix(prefix, ".") { + prefix += "." + } + + res := make(Params, 0, len(p)) + + 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}) + } + } + + return res +} + +func (p Params) StringMap() map[string]string { + res := make(map[string]string) + for _, param := range p { + if param.Index == -1 { + res[param.Key] = param.Value + } + } + + return res +} + +func (p Params) StringPtrMap() map[string]*string { + res := make(map[string]*string) + for _, param := range p { + if param.Index == -1 { + if param.Value == "NULL" { + res[param.Key] = nil + } else { + res[param.Key] = ¶m.Value + } + } + } + + return res +} + +func (p Params) Strings(minIndex int) []string { + res := make([]string, 0, len(p)) + for _, param := range p { + if param.Index >= minIndex { + res = append(res, param.Value) + } + } + + return res +} + +type Command struct { + Name string + Params Params +} + +func parseCommand(args []string) Command { + if len(args) == 0 { + return Command{Name: "help"} + } + + cmd := Command{ + Name: args[0], + Params: make(Params, 0, len(args)-1), + } + + 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]}) + } else { + cmd.Params = append(cmd.Params, Param{Index: nextIndex, Value: arg}) + nextIndex += 1 + } + } + + return cmd +} diff --git a/cmd/lucy/help.go b/cmd/lucy/help.go new file mode 100644 index 0000000..72bbad1 --- /dev/null +++ b/cmd/lucy/help.go @@ -0,0 +1,18 @@ +package main + +var helpString = ` +lucy – lucifer CLI + +EXAMPLES + lucy set tag:Hexagon color=hs:35,1 intensity=0.3 + lucy run SetProfile name=evening + +DEVICE COMMANDS + list + set + tag <[+/-]tag-name> –-Tag + update + +EVENT COMMANDS + run <*=S> +`[1:] diff --git a/cmd/lucy/main.go b/cmd/lucy/main.go new file mode 100644 index 0000000..c87ef3a --- /dev/null +++ b/cmd/lucy/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "fmt" + "git.aiterp.net/lucifer/new-server/app/client" + "git.aiterp.net/lucifer/new-server/models" + "log" + "os" + "strings" +) + +func main() { + c := client.Client{APIRoot: "http://10.24.4.1:8000"} + cmd := parseCommand(os.Args[1:]) + + switch cmd.Name { + // DEVICE + case "list": + { + fetchStr := cmd.Params.Get(0).String() + if fetchStr == nil { + s := "all" + fetchStr = &s + } + + devices, err := c.GetDevices(context.Background(), *fetchStr) + if err != nil { + log.Fatalln(err) + } + + WriteDeviceStateTable(os.Stdout, devices) + } + case "set": + { + devices, err := c.PutDeviceState(context.Background(), 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(), + }) + if err != nil { + log.Fatalln(err) + } + + WriteDeviceStateTable(os.Stdout, devices) + } + case "tag": + { + var addTags []string + var removeTags []string + + for _, tag := range cmd.Params.Strings(1) { + if strings.HasPrefix(tag, "+") { + addTags = append(addTags, tag[1:]) + } else if strings.HasPrefix(tag, "-") { + removeTags = append(removeTags, tag[1:]) + } else { + addTags = append(addTags, tag) + } + } + + devices, err := c.PutDeviceTags( + context.Background(), + cmd.Params.Get(0).StringOr("all"), + addTags, + removeTags, + ) + if err != nil { + log.Fatalln(err) + } + + WriteDeviceInfoTable(os.Stdout, devices) + } + case "update": + { + devices, err := c.PutDevice(context.Background(), cmd.Params.Get(0).StringOr("all"), models.DeviceUpdate{ + Name: cmd.Params.Get("name").String(), + Icon: cmd.Params.Get("icon").String(), + UserProperties: cmd.Params.Subset("prop.").StringPtrMap(), + }) + if err != nil { + log.Fatalln(err) + } + + WriteDeviceInfoTable(os.Stdout, devices) + } + + // EVENT + case "run": + { + name := cmd.Params.Get(0).String() + if name == nil { + return + } + + event := models.Event{ + Name: *name, + Payload: cmd.Params.StringMap(), + } + + err := c.FireEvent(context.Background(), event) + if err != nil { + log.Fatalln(err) + } + } + + default: + if cmd.Name != "help" { + log.Println("Unknown command:", cmd.Name) + } + + _, _ = fmt.Fprintln(os.Stderr, helpString[1:]) + } +} diff --git a/cmd/lucy/tables.go b/cmd/lucy/tables.go new file mode 100644 index 0000000..394eec0 --- /dev/null +++ b/cmd/lucy/tables.go @@ -0,0 +1,74 @@ +package main + +import ( + "git.aiterp.net/lucifer/new-server/models" + "github.com/olekukonko/tablewriter" + "io" + "strconv" + "strings" +) + +func WriteDeviceStateTable(w io.Writer, devices []models.Device) { + table := tablewriter.NewWriter(w) + table.SetHeader([]string{"ID", "NAME", "POWER", "COLOR", "INTENSITY", "TEMPERATURE"}) + + table.SetReflowDuringAutoWrap(true) + + for _, v := range devices { + powerStr := "" + if v.HasCapability(models.DCPower) { + powerStr = strconv.FormatBool(v.State.Power) + } + + colorStr := "" + if v.HasCapability(models.DCColorHSK, models.DCColorHS, models.DCColorKelvin) { + colorStr = v.State.Color.String() + } + + temperatureString := "" + if v.HasCapability(models.DCTemperatureControl, models.DCTemperatureSensor) { + temperatureString = strconv.FormatFloat(v.State.Temperature, 'f', -1, 64) + } + + intensityString := "" + if v.HasCapability(models.DCIntensity) { + intensityString = strconv.FormatFloat(v.State.Intensity, 'f', -1, 64) + } + + table.Append([]string{ + strconv.Itoa(v.ID), + v.Name, + powerStr, + colorStr, + intensityString, + temperatureString, + }) + } + + table.Render() +} + +func WriteDeviceInfoTable(w io.Writer, devices []models.Device) { + table := tablewriter.NewWriter(w) + table.SetHeader([]string{"ID", "NAME", "ICON", "BUTTONS", "TAGS", "USER PROPERTIES"}) + + table.SetReflowDuringAutoWrap(false) + + for _, v := range devices { + propStr := "" + for key, value := range v.UserProperties { + propStr += key + "=" + value + " " + } + + table.Append([]string{ + strconv.Itoa(v.ID), + v.Name, + v.Icon, + strings.Join(v.ButtonNames, ","), + strings.Join(v.Tags, ","), + propStr, + }) + } + + table.Render() +} diff --git a/go.mod b/go.mod index 57bc5e1..54d1ef7 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/lib/pq v1.10.3 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 // indirect github.com/pressly/goose v2.7.0+incompatible golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect diff --git a/go.sum b/go.sum index 06f4fe8..4a62778 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -45,6 +47,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/mysql/devicerepo.go b/internal/mysql/devicerepo.go index ad72759..df6227c 100644 --- a/internal/mysql/devicerepo.go +++ b/internal/mysql/devicerepo.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "database/sql" "encoding/json" "git.aiterp.net/lucifer/new-server/models" sq "github.com/Masterminds/squirrel" @@ -66,6 +67,8 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference q = q.Where(sq.Eq{"id": strings.Split(value, ",")}) case models.RKBridgeID: q = q.Where(sq.Eq{"bridge_id": strings.Split(value, ",")}) + case models.RKName: + q = q.Where(sq.Like{"name": value}) case models.RKTag: allTags := strings.Split(strings.ReplaceAll(strings.ReplaceAll(value, "-", ",-"), "+", ",+"), ",") optionalTags := make([]string, 0, len(allTags)) @@ -91,8 +94,12 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference return []models.Device{}, nil } - query, args, err := q.ToSql() + query, args, err := q.OrderBy("name", "id").ToSql() if err != nil { + if err == sql.ErrNoRows { + return []models.Device{}, nil + } + return nil, dbErr(err) } diff --git a/models/colorvalue.go b/models/colorvalue.go index a9c5627..0bfff3e 100644 --- a/models/colorvalue.go +++ b/models/colorvalue.go @@ -23,10 +23,10 @@ func (c *ColorValue) IsKelvin() bool { func (c *ColorValue) String() string { if c.Kelvin > 0 { - return fmt.Sprintf("kelvin:%d", c.Kelvin) + return fmt.Sprintf("k:%d", c.Kelvin) } - return fmt.Sprintf("hsv:%f,%f", c.Hue, c.Saturation) + return fmt.Sprintf("hs:%g,%g", c.Hue, c.Saturation) } func ParseColorValue(raw string) (ColorValue, error) { diff --git a/models/device.go b/models/device.go index fe02e23..de51e88 100644 --- a/models/device.go +++ b/models/device.go @@ -19,6 +19,12 @@ type Device struct { Tags []string `json:"tags"` } +type DeviceUpdate struct { + Icon *string `json:"icon"` + Name *string `json:"name"` + UserProperties map[string]*string `json:"userProperties"` +} + // DeviceState contains optional state values that // - Power: Whether the device is powered on // - Color: Color value, if a color setting can be set on the device @@ -32,10 +38,10 @@ type DeviceState struct { } type NewDeviceState struct { - Power *bool `json:"power"` - Color *string `json:"color"` + Power *bool `json:"power"` + Color *string `json:"color"` Intensity *float64 `json:"intensity"` - Temperature int `json:"temperature"` + Temperature *int `json:"temperature"` } type DeviceCapability string @@ -79,6 +85,25 @@ var Capabilities = []DeviceCapability{ DCTemperatureSensor, } +func (d *Device) ApplyUpdate(update DeviceUpdate) { + if update.Name != nil { + d.Name = *update.Name + } + if update.Icon != nil { + d.Icon = *update.Icon + } + if d.UserProperties == nil { + d.UserProperties = make(map[string]string) + } + for key, value := range update.UserProperties { + if value != nil { + d.UserProperties[key] = *value + } else { + delete(d.UserProperties, key) + } + } +} + func (d *Device) Validate() error { d.Name = strings.Trim(d.Name, " \t\n ") if d.Name == "" { diff --git a/models/shared.go b/models/shared.go index ae35a83..f92a20f 100644 --- a/models/shared.go +++ b/models/shared.go @@ -7,6 +7,5 @@ var ( RKBridgeID ReferenceKind = "BridgeID" RKTag ReferenceKind = "Tag" RKAll ReferenceKind = "All" + RKName ReferenceKind = "Name" ) - -