Gisle Aune
3 years ago
13 changed files with 632 additions and 11 deletions
-
43app/api/devices.go
-
136app/client/client.go
-
18app/services/events.go
-
183cmd/lucy/command.go
-
18cmd/lucy/help.go
-
115cmd/lucy/main.go
-
74cmd/lucy/tables.go
-
1go.mod
-
4go.sum
-
9internal/mysql/devicerepo.go
-
4models/colorvalue.go
-
27models/device.go
-
3models/shared.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, |
||||
|
} |
@ -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 |
||||
|
} |
@ -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 <search> |
||||
|
set <search> <power=B> <color=C> <intensity=F> <temperature=N> |
||||
|
tag <search> <[+/-]tag-name> –-Tag |
||||
|
update <search> <name=S> <icon=S> <prop.*=S/NULL> |
||||
|
|
||||
|
EVENT COMMANDS |
||||
|
run <event name> <*=S> |
||||
|
`[1:] |
@ -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:]) |
||||
|
} |
||||
|
} |
@ -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() |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue