8 Commits
Author SHA1 Message Date
gisle 07173e4e56 Merge branch 'asmodeus' into feature-scenesystem
continuous-integration/drone/push Build is passing
2021-10-04 20:14:16 +02:00
gisle 7b0028ee2b new scene system is kind of working. I haven't tested changing between scenes and Transition effect yet, though.
continuous-integration/drone/push Build is passing
2021-10-04 20:13:57 +02:00
gisle d23a7327ef Merge branch 'asmodeus' into feature-scenesystem
continuous-integration/drone/push Build is failing
2021-10-03 18:42:33 +02:00
gisle a3695a3ddd some more work.
continuous-integration/drone/push Build is failing
2021-09-28 21:57:28 +02:00
gisle ffa757f742 rewrite of publishing mechanism to make scenes first-class.
continuous-integration/drone/push Build is failing
2021-09-26 22:15:49 +02:00
gisle f87c69efc7 this does not work, it needs massive changes.
continuous-integration/drone/push Build is passing
2021-09-26 16:20:02 +02:00
gisle b4fd6df454 Merge branch 'asmodeus' into feature-scenesystem
continuous-integration/drone/push Build is failing
2021-09-26 10:51:28 +02:00
gisle 44716c84d1 scene system so far.
continuous-integration/drone/push Build is failing
2021-09-25 19:04:54 +02:00
28 changed files with 85 additions and 976 deletions
+26 -59
View File
@@ -3,9 +3,9 @@ package api
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
"log"
"time"
)
@@ -17,21 +17,11 @@ func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error)
func Devices(r gin.IRoutes) {
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
devices, err := config.DeviceRepository().FetchByReference(ctxOf(c), models.RKAll, "")
if err != nil {
return nil, err
}
return withSceneState(devices), nil
return config.DeviceRepository().FetchByReference(ctxOf(c), models.RKAll, "")
}))
r.GET("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
if err != nil {
return nil, err
}
return withSceneState(devices), nil
return fetchDevices(ctxOf(c), c.Param("fetch"))
}))
r.PUT("", handler(func(c *gin.Context) (interface{}, error) {
@@ -82,7 +72,7 @@ func Devices(r gin.IRoutes) {
}
}()
return withSceneState(changed), nil
return changed, nil
}))
r.PUT("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
@@ -110,7 +100,7 @@ func Devices(r gin.IRoutes) {
}
}
return withSceneState(devices), nil
return devices, nil
}))
r.PUT("/:fetch/state", handler(func(c *gin.Context) (interface{}, error) {
@@ -137,12 +127,17 @@ func Devices(r gin.IRoutes) {
config.PublishChannel <- devices
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
if err != nil {
log.Println("Failed to save devices states")
}
go func() {
for _, device := range devices {
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
if err != nil {
log.Println("Failed to save device for state:", err)
continue
}
}
}()
return withSceneState(devices), nil
return devices, nil
}))
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) {
@@ -199,7 +194,7 @@ func Devices(r gin.IRoutes) {
}
}
return withSceneState(devices), nil
return devices, nil
}))
r.PUT("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
@@ -226,53 +221,25 @@ func Devices(r gin.IRoutes) {
}
body.StartTime = time.Now()
pushMode := c.Query("push") == "true"
for i := range devices {
if pushMode {
devices[i].SceneAssignments = append(devices[i].SceneAssignments, body)
} else {
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
}
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
}
config.PublishChannel <- devices
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
if err != nil {
return nil, err
}
return withSceneState(devices), nil
}))
r.DELETE("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
if err != nil {
return nil, err
}
if len(devices) == 0 {
return []models.Device{}, nil
}
eg := errgroup.Group{}
for i := range devices {
devices[i].SceneAssignments = []models.DeviceSceneAssignment{}
}
config.PublishChannel <- devices
device := devices[i]
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
eg.Go(func() error {
return config.DeviceRepository().Save(ctxOf(c), &device, 0)
})
}
err = eg.Wait()
if err != nil {
return nil, err
}
return withSceneState(devices), nil
return devices, nil
}))
}
func withSceneState(devices []models.Device) []models.Device {
res := make([]models.Device, 0, len(devices))
for _, device := range devices {
device.SceneState = publisher.Global().SceneState(device.ID)
res = append(res, device)
}
return res
}
+1 -32
View File
@@ -2,7 +2,6 @@ package api
import (
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
)
@@ -33,36 +32,6 @@ func Scenes(r gin.IRoutes) {
return nil, err
}
publisher.Global().UpdateScene(body)
return body, nil
}))
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
var body models.Scene
err := parseBody(c, &body)
if err != nil {
return nil, err
}
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
if err != nil {
return nil, err
}
body.ID = scene.ID
err = body.Validate()
if err != nil {
return nil, err
}
err = config.SceneRepository().Save(ctxOf(c), &body)
if err != nil {
return nil, err
}
publisher.Global().UpdateScene(body)
return body, nil
}))
}
}
-25
View File
@@ -60,31 +60,6 @@ func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTag
return devices, nil
}
func (client *Client) AssignDevice(ctx context.Context, fetchStr string, push bool, assignment models.DeviceSceneAssignment) ([]models.Device, error) {
query := ""
if push {
query = "?push=true"
}
devices := make([]models.Device, 0, 16)
err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/scene"+query, &devices, assignment)
if err != nil {
return nil, err
}
return devices, nil
}
func (client *Client) ClearDevice(ctx context.Context, fetchStr string) ([]models.Device, error) {
devices := make([]models.Device, 0, 16)
err := client.Fetch(ctx, "DELETE", "/api/devices/"+fetchStr+"/scene", &devices, nil)
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 {
+1 -1
View File
@@ -28,7 +28,7 @@ func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHand
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?hard=true", handler.ID), &response, handler)
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d", handler.ID), &response, handler)
if err != nil {
return nil, err
}
-16
View File
@@ -1,16 +0,0 @@
package client
import (
"context"
"git.aiterp.net/lucifer/new-server/models"
)
func (client *Client) GetScenes(ctx context.Context) ([]models.Scene, error) {
scenes := make([]models.Scene, 0, 16)
err := client.Fetch(ctx, "GET", "/api/scenes", &scenes, nil)
if err != nil {
return nil, err
}
return scenes, nil
}
+10
View File
@@ -5,6 +5,7 @@ import (
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"strconv"
"sync"
"time"
)
@@ -52,6 +53,15 @@ func runConnectToBridges() error {
log.Printf("Running bridge \"%s\" (%d)", bridge.Name, bridge.ID)
go func(bridge models.Bridge, cancel func()) {
savedDevices, err := config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, strconv.Itoa(bridge.ID))
if err != nil {
log.Println("Failed to fetch devices from db for refresh:", err)
}
err = driver.Publish(ctx, bridge, savedDevices)
if err != nil {
log.Println("Failed to publish devices from db before run:", err)
}
err = driver.Run(ctx, bridge, config.EventChannel)
log.Printf("Bridge \"%s\" (%d) stopped: %s", bridge.Name, bridge.ID, err)
-9
View File
@@ -155,15 +155,6 @@ func handleEvent(event models.Event) (responses []models.Event) {
if err != nil {
log.Println("Error updating state for device", device.ID, "err:", err)
}
if action.SetScene != nil {
action.SetScene.StartTime = time.Now()
allDevices[i].SceneAssignments = []models.DeviceSceneAssignment{*action.SetScene}
}
if action.PushScene != nil {
action.PushScene.StartTime = time.Now()
allDevices[i].SceneAssignments = append(allDevices[i].SceneAssignments, *action.PushScene)
}
}
config.PublishChannel <- allDevices
+6 -46
View File
@@ -19,29 +19,6 @@ type Publisher struct {
waiting map[int]chan struct{}
}
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
p.mu.Lock()
defer p.mu.Unlock()
if s := p.sceneAssignment[deviceID]; s != nil {
return s.LastState(deviceID)
}
return nil
}
func (p *Publisher) UpdateScene(data models.Scene) {
p.mu.Lock()
p.sceneData[data.ID] = &data
for _, scene := range p.scenes {
if scene.data.ID == data.ID {
scene.UpdateScene(data)
}
}
p.mu.Unlock()
}
func (p *Publisher) ReloadScenes(ctx context.Context) error {
scenes, err := config.SceneRepository().FetchAll(ctx)
if err != nil {
@@ -49,12 +26,8 @@ func (p *Publisher) ReloadScenes(ctx context.Context) error {
}
p.mu.Lock()
for i, scene := range scenes {
p.sceneData[scene.ID] = &scenes[i]
}
for _, scene := range p.scenes {
scene.UpdateScene(*p.sceneData[scene.data.ID])
for _, scene := range scenes {
p.sceneData[scene.ID] = &scene
}
p.mu.Unlock()
@@ -117,12 +90,10 @@ func (p *Publisher) Run() {
p.mu.Lock()
for i, scene := range p.scenes {
if (!scene.endTime.IsZero() && time.Now().After(scene.endTime)) || scene.Empty() {
if !scene.endTime.IsZero() && time.Now().After(scene.endTime) {
deleteList = append(deleteList, i-len(deleteList))
updatedList = append(updatedList, scene.AllDevices()...)
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
for _, device := range scene.AllDevices() {
p.sceneAssignment[device.ID] = nil
}
@@ -138,7 +109,7 @@ func (p *Publisher) Run() {
for _, i := range deleteList {
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
}
for _, device := range updatedList {
if !p.started[device.BridgeID] {
p.started[device.BridgeID] = true
@@ -177,11 +148,6 @@ func (p *Publisher) reassignDevice(device models.Device) bool {
// Stop here, no scene should be assigned.
return false
} else {
if p.sceneData[selectedAssignment.SceneID] == nil {
// Freeze until scene becomes available.
return false
}
}
if p.sceneAssignment[device.ID] != nil {
@@ -209,7 +175,6 @@ func (p *Publisher) reassignDevice(device models.Device) bool {
endTime: selectedAssignment.StartTime.Add(time.Duration(selectedAssignment.DurationMS) * time.Millisecond),
roleMap: make(map[int]int, 16),
roleList: make(map[int][]models.Device, 16),
lastStates: make(map[int]models.DeviceState, 16),
due: true,
}
p.sceneAssignment[device.ID] = newScene
@@ -293,12 +258,8 @@ var publisher = Publisher{
waiting: make(map[int]chan struct{}),
}
func Global() *Publisher {
return &publisher
}
func Initialize(ctx context.Context) error {
err := publisher.ReloadScenes(ctx)
err :=publisher.ReloadScenes(ctx)
if err != nil {
return err
}
@@ -307,9 +268,8 @@ func Initialize(ctx context.Context) error {
return err
}
go publisher.Run()
time.Sleep(time.Millisecond * 50)
go publisher.PublishChannel(config.PublishChannel)
go publisher.Run()
return nil
}
+6 -29
View File
@@ -6,13 +6,12 @@ import (
)
type Scene struct {
data *models.Scene
group string
startTime time.Time
endTime time.Time
roleMap map[int]int
roleList map[int][]models.Device
lastStates map[int]models.DeviceState
data *models.Scene
group string
startTime time.Time
endTime time.Time
roleMap map[int]int
roleList map[int][]models.Device
due bool
lastInterval int64
@@ -85,17 +84,6 @@ func (s *Scene) RemoveDevice(device models.Device) {
s.due = true
delete(s.roleMap, device.ID)
delete(s.lastStates, device.ID)
}
func (s *Scene) Empty() bool {
for _, list := range s.roleList {
if len(list) > 0 {
return false
}
}
return true
}
func (s *Scene) Due() bool {
@@ -164,8 +152,6 @@ func (s *Scene) Run() []models.Device {
continue
}
s.lastStates[device.ID] = device.State
updatedDevices = append(updatedDevices, device)
}
}
@@ -175,12 +161,3 @@ func (s *Scene) Run() []models.Device {
return updatedDevices
}
func (s *Scene) LastState(id int) *models.DeviceState {
lastState, ok := s.lastStates[id]
if !ok {
return nil
}
return &lastState
}
-12
View File
@@ -5,7 +5,6 @@ import (
"log"
"strconv"
"strings"
"time"
)
type Param struct {
@@ -222,17 +221,6 @@ func (p Params) DeviceState(prefix string) models.NewDeviceState {
}
}
func (p Params) SceneAssignment(prefix string) *models.DeviceSceneAssignment {
if p.Get(prefix+"scene") == nil {
return nil
}
return &models.DeviceSceneAssignment{
SceneID: p.Get(prefix+"scene").IntOr(-1),
Group: p.Get(prefix+"scene.group").StringOr(time.Now().Format(time.RFC3339)),
DurationMS: int64(p.Get(prefix+"scene.duration").IntOr(0)),
}
}
type Command struct {
Name string
+1 -9
View File
@@ -99,7 +99,7 @@ func handlerCmd(
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
// Remove keys
for _, elem := range cmd.Params.Strings(1) {
if !strings.HasPrefix(elem, "-") {
if elem[0] != '-' {
continue
}
keyToRemove := elem[1:]
@@ -128,10 +128,6 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
model.From = models.Never
case "to":
model.To = models.Never
case "set-scene":
model.Actions.SetScene = nil
case "push-scene":
model.Actions.PushScene = nil
}
}
}
@@ -177,9 +173,5 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
}
}
// Scenes
model.Actions.SetScene = cmd.Params.SceneAssignment("set-")
model.Actions.PushScene = cmd.Params.SceneAssignment("push-")
return model
}
-3
View File
@@ -96,9 +96,6 @@ func main() {
case "handler":
handlerCmd(ctx, c)
case "scene":
sceneCmd(ctx, c)
// EVENT
case "run":
{
-165
View File
@@ -1,165 +0,0 @@
package main
import (
"context"
"git.aiterp.net/lucifer/new-server/app/client"
"git.aiterp.net/lucifer/new-server/models"
"gopkg.in/yaml.v2"
"log"
"os"
"strconv"
"strings"
"unicode"
)
func sceneCmd(
ctx context.Context,
c client.Client,
) {
cmd := parseCommand(os.Args[2:])
switch cmd.Name {
case "create", "update":
{
fileName := cmd.Params.Get(0).String()
if fileName == nil {
log.Fatalln("Missing filename")
}
file, err := os.Open(*fileName)
if err != nil {
log.Fatalln("Failed to open file:", err)
}
defer file.Close()
yamlData := make(map[string]interface{})
err = yaml.NewDecoder(file).Decode(yamlData)
if err != nil {
log.Fatalln("Failed to decode file:", err)
return
}
yamlData = camelCasify(yamlData)
name, nameOk := yamlData["name"]
if !nameOk {
log.Fatalln("Missing name in yaml data.")
}
var scene models.Scene
if cmd.Name == "create" {
err := c.Fetch(ctx, "POST", "/api/scenes", &scene, yamlData)
if err != nil {
log.Fatalln("Failed to create scene:", err)
return
}
} else {
scenes, err := c.GetScenes(ctx)
if err != nil {
log.Fatalln("Failed to fetch existing scenes:", err)
return
}
id := -1
for _, scene := range scenes {
if scene.Name == name {
id = scene.ID
break
}
}
if id == -1 {
log.Fatalln("Could not find scene with name", name)
return
}
err = c.Fetch(ctx, "PUT", "/api/scenes/"+strconv.Itoa(id), &scene, yamlData)
if err != nil {
log.Fatalln("Failed to update scene:", err)
return
}
}
}
case "push", "assign":
{
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>")
}
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
SceneID: *id,
Group: cmd.Params.Get("group").StringOr(*fetch),
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
})
if err != nil {
log.Println("Could not assign devices:", err)
return
}
WriteDeviceInfoTable(os.Stdout, devices)
}
case "clear":
fetch := cmd.Params.Get(0).String()
if fetch == nil {
log.Println("Usage: lucy scene clear <fetch>")
}
devices, err := c.ClearDevice(ctx, *fetch)
if err != nil {
log.Println("Could not clear devices:", err)
return
}
WriteDeviceInfoTable(os.Stdout, devices)
}
}
func camelCasify(m map[string]interface{}) map[string]interface{} {
m2 := make(map[string]interface{}, len(m))
for key, value := range m {
b := strings.Builder{}
snake := false
for _, ch := range key {
if ch == '_' {
snake = true
} else if snake {
b.WriteRune(unicode.ToUpper(ch))
snake = false
} else {
b.WriteRune(ch)
}
}
switch value := value.(type) {
case []interface{}:
valueCopy := make([]interface{}, len(value))
for i, elem := range value {
switch elem := elem.(type) {
case map[interface{}]interface{}:
m3 := make(map[string]interface{})
for k, v := range elem {
if kStr, ok := k.(string); ok {
m3[kStr] = v
}
}
valueCopy[i] = camelCasify(m3)
case map[string]interface{}:
valueCopy[i] = camelCasify(elem)
default:
valueCopy[i] = elem
}
}
m2[b.String()] = valueCopy
case map[string]interface{}:
m2[b.String()] = camelCasify(value)
default:
m2[b.String()] = value
}
}
return m2
}
+3 -34
View File
@@ -14,32 +14,16 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
table.SetHeader([]string{"ID", "NAME", "POWER", "COLOR", "INTENSITY", "TEMPERATURE"})
table.SetReflowDuringAutoWrap(true)
table.SetColumnAlignment([]int{
tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_LEFT,
tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_LEFT,
tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_RIGHT,
})
for _, v := range devices {
powerStr := ""
if v.HasCapability(models.DCPower) {
if v.SceneState != nil && v.SceneState.Power != v.State.Power {
powerStr = strconv.FormatBool(v.SceneState.Power) + "*"
} else {
powerStr = strconv.FormatBool(v.State.Power)
}
powerStr = strconv.FormatBool(v.State.Power)
}
colorStr := ""
if v.HasCapability(models.DCColorHSK, models.DCColorHS, models.DCColorKelvin) {
if v.SceneState != nil && v.SceneState.Color.String() != v.State.Color.String() {
colorStr = v.SceneState.Color.String() + "*"
} else {
colorStr = v.State.Color.String()
}
colorStr = v.State.Color.String()
}
temperatureString := ""
@@ -49,11 +33,7 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
intensityString := ""
if v.HasCapability(models.DCIntensity) {
if v.SceneState != nil && v.SceneState.Intensity != v.State.Intensity {
intensityString = strconv.FormatFloat(v.SceneState.Intensity, 'g', 2, 64) + "*"
} else {
intensityString = strconv.FormatFloat(v.State.Intensity, 'f', -1, 64)
}
intensityString = strconv.FormatFloat(v.State.Intensity, 'f', -1, 64)
}
table.Append([]string{
@@ -124,17 +104,6 @@ func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
if h.Actions.FireEvent != nil {
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
}
if h.Actions.FireEvent != nil {
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
}
if h.Actions.SetScene != nil {
s := h.Actions.SetScene
actionStr += fmt.Sprintf("setScene=(id=%d,group=\"%s\",duration=%d) ", s.SceneID, s.Group, s.DurationMS)
}
if h.Actions.PushScene != nil {
s := h.Actions.PushScene
actionStr += fmt.Sprintf("pushScene=(id=%d,group=\"%s\",duration=%d) ", s.SceneID, s.Group, s.DurationMS)
}
eventName := h.EventName
if h.OneShot {
+15 -133
View File
@@ -6,11 +6,10 @@ import (
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup"
"io"
"log"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -98,17 +97,17 @@ func (b *Bridge) Refresh(ctx context.Context) error {
func (b *Bridge) SyncStale(ctx context.Context) error {
indices := make([]int, 0, 4)
arrayIndices := make([]int, 0, 4)
inputs := make([]LightStateInput, 0, 4)
eg, ctx := errgroup.WithContext(ctx)
b.mu.Lock()
for i, state := range b.lightStates {
for _, state := range b.lightStates {
if !state.stale {
continue
}
indices = append(indices, state.index)
arrayIndices = append(arrayIndices, i)
inputs = append(inputs, state.input)
}
b.mu.Unlock()
@@ -117,85 +116,24 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
return nil
}
groups := make([]*syncGroup, 0, 4)
for i := range inputs {
input := inputs[i]
for i, input := range inputs {
iCopy := i
index := indices[i]
inputCopy := input
found := false
for _, group := range groups {
if group.State.Equal(input) {
group.Indexes = append(group.Indexes, indices[i])
group.ArrayIndexes = append(group.ArrayIndexes, arrayIndices[i])
found = true
}
}
if !found {
groups = append(groups, &syncGroup{
GroupIndex: -1,
State: input,
Indexes: []int{indices[i]},
ArrayIndexes: []int{arrayIndices[i]},
})
}
}
groupMap, err := b.getGroups(ctx)
if err != nil {
return err
}
for id, data := range groupMap {
for _, group := range groups {
if group.Matches(&data) {
group.GroupIndex = id
break
}
}
}
for _, group := range groups {
if group.GroupIndex == -1 {
data := GroupData{
Name: "lucifer_auto_group",
Lights: []string{},
}
for _, idx := range group.Indexes {
data.Lights = append(data.Lights, strconv.Itoa(idx))
}
id, err := b.postGroup(ctx, data)
eg.Go(func() error {
err := b.putLightState(ctx, index, inputCopy)
if err != nil {
return err
}
group.GroupIndex = id
}
b.lightStates[iCopy].stale = false
return nil
})
}
log.Println("Updating", len(inputs), "lights on Hue bridge", b.externalID, "in", len(groups), "groups")
for _, group := range groups {
err := b.putGroupLightState(ctx, group.GroupIndex, group.State)
if err != nil {
return err
}
b.mu.Lock()
for _, arrayIndex := range group.ArrayIndexes {
b.lightStates[arrayIndex].stale = false
}
b.mu.Unlock()
if groupMap[group.GroupIndex].Name == "lucifer_auto_group" {
err := b.deleteGroup(ctx, group.GroupIndex)
if err != nil {
log.Println("Could not delete temporary group", err)
}
}
}
return nil
return eg.Wait()
}
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
@@ -232,10 +170,6 @@ func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateI
return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
}
func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightStateInput) error {
return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, 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)
@@ -273,34 +207,6 @@ func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
return result, nil
}
func (b *Bridge) getGroups(ctx context.Context) (map[int]GroupData, error) {
result := make(map[int]GroupData, 16)
err := b.get(ctx, "groups", &result)
if err != nil {
return nil, err
}
return result, nil
}
func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
var res []struct {
Success struct {
ID string `json:"id"`
} `json:"success"`
}
err := b.post(ctx, "groups", input, &res)
id, _ := strconv.Atoi(res[0].Success.ID)
return id, err
}
func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
}
func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
if b.token != "" {
resource = b.token + "/" + resource
@@ -320,30 +226,6 @@ func (b *Bridge) get(ctx context.Context, resource string, target interface{}) e
return json.NewDecoder(res.Body).Decode(target)
}
func (b *Bridge) delete(ctx context.Context, resource string, target interface{}) error {
if b.token != "" {
resource = b.token + "/" + resource
}
req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
if err != nil {
return err
}
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer res.Body.Close()
if target == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(target)
}
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
rb, err := reqBody(body)
if err != nil {
-88
View File
@@ -3,14 +3,8 @@ package hue
import (
"encoding/xml"
"errors"
"strconv"
)
type GroupData struct {
Name string `json:"name"`
Lights []string `json:"lights"`
}
type DiscoveryEntry struct {
Id string `json:"id"`
InternalIPAddress string `json:"internalipaddress"`
@@ -89,58 +83,6 @@ type LightStateInput struct {
TransitionTime *int `json:"transitiontime,omitempty"`
}
func (input *LightStateInput) Equal(other LightStateInput) bool {
if (input.On != nil) != (other.On != nil) {
return false
} else if input.On != nil && *input.On != *other.On {
return false
}
if (input.Bri != nil) != (other.Bri != nil) {
return false
} else if input.Bri != nil && *input.Bri != *other.Bri {
return false
}
if (input.Hue != nil) != (other.Hue != nil) {
return false
} else if input.Hue != nil && *input.Hue != *other.Hue {
return false
}
if (input.Sat != nil) != (other.Sat != nil) {
return false
} else if input.Sat != nil && *input.Sat != *other.Sat {
return false
}
if (input.Effect != nil) != (other.Effect != nil) {
return false
} else if input.Effect != nil && *input.Effect != *other.Effect {
return false
}
if (input.XY != nil) != (other.XY != nil) {
return false
} else if input.XY != nil && *input.XY != *other.XY {
return false
}
if (input.CT != nil) != (other.CT != nil) {
return false
} else if input.CT != nil && *input.CT != *other.CT {
return false
}
if (input.Alert != nil) != (other.Alert != nil) {
return false
} else if input.Alert != nil && *input.Alert != *other.Alert {
return false
}
return true
}
type LightData struct {
State LightState `json:"state"`
Type string `json:"type"`
@@ -250,36 +192,6 @@ type BridgeDeviceInfo struct {
} `xml:"device"`
}
type syncGroup struct {
GroupIndex int
State LightStateInput
Indexes []int
ArrayIndexes []int
}
func (sg *syncGroup) Matches(group *GroupData) bool {
if len(sg.Indexes) != len(group.Lights) {
return false
}
for _, idx := range sg.Indexes {
found := false
for _, idx2 := range group.Lights {
n := strconv.Itoa(idx)
if n == idx2 {
found = true
break
}
}
if !found {
return false
}
}
return true
}
var buttonNames = []string{"On", "DimUp", "DimDown", "Off"}
var errLinkButtonNotPressed = errors.New("link button not pressed")
+1 -37
View File
@@ -1,7 +1,6 @@
package hue
import (
"encoding/json"
"git.aiterp.net/lucifer/new-server/models"
"strconv"
"time"
@@ -17,37 +16,12 @@ type hueLightState struct {
stale bool
}
func (s *hueLightState) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Index int `json:"index"`
UniqueID string `json:"uniqueId"`
ExternalID int `json:"externalId"`
Info LightData `json:"info"`
Input LightStateInput `json:"input"`
Stale bool `json:"stale"`
}{
Index: s.index,
UniqueID: s.uniqueID,
ExternalID: s.externalID,
Info: s.info,
Input: s.input,
Stale: s.stale,
})
}
func (s *hueLightState) Update(state models.DeviceState) {
input := LightStateInput{}
if state.Power {
input.On = ptrBool(true)
if state.Color.IsKelvin() {
input.CT = ptrInt(1000000 / state.Color.Kelvin)
if *input.CT < s.info.Capabilities.Control.CT.Min {
*input.CT = s.info.Capabilities.Control.CT.Min
}
if *input.CT > s.info.Capabilities.Control.CT.Max {
*input.CT = s.info.Capabilities.Control.CT.Max
}
if s.input.CT == nil || *s.input.CT != *input.CT {
s.stale = true
}
@@ -93,13 +67,6 @@ func (s *hueLightState) Update(state models.DeviceState) {
}
func (s *hueLightState) CheckStaleness(state LightState) {
if s.input.On == nil || state.On != *s.input.On {
s.stale = true
}
if !state.On {
return
}
if state.ColorMode == "xy" {
s.stale = true
if s.input.CT == nil && s.input.Hue == nil {
@@ -113,7 +80,7 @@ func (s *hueLightState) CheckStaleness(state LightState) {
s.stale = true
}
} else {
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat != *s.input.Sat {
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat == *s.input.Sat {
s.stale = true
}
}
@@ -155,9 +122,6 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
if time.Since(stateTime) > time.Second*3 {
return nil
}
if state.prevData == nil {
return nil
}
pe := state.prevData.State.ButtonEvent
ce := newData.State.ButtonEvent
+1 -1
View File
@@ -177,7 +177,7 @@ func (b *bridge) Update(devices []models.Device) {
red, green, blue := color.RGB255()
newColor := [4]byte{red, green, blue, 255}
if newColor != panel.ColorRGBA {
panel.update(newColor, time.Now().Add(time.Millisecond*220))
panel.update(newColor, time.Now().Add(time.Millisecond*120))
}
} else {
panel.update([4]byte{0, 0, 0, 0}, time.Now())
-5
View File
@@ -2,7 +2,6 @@ package mysql
import (
"context"
"database/sql"
"encoding/json"
"git.aiterp.net/lucifer/new-server/models"
"github.com/jmoiron/sqlx"
@@ -33,10 +32,6 @@ func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
scenes := make([]sceneRecord, 0, 8)
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
if err != nil {
if err == sql.ErrNoRows {
return []models.Scene{}, nil
}
return nil, dbErr(err)
}
+1 -3
View File
@@ -26,11 +26,9 @@ func (c *ColorValue) String() string {
return fmt.Sprintf("k:%d", c.Kelvin)
}
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
return fmt.Sprintf("hs:%g,%g", c.Hue, c.Saturation)
}
func ParseColorValue(raw string) (ColorValue, error) {
tokens := strings.SplitN(raw, ":", 2)
if len(tokens) != 2 {
+1 -2
View File
@@ -18,7 +18,6 @@ type Device struct {
DriverProperties map[string]interface{} `json:"driverProperties"`
UserProperties map[string]string `json:"userProperties"`
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
SceneState *DeviceState `json:"sceneState"`
State DeviceState `json:"state"`
Tags []string `json:"tags"`
}
@@ -201,7 +200,7 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
if s.Color != nil {
c, err := ParseColorValue(*s.Color)
if err == nil {
c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360)
c.Hue = math.Mod(device.State.Color.Hue + c.Hue, 360)
c.Saturation *= device.State.Color.Saturation
c.Kelvin += device.State.Color.Kelvin
}
+6 -29
View File
@@ -5,7 +5,6 @@ import (
"regexp"
"strconv"
"strings"
"time"
)
type EventHandler struct {
@@ -169,20 +168,6 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
}
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
case "scene":
if len(device.SceneAssignments) == 0 {
return false, false
}
sceneId := -1
for _, assignment := range device.SceneAssignments {
duration := time.Duration(assignment.DurationMS) * time.Millisecond
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
sceneId = assignment.SceneID
}
}
return c.matches(strconv.Itoa(sceneId)), false
default:
return false, true
}
@@ -229,14 +214,12 @@ func (c *EventCondition) matches(value string) bool {
}
type EventAction struct {
SetPower *bool `json:"setPower"`
SetColor *string `json:"setColor"`
SetIntensity *float64 `json:"setIntensity"`
SetTemperature *int `json:"setTemperature"`
AddIntensity *float64 `json:"addIntensity"`
FireEvent *Event `json:"fireEvent"`
SetScene *DeviceSceneAssignment `json:"setScene"`
PushScene *DeviceSceneAssignment `json:"pushScene"`
SetPower *bool `json:"setPower"`
SetColor *string `json:"setColor"`
SetIntensity *float64 `json:"setIntensity"`
SetTemperature *int `json:"setTemperature"`
AddIntensity *float64 `json:"addIntensity"`
FireEvent *Event `json:"fireEvent"`
}
func (action *EventAction) Apply(other EventAction) {
@@ -255,12 +238,6 @@ func (action *EventAction) Apply(other EventAction) {
if action.FireEvent == nil {
action.FireEvent = other.FireEvent
}
if action.SetScene == nil {
action.SetScene = other.SetScene
}
if action.PushScene == nil {
action.PushScene = other.PushScene
}
}
func (c EventCondition) String() string {
+6 -10
View File
@@ -56,7 +56,7 @@ func (s *Scene) RoleIndex(device *Device) int {
type SceneEffect string
const (
SEStatic SceneEffect = "Static"
SEStatic SceneEffect = "Plain"
SERandom SceneEffect = "Random"
SEGradient SceneEffect = "Gradient"
SEWalkingGradient SceneEffect = "WalkingGradient"
@@ -114,13 +114,13 @@ func (r *SceneRole) Validate() error {
}
switch r.PowerMode {
case SPScene, SPDevice, SPBoth:
case SPScene, SPDevice:
default:
return ErrSceneRoleUnknownPowerMode
}
switch r.Effect {
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
case SEStatic, SERandom, SEGradient, SETransition:
default:
return ErrSceneRoleUnknownEffect
}
@@ -188,12 +188,7 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
case SPDevice:
newState.Power = nil
case SPScene:
// Do nothing
case SPBoth:
if newState.Power != nil {
powerIntersection := *newState.Power && device.State.Power
newState.Power = &powerIntersection
}
// Do nothing
}
return
@@ -206,6 +201,8 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
return r.States[0]
}
if r.Interpolate {
if len(r.States) == 2 {
return r.States[0].Interpolate(r.States[1], fac)
@@ -231,7 +228,6 @@ type ScenePowerMode string
const (
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
SPScene ScenePowerMode = "Scene" // Scene state decide power.
SPBoth ScenePowerMode = "Both" // Power is only on if both Device and Scene says it is.
)
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
-63
View File
@@ -1,63 +0,0 @@
name: Evening
interval: 1500
roles:
- effect: WalkingGradient
power_mode: Device
target_kind: Tag
target_value: Hexagon
interpolate: true
relative: false
order: -name
states:
- color: 'hs:30,1'
intensity: 0.20
- color: 'hs:30,1'
intensity: 0.15
- effect: Gradient
power_mode: Device
target_kind: Tag
target_value: SquareLeft
interpolate: true
relative: false
order: +name
states:
- color: 'hs:25,1'
intensity: 0.075
- color: 'hs:50,1'
intensity: 0.15
- effect: Random
power_mode: Device
target_kind: Tag
target_value: SquareRight
interpolate: true
relative: false
order: +name
states:
- color: 'hs:220,0.7'
intensity: 0.22
- color: 'hs:220,0.6'
intensity: 0.25
- effect: Static
power_mode: Device
target_kind: Tag
target_value: Accent
interpolate: true
relative: false
order: +name
states:
- color: 'hs:250,0.7'
intensity: 0.25
- effect: Static
power_mode: Device
target_kind: All
target_value: ""
interpolate: true
relative: false
order: +name
states:
- color: 'k:2000'
intensity: 0.3
-26
View File
@@ -1,26 +0,0 @@
name: Flash
interval: 0
roles:
- effect: Static
power_mode: Device
target_kind: Tag
target_value: Nanoleaf
states:
- intensity: 1
color: hs:220,0.1
- effect: Static
power_mode: Device
target_kind: Tag
target_value: Hue
states:
- intensity: 1
color: k:6500
- effect: Static
power_mode: Device
target_kind: All
target_value: ""
states:
- intensity: 1
color: hs:0,0
-57
View File
@@ -1,57 +0,0 @@
name: Late
interval: 60000
roles:
- effect: Random
power_mode: Device
target_kind: Tag
target_value: Hexagon
interpolate: true
relative: false
order: +name
states:
- color: 'hs:35,1'
intensity: 0.08
- color: 'hs:35,1'
intensity: 0.085
- color: 'hs:35,1'
intensity: 0.09
- color: 'hs:35,1'
intensity: 0.12
- effect: Gradient
power_mode: Both
target_kind: Tag
target_value: Square
interpolate: true
relative: false
order: +name
states:
- color: 'hs:25,1'
intensity: 0.05
power: off
- color: 'hs:30,1'
intensity: 0.06
power: off
- color: 'hs:35,1'
intensity: 0.07
power: on
- effect: Static
power_mode: Device
target_kind: Tag
target_value: Nightstand
interpolate: true
relative: false
order: +name
states:
- color: 'k:2000'
intensity: 0.15
- effect: Static
power_mode: Scene
target_kind: All
target_value: ''
interpolate: true
relative: false
states:
- power: false
-40
View File
@@ -1,40 +0,0 @@
name: Morning
interval: 1000
roles:
- effect: WalkingGradient
power_mode: Device
target_kind: Tag
target_value: Hexagon
interpolate: true
relative: false
order: +name
states:
- color: 'hs:220,0.4'
intensity: 0.65
- color: 'hs:220,0.5'
intensity: 0.50
- effect: Random
power_mode: Device
target_kind: Tag
target_value: Square
interpolate: true
relative: false
order: +name
states:
- color: 'hs:220,0.6'
intensity: 0.30
- color: 'hs:220,0.5'
intensity: 0.35
- effect: Static
power_mode: Device
target_kind: All
target_value: ''
interpolate: true
relative: false
order: +name
states:
- color: 'hs:250,0.6'
intensity: 0.3
-42
View File
@@ -1,42 +0,0 @@
name: Saturday
interval: 700
roles:
- effect: WalkingGradient
power_mode: Device
target_kind: Tag
target_value: Hexagon
interpolate: true
relative: false
order: +name
states:
- color: 'hs:220,0.5'
intensity: 0.25
- color: 'hs:220,0.5'
intensity: 0.30
- effect: Random
power_mode: Device
target_kind: Tag
target_value: Square
interpolate: true
relative: false
order: +name
states:
- color: 'hs:220,0.6'
intensity: 0.15
- color: 'hs:220,0.55'
intensity: 0.16
- color: 'hs:220,0.5'
intensity: 0.20
- effect: Static
power_mode: Device
target_kind: All
target_value: ''
interpolate: true
relative: false
order: +name
states:
- color: 'hs:250,0.7'
intensity: 0.3