Compare commits
11
Commits
3.4.0
...
cedd0d1d28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cedd0d1d28 | ||
|
|
7471102413 | ||
|
|
bf4d000455 | ||
|
|
6947fc3461 | ||
|
|
600c90a614 | ||
|
|
a08685c3cd | ||
|
|
15536c6078 | ||
|
|
2bb2154e5b | ||
|
|
8c2ef2dac3 | ||
|
|
f4e584938b | ||
|
|
8c9b47aa1d |
+62
-7
@@ -7,6 +7,7 @@ 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"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -217,14 +218,28 @@ func Devices(r gin.IRoutes) {
|
|||||||
return []models.Device{}, nil
|
return []models.Device{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
var scene *models.Scene
|
||||||
if err != nil {
|
if body.SceneName != "" {
|
||||||
return nil, err
|
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"
|
pushMode := c.Query("push") == "true"
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
@@ -265,6 +280,46 @@ func Devices(r gin.IRoutes) {
|
|||||||
|
|
||||||
return withSceneState(devices), nil
|
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 {
|
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/app/services/publisher"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Scenes(r gin.IRoutes) {
|
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) {
|
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
return config.SceneRepository().FetchAll(ctxOf(c))
|
return config.SceneRepository().FetchAll(ctxOf(c))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
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) {
|
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -45,7 +57,7 @@ func Scenes(r gin.IRoutes) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
scene, err := findScene(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -1,17 +1,34 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Location() *time.Location {
|
||||||
|
env, ok := os.LookupEnv("LUCIFER_LOCATION")
|
||||||
|
if !ok {
|
||||||
|
return time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := time.LoadLocation(env)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Location lookup failed: %s (%s)", env, err.Error())
|
||||||
|
return time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
func MySqlHost() string { return strEnv("LUCIFER_MYSQL_HOST") }
|
func MySqlHost() string { return strEnv("LUCIFER_MYSQL_HOST") }
|
||||||
func MySqlPort() int { return intEnv("LUCIFER_MYSQL_PORT") }
|
func MySqlPort() int { return intEnv("LUCIFER_MYSQL_PORT") }
|
||||||
func MySqlSchema() string { return strEnv("LUCIFER_MYSQL_SCHEMA") }
|
func MySqlSchema() string { return strEnv("LUCIFER_MYSQL_SCHEMA") }
|
||||||
func MySqlUsername() string { return strEnv("LUCIFER_MYSQL_USERNAME") }
|
func MySqlUsername() string { return strEnv("LUCIFER_MYSQL_USERNAME") }
|
||||||
func MySQlPassword() string { return strEnv("LUCIFER_MYSQL_PASSWORD") }
|
func MySQlPassword() string { return strEnv("LUCIFER_MYSQL_PASSWORD") }
|
||||||
|
|
||||||
func ServerPort() int { return intEnv("LUCIFER_SERVER_PORT") }
|
func ServerPort() int { return intEnv("LUCIFER_SERVER_PORT") }
|
||||||
|
|
||||||
func strEnv(key string) string {
|
func strEnv(key string) string {
|
||||||
env, ok := os.LookupEnv(key)
|
env, ok := os.LookupEnv(key)
|
||||||
|
|||||||
+4
-1
@@ -7,13 +7,14 @@ import (
|
|||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/app/services"
|
"git.aiterp.net/lucifer/new-server/app/services"
|
||||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartServer() {
|
func StartServer() {
|
||||||
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second * 10)
|
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := publisher.Initialize(setupCtx)
|
err := publisher.Initialize(setupCtx)
|
||||||
@@ -38,5 +39,7 @@ func StartServer() {
|
|||||||
api.EventHandlers(apiGin.Group("/event-handlers"))
|
api.EventHandlers(apiGin.Group("/event-handlers"))
|
||||||
api.Scenes(apiGin.Group("/scenes"))
|
api.Scenes(apiGin.Group("/scenes"))
|
||||||
|
|
||||||
|
models.TimeOfDayTimeZone = config.Location()
|
||||||
|
|
||||||
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package handlerfactory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FactoryConfig struct {
|
||||||
|
Mode FCMOde `yaml:"mode"`
|
||||||
|
Target string `yaml:"target"`
|
||||||
|
SceneNames []string `yaml:"scenes"`
|
||||||
|
TriggerDeviceID string `yaml:"trigger_device_id"`
|
||||||
|
After []After `yaml:"after"`
|
||||||
|
From string `yaml:"from"`
|
||||||
|
To string `yaml:"to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FCMOde string
|
||||||
|
|
||||||
|
var (
|
||||||
|
SceneDimmer FCMOde = "SceneDimmer"
|
||||||
|
MotionSensor FCMOde = "MotionSensor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type After struct {
|
||||||
|
Minutes int `yaml:"minutes"`
|
||||||
|
NewState models.NewDeviceState `yaml:"new_state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseConfigs(reader io.Reader) ([]FactoryConfig, error) {
|
||||||
|
readAll, err := ioutil.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
splits := strings.Split(string(readAll), "\n---\n")
|
||||||
|
configs := make([]FactoryConfig, 0, 8)
|
||||||
|
for _, split := range splits {
|
||||||
|
var factoryConfig FactoryConfig
|
||||||
|
err := yaml.NewDecoder(strings.NewReader(split)).Decode(&factoryConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configs = append(configs, factoryConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FactoryConfig) FromTime() models.TimeOfDay {
|
||||||
|
tod, _ := models.ParseTimeOfDay(f.From)
|
||||||
|
return tod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FactoryConfig) ToTime() models.TimeOfDay {
|
||||||
|
tod, _ := models.ParseTimeOfDay(f.To)
|
||||||
|
return tod
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package handlerfactory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateHandlers(
|
||||||
|
config FactoryConfig,
|
||||||
|
scenes []models.Scene,
|
||||||
|
existing []models.EventHandler,
|
||||||
|
) ([]models.EventHandler, []models.EventHandler) {
|
||||||
|
switch config.Mode {
|
||||||
|
case SceneDimmer:
|
||||||
|
return sceneDimmer(config, scenes, existing)
|
||||||
|
case MotionSensor:
|
||||||
|
return motionSensor(config, existing)
|
||||||
|
default:
|
||||||
|
return []models.EventHandler{}, []models.EventHandler{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDimmer(
|
||||||
|
config FactoryConfig,
|
||||||
|
scenes []models.Scene,
|
||||||
|
existing []models.EventHandler,
|
||||||
|
) ([]models.EventHandler, []models.EventHandler) {
|
||||||
|
kind, refValue := models.ParseFetchString(config.Target)
|
||||||
|
|
||||||
|
toRemove := make([]models.EventHandler, 0, 8)
|
||||||
|
for _, old := range existing {
|
||||||
|
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
|
||||||
|
toRemove = append(toRemove, old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedScenes := make([]models.Scene, 0, len(config.SceneNames))
|
||||||
|
for _, name := range config.SceneNames {
|
||||||
|
for _, scene := range scenes {
|
||||||
|
if name == scene.Name {
|
||||||
|
resolvedScenes = append(resolvedScenes, scene)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scenes) < 2 {
|
||||||
|
return []models.EventHandler{}, []models.EventHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers := make([]models.EventHandler, 0, len(scenes)+4)
|
||||||
|
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "On"},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetPower: &myTrue},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "Off"},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetPower: &myFalse},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, scene := range resolvedScenes {
|
||||||
|
if i > 0 {
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "DimDown"},
|
||||||
|
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||||
|
SceneID: resolvedScenes[i-1].ID,
|
||||||
|
Group: refValue,
|
||||||
|
DurationMS: 0,
|
||||||
|
}},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(resolvedScenes)-1 {
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "DimUp"},
|
||||||
|
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||||
|
SceneID: resolvedScenes[i+1].ID,
|
||||||
|
Group: refValue,
|
||||||
|
DurationMS: 0,
|
||||||
|
}},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "DimDown"},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 90,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||||
|
SceneID: resolvedScenes[len(resolvedScenes)-1].ID,
|
||||||
|
Group: refValue,
|
||||||
|
DurationMS: 0,
|
||||||
|
}},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "ButtonPressed",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"buttonName": {EQ: "DimUp"},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 90,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||||
|
SceneID: resolvedScenes[0].ID,
|
||||||
|
Group: refValue,
|
||||||
|
DurationMS: 0,
|
||||||
|
}},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return optimizeLists(handlers, toRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
func motionSensor(
|
||||||
|
config FactoryConfig,
|
||||||
|
existing []models.EventHandler,
|
||||||
|
) ([]models.EventHandler, []models.EventHandler) {
|
||||||
|
kind, refValue := models.ParseFetchString(config.Target)
|
||||||
|
|
||||||
|
toRemove := make([]models.EventHandler, 0, 8)
|
||||||
|
for _, old := range existing {
|
||||||
|
if old.From != config.FromTime() || old.To != config.ToTime() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
|
||||||
|
toRemove = append(toRemove, old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers := make([]models.EventHandler, 0, len(config.After))
|
||||||
|
for _, after := range config.After {
|
||||||
|
if after.Minutes == 0 {
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "SensorPresenceStarted",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{
|
||||||
|
SetPower: after.NewState.Power,
|
||||||
|
SetColor: after.NewState.Color,
|
||||||
|
SetIntensity: after.NewState.Intensity,
|
||||||
|
SetTemperature: after.NewState.Temperature,
|
||||||
|
},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handlers = append(handlers, models.EventHandler{
|
||||||
|
EventName: "SensorPresenceEnded",
|
||||||
|
Conditions: map[string]models.EventCondition{
|
||||||
|
"deviceId": {EQ: config.TriggerDeviceID},
|
||||||
|
"minutesElapsed": {EQ: fmt.Sprintf("%d", after.Minutes)},
|
||||||
|
},
|
||||||
|
OneShot: false,
|
||||||
|
Priority: 100,
|
||||||
|
TargetKind: kind,
|
||||||
|
TargetValue: refValue,
|
||||||
|
Actions: models.EventAction{
|
||||||
|
SetPower: after.NewState.Power,
|
||||||
|
SetColor: after.NewState.Color,
|
||||||
|
SetIntensity: after.NewState.Intensity,
|
||||||
|
SetTemperature: after.NewState.Temperature,
|
||||||
|
},
|
||||||
|
From: config.FromTime(),
|
||||||
|
To: config.ToTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimizeLists(handlers, toRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
var myTrue = true
|
||||||
|
var myFalse = false
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package handlerfactory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func optimizeLists(
|
||||||
|
add []models.EventHandler,
|
||||||
|
del []models.EventHandler,
|
||||||
|
) ([]models.EventHandler, []models.EventHandler) {
|
||||||
|
newAdd := make([]models.EventHandler, 0, len(add))
|
||||||
|
newDel := make([]models.EventHandler, 0, len(del))
|
||||||
|
|
||||||
|
OptimizeLoop1:
|
||||||
|
for _, addItem := range add {
|
||||||
|
for _, delItem := range del {
|
||||||
|
if checkDuplicate(&addItem, &delItem) {
|
||||||
|
continue OptimizeLoop1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAdd = append(newAdd, addItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
OptimizeLoop2:
|
||||||
|
for _, delItem := range del {
|
||||||
|
for _, addItem := range add {
|
||||||
|
if checkDuplicate(&addItem, &delItem) {
|
||||||
|
continue OptimizeLoop2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newDel = append(newDel, delItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAdd, newDel
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDuplicate(a *models.EventHandler, b *models.EventHandler) bool {
|
||||||
|
check := true
|
||||||
|
check = check && a.EventName == b.EventName
|
||||||
|
check = check && len(a.Conditions) == len(b.Conditions)
|
||||||
|
for k := range a.Conditions {
|
||||||
|
check = check && a.Conditions[k] == b.Conditions[k]
|
||||||
|
}
|
||||||
|
check = check && a.OneShot == b.OneShot
|
||||||
|
check = check && a.Priority == b.Priority
|
||||||
|
check = check && a.TargetKind == b.TargetKind
|
||||||
|
check = check && a.TargetValue == b.TargetValue
|
||||||
|
check = check && a.From == b.From
|
||||||
|
check = check && a.To == b.To
|
||||||
|
|
||||||
|
aPower := a.Actions.SetPower
|
||||||
|
bPower := b.Actions.SetPower
|
||||||
|
powerEq := (aPower == nil && bPower == nil) || (aPower != nil && bPower != nil && *aPower == *bPower)
|
||||||
|
|
||||||
|
aColor := a.Actions.SetColor
|
||||||
|
bColor := b.Actions.SetColor
|
||||||
|
colorEq := (aColor == nil && bColor == nil) || (aColor != nil && bColor != nil && *aColor == *bColor)
|
||||||
|
|
||||||
|
aInt := a.Actions.SetIntensity
|
||||||
|
bInt := b.Actions.SetIntensity
|
||||||
|
intEq := (aInt == nil && bInt == nil) || (aInt != nil && bInt != nil && *aInt == *bInt)
|
||||||
|
|
||||||
|
aTemp := a.Actions.SetTemperature
|
||||||
|
bTemp := b.Actions.SetTemperature
|
||||||
|
tempEq := (aTemp == nil && bTemp == nil) || (aTemp != nil && bTemp != nil && *aTemp == *bTemp)
|
||||||
|
|
||||||
|
check = check && powerEq && colorEq && intEq && tempEq
|
||||||
|
|
||||||
|
return check
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/client"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/services/handlerfactory"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func factoryCmd(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
) {
|
||||||
|
cmd := parseCommand(os.Args[2:])
|
||||||
|
|
||||||
|
switch cmd.Name {
|
||||||
|
case "run":
|
||||||
|
fileName := cmd.Params.Get(0).String()
|
||||||
|
if fileName == nil {
|
||||||
|
log.Fatalln("Missing filename")
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes, err := c.GetScenes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers, err := c.GetHandlers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(*fileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open file:", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
configs, err := handlerfactory.ParseConfigs(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to parse file:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, config := range configs {
|
||||||
|
toAdd, toRemove := handlerfactory.CreateHandlers(config, scenes, handlers)
|
||||||
|
|
||||||
|
for _, removing := range toRemove {
|
||||||
|
_, err := c.DeleteHandler(ctx, removing.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Removed handler %d", removing.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, adding := range toAdd {
|
||||||
|
res, err := c.AddHandler(ctx, adding)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Added handler %d", res.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if cmd.Name != "help" {
|
||||||
|
log.Println("Unknown command:", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-9
@@ -76,17 +76,33 @@ func handlerCmd(
|
|||||||
|
|
||||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
||||||
case "delete":
|
case "delete":
|
||||||
id := cmd.Params.Get(0).Int()
|
if cmd.Params.Get(0).StringOr("_not") == "all" {
|
||||||
if id == nil {
|
handlers, err := c.GetHandlers(ctx)
|
||||||
log.Fatalln("ID missing")
|
if err != nil {
|
||||||
}
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
handler, err := c.DeleteHandler(ctx, *id)
|
for _, handler := range handlers {
|
||||||
if err != nil {
|
_, err = c.DeleteHandler(ctx, handler.ID)
|
||||||
log.Fatalln(err)
|
if err != nil {
|
||||||
}
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
|
log.Printf("Deleted %d handlers", len(handlers))
|
||||||
|
} else {
|
||||||
|
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:
|
default:
|
||||||
if cmd.Name != "help" {
|
if cmd.Name != "help" {
|
||||||
log.Println("Unknown command:", cmd.Name)
|
log.Println("Unknown command:", cmd.Name)
|
||||||
@@ -157,6 +173,7 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
|
|||||||
model.Actions.SetPower = nds.Power
|
model.Actions.SetPower = nds.Power
|
||||||
model.Actions.SetColor = nds.Color
|
model.Actions.SetColor = nds.Color
|
||||||
model.Actions.SetIntensity = nds.Intensity
|
model.Actions.SetIntensity = nds.Intensity
|
||||||
|
model.Actions.SetTemperature = nds.Temperature
|
||||||
|
|
||||||
// Add intensity
|
// Add intensity
|
||||||
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
||||||
|
|||||||
@@ -32,6 +32,38 @@ EVENT HANDLER COMMANDS
|
|||||||
<-fire> <-fire.payload.*>
|
<-fire> <-fire.payload.*>
|
||||||
handler delete <id>
|
handler delete <id>
|
||||||
|
|
||||||
|
EVENT HANDLER FACTORY COMMANDS
|
||||||
|
|
||||||
|
factory run <file.yaml>
|
||||||
|
|
||||||
|
File example (use '---' to delimit multiple configs in one file):
|
||||||
|
mode: SceneDimmer
|
||||||
|
target: tag:Soverom
|
||||||
|
scenes:
|
||||||
|
- kveldslys
|
||||||
|
- landskap
|
||||||
|
- vasking
|
||||||
|
trigger_device_id: 14
|
||||||
|
---
|
||||||
|
mode: MotionSensor
|
||||||
|
target: tag:Bod
|
||||||
|
trigger_device_id: 36
|
||||||
|
after:
|
||||||
|
- minutes: 0
|
||||||
|
new_state:
|
||||||
|
power: true
|
||||||
|
color: hs:240,0.6
|
||||||
|
intensity: 0.8
|
||||||
|
- minutes: 1
|
||||||
|
new_state:
|
||||||
|
power: false
|
||||||
|
from: 08:00
|
||||||
|
to: 15:59:59
|
||||||
|
---
|
||||||
|
(more modes coming soon)
|
||||||
|
|
||||||
|
WARNING: Similar event handlers will be removed!
|
||||||
|
|
||||||
EVENT COMMANDS
|
EVENT COMMANDS
|
||||||
run <event name> <*=S>
|
run <event name> <*=S>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
+7
-5
@@ -37,8 +37,8 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Params.Get("info").StringOr("false") == "true" {
|
if info := cmd.Params.Get("info").String(); info != nil {
|
||||||
WriteDeviceInfoTable(os.Stdout, devices)
|
WriteDeviceInfo(os.Stdout, devices, strings.Split(*info, ","))
|
||||||
} else {
|
} else {
|
||||||
WriteDeviceStateTable(os.Stdout, devices)
|
WriteDeviceStateTable(os.Stdout, devices)
|
||||||
}
|
}
|
||||||
@@ -82,8 +82,8 @@ func main() {
|
|||||||
case "update":
|
case "update":
|
||||||
{
|
{
|
||||||
devices, err := c.PutDevice(ctx, 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(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -93,6 +93,9 @@ func main() {
|
|||||||
WriteDeviceInfoTable(os.Stdout, devices)
|
WriteDeviceInfoTable(os.Stdout, devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "factory":
|
||||||
|
factoryCmd(ctx, c)
|
||||||
|
|
||||||
case "handler":
|
case "handler":
|
||||||
handlerCmd(ctx, c)
|
handlerCmd(ctx, c)
|
||||||
|
|
||||||
@@ -126,4 +129,3 @@ func main() {
|
|||||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -19,6 +19,16 @@ func sceneCmd(
|
|||||||
cmd := parseCommand(os.Args[2:])
|
cmd := parseCommand(os.Args[2:])
|
||||||
|
|
||||||
switch cmd.Name {
|
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":
|
case "create", "update":
|
||||||
{
|
{
|
||||||
fileName := cmd.Params.Get(0).String()
|
fileName := cmd.Params.Get(0).String()
|
||||||
@@ -83,15 +93,22 @@ func sceneCmd(
|
|||||||
{
|
{
|
||||||
fetch := cmd.Params.Get(0).String()
|
fetch := cmd.Params.Get(0).String()
|
||||||
id := cmd.Params.Get(1).Int()
|
id := cmd.Params.Get(1).Int()
|
||||||
if fetch == nil || id == nil {
|
name := cmd.Params.Get(1).String()
|
||||||
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
|
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{
|
assignment := models.DeviceSceneAssignment{
|
||||||
SceneID: *id,
|
|
||||||
Group: cmd.Params.Get("group").StringOr(*fetch),
|
Group: cmd.Params.Get("group").StringOr(*fetch),
|
||||||
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
|
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 {
|
if err != nil {
|
||||||
log.Println("Could not assign devices:", err)
|
log.Println("Could not assign devices:", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -94,6 +94,24 @@ func WriteDeviceInfoTable(w io.Writer, devices []models.Device) {
|
|||||||
table.Render()
|
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) {
|
func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
|
||||||
table := tablewriter.NewWriter(w)
|
table := tablewriter.NewWriter(w)
|
||||||
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
|
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -23,6 +23,7 @@ type Bridge struct {
|
|||||||
externalID int
|
externalID int
|
||||||
lightStates []*hueLightState
|
lightStates []*hueLightState
|
||||||
sensorStates []*hueSensorState
|
sensorStates []*hueSensorState
|
||||||
|
quarantine map[string]time.Time
|
||||||
syncingPublish uint32
|
syncingPublish uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
|||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
for index, light := range lightMap {
|
for index, light := range lightMap {
|
||||||
|
if time.Now().Before(b.quarantine[light.Uniqueid]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var state *hueLightState
|
var state *hueLightState
|
||||||
for _, existingState := range b.lightStates {
|
for _, existingState := range b.lightStates {
|
||||||
if existingState.index == index {
|
if existingState.index == index {
|
||||||
@@ -68,6 +73,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
|||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
for index, sensor := range sensorMap {
|
for index, sensor := range sensorMap {
|
||||||
|
if time.Now().Before(b.quarantine[sensor.UniqueID]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var state *hueSensorState
|
var state *hueSensorState
|
||||||
for _, existingState := range b.sensorStates {
|
for _, existingState := range b.sensorStates {
|
||||||
if existingState.index == index {
|
if existingState.index == index {
|
||||||
@@ -98,17 +107,17 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
|||||||
|
|
||||||
func (b *Bridge) SyncStale(ctx context.Context) error {
|
func (b *Bridge) SyncStale(ctx context.Context) error {
|
||||||
indices := make([]int, 0, 4)
|
indices := make([]int, 0, 4)
|
||||||
arrayIndices := make([]int, 0, 4)
|
|
||||||
inputs := make([]LightStateInput, 0, 4)
|
inputs := make([]LightStateInput, 0, 4)
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
for i, state := range b.lightStates {
|
for _, state := range b.lightStates {
|
||||||
if !state.stale {
|
if !state.stale {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
indices = append(indices, state.index)
|
indices = append(indices, state.index)
|
||||||
arrayIndices = append(arrayIndices, i)
|
|
||||||
inputs = append(inputs, state.input)
|
inputs = append(inputs, state.input)
|
||||||
}
|
}
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
@@ -117,83 +126,60 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make([]*syncGroup, 0, 4)
|
for i, input := range inputs {
|
||||||
for i := range inputs {
|
iCopy := i
|
||||||
input := inputs[i]
|
index := indices[i]
|
||||||
|
inputCopy := input
|
||||||
|
|
||||||
found := false
|
eg.Go(func() error {
|
||||||
for _, group := range groups {
|
err := b.putLightState(ctx, index, inputCopy)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
|
||||||
err := b.putGroupLightState(ctx, group.GroupIndex, group.State)
|
// Find index
|
||||||
if err != nil {
|
b.mu.Lock()
|
||||||
return err
|
found := false
|
||||||
}
|
index := -1
|
||||||
|
for i, ls := range b.lightStates {
|
||||||
b.mu.Lock()
|
if ls.uniqueID == device.InternalID {
|
||||||
for _, arrayIndex := range group.ArrayIndexes {
|
found = true
|
||||||
b.lightStates[arrayIndex].stale = false
|
index = i
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -236,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)
|
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) {
|
func (b *Bridge) getToken(ctx context.Context) (string, error) {
|
||||||
result := make([]CreateUserResponse, 0, 1)
|
result := make([]CreateUserResponse, 0, 1)
|
||||||
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
||||||
@@ -296,7 +286,6 @@ func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
|
|||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
|
func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
|
||||||
return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
|
return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
|
||||||
}
|
}
|
||||||
@@ -343,7 +332,6 @@ func (b *Bridge) delete(ctx context.Context, resource string, target interface{}
|
|||||||
return json.NewDecoder(res.Body).Decode(target)
|
return json.NewDecoder(res.Body).Decode(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
||||||
rb, err := reqBody(body)
|
rb, err := reqBody(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -322,8 +322,8 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fastTicker := time.NewTicker(time.Second / 10)
|
fastTicker := time.NewTicker(time.Second / 5)
|
||||||
slowTicker := time.NewTicker(time.Second / 3)
|
slowTicker := time.NewTicker(time.Second / 2)
|
||||||
selectedTicker := fastTicker
|
selectedTicker := fastTicker
|
||||||
ticksUntilRefresh := 0
|
ticksUntilRefresh := 0
|
||||||
ticksSinceChange := 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) {
|
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
for _, bridge := range d.bridges {
|
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)
|
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) {
|
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
|
||||||
scenes := make([]sceneRecord, 0, 8)
|
scenes := make([]sceneRecord, 0, 8)
|
||||||
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
||||||
|
|||||||
@@ -198,6 +198,10 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
|||||||
d.State.Intensity = *newState.Intensity
|
d.State.Intensity = *newState.Intensity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newState.Temperature != nil && d.HasCapability(DCTemperatureControl) {
|
||||||
|
d.State.Temperature = *newState.Temperature
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ type Driver interface {
|
|||||||
Publish(ctx context.Context, bridge Bridge, devices []Device) error
|
Publish(ctx context.Context, bridge Bridge, devices []Device) error
|
||||||
Run(ctx context.Context, bridge Bridge, ch chan<- Event) 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 ErrBridgeSearchFailed = errors.New("bridge search failed")
|
||||||
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
|
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 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 ErrInvalidAddress = errors.New("invalid mac address")
|
||||||
var ErrPayloadTooShort = errors.New("payload too short")
|
var ErrPayloadTooShort = errors.New("payload too short")
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ const (
|
|||||||
// instance is created for the group.
|
// instance is created for the group.
|
||||||
type DeviceSceneAssignment struct {
|
type DeviceSceneAssignment struct {
|
||||||
SceneID int `json:"sceneId"`
|
SceneID int `json:"sceneId"`
|
||||||
|
SceneName string `json:"sceneName"`
|
||||||
Group string `json:"group"`
|
Group string `json:"group"`
|
||||||
StartTime time.Time `json:"start"`
|
StartTime time.Time `json:"start"`
|
||||||
DurationMS int64 `json:"durationMs"`
|
DurationMS int64 `json:"durationMs"`
|
||||||
@@ -306,6 +307,7 @@ type DeviceSceneAssignment struct {
|
|||||||
|
|
||||||
type SceneRepository interface {
|
type SceneRepository interface {
|
||||||
Find(ctx context.Context, id int) (*Scene, error)
|
Find(ctx context.Context, id int) (*Scene, error)
|
||||||
|
FindName(ctx context.Context, name string) (*Scene, error)
|
||||||
FetchAll(ctx context.Context) ([]Scene, error)
|
FetchAll(ctx context.Context) ([]Scene, error)
|
||||||
Save(ctx context.Context, bridge *Scene) error
|
Save(ctx context.Context, bridge *Scene) error
|
||||||
Delete(ctx context.Context, bridge *Scene) error
|
Delete(ctx context.Context, bridge *Scene) error
|
||||||
|
|||||||
+5
-3
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var TimeOfDayTimeZone = time.UTC
|
||||||
|
|
||||||
type TimeOfDay int
|
type TimeOfDay int
|
||||||
|
|
||||||
func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
|
func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
|
||||||
@@ -29,7 +31,6 @@ func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
t2, err := ParseTimeOfDay(str)
|
t2, err := ParseTimeOfDay(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -82,11 +83,12 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CurrentTimeOfDay() TimeOfDay {
|
func CurrentTimeOfDay() TimeOfDay {
|
||||||
return TimeOfDayFromDate(time.Now().Local())
|
return TimeOfDayFromDate(time.Now().In(TimeOfDayTimeZone))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeOfDayFromDate(date time.Time) TimeOfDay {
|
func TimeOfDayFromDate(date time.Time) TimeOfDay {
|
||||||
return NewTimeOfDay(date.Hour(), date.Minute(), date.Second())
|
localDate := date.In(TimeOfDayTimeZone)
|
||||||
|
return NewTimeOfDay(localDate.Hour(), localDate.Minute(), localDate.Second())
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {
|
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {
|
||||||
|
|||||||
Reference in New Issue
Block a user