Compare commits
4
Commits
3.5.1
...
cedd0d1d28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cedd0d1d28 | ||
|
|
7471102413 | ||
|
|
bf4d000455 | ||
|
|
6947fc3461 |
+62
-7
@@ -7,6 +7,7 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -217,14 +218,28 @@ func Devices(r gin.IRoutes) {
|
||||
return []models.Device{}, nil
|
||||
}
|
||||
|
||||
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var scene *models.Scene
|
||||
if body.SceneName != "" {
|
||||
scene, err = config.SceneRepository().FindName(ctxOf(c), body.SceneName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
body.SceneID = scene.ID
|
||||
} else {
|
||||
scene, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
body.SceneName = scene.Name
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
|
||||
pushMode := c.Query("push") == "true"
|
||||
for i := range devices {
|
||||
@@ -265,6 +280,46 @@ func Devices(r gin.IRoutes) {
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.DELETE("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
|
||||
id, err := strconv.Atoi(c.Param("fetch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
device, err := config.DeviceRepository().Find(ctxOf(c), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Query("forgetmenot") != "true" {
|
||||
bridge, err := config.BridgeRepository().Find(ctxOf(c), device.BridgeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driver, err := config.DriverProvider().Provide(bridge.Driver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forgettableDriver, ok := driver.(models.ForgettableDriver)
|
||||
if !ok {
|
||||
return nil, models.ErrCannotForget
|
||||
}
|
||||
err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = config.DeviceRepository().Delete(ctxOf(c), device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}))
|
||||
}
|
||||
|
||||
func withSceneState(devices []models.Device) []models.Device {
|
||||
|
||||
+14
-2
@@ -5,15 +5,27 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func Scenes(r gin.IRoutes) {
|
||||
nonNumericRegex := regexp.MustCompile("[a-zA-Z ]")
|
||||
|
||||
findScene := func(c *gin.Context) (*models.Scene, error) {
|
||||
param := c.Param("id")
|
||||
if nonNumericRegex.MatchString(param) {
|
||||
return config.SceneRepository().FindName(ctxOf(c), param)
|
||||
} else {
|
||||
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
}
|
||||
}
|
||||
|
||||
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().FetchAll(ctxOf(c))
|
||||
}))
|
||||
|
||||
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
return findScene(c)
|
||||
}))
|
||||
|
||||
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -45,7 +57,7 @@ func Scenes(r gin.IRoutes) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
scene, err := findScene(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func WriteDeviceInfo(w io.Writer, devices []models.Device, fields []string) {
|
||||
if len(fields) == 1 && fields[0] == "all" {
|
||||
fields = allFields
|
||||
} else {
|
||||
fields = append(fields[:0:0], fields...)
|
||||
for i, field := range fields {
|
||||
if aliased, ok := fieldAliases[field]; ok {
|
||||
fields[i] = aliased
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length := 0
|
||||
for _, field := range fields {
|
||||
if mlFields[field] {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(field) > length {
|
||||
length = len(field)
|
||||
}
|
||||
}
|
||||
|
||||
for i, device := range devices {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if formatter, ok := fieldFormatters[field]; ok {
|
||||
_, _ = fmt.Fprintf(w, "%-*s %s\n", length+1, field+":", formatter(&device))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "%-*s (unknown field)\n", length+1, field+":")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var allFields = []string{
|
||||
"id", "name", "icon", "bridgeId", "tags", "buttonNames",
|
||||
"power", "color", "intensity", "temperature",
|
||||
"internalId", "sceneAssignment", "userProperties", "driverProperties",
|
||||
}
|
||||
|
||||
var mlFields = map[string]bool{
|
||||
"sceneAssignment": true,
|
||||
"userProperties": true,
|
||||
"driverProperties": true,
|
||||
}
|
||||
|
||||
var fieldFormatters = map[string]func(d *models.Device) string{
|
||||
"id": func(d *models.Device) string { return fmt.Sprint(d.ID) },
|
||||
"internalId": func(d *models.Device) string { return d.InternalID },
|
||||
"name": func(d *models.Device) string { return d.Name },
|
||||
"icon": func(d *models.Device) string { return d.Icon },
|
||||
"bridgeId": func(d *models.Device) string { return fmt.Sprint(d.BridgeID) },
|
||||
"tags": func(d *models.Device) string { return strings.Join(d.Tags, ", ") },
|
||||
"buttonNames": func(d *models.Device) string { return strings.Join(d.ButtonNames, ", ") },
|
||||
"power": func(d *models.Device) string { return fmt.Sprint(d.State.Power) },
|
||||
"color": func(d *models.Device) string { return d.State.Color.String() },
|
||||
"intensity": func(d *models.Device) string { return fmt.Sprint(d.State.Intensity) },
|
||||
"temperature": func(d *models.Device) string { return fmt.Sprint(d.State.Temperature) },
|
||||
|
||||
"sceneAssignment": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(64)
|
||||
for _, scene := range d.SceneAssignments {
|
||||
_, _ = fmt.Fprintf(str, "\n - id: %d\n", scene.SceneID)
|
||||
_, _ = fmt.Fprintf(str, " duration: %d\n", scene.DurationMS)
|
||||
_, _ = fmt.Fprintf(str, " group: %s\n", scene.Group)
|
||||
_, _ = fmt.Fprintf(str, " startTime: %s", scene.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if len(d.SceneAssignments) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"userProperties": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(64)
|
||||
str.WriteString("\n")
|
||||
keys := make([]string, 0, len(d.UserProperties))
|
||||
for key := range d.UserProperties {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
_, _ = fmt.Fprintf(str, " %s: %s\n", key, d.UserProperties[key])
|
||||
}
|
||||
if len(d.UserProperties) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"driverProperties": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(256)
|
||||
str.WriteString("\n")
|
||||
keys := make([]string, 0, len(d.DriverProperties))
|
||||
for key := range d.DriverProperties {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
j, _ := json.Marshal(d.DriverProperties[key])
|
||||
_, _ = fmt.Fprintf(str, " %s: %s\n", key, string(j))
|
||||
}
|
||||
if len(d.DriverProperties) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"capabilities": func(d *models.Device) string {
|
||||
capStr := make([]string, len(d.Capabilities))
|
||||
for i := range d.Capabilities {
|
||||
capStr[i] = string(d.Capabilities[i])
|
||||
}
|
||||
return strings.Join(capStr, ", ")
|
||||
},
|
||||
}
|
||||
|
||||
var fieldAliases = map[string]string{
|
||||
"bridge": "bridgeId",
|
||||
"buttons": "buttonNames",
|
||||
"internal": "internalId",
|
||||
"caps": "capabilities",
|
||||
"cap": "capabilities",
|
||||
"userprops": "userProperties",
|
||||
"uprops": "userProperties",
|
||||
"driverprops": "driverProperties",
|
||||
"dprops": "driverProperties",
|
||||
"internalid": "internalId",
|
||||
"bridgeid": "bridgeId",
|
||||
"buttonnames": "buttonNames",
|
||||
"scene": "sceneAssignment",
|
||||
}
|
||||
+2
-2
@@ -37,8 +37,8 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if cmd.Params.Get("info").StringOr("false") == "true" {
|
||||
WriteDeviceInfoTable(os.Stdout, devices)
|
||||
if info := cmd.Params.Get("info").String(); info != nil {
|
||||
WriteDeviceInfo(os.Stdout, devices, strings.Split(*info, ","))
|
||||
} else {
|
||||
WriteDeviceStateTable(os.Stdout, devices)
|
||||
}
|
||||
|
||||
+22
-5
@@ -19,6 +19,16 @@ func sceneCmd(
|
||||
cmd := parseCommand(os.Args[2:])
|
||||
|
||||
switch cmd.Name {
|
||||
case "list":
|
||||
{
|
||||
scenes, err := c.GetScenes(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load scenes:", err)
|
||||
}
|
||||
|
||||
WriteSceneTable(os.Stdout, scenes)
|
||||
}
|
||||
|
||||
case "create", "update":
|
||||
{
|
||||
fileName := cmd.Params.Get(0).String()
|
||||
@@ -83,15 +93,22 @@ func sceneCmd(
|
||||
{
|
||||
fetch := cmd.Params.Get(0).String()
|
||||
id := cmd.Params.Get(1).Int()
|
||||
if fetch == nil || id == nil {
|
||||
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
|
||||
name := cmd.Params.Get(1).String()
|
||||
if fetch == nil || (id == nil && name == nil) {
|
||||
log.Println("Usage: lucy scene assign <fetch> <id|name> <group=S> <duration=I>")
|
||||
}
|
||||
|
||||
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
|
||||
SceneID: *id,
|
||||
assignment := models.DeviceSceneAssignment{
|
||||
Group: cmd.Params.Get("group").StringOr(*fetch),
|
||||
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
|
||||
})
|
||||
}
|
||||
if id != nil {
|
||||
assignment.SceneID = *id
|
||||
} else {
|
||||
assignment.SceneName = *name
|
||||
}
|
||||
|
||||
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", assignment)
|
||||
if err != nil {
|
||||
log.Println("Could not assign devices:", err)
|
||||
return
|
||||
|
||||
@@ -94,6 +94,24 @@ func WriteDeviceInfoTable(w io.Writer, devices []models.Device) {
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func WriteSceneTable(w io.Writer, scenes []models.Scene) {
|
||||
table := tablewriter.NewWriter(w)
|
||||
table.SetHeader([]string{"ID", "NAME", "ROLES", "INTERVAL"})
|
||||
|
||||
table.SetReflowDuringAutoWrap(false)
|
||||
|
||||
for _, v := range scenes {
|
||||
table.Append([]string{
|
||||
fmt.Sprint(v.ID),
|
||||
v.Name,
|
||||
fmt.Sprint(len(v.Roles)),
|
||||
fmt.Sprint(v.IntervalMS),
|
||||
})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
|
||||
table := tablewriter.NewWriter(w)
|
||||
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
|
||||
|
||||
@@ -23,6 +23,7 @@ type Bridge struct {
|
||||
externalID int
|
||||
lightStates []*hueLightState
|
||||
sensorStates []*hueSensorState
|
||||
quarantine map[string]time.Time
|
||||
syncingPublish uint32
|
||||
}
|
||||
|
||||
@@ -34,6 +35,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
||||
|
||||
b.mu.Lock()
|
||||
for index, light := range lightMap {
|
||||
if time.Now().Before(b.quarantine[light.Uniqueid]) {
|
||||
continue
|
||||
}
|
||||
|
||||
var state *hueLightState
|
||||
for _, existingState := range b.lightStates {
|
||||
if existingState.index == index {
|
||||
@@ -68,6 +73,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
||||
|
||||
b.mu.Lock()
|
||||
for index, sensor := range sensorMap {
|
||||
if time.Now().Before(b.quarantine[sensor.UniqueID]) {
|
||||
continue
|
||||
}
|
||||
|
||||
var state *hueSensorState
|
||||
for _, existingState := range b.sensorStates {
|
||||
if existingState.index == index {
|
||||
@@ -137,6 +146,44 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
|
||||
// Find index
|
||||
b.mu.Lock()
|
||||
found := false
|
||||
index := -1
|
||||
for i, ls := range b.lightStates {
|
||||
if ls.uniqueID == device.InternalID {
|
||||
found = true
|
||||
index = i
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if !found {
|
||||
return models.ErrNotFound
|
||||
}
|
||||
|
||||
// Delete light from bridge
|
||||
err := b.deleteLight(ctx, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway.
|
||||
b.mu.Lock()
|
||||
for i, ls := range b.lightStates {
|
||||
if ls.uniqueID == device.InternalID {
|
||||
b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...)
|
||||
}
|
||||
}
|
||||
if b.quarantine == nil {
|
||||
b.quarantine = make(map[string]time.Time, 1)
|
||||
}
|
||||
b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30)
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
||||
sensorMap, err := b.getSensors(ctx)
|
||||
if err != nil {
|
||||
@@ -175,6 +222,10 @@ func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightS
|
||||
return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) deleteLight(ctx context.Context, index int) error {
|
||||
return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) getToken(ctx context.Context) (string, error) {
|
||||
result := make([]CreateUserResponse, 0, 1)
|
||||
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
||||
|
||||
@@ -322,8 +322,8 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
|
||||
return err
|
||||
}
|
||||
|
||||
fastTicker := time.NewTicker(time.Second / 10)
|
||||
slowTicker := time.NewTicker(time.Second / 3)
|
||||
fastTicker := time.NewTicker(time.Second / 5)
|
||||
slowTicker := time.NewTicker(time.Second / 2)
|
||||
selectedTicker := fastTicker
|
||||
ticksUntilRefresh := 0
|
||||
ticksSinceChange := 0
|
||||
@@ -372,6 +372,15 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) ForgetDevice(ctx context.Context, bridge models.Bridge, device models.Device) error {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.ForgetDevice(ctx, device)
|
||||
}
|
||||
|
||||
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
|
||||
d.mu.Lock()
|
||||
for _, bridge := range d.bridges {
|
||||
|
||||
@@ -29,6 +29,16 @@ func (r *SceneRepo) Find(ctx context.Context, id int) (*models.Scene, error) {
|
||||
return r.populateOne(&scene)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) FindName(ctx context.Context, name string) (*models.Scene, error) {
|
||||
var scene sceneRecord
|
||||
err := r.DBX.GetContext(ctx, &scene, "SELECT * FROM scene WHERE name = ?", name)
|
||||
if err != nil {
|
||||
return nil, dbErr(err)
|
||||
}
|
||||
|
||||
return r.populateOne(&scene)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
|
||||
scenes := make([]sceneRecord, 0, 8)
|
||||
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
||||
|
||||
@@ -16,3 +16,7 @@ type Driver interface {
|
||||
Publish(ctx context.Context, bridge Bridge, devices []Device) error
|
||||
Run(ctx context.Context, bridge Bridge, ch chan<- Event) error
|
||||
}
|
||||
|
||||
type ForgettableDriver interface {
|
||||
ForgetDevice(ctx context.Context, bridge Bridge, device Device) error
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ var ErrUnexpectedResponse = errors.New("driver api returned unexpected response
|
||||
var ErrBridgeSearchFailed = errors.New("bridge search failed")
|
||||
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
|
||||
var ErrCannotForwardRequest = errors.New("driver is not able to forward requests")
|
||||
var ErrCannotForget = errors.New("forget not supported on this bridge type")
|
||||
|
||||
var ErrInvalidAddress = errors.New("invalid mac address")
|
||||
var ErrPayloadTooShort = errors.New("payload too short")
|
||||
|
||||
@@ -299,6 +299,7 @@ const (
|
||||
// instance is created for the group.
|
||||
type DeviceSceneAssignment struct {
|
||||
SceneID int `json:"sceneId"`
|
||||
SceneName string `json:"sceneName"`
|
||||
Group string `json:"group"`
|
||||
StartTime time.Time `json:"start"`
|
||||
DurationMS int64 `json:"durationMs"`
|
||||
@@ -306,6 +307,7 @@ type DeviceSceneAssignment struct {
|
||||
|
||||
type SceneRepository interface {
|
||||
Find(ctx context.Context, id int) (*Scene, error)
|
||||
FindName(ctx context.Context, name string) (*Scene, error)
|
||||
FetchAll(ctx context.Context) ([]Scene, error)
|
||||
Save(ctx context.Context, bridge *Scene) error
|
||||
Delete(ctx context.Context, bridge *Scene) error
|
||||
|
||||
Reference in New Issue
Block a user