Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01975c74a9 | ||
|
|
cdf5173503 | ||
|
|
def494ba45 | ||
|
|
1b7e495a59 | ||
|
|
07173e4e56 | ||
|
|
7b0028ee2b | ||
|
|
d23a7327ef | ||
|
|
a3695a3ddd | ||
|
|
ffa757f742 | ||
|
|
f87c69efc7 | ||
|
|
b4fd6df454 | ||
|
|
44716c84d1 |
@@ -4,6 +4,15 @@ kind: pipeline
|
|||||||
type: docker
|
type: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: go-build
|
||||||
|
image: golang:1.17
|
||||||
|
commands:
|
||||||
|
- go test -v ./...
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- tag
|
||||||
|
|
||||||
- name: go-test
|
- name: go-test
|
||||||
image: golang:1.17
|
image: golang:1.17
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ func Bridges(r gin.IRoutes) {
|
|||||||
return []models.Bridge{bridge}, nil
|
return []models.Bridge{bridge}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.Token, body.DryRun)
|
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-66
@@ -7,7 +7,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -138,10 +137,12 @@ func Devices(r gin.IRoutes) {
|
|||||||
|
|
||||||
config.PublishChannel <- devices
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
go func() {
|
||||||
if err != nil {
|
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||||
log.Println("Failed to save devices states")
|
if err != nil {
|
||||||
}
|
log.Println("Failed to save devices states")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return withSceneState(devices), nil
|
return withSceneState(devices), nil
|
||||||
}))
|
}))
|
||||||
@@ -218,28 +219,14 @@ func Devices(r gin.IRoutes) {
|
|||||||
return []models.Device{}, nil
|
return []models.Device{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var scene *models.Scene
|
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||||
if body.SceneName != "" {
|
if err != nil {
|
||||||
scene, err = config.SceneRepository().FindName(ctxOf(c), body.SceneName)
|
return nil, err
|
||||||
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 {
|
||||||
@@ -280,46 +267,6 @@ 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 {
|
||||||
|
|||||||
+2
-14
@@ -5,27 +5,15 @@ 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 findScene(c)
|
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -57,7 +45,7 @@ func Scenes(r gin.IRoutes) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
scene, err := findScene(c)
|
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers"
|
"git.aiterp.net/lucifer/new-server/internal/drivers"
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/hue"
|
"git.aiterp.net/lucifer/new-server/internal/drivers/hue"
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/hue2"
|
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/lifx"
|
"git.aiterp.net/lucifer/new-server/internal/drivers/lifx"
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/mill"
|
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
|
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -19,9 +17,7 @@ func DriverProvider() models.DriverProvider {
|
|||||||
dp = drivers.DriverMap{
|
dp = drivers.DriverMap{
|
||||||
models.DTNanoLeaf: &nanoleaf.Driver{},
|
models.DTNanoLeaf: &nanoleaf.Driver{},
|
||||||
models.DTHue: &hue.Driver{},
|
models.DTHue: &hue.Driver{},
|
||||||
models.DTHue2: &hue2.Driver{},
|
|
||||||
models.DTLIFX: &lifx.Driver{},
|
models.DTLIFX: &lifx.Driver{},
|
||||||
models.DTMill: &mill.Driver{},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-18
@@ -1,34 +1,17 @@
|
|||||||
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)
|
||||||
|
|||||||
+1
-4
@@ -7,14 +7,13 @@ 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)
|
||||||
@@ -39,7 +38,5 @@ 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())))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-24
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
@@ -67,6 +66,7 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
err := handleSpecial(event)
|
err := handleSpecial(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Special event handler error (%s): %v", event.Name, err)
|
log.Printf("Special event handler error (%s): %v", event.Name, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
paramStrings := make([]string, 0, 8)
|
paramStrings := make([]string, 0, 8)
|
||||||
@@ -220,29 +220,6 @@ func handleSpecial(event models.Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return driver.Publish(ctx, bridge, devices)
|
return driver.Publish(ctx, bridge, devices)
|
||||||
case models.ENSensorPresenceStarted, models.ENSensorPresenceEnded, models.ENSensorTemperature:
|
|
||||||
id, _ := strconv.Atoi(event.Payload["deviceId"])
|
|
||||||
updateTime, _ := strconv.ParseInt(event.Payload["lastUpdated"], 10, 64)
|
|
||||||
state := models.SceneSensor{
|
|
||||||
ID: id,
|
|
||||||
UpdateTime: time.Unix(updateTime, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch event.Name {
|
|
||||||
case models.ENSensorPresenceStarted:
|
|
||||||
presence := true
|
|
||||||
state.Presence = &presence
|
|
||||||
case models.ENSensorPresenceEnded:
|
|
||||||
presence := false
|
|
||||||
state.Presence = &presence
|
|
||||||
case models.ENSensorTemperature:
|
|
||||||
temperature, _ := strconv.ParseFloat(event.Payload["temperature"], 64)
|
|
||||||
state.Temperature = &temperature
|
|
||||||
}
|
|
||||||
|
|
||||||
publisher.Global().UpdateSensor(state)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ type Publisher struct {
|
|||||||
started map[int]bool
|
started map[int]bool
|
||||||
pending map[int][]models.Device
|
pending map[int][]models.Device
|
||||||
waiting map[int]chan struct{}
|
waiting map[int]chan struct{}
|
||||||
sensorCache map[int]models.SceneSensor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
|
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
|
||||||
@@ -43,15 +42,6 @@ func (p *Publisher) UpdateScene(data models.Scene) {
|
|||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Publisher) UpdateSensor(data models.SceneSensor) {
|
|
||||||
p.mu.Lock()
|
|
||||||
if assignment := p.sceneAssignment[data.ID]; assignment != nil {
|
|
||||||
assignment.UpdateSensor(data)
|
|
||||||
}
|
|
||||||
p.sensorCache[data.ID] = data
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
||||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
scenes, err := config.SceneRepository().FetchAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -87,8 +77,6 @@ func (p *Publisher) Publish(devices ...models.Device) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
unwaitMap := make(map[int]bool)
|
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
@@ -102,20 +90,12 @@ func (p *Publisher) Publish(devices ...models.Device) {
|
|||||||
|
|
||||||
if p.sceneAssignment[device.ID] != nil {
|
if p.sceneAssignment[device.ID] != nil {
|
||||||
p.sceneAssignment[device.ID].UpsertDevice(device)
|
p.sceneAssignment[device.ID].UpsertDevice(device)
|
||||||
|
|
||||||
if cache, ok := p.sensorCache[device.ID]; ok {
|
|
||||||
p.sceneAssignment[device.ID].UpdateSensor(cache)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
||||||
unwaitMap[device.BridgeID] = true
|
if p.waiting[device.BridgeID] != nil {
|
||||||
}
|
close(p.waiting[device.BridgeID])
|
||||||
}
|
p.waiting[device.BridgeID] = nil
|
||||||
|
}
|
||||||
for bridgeID := range unwaitMap {
|
|
||||||
if p.waiting[bridgeID] != nil {
|
|
||||||
close(p.waiting[bridgeID])
|
|
||||||
p.waiting[bridgeID] = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,21 +110,19 @@ func (p *Publisher) Run() {
|
|||||||
ticker := time.NewTicker(time.Millisecond * 100)
|
ticker := time.NewTicker(time.Millisecond * 100)
|
||||||
deleteList := make([]int, 0, 8)
|
deleteList := make([]int, 0, 8)
|
||||||
updatedList := make([]models.Device, 0, 16)
|
updatedList := make([]models.Device, 0, 16)
|
||||||
unwaitMap := make(map[int]bool)
|
reassignList := make([]models.Device, 0, 16)
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
deleteList = deleteList[:0]
|
deleteList = deleteList[:0]
|
||||||
updatedList = updatedList[:0]
|
updatedList = updatedList[:0]
|
||||||
|
reassignList = reassignList[:0]
|
||||||
for key := range unwaitMap {
|
|
||||||
delete(unwaitMap, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
for i, scene := range p.scenes {
|
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)) || scene.Empty() {
|
||||||
deleteList = append(deleteList, i-len(deleteList))
|
deleteList = append(deleteList, i-len(deleteList))
|
||||||
updatedList = append(updatedList, scene.AllDevices()...)
|
updatedList = append(updatedList, scene.AllDevices()...)
|
||||||
|
reassignList = append(reassignList, scene.AllDevices()...)
|
||||||
|
|
||||||
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
|
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
|
||||||
|
|
||||||
@@ -164,6 +142,14 @@ func (p *Publisher) Run() {
|
|||||||
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, device := range reassignList {
|
||||||
|
p.reassignDevice(device)
|
||||||
|
|
||||||
|
if p.sceneAssignment[device.ID] != nil {
|
||||||
|
p.sceneAssignment[device.ID].UpsertDevice(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, device := range updatedList {
|
for _, device := range updatedList {
|
||||||
if !p.started[device.BridgeID] {
|
if !p.started[device.BridgeID] {
|
||||||
p.started[device.BridgeID] = true
|
p.started[device.BridgeID] = true
|
||||||
@@ -171,13 +157,9 @@ func (p *Publisher) Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
||||||
unwaitMap[device.BridgeID] = true
|
if p.waiting[device.BridgeID] != nil {
|
||||||
}
|
close(p.waiting[device.BridgeID])
|
||||||
|
p.waiting[device.BridgeID] = nil
|
||||||
for bridgeID := range unwaitMap {
|
|
||||||
if p.waiting[bridgeID] != nil {
|
|
||||||
close(p.waiting[bridgeID])
|
|
||||||
p.waiting[bridgeID] = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
@@ -188,10 +170,10 @@ func (p *Publisher) Run() {
|
|||||||
// should trigger an update.
|
// should trigger an update.
|
||||||
func (p *Publisher) reassignDevice(device models.Device) bool {
|
func (p *Publisher) reassignDevice(device models.Device) bool {
|
||||||
var selectedAssignment *models.DeviceSceneAssignment
|
var selectedAssignment *models.DeviceSceneAssignment
|
||||||
for _, assignment := range device.SceneAssignments {
|
for i, assignment := range device.SceneAssignments {
|
||||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||||
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
||||||
selectedAssignment = &assignment
|
selectedAssignment = &device.SceneAssignments[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,8 +267,8 @@ func (p *Publisher) runBridge(id int) {
|
|||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := append(p.pending[id][:0:0], p.pending[id]...)
|
updates := p.pending[id]
|
||||||
p.pending[id] = p.pending[id][:0]
|
p.pending[id] = p.pending[id][:0:0]
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
// Only allow the latest update per device (this avoids slow bridges causing a backlog of cations).
|
// Only allow the latest update per device (this avoids slow bridges causing a backlog of cations).
|
||||||
@@ -320,7 +302,6 @@ var publisher = Publisher{
|
|||||||
started: make(map[int]bool),
|
started: make(map[int]bool),
|
||||||
pending: make(map[int][]models.Device),
|
pending: make(map[int][]models.Device),
|
||||||
waiting: make(map[int]chan struct{}),
|
waiting: make(map[int]chan struct{}),
|
||||||
sensorCache: make(map[int]models.SceneSensor),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Global() *Publisher {
|
func Global() *Publisher {
|
||||||
|
|||||||
@@ -13,25 +13,11 @@ type Scene struct {
|
|||||||
roleMap map[int]int
|
roleMap map[int]int
|
||||||
roleList map[int][]models.Device
|
roleList map[int][]models.Device
|
||||||
lastStates map[int]models.DeviceState
|
lastStates map[int]models.DeviceState
|
||||||
sensors []models.SceneSensor
|
|
||||||
|
|
||||||
due bool
|
due bool
|
||||||
lastInterval int64
|
lastInterval int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) UpdateSensor(data models.SceneSensor) {
|
|
||||||
s.due = true
|
|
||||||
|
|
||||||
for i, sensor := range s.sensors {
|
|
||||||
if sensor.ID == data.ID {
|
|
||||||
s.sensors[i] = data
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.sensors = append(s.sensors, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateScene updates the scene data and re-seats all devices.
|
// UpdateScene updates the scene data and re-seats all devices.
|
||||||
func (s *Scene) UpdateScene(data models.Scene) {
|
func (s *Scene) UpdateScene(data models.Scene) {
|
||||||
devices := make([]models.Device, 0, 16)
|
devices := make([]models.Device, 0, 16)
|
||||||
@@ -100,13 +86,6 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
|||||||
|
|
||||||
delete(s.roleMap, device.ID)
|
delete(s.roleMap, device.ID)
|
||||||
delete(s.lastStates, device.ID)
|
delete(s.lastStates, device.ID)
|
||||||
|
|
||||||
for i, sensor := range s.sensors {
|
|
||||||
if sensor.ID == device.ID {
|
|
||||||
s.sensors = append(s.sensors[:i], s.sensors[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) Empty() bool {
|
func (s *Scene) Empty() bool {
|
||||||
@@ -151,7 +130,6 @@ func (s *Scene) Run() []models.Device {
|
|||||||
return []models.Device{}
|
return []models.Device{}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now()
|
|
||||||
intervalNumber := int64(0)
|
intervalNumber := int64(0)
|
||||||
intervalMax := int64(1)
|
intervalMax := int64(1)
|
||||||
if s.data.IntervalMS > 0 {
|
if s.data.IntervalMS > 0 {
|
||||||
@@ -171,31 +149,14 @@ func (s *Scene) Run() []models.Device {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sensorCount := 0
|
|
||||||
for _, device := range list {
|
|
||||||
if device.IsOnlySensor() {
|
|
||||||
sensorCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
role := s.data.Roles[i]
|
role := s.data.Roles[i]
|
||||||
|
|
||||||
jSkip := 0
|
|
||||||
|
|
||||||
for j, device := range list {
|
for j, device := range list {
|
||||||
if device.IsOnlySensor() {
|
|
||||||
updatedDevices = append(updatedDevices, device)
|
|
||||||
jSkip += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
newState := role.ApplyEffect(&device, models.SceneRunContext{
|
newState := role.ApplyEffect(&device, models.SceneRunContext{
|
||||||
CurrentTime: currentTime,
|
Index: j,
|
||||||
Index: j - jSkip,
|
Length: len(list),
|
||||||
Length: len(list) - sensorCount,
|
|
||||||
IntervalNumber: intervalNumber,
|
IntervalNumber: intervalNumber,
|
||||||
IntervalMax: intervalMax,
|
IntervalMax: intervalMax,
|
||||||
Sensors: s.sensors,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
err := device.SetState(newState)
|
err := device.SetState(newState)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find bridge
|
// Find bridge
|
||||||
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, *flagToken, !*flagPair)
|
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to search bridge:", err)
|
log.Fatalln("Failed to search bridge:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
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:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-27
@@ -76,33 +76,17 @@ func handlerCmd(
|
|||||||
|
|
||||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
||||||
case "delete":
|
case "delete":
|
||||||
if cmd.Params.Get(0).StringOr("_not") == "all" {
|
id := cmd.Params.Get(0).Int()
|
||||||
handlers, err := c.GetHandlers(ctx)
|
if id == nil {
|
||||||
if err != nil {
|
log.Fatalln("ID missing")
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, handler := range handlers {
|
|
||||||
_, err = c.DeleteHandler(ctx, handler.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -173,7 +157,6 @@ 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,38 +32,6 @@ 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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
+5
-7
@@ -37,8 +37,8 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if info := cmd.Params.Get("info").String(); info != nil {
|
if cmd.Params.Get("info").StringOr("false") == "true" {
|
||||||
WriteDeviceInfo(os.Stdout, devices, strings.Split(*info, ","))
|
WriteDeviceInfoTable(os.Stdout, devices)
|
||||||
} 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,9 +93,6 @@ 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)
|
||||||
|
|
||||||
@@ -129,3 +126,4 @@ func main() {
|
|||||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-22
@@ -19,16 +19,6 @@ 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()
|
||||||
@@ -93,22 +83,15 @@ 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()
|
||||||
name := cmd.Params.Get(1).String()
|
if fetch == nil || id == nil {
|
||||||
if fetch == nil || (id == nil && name == nil) {
|
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
|
||||||
log.Println("Usage: lucy scene assign <fetch> <id|name> <group=S> <duration=I>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assignment := models.DeviceSceneAssignment{
|
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", 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
|
||||||
|
|||||||
+1
-19
@@ -44,7 +44,7 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
|
|||||||
|
|
||||||
temperatureString := ""
|
temperatureString := ""
|
||||||
if v.HasCapability(models.DCTemperatureControl, models.DCTemperatureSensor) {
|
if v.HasCapability(models.DCTemperatureControl, models.DCTemperatureSensor) {
|
||||||
temperatureString = fmt.Sprintf("%d", v.State.Temperature)
|
temperatureString = strconv.FormatFloat(v.State.Temperature, 'f', -1, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
intensityString := ""
|
intensityString := ""
|
||||||
@@ -94,24 +94,6 @@ 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"})
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/internal/drivers/hue2"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
client := hue2.NewClient("10.80.8.8", "o2XKGgmVUGNBghYFdLUCVuinOTMxFH4pHV9PuTbU")
|
|
||||||
bridge := hue2.NewBridge(client)
|
|
||||||
|
|
||||||
err := bridge.RefreshAll(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
j, _ := json.Marshal(bridge.GenerateDevices())
|
|
||||||
fmt.Println(string(j))
|
|
||||||
|
|
||||||
ch := make(chan models.Event)
|
|
||||||
go func() {
|
|
||||||
for event := range ch {
|
|
||||||
log.Println("EVENT", event.Name, event.Payload)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i, dev := range bridge.GenerateDevices() {
|
|
||||||
device := dev
|
|
||||||
switch device.InternalID {
|
|
||||||
case "6d5a45b0-ec69-4417-8588-717358b05086":
|
|
||||||
c, _ := models.ParseColorValue("xy:0.22,0.18")
|
|
||||||
device.State.Color = c
|
|
||||||
device.State.Intensity = 0.3
|
|
||||||
case "a71128f4-5295-4ae4-9fbc-5541abc8739b":
|
|
||||||
c, _ := models.ParseColorValue("k:6500")
|
|
||||||
device.State.Color = c
|
|
||||||
device.State.Intensity = 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
device.ID = i + 1
|
|
||||||
bridge.Update(device)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bridge.Run(context.Background(), ch)
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,7 +22,6 @@ type Bridge struct {
|
|||||||
externalID int
|
externalID int
|
||||||
lightStates []*hueLightState
|
lightStates []*hueLightState
|
||||||
sensorStates []*hueSensorState
|
sensorStates []*hueSensorState
|
||||||
quarantine map[string]time.Time
|
|
||||||
syncingPublish uint32
|
syncingPublish uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +33,6 @@ 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 {
|
||||||
@@ -73,10 +67,6 @@ 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 {
|
||||||
@@ -146,44 +136,6 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
|
|||||||
return eg.Wait()
|
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) {
|
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
||||||
sensorMap, err := b.getSensors(ctx)
|
sensorMap, err := b.getSensors(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -218,14 +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)
|
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) 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)
|
||||||
@@ -263,33 +207,6 @@ func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
|
|||||||
return result, nil
|
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 {
|
func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
|
||||||
if b.token != "" {
|
if b.token != "" {
|
||||||
resource = b.token + "/" + resource
|
resource = b.token + "/" + resource
|
||||||
@@ -309,29 +226,6 @@ func (b *Bridge) get(ctx context.Context, resource string, target interface{}) e
|
|||||||
return json.NewDecoder(res.Body).Decode(target)
|
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 {
|
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 {
|
||||||
|
|||||||
@@ -3,14 +3,8 @@ package hue
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupData struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Lights []string `json:"lights"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscoveryEntry struct {
|
type DiscoveryEntry struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
InternalIPAddress string `json:"internalipaddress"`
|
InternalIPAddress string `json:"internalipaddress"`
|
||||||
@@ -89,58 +83,6 @@ type LightStateInput struct {
|
|||||||
TransitionTime *int `json:"transitiontime,omitempty"`
|
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 {
|
type LightData struct {
|
||||||
State LightState `json:"state"`
|
State LightState `json:"state"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -250,36 +192,6 @@ type BridgeDeviceInfo struct {
|
|||||||
} `xml:"device"`
|
} `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 buttonNames = []string{"On", "DimUp", "DimDown", "Off"}
|
||||||
|
|
||||||
var errLinkButtonNotPressed = errors.New("link button not pressed")
|
var errLinkButtonNotPressed = errors.New("link button not pressed")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Driver struct {
|
|||||||
bridges []*Bridge
|
bridges []*Bridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
|
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
|
||||||
if address == "" {
|
if address == "" {
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
return nil, models.ErrAddressOnlyDryRunnable
|
return nil, models.ErrAddressOnlyDryRunnable
|
||||||
@@ -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 / 5)
|
fastTicker := time.NewTicker(time.Second / 10)
|
||||||
slowTicker := time.NewTicker(time.Second / 2)
|
slowTicker := time.NewTicker(time.Second / 3)
|
||||||
selectedTicker := fastTicker
|
selectedTicker := fastTicker
|
||||||
ticksUntilRefresh := 0
|
ticksUntilRefresh := 0
|
||||||
ticksSinceChange := 0
|
ticksSinceChange := 0
|
||||||
@@ -372,15 +372,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package hue
|
package hue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,37 +16,12 @@ type hueLightState struct {
|
|||||||
stale bool
|
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) {
|
func (s *hueLightState) Update(state models.DeviceState) {
|
||||||
input := LightStateInput{}
|
input := LightStateInput{}
|
||||||
if state.Power {
|
if state.Power {
|
||||||
input.On = ptrBool(true)
|
input.On = ptrBool(true)
|
||||||
if state.Color.IsKelvin() {
|
if state.Color.IsKelvin() {
|
||||||
input.CT = ptrInt(1000000 / state.Color.Kelvin)
|
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 {
|
if s.input.CT == nil || *s.input.CT != *input.CT {
|
||||||
s.stale = true
|
s.stale = true
|
||||||
}
|
}
|
||||||
@@ -93,13 +67,6 @@ func (s *hueLightState) Update(state models.DeviceState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *hueLightState) CheckStaleness(state LightState) {
|
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" {
|
if state.ColorMode == "xy" {
|
||||||
s.stale = true
|
s.stale = true
|
||||||
if s.input.CT == nil && s.input.Hue == nil {
|
if s.input.CT == nil && s.input.Hue == nil {
|
||||||
@@ -113,7 +80,7 @@ func (s *hueLightState) CheckStaleness(state LightState) {
|
|||||||
s.stale = true
|
s.stale = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
s.stale = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +186,6 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
|||||||
"deviceInternalId": newData.UniqueID,
|
"deviceInternalId": newData.UniqueID,
|
||||||
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
|
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
|
||||||
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
|
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
|
||||||
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +202,6 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
|||||||
"temperature": strconv.FormatFloat(float64(newData.State.Temperature)/100, 'f', 2, 64),
|
"temperature": strconv.FormatFloat(float64(newData.State.Temperature)/100, 'f', 2, 64),
|
||||||
"deviceId": strconv.Itoa(state.externalID),
|
"deviceId": strconv.Itoa(state.externalID),
|
||||||
"deviceInternalId": newData.UniqueID,
|
"deviceInternalId": newData.UniqueID,
|
||||||
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,620 +0,0 @@
|
|||||||
package hue2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bridge struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
externalID int
|
|
||||||
client *Client
|
|
||||||
needsUpdate chan struct{}
|
|
||||||
devices map[string]models.Device
|
|
||||||
resources map[string]*ResourceData
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBridge(client *Client) *Bridge {
|
|
||||||
return &Bridge{
|
|
||||||
client: client,
|
|
||||||
needsUpdate: make(chan struct{}, 4),
|
|
||||||
devices: make(map[string]models.Device, 64),
|
|
||||||
resources: make(map[string]*ResourceData, 256),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Run(ctx context.Context, eventCh chan<- models.Event) error {
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Connecting SSE...")
|
|
||||||
sse := b.client.SSE(ctx)
|
|
||||||
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Refreshing...")
|
|
||||||
err := b.RefreshAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Running updates...")
|
|
||||||
updated, err := b.MakeCongruent(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (startup)")
|
|
||||||
|
|
||||||
lightRefreshTimer := time.NewTicker(time.Second * 5)
|
|
||||||
defer lightRefreshTimer.Stop()
|
|
||||||
|
|
||||||
motionTicker := time.NewTicker(time.Second * 10)
|
|
||||||
defer motionTicker.Stop()
|
|
||||||
|
|
||||||
absences := make(map[int]time.Time)
|
|
||||||
lastPress := make(map[string]time.Time)
|
|
||||||
lastUpdate := time.Now()
|
|
||||||
needFull := false
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-lightRefreshTimer.C:
|
|
||||||
if time.Since(lastUpdate) < time.Second*2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if needFull {
|
|
||||||
err := b.RefreshAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := b.Refresh(ctx, "light")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := b.MakeCongruent(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if updated > 0 {
|
|
||||||
// Force this to cool for 30 seconds unless a manual update occurs.
|
|
||||||
if updated > 10 {
|
|
||||||
lastUpdate = time.Now().Add(time.Second * 15)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (regular check)")
|
|
||||||
}
|
|
||||||
case <-b.needsUpdate:
|
|
||||||
lastUpdate = time.Now()
|
|
||||||
|
|
||||||
updated, err := b.MakeCongruent(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if updated > 0 {
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (publish)")
|
|
||||||
}
|
|
||||||
case <-motionTicker.C:
|
|
||||||
for id, absenceTime := range absences {
|
|
||||||
seconds := int(time.Since(absenceTime).Seconds())
|
|
||||||
if seconds < 10 || seconds%60 >= 10 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCh <- models.Event{
|
|
||||||
Name: models.ENSensorPresenceEnded,
|
|
||||||
Payload: map[string]string{
|
|
||||||
"deviceId": fmt.Sprint(id),
|
|
||||||
"minutesElapsed": fmt.Sprint(seconds / 60),
|
|
||||||
"secondsElapsed": fmt.Sprint(seconds),
|
|
||||||
"lastUpdated": fmt.Sprint(absenceTime.Unix()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case data, ok := <-sse:
|
|
||||||
if !ok {
|
|
||||||
return errors.New("SSE Disconnected")
|
|
||||||
}
|
|
||||||
b.applyPatches(data.Data)
|
|
||||||
|
|
||||||
updated, err := b.MakeCongruent(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if updated > 0 {
|
|
||||||
log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (SSE)")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, patch := range data.Data {
|
|
||||||
if patch.Owner == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
if b.resources[patch.Owner.ID] == nil {
|
|
||||||
needFull = true
|
|
||||||
}
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
device, deviceOK := b.devices[patch.Owner.ID]
|
|
||||||
if !deviceOK || device.ID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if patch.Button != nil {
|
|
||||||
valid := false
|
|
||||||
if patch.Button.LastEvent == "initial_press" || patch.Button.LastEvent == "repeat" {
|
|
||||||
valid = true
|
|
||||||
} else if patch.Button.LastEvent == "long_release" {
|
|
||||||
valid = false
|
|
||||||
} else {
|
|
||||||
valid = data.CreationTime.Sub(lastPress[patch.ID]) >= time.Second*2
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid {
|
|
||||||
lastPress[patch.ID] = data.CreationTime
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
owner := b.resources[patch.Owner.ID]
|
|
||||||
b.mu.Unlock()
|
|
||||||
if owner != nil {
|
|
||||||
index := owner.ServiceIndex("button", patch.ID)
|
|
||||||
if index != -1 {
|
|
||||||
eventCh <- models.Event{
|
|
||||||
Name: models.ENButtonPressed,
|
|
||||||
Payload: map[string]string{
|
|
||||||
"deviceId": fmt.Sprint(device.ID),
|
|
||||||
"hueButtonEvent": patch.Button.LastEvent,
|
|
||||||
"buttonIndex": fmt.Sprint(index),
|
|
||||||
"buttonName": device.ButtonNames[index],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.Temperature != nil && patch.Temperature.Valid {
|
|
||||||
eventCh <- models.Event{
|
|
||||||
Name: models.ENSensorTemperature,
|
|
||||||
Payload: map[string]string{
|
|
||||||
"deviceId": fmt.Sprint(device.ID),
|
|
||||||
"deviceInternalId": patch.Owner.ID,
|
|
||||||
"temperature": fmt.Sprint(patch.Temperature.Temperature),
|
|
||||||
"lastUpdated": fmt.Sprint(data.CreationTime.Unix()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.Motion != nil && patch.Motion.Valid {
|
|
||||||
if patch.Motion.Motion {
|
|
||||||
eventCh <- models.Event{
|
|
||||||
Name: models.ENSensorPresenceStarted,
|
|
||||||
Payload: map[string]string{
|
|
||||||
"deviceId": fmt.Sprint(device.ID),
|
|
||||||
"deviceInternalId": patch.Owner.ID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(absences, device.ID)
|
|
||||||
} else {
|
|
||||||
eventCh <- models.Event{
|
|
||||||
Name: models.ENSensorPresenceEnded,
|
|
||||||
Payload: map[string]string{
|
|
||||||
"deviceId": fmt.Sprint(device.ID),
|
|
||||||
"deviceInternalId": device.InternalID,
|
|
||||||
"minutesElapsed": "0",
|
|
||||||
"secondsElapsed": "0",
|
|
||||||
"lastUpdated": fmt.Sprint(data.CreationTime.Unix()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
absences[device.ID] = data.CreationTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Update(devices ...models.Device) {
|
|
||||||
b.mu.Lock()
|
|
||||||
for _, device := range devices {
|
|
||||||
b.devices[device.InternalID] = device
|
|
||||||
}
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case b.needsUpdate <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) {
|
|
||||||
// Exhaust the channel to avoid more updates.
|
|
||||||
exhausted := false
|
|
||||||
for !exhausted {
|
|
||||||
select {
|
|
||||||
case <-b.needsUpdate:
|
|
||||||
default:
|
|
||||||
exhausted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
dur := time.Millisecond * 100
|
|
||||||
updates := make(map[string]ResourceUpdate)
|
|
||||||
for _, device := range b.devices {
|
|
||||||
resource := b.resources[device.InternalID]
|
|
||||||
|
|
||||||
// Update device
|
|
||||||
if resource.Metadata.Name != device.Name {
|
|
||||||
name := device.Name
|
|
||||||
updates["device/"+resource.ID] = ResourceUpdate{
|
|
||||||
Name: &name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update light
|
|
||||||
if lightID := resource.ServiceID("light"); lightID != nil {
|
|
||||||
light := b.resources[*lightID]
|
|
||||||
update := ResourceUpdate{TransitionDuration: &dur}
|
|
||||||
changed := false
|
|
||||||
|
|
||||||
lightsOut := light.Power != nil && !device.State.Power
|
|
||||||
if !lightsOut {
|
|
||||||
if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
|
|
||||||
mirek := 1000000 / device.State.Color.Kelvin
|
|
||||||
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
|
|
||||||
mirek = light.ColorTemperature.MirekSchema.MirekMinimum
|
|
||||||
}
|
|
||||||
if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
|
|
||||||
mirek = light.ColorTemperature.MirekSchema.MirekMaximum
|
|
||||||
}
|
|
||||||
if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek {
|
|
||||||
update.Mirek = &mirek
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
} else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil {
|
|
||||||
xy = light.Color.Gamut.Conform(xy).Round()
|
|
||||||
if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
|
|
||||||
update.ColorXY = &xy
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.02 {
|
|
||||||
brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
|
|
||||||
update.Brightness = &brightness
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if light.Power != nil && light.Power.On != device.State.Power {
|
|
||||||
update.Power = &device.State.Power
|
|
||||||
if device.State.Power {
|
|
||||||
brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
|
|
||||||
update.Brightness = &brightness
|
|
||||||
}
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
updates["light/"+light.ID] = update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updates) > 0 {
|
|
||||||
// Optimistically apply the updates to the states, so that the driver assumes they are set until
|
|
||||||
// proven otherwise by the SSE client.
|
|
||||||
newResources := make(map[string]*ResourceData, len(b.resources))
|
|
||||||
for key, value := range b.resources {
|
|
||||||
newResources[key] = value
|
|
||||||
}
|
|
||||||
for key, update := range updates {
|
|
||||||
id := strings.SplitN(key, "/", 2)[1]
|
|
||||||
newResources[id] = newResources[id].WithUpdate(update)
|
|
||||||
}
|
|
||||||
b.resources = newResources
|
|
||||||
}
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
|
||||||
for key := range updates {
|
|
||||||
update := updates[key]
|
|
||||||
split := strings.SplitN(key, "/", 2)
|
|
||||||
link := ResourceLink{Kind: split[0], ID: split[1]}
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
|
||||||
return b.client.UpdateResource(ctx, link, update)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err := eg.Wait()
|
|
||||||
if err != nil {
|
|
||||||
// Try to restore light states.
|
|
||||||
_ = b.Refresh(ctx, "light")
|
|
||||||
|
|
||||||
return len(updates), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(updates), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GenerateDevices() []models.Device {
|
|
||||||
b.mu.Lock()
|
|
||||||
resources := b.resources
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
devices := make([]models.Device, 0, 16)
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
device := models.Device{
|
|
||||||
BridgeID: b.externalID,
|
|
||||||
InternalID: resource.ID,
|
|
||||||
Name: resource.Metadata.Name,
|
|
||||||
DriverProperties: map[string]interface{}{
|
|
||||||
"archetype": resource.Metadata.Archetype,
|
|
||||||
"name": resource.ProductData.ProductName,
|
|
||||||
"product": resource.ProductData,
|
|
||||||
"legacyId": resource.LegacyID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set icon
|
|
||||||
if resource.ProductData.ProductName == "Hue dimmer switch" {
|
|
||||||
device.Icon = "switch"
|
|
||||||
} else if resource.ProductData.ProductName == "Hue motion sensor" {
|
|
||||||
device.Icon = "sensor"
|
|
||||||
} else {
|
|
||||||
device.Icon = "lightbulb"
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonCount := 0
|
|
||||||
for _, ptr := range resource.Services {
|
|
||||||
switch ptr.Kind {
|
|
||||||
case "device_power":
|
|
||||||
{
|
|
||||||
device.DriverProperties["battery"] = resources[ptr.ID].PowerState
|
|
||||||
}
|
|
||||||
case "button":
|
|
||||||
{
|
|
||||||
buttonCount += 1
|
|
||||||
}
|
|
||||||
case "zigbee_connectivity":
|
|
||||||
{
|
|
||||||
device.DriverProperties["zigbee"] = resources[ptr.ID].Status
|
|
||||||
}
|
|
||||||
case "motion":
|
|
||||||
{
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCPresence)
|
|
||||||
}
|
|
||||||
case "temperature":
|
|
||||||
{
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
|
|
||||||
}
|
|
||||||
case "light":
|
|
||||||
{
|
|
||||||
light := resources[ptr.ID]
|
|
||||||
|
|
||||||
if light.Power != nil {
|
|
||||||
device.State.Power = light.Power.On
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCPower)
|
|
||||||
}
|
|
||||||
if light.Dimming != nil {
|
|
||||||
device.State.Intensity = light.Dimming.Brightness / 100
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCIntensity)
|
|
||||||
}
|
|
||||||
if light.ColorTemperature != nil {
|
|
||||||
if light.ColorTemperature.Mirek != nil {
|
|
||||||
device.State.Color = models.ColorValue{
|
|
||||||
Kelvin: int(1000000 / *light.ColorTemperature.Mirek),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
|
|
||||||
device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
|
|
||||||
device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum
|
|
||||||
}
|
|
||||||
if light.Color != nil {
|
|
||||||
if device.State.Color.IsEmpty() {
|
|
||||||
device.State.Color = models.ColorValue{
|
|
||||||
XY: &light.Color.XY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
device.DriverProperties["colorGamut"] = light.Color.Gamut
|
|
||||||
device.DriverProperties["colorGamutType"] = light.Color.GamutType
|
|
||||||
device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if buttonCount == 4 {
|
|
||||||
device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"}
|
|
||||||
} else if buttonCount == 1 {
|
|
||||||
device.ButtonNames = []string{"Button"}
|
|
||||||
} else {
|
|
||||||
for n := 1; n <= buttonCount; n++ {
|
|
||||||
device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices = append(devices, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Refresh(ctx context.Context, kind string) error {
|
|
||||||
if kind == "device" {
|
|
||||||
// Device refresh requires the full deal as services are taken for granted.
|
|
||||||
return b.RefreshAll(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
resources, err := b.client.Resources(ctx, kind)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
oldResources := b.resources
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
newResources := make(map[string]*ResourceData, len(b.resources))
|
|
||||||
for key, value := range oldResources {
|
|
||||||
if value.Type != kind {
|
|
||||||
newResources[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := range resources {
|
|
||||||
resource := resources[i]
|
|
||||||
newResources[resource.ID] = &resource
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
b.resources = newResources
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) RefreshAll(ctx context.Context) error {
|
|
||||||
allResources, err := b.client.AllResources(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resources := make(map[string]*ResourceData, len(allResources))
|
|
||||||
for i := range allResources {
|
|
||||||
resource := allResources[i]
|
|
||||||
resources[resource.ID] = &resource
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
b.resources = resources
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) applyPatches(patches []ResourceData) {
|
|
||||||
b.mu.Lock()
|
|
||||||
newResources := make(map[string]*ResourceData, len(b.resources))
|
|
||||||
for key, value := range b.resources {
|
|
||||||
newResources[key] = value
|
|
||||||
}
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
for _, patch := range patches {
|
|
||||||
if res := newResources[patch.ID]; res != nil {
|
|
||||||
resCopy := *res
|
|
||||||
|
|
||||||
if patch.Power != nil && resCopy.Power != nil {
|
|
||||||
cp := *resCopy.Power
|
|
||||||
resCopy.Power = &cp
|
|
||||||
resCopy.Power.On = patch.Power.On
|
|
||||||
}
|
|
||||||
if patch.Color != nil && resCopy.Color != nil {
|
|
||||||
cp := *resCopy.Color
|
|
||||||
resCopy.Color = &cp
|
|
||||||
resCopy.Color.XY = patch.Color.XY
|
|
||||||
|
|
||||||
cp2 := *resCopy.ColorTemperature
|
|
||||||
resCopy.ColorTemperature = &cp2
|
|
||||||
resCopy.ColorTemperature.Mirek = nil
|
|
||||||
}
|
|
||||||
if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil {
|
|
||||||
cp := *resCopy.ColorTemperature
|
|
||||||
resCopy.ColorTemperature = &cp
|
|
||||||
resCopy.ColorTemperature.Mirek = patch.ColorTemperature.Mirek
|
|
||||||
}
|
|
||||||
if patch.Dimming != nil && resCopy.Dimming != nil {
|
|
||||||
cp := *resCopy.Dimming
|
|
||||||
resCopy.Dimming = &cp
|
|
||||||
resCopy.Dimming.Brightness = patch.Dimming.Brightness
|
|
||||||
}
|
|
||||||
if patch.Dynamics != nil {
|
|
||||||
resCopy.Dynamics = patch.Dynamics
|
|
||||||
}
|
|
||||||
if patch.Alert != nil {
|
|
||||||
resCopy.Alert = patch.Alert
|
|
||||||
}
|
|
||||||
if patch.PowerState != nil {
|
|
||||||
resCopy.PowerState = patch.PowerState
|
|
||||||
}
|
|
||||||
if patch.Temperature != nil {
|
|
||||||
resCopy.Temperature = patch.Temperature
|
|
||||||
}
|
|
||||||
if patch.Status != nil {
|
|
||||||
resCopy.Status = patch.Status
|
|
||||||
}
|
|
||||||
resCopy.Metadata.Name = patch.Metadata.Name
|
|
||||||
|
|
||||||
newResources[patch.ID] = &resCopy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
b.resources = newResources
|
|
||||||
b.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) SearchDevices(ctx context.Context, timeout time.Duration) ([]models.Device, error) {
|
|
||||||
// Record the current state.
|
|
||||||
b.mu.Lock()
|
|
||||||
before := b.resources
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
// Spend half the time waiting for devices
|
|
||||||
// TODO: Wait for v2 endpoint
|
|
||||||
err := b.client.LegacyDiscover(ctx, "sensors")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout / 1):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spend half the time waiting for lights
|
|
||||||
// TODO: Wait for v2 endpoint
|
|
||||||
err = b.client.LegacyDiscover(ctx, "lights")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout / 1):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform a full refresh.
|
|
||||||
err = b.RefreshAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for new devices
|
|
||||||
devices := b.GenerateDevices()
|
|
||||||
newDevices := make([]models.Device, 0)
|
|
||||||
for _, device := range devices {
|
|
||||||
if before[device.InternalID] == nil {
|
|
||||||
newDevices = append(newDevices, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return said devices.
|
|
||||||
return newDevices, nil
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package hue2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewClient(host, token string) *Client {
|
|
||||||
ch := make(chan struct{}, 5)
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
ch <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
host: host,
|
|
||||||
token: token,
|
|
||||||
ch: ch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
host string
|
|
||||||
token string
|
|
||||||
ch chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Register(ctx context.Context) (string, error) {
|
|
||||||
result := make([]CreateUserResponse, 0, 1)
|
|
||||||
err := c.post(ctx, "api/", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) == 0 || result[0].Error != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
return c.Register(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result[0].Success == nil {
|
|
||||||
return "", models.ErrUnexpectedResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
c.token = result[0].Success.Username
|
|
||||||
|
|
||||||
return result[0].Success.Username, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) {
|
|
||||||
res := struct {
|
|
||||||
Error interface{}
|
|
||||||
Data []ResourceData
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := c.get(ctx, "clip/v2/resource", &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) {
|
|
||||||
res := struct {
|
|
||||||
Error interface{}
|
|
||||||
Data []ResourceData
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := c.get(ctx, "clip/v2/resource/"+kind, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error {
|
|
||||||
return c.put(ctx, link.Path(), update, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) LegacyDiscover(ctx context.Context, kind string) error {
|
|
||||||
return c.legacyPost(ctx, kind, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SSE(ctx context.Context) <-chan SSEUpdate {
|
|
||||||
ch := make(chan SSEUpdate, 4)
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{
|
|
||||||
"Accept": "text/event-stream",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Println("SSE Connect error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
br := bufio.NewReader(reader)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := br.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
log.Println("SSE Read error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
line = strings.Trim(line, " \t\r\n")
|
|
||||||
kv := strings.SplitN(line, ": ", 2)
|
|
||||||
if len(kv) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kv[0] {
|
|
||||||
case "data":
|
|
||||||
var data []SSEUpdate
|
|
||||||
err := json.Unmarshal([]byte(kv[1]), &data)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Parsing SSE event failed:", err)
|
|
||||||
log.Println(" json:", kv[1])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, obj := range data {
|
|
||||||
ch <- obj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case <-c.ch:
|
|
||||||
defer func() {
|
|
||||||
c.ch <- struct{}{}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range headers {
|
|
||||||
req.Header.Set(key, value)
|
|
||||||
}
|
|
||||||
req.Header.Set("hue-application-key", c.token)
|
|
||||||
|
|
||||||
client := httpClient
|
|
||||||
if headers["Accept"] == "text/event-stream" {
|
|
||||||
client = sseClient
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
_ = res.Body.Close()
|
|
||||||
return nil, errors.New(res.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) get(ctx context.Context, path string, target interface{}) error {
|
|
||||||
body, err := c.getReader(ctx, path, map[string]string{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return json.NewDecoder(body).Decode(&target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-c.ch:
|
|
||||||
defer func() {
|
|
||||||
c.ch <- struct{}{}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
rb, err := reqBody(body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("hue-application-key", c.token)
|
|
||||||
|
|
||||||
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 (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-c.ch:
|
|
||||||
defer func() {
|
|
||||||
c.ch <- struct{}{}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
rb, err := reqBody(body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.token != "" {
|
|
||||||
req.Header.Set("hue-application-key", c.token)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-c.ch:
|
|
||||||
defer func() {
|
|
||||||
c.ch <- struct{}{}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
rb, err := reqBody(body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.token != "" {
|
|
||||||
resource = c.token + "/" + resource
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb)
|
|
||||||
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 reqBody(body interface{}) (io.Reader, error) {
|
|
||||||
if body == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := body.(type) {
|
|
||||||
case []byte:
|
|
||||||
return bytes.NewReader(v), nil
|
|
||||||
case string:
|
|
||||||
return strings.NewReader(v), nil
|
|
||||||
case io.Reader:
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
jsonData, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes.NewReader(jsonData), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sseClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: time.Hour * 1000000,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
MaxIdleConns: 5,
|
|
||||||
MaxIdleConnsPerHost: 1,
|
|
||||||
IdleConnTimeout: 0,
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
Timeout: time.Hour * 1000000,
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
MaxIdleConns: 256,
|
|
||||||
MaxIdleConnsPerHost: 16,
|
|
||||||
IdleConnTimeout: 10 * time.Minute,
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
Timeout: time.Minute,
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
package hue2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeviceData struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
LegacyID string `json:"id_v1"`
|
|
||||||
Metadata DeviceMetadata `json:"metadata"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
ProductData DeviceProductData `json:"product_data"`
|
|
||||||
Services []ResourceLink `json:"services"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeviceMetadata struct {
|
|
||||||
Archetype string `json:"archetype"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeviceProductData struct {
|
|
||||||
Certified bool `json:"certified"`
|
|
||||||
ManufacturerName string `json:"manufacturer_name"`
|
|
||||||
ModelID string `json:"model_id"`
|
|
||||||
ProductArchetype string `json:"product_archetype"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
SoftwareVersion string `json:"software_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SSEUpdate struct {
|
|
||||||
CreationTime time.Time `json:"creationTime"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data []ResourceData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceData struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
LegacyID string `json:"id_v1"`
|
|
||||||
Metadata DeviceMetadata `json:"metadata"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
Mode *string `json:"mode"`
|
|
||||||
|
|
||||||
Owner *ResourceLink `json:"owner"`
|
|
||||||
ProductData *DeviceProductData `json:"product_data"`
|
|
||||||
Services []ResourceLink `json:"services"`
|
|
||||||
Button *SensorButton `json:"button"`
|
|
||||||
Power *LightPower `json:"on"`
|
|
||||||
Color *LightColor `json:"color"`
|
|
||||||
ColorTemperature *LightCT `json:"color_temperature"`
|
|
||||||
Dimming *LightDimming `json:"dimming"`
|
|
||||||
Dynamics *LightDynamics `json:"dynamics"`
|
|
||||||
Alert *LightAlert `json:"alert"`
|
|
||||||
PowerState *PowerState `json:"power_state"`
|
|
||||||
Temperature *SensorTemperature `json:"temperature"`
|
|
||||||
Motion *SensorMotion `json:"motion"`
|
|
||||||
Status *string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *ResourceData) ServiceID(kind string) *string {
|
|
||||||
for _, ptr := range res.Services {
|
|
||||||
if ptr.Kind == kind {
|
|
||||||
return &ptr.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *ResourceData) ServiceIndex(kind string, id string) int {
|
|
||||||
idx := 0
|
|
||||||
for _, link := range res.Services {
|
|
||||||
if link.ID == id {
|
|
||||||
return idx
|
|
||||||
} else if link.Kind == kind {
|
|
||||||
idx += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *ResourceData) WithUpdate(update ResourceUpdate) *ResourceData {
|
|
||||||
resCopy := *res
|
|
||||||
|
|
||||||
if update.Name != nil {
|
|
||||||
resCopy.Metadata.Name = *update.Name
|
|
||||||
}
|
|
||||||
if update.Power != nil {
|
|
||||||
cp := *resCopy.Power
|
|
||||||
resCopy.Power = &cp
|
|
||||||
resCopy.Power.On = *update.Power
|
|
||||||
}
|
|
||||||
if update.ColorXY != nil {
|
|
||||||
cp := *resCopy.Color
|
|
||||||
resCopy.Color = &cp
|
|
||||||
resCopy.Color.XY = *update.ColorXY
|
|
||||||
|
|
||||||
if resCopy.ColorTemperature != nil {
|
|
||||||
cp := *resCopy.ColorTemperature
|
|
||||||
resCopy.ColorTemperature = &cp
|
|
||||||
resCopy.ColorTemperature.Mirek = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if update.Mirek != nil {
|
|
||||||
cp := *resCopy.ColorTemperature
|
|
||||||
resCopy.ColorTemperature = &cp
|
|
||||||
mirek := *update.Mirek
|
|
||||||
resCopy.ColorTemperature.Mirek = &mirek
|
|
||||||
}
|
|
||||||
if update.Brightness != nil {
|
|
||||||
cp := *resCopy.Dimming
|
|
||||||
resCopy.Dimming = &cp
|
|
||||||
resCopy.Dimming.Brightness = *update.Brightness
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
type SensorButton struct {
|
|
||||||
LastEvent string `json:"last_event"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SensorMotion struct {
|
|
||||||
Motion bool `json:"motion"`
|
|
||||||
Valid bool `json:"motion_valid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SensorTemperature struct {
|
|
||||||
Temperature float64 `json:"temperature"`
|
|
||||||
Valid bool `json:"temperature_valid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PowerState struct {
|
|
||||||
BatteryState string `json:"battery_state"`
|
|
||||||
BatteryLevel float64 `json:"battery_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightPower struct {
|
|
||||||
On bool `json:"on"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightDimming struct {
|
|
||||||
Brightness float64 `json:"brightness"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightColor struct {
|
|
||||||
Gamut models.ColorGamut `json:"gamut"`
|
|
||||||
GamutType string `json:"gamut_type"`
|
|
||||||
XY models.ColorXY `json:"xy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightCT struct {
|
|
||||||
Mirek *int `json:"mirek"`
|
|
||||||
MirekSchema LightCTMirekSchema `json:"mirek_schema"`
|
|
||||||
MirekValid bool `json:"mirek_valid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightCTMirekSchema struct {
|
|
||||||
MirekMaximum int `json:"mirek_maximum"`
|
|
||||||
MirekMinimum int `json:"mirek_minimum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightDynamics struct {
|
|
||||||
Speed float64 `json:"speed"`
|
|
||||||
SpeedValid bool `json:"speed_valid"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
StatusValues []string `json:"status_values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightAlert struct {
|
|
||||||
ActionValues []string `json:"action_values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceUpdate struct {
|
|
||||||
Name *string
|
|
||||||
Power *bool
|
|
||||||
ColorXY *models.ColorXY
|
|
||||||
Brightness *float64
|
|
||||||
Mirek *int
|
|
||||||
TransitionDuration *time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
|
|
||||||
chunks := make([]string, 0, 4)
|
|
||||||
if r.Name != nil {
|
|
||||||
s, _ := json.Marshal(*r.Name)
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"metadata":{"name":%s}`, string(s)))
|
|
||||||
}
|
|
||||||
if r.Power != nil {
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power))
|
|
||||||
}
|
|
||||||
if r.ColorXY != nil {
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y))
|
|
||||||
}
|
|
||||||
if r.Brightness != nil {
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness))
|
|
||||||
}
|
|
||||||
if r.Mirek != nil {
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek))
|
|
||||||
}
|
|
||||||
if r.TransitionDuration != nil {
|
|
||||||
chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceLink struct {
|
|
||||||
ID string `json:"rid"`
|
|
||||||
Kind string `json:"rtype"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rl *ResourceLink) Path() string {
|
|
||||||
return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateUserInput struct {
|
|
||||||
DeviceType string `json:"devicetype"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateUserResponse struct {
|
|
||||||
Success *struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
} `json:"success"`
|
|
||||||
Error *struct {
|
|
||||||
Type int `json:"type"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
} `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscoveryEntry struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
InternalIPAddress string `json:"internalipaddress"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BridgeDeviceInfo struct {
|
|
||||||
XMLName xml.Name `xml:"root"`
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
SpecVersion struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Major string `xml:"major"`
|
|
||||||
Minor string `xml:"minor"`
|
|
||||||
} `xml:"specVersion"`
|
|
||||||
URLBase string `xml:"URLBase"`
|
|
||||||
Device struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
DeviceType string `xml:"deviceType"`
|
|
||||||
FriendlyName string `xml:"friendlyName"`
|
|
||||||
Manufacturer string `xml:"manufacturer"`
|
|
||||||
ManufacturerURL string `xml:"manufacturerURL"`
|
|
||||||
ModelDescription string `xml:"modelDescription"`
|
|
||||||
ModelName string `xml:"modelName"`
|
|
||||||
ModelNumber string `xml:"modelNumber"`
|
|
||||||
ModelURL string `xml:"modelURL"`
|
|
||||||
SerialNumber string `xml:"serialNumber"`
|
|
||||||
UDN string `xml:"UDN"`
|
|
||||||
PresentationURL string `xml:"presentationURL"`
|
|
||||||
IconList struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Icon struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Mimetype string `xml:"mimetype"`
|
|
||||||
Height string `xml:"height"`
|
|
||||||
Width string `xml:"width"`
|
|
||||||
Depth string `xml:"depth"`
|
|
||||||
URL string `xml:"url"`
|
|
||||||
} `xml:"icon"`
|
|
||||||
} `xml:"iconList"`
|
|
||||||
} `xml:"device"`
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package hue2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Driver struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
bridges []*Bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]models.Bridge, error) {
|
|
||||||
if address == "" {
|
|
||||||
if !dryRun {
|
|
||||||
return nil, models.ErrAddressOnlyDryRunnable
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := http.Get("https://discovery.meethue.com")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
entries := make([]DiscoveryEntry, 0, 8)
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&entries)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bridges := make([]models.Bridge, 0, len(entries))
|
|
||||||
for _, entry := range entries {
|
|
||||||
bridges = append(bridges, models.Bridge{
|
|
||||||
ID: -1,
|
|
||||||
Name: entry.Id,
|
|
||||||
Driver: models.DTHue2,
|
|
||||||
Address: entry.InternalIPAddress,
|
|
||||||
Token: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridges, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceInfo := BridgeDeviceInfo{}
|
|
||||||
res, err := http.Get(fmt.Sprintf("http://%s/description.xml", address))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
err = xml.NewDecoder(res.Body).Decode(&deviceInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge := models.Bridge{
|
|
||||||
ID: -1,
|
|
||||||
Name: deviceInfo.Device.FriendlyName,
|
|
||||||
Driver: models.DTHue2,
|
|
||||||
Address: address,
|
|
||||||
Token: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dryRun {
|
|
||||||
client := NewClient(address, "")
|
|
||||||
|
|
||||||
timeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
bridge.Token, err = client.Register(timeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []models.Bridge{bridge}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SearchDevices(ctx context.Context, bridge models.Bridge, timeout time.Duration) ([]models.Device, error) {
|
|
||||||
return d.ensureBridge(bridge).SearchDevices(ctx, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ListDevices(_ context.Context, bridge models.Bridge) ([]models.Device, error) {
|
|
||||||
return d.ensureBridge(bridge).GenerateDevices(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) Publish(_ context.Context, bridge models.Bridge, devices []models.Device) error {
|
|
||||||
d.ensureBridge(bridge).Update(devices...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models.Event) error {
|
|
||||||
return d.ensureBridge(bridge).Run(ctx, ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ensureBridge(info models.Bridge) *Bridge {
|
|
||||||
d.mu.Lock()
|
|
||||||
for _, bridge := range d.bridges {
|
|
||||||
if bridge.client.host == info.Address {
|
|
||||||
d.mu.Unlock()
|
|
||||||
return bridge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bridge := NewBridge(NewClient(info.Address, info.Token))
|
|
||||||
bridge.externalID = info.ID
|
|
||||||
d.bridges = append(d.bridges, bridge)
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
return bridge
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ type Driver struct {
|
|||||||
bridges []*Bridge
|
bridges []*Bridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, _ bool) ([]models.Bridge, error) {
|
func (d *Driver) SearchBridge(ctx context.Context, address string, _ bool) ([]models.Bridge, error) {
|
||||||
if address == "" {
|
if address == "" {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
package mill
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type bridge struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
luciferID int
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
|
|
||||||
token string
|
|
||||||
userId int
|
|
||||||
mustRefreshBy time.Time
|
|
||||||
|
|
||||||
luciferMillIDMap map[int]int
|
|
||||||
millLuciferIDMap map[int]int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) listDevices(ctx context.Context) ([]models.Device, error) {
|
|
||||||
err := b.authenticate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var shlRes listHomeResBody
|
|
||||||
err = b.command(ctx, "selectHomeList", listHomeReqBody{}, &shlRes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
devices := make([]millDevice, 0, 16)
|
|
||||||
for _, home := range shlRes.HomeList {
|
|
||||||
var gidRes listDeviceResBody
|
|
||||||
err = b.command(ctx, "getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
devices = append(devices, gidRes.DeviceInfo...)
|
|
||||||
}
|
|
||||||
|
|
||||||
luciferDevices := make([]models.Device, len(devices), len(devices))
|
|
||||||
for i, device := range devices {
|
|
||||||
luciferDevices[i] = models.Device{
|
|
||||||
ID: b.millLuciferIDMap[device.DeviceID],
|
|
||||||
BridgeID: b.luciferID,
|
|
||||||
InternalID: fmt.Sprintf("%d", device.DeviceID),
|
|
||||||
Icon: "heater",
|
|
||||||
Name: device.DeviceName,
|
|
||||||
Capabilities: []models.DeviceCapability{models.DCTemperatureControl, models.DCPower},
|
|
||||||
ButtonNames: nil,
|
|
||||||
DriverProperties: map[string]interface{}{
|
|
||||||
"subDomain": fmt.Sprintf("%d", device.SubDomainID),
|
|
||||||
},
|
|
||||||
UserProperties: nil,
|
|
||||||
SceneAssignments: nil,
|
|
||||||
SceneState: nil,
|
|
||||||
State: models.DeviceState{
|
|
||||||
Power: device.PowerStatus > 0,
|
|
||||||
Temperature: device.HolidayTemp,
|
|
||||||
},
|
|
||||||
Tags: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return luciferDevices, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) pushStateChange(ctx context.Context, deviceModel models.Device) error {
|
|
||||||
b.mu.Lock()
|
|
||||||
if b.luciferMillIDMap == nil {
|
|
||||||
b.luciferMillIDMap = make(map[int]int, 4)
|
|
||||||
b.millLuciferIDMap = make(map[int]int, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.luciferMillIDMap[deviceModel.ID] == 0 {
|
|
||||||
millID, _ := strconv.Atoi(deviceModel.InternalID)
|
|
||||||
b.luciferMillIDMap[deviceModel.ID] = millID
|
|
||||||
b.millLuciferIDMap[millID] = deviceModel.ID
|
|
||||||
}
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
status := 0
|
|
||||||
if deviceModel.State.Power {
|
|
||||||
status = 1
|
|
||||||
}
|
|
||||||
powerReq := deviceControlReqBody{
|
|
||||||
SubDomain: deviceModel.DriverProperties["subDomain"].(string),
|
|
||||||
DeviceID: b.luciferMillIDMap[deviceModel.ID],
|
|
||||||
TestStatus: 1,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
err := b.command(ctx, "deviceControl", powerReq, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tempReq := changeInfoReqBody{
|
|
||||||
DeviceID: b.luciferMillIDMap[deviceModel.ID],
|
|
||||||
Value: deviceModel.State.Temperature,
|
|
||||||
TimeZoneNum: "+02:00",
|
|
||||||
Key: "holidayTemp",
|
|
||||||
}
|
|
||||||
err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
|
|
||||||
err := b.authenticate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
url := serviceEndpoint + command
|
|
||||||
method := "POST"
|
|
||||||
nonce := makeNonce()
|
|
||||||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
|
||||||
timeout := "300"
|
|
||||||
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write([]byte(timeout))
|
|
||||||
h.Write([]byte(timestamp))
|
|
||||||
h.Write([]byte(nonce))
|
|
||||||
h.Write([]byte(b.token))
|
|
||||||
signature := fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
addDefaultHeaders(req)
|
|
||||||
req.Header.Add("X-Zc-Timestamp", timestamp)
|
|
||||||
req.Header.Add("X-Zc-Timeout", timeout)
|
|
||||||
req.Header.Add("X-Zc-Nonce", nonce)
|
|
||||||
req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
|
|
||||||
req.Header.Add("X-Zc-User-Signature", signature)
|
|
||||||
req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrCannotForwardRequest
|
|
||||||
} else if res.StatusCode != 200 {
|
|
||||||
return models.ErrIncorrectToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&target)
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrUnexpectedResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) authenticate(ctx context.Context) error {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
|
|
||||||
body, err := json.Marshal(authReqBody{
|
|
||||||
Account: b.username,
|
|
||||||
Password: b.password,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrMissingToken
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint + "login", bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrMissingToken
|
|
||||||
}
|
|
||||||
|
|
||||||
addDefaultHeaders(req)
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrCannotForwardRequest
|
|
||||||
} else if res.StatusCode != 200 {
|
|
||||||
return models.ErrIncorrectToken
|
|
||||||
}
|
|
||||||
|
|
||||||
var resBody authResBody
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&resBody)
|
|
||||||
if err != nil {
|
|
||||||
return models.ErrBridgeSearchFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Mill: Authenticated as %s", resBody.NickName)
|
|
||||||
b.userId = resBody.UserID
|
|
||||||
b.token = resBody.Token
|
|
||||||
b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNonce() string {
|
|
||||||
buf := make([]byte, 8)
|
|
||||||
_, _ = io.ReadFull(rand.Reader, buf)
|
|
||||||
|
|
||||||
return fmt.Sprintf("%x", buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addDefaultHeaders(req *http.Request) {
|
|
||||||
req.Header.Add("Content-Type", "application/x-zc-object")
|
|
||||||
req.Header.Add("Connection", "Keep-Alive")
|
|
||||||
req.Header.Add("X-Zc-Major-Domain", "seanywell")
|
|
||||||
req.Header.Add("X-Zc-Msg-Name", "millService")
|
|
||||||
req.Header.Add("X-Zc-Sub-Domain", "milltype")
|
|
||||||
req.Header.Add("X-Zc-Seq-Id", "1")
|
|
||||||
req.Header.Add("X-Zc-Version", "1")
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package mill
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Driver struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
bridges map[int]*bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SearchBridge(ctx context.Context, address, token string, _ bool) ([]models.Bridge, error) {
|
|
||||||
bridgeData := models.Bridge{
|
|
||||||
Name: fmt.Sprintf("Mill account (%s)", address),
|
|
||||||
Driver: models.DTMill,
|
|
||||||
Address: address,
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := d.ensureBridge(bridgeData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.authenticate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return []models.Bridge{bridgeData}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SearchDevices(context.Context, models.Bridge, time.Duration) ([]models.Device, error) {
|
|
||||||
// You would have to configure devices with the Mill app, unfortunately.
|
|
||||||
return []models.Device{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
|
|
||||||
b, err := d.ensureBridge(bridge)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.listDevices(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
|
|
||||||
b, err := d.ensureBridge(bridge)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, device := range devices {
|
|
||||||
err = b.pushStateChange(ctx, device)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) Run(ctx context.Context, _ models.Bridge, _ chan<- models.Event) error {
|
|
||||||
// TODO: Maybe do something with the thermostat on the device
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ensureBridge(model models.Bridge) (*bridge, error) {
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
|
|
||||||
if d.bridges == nil {
|
|
||||||
d.bridges = make(map[int]*bridge, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.bridges[model.ID] == nil {
|
|
||||||
newBridge := &bridge{
|
|
||||||
luciferID: model.ID,
|
|
||||||
username: model.Address,
|
|
||||||
password: model.Token,
|
|
||||||
}
|
|
||||||
|
|
||||||
if model.ID <= 0 {
|
|
||||||
return newBridge, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.bridges[model.ID] = newBridge
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.bridges[model.ID], nil
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package mill
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const accountEndpoint = "https://eurouter.ablecloud.cn:9005/zc-account/v1/"
|
|
||||||
|
|
||||||
const serviceEndpoint = "https://eurouter.ablecloud.cn:9005/millService/v1/"
|
|
||||||
|
|
||||||
type millHome struct {
|
|
||||||
HomeID int64 `json:"homeId"`
|
|
||||||
HomeName string `json:"homeName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type millDevice struct {
|
|
||||||
DeviceID int `json:"deviceId"`
|
|
||||||
DeviceName string `json:"deviceName"`
|
|
||||||
PowerStatus int `json:"powerStatus"`
|
|
||||||
HolidayTemp int `json:"holidayTemp"`
|
|
||||||
CurrentTemp float64 `json:"currentTemp"`
|
|
||||||
SubDomainID int `json:"subDomainId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authReqBody struct {
|
|
||||||
Account string `json:"account"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authResBody struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
UserID int `json:"userId"`
|
|
||||||
NickName string `json:"nickName"`
|
|
||||||
TokenExpire string `json:"tokenExpire"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listHomeReqBody struct{}
|
|
||||||
|
|
||||||
type listHomeResBody struct {
|
|
||||||
HomeList []millHome `json:"homeList"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listDeviceReqBody struct {
|
|
||||||
HomeID int64 `json:"homeId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listDeviceResBody struct {
|
|
||||||
DeviceInfo []millDevice `json:"deviceInfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type changeInfoReqBody struct {
|
|
||||||
HomeType int `json:"homeType"`
|
|
||||||
DeviceID int `json:"deviceId"`
|
|
||||||
Value int `json:"value"`
|
|
||||||
TimeZoneNum string `json:"timeZoneNum"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type deviceControlReqBody struct {
|
|
||||||
SubDomain string `json:"subDomain"`
|
|
||||||
DeviceID int `json:"deviceId"`
|
|
||||||
TestStatus int `json:"testStatus"`
|
|
||||||
Operation int `json:"operation"`
|
|
||||||
Status int `json:"status"`
|
|
||||||
WindStatus int `json:"windStatus"`
|
|
||||||
TempType int `json:"tempType"`
|
|
||||||
PowerLevel int `json:"powerLevel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var location *time.Location
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
myLocation, err := time.LoadLocation("Europe/Oslo")
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
location = myLocation
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ type Driver struct {
|
|||||||
|
|
||||||
// SearchBridge checks the bridge at the address. If it's not a dry-run, you must hold down the power button
|
// SearchBridge checks the bridge at the address. If it's not a dry-run, you must hold down the power button
|
||||||
// before calling this function and wait for the pattern.
|
// before calling this function and wait for the pattern.
|
||||||
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
|
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
|
||||||
res, err := http.Get(fmt.Sprintf("http://%s/device_info", address))
|
res, err := http.Get(fmt.Sprintf("http://%s/device_info", address))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ type deviceRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type deviceStateRecord struct {
|
type deviceStateRecord struct {
|
||||||
DeviceID int `db:"device_id"`
|
DeviceID int `db:"device_id"`
|
||||||
Hue float64 `db:"hue"`
|
Hue float64 `db:"hue"`
|
||||||
Saturation float64 `db:"saturation"`
|
Saturation float64 `db:"saturation"`
|
||||||
Kelvin int `db:"kelvin"`
|
Kelvin int `db:"kelvin"`
|
||||||
Power bool `db:"power"`
|
Power bool `db:"power"`
|
||||||
Intensity float64 `db:"intensity"`
|
Intensity float64 `db:"intensity"`
|
||||||
Temperature int `db:"temperature"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type devicePropertyRecord struct {
|
type devicePropertyRecord struct {
|
||||||
@@ -228,16 +227,15 @@ func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices
|
|||||||
|
|
||||||
if mode == 0 || mode&models.SMState != 0 {
|
if mode == 0 || mode&models.SMState != 0 {
|
||||||
_, err = tx.NamedExecContext(ctx, `
|
_, err = tx.NamedExecContext(ctx, `
|
||||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, temperature)
|
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
||||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :temperature)
|
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
||||||
`, deviceStateRecord{
|
`, deviceStateRecord{
|
||||||
DeviceID: record.ID,
|
DeviceID: record.ID,
|
||||||
Hue: device.State.Color.Hue,
|
Hue: device.State.Color.Hue,
|
||||||
Saturation: device.State.Color.Saturation,
|
Saturation: device.State.Color.Saturation,
|
||||||
Kelvin: device.State.Color.Kelvin,
|
Kelvin: device.State.Color.Kelvin,
|
||||||
Power: device.State.Power,
|
Power: device.State.Power,
|
||||||
Intensity: device.State.Intensity,
|
Intensity: device.State.Intensity,
|
||||||
Temperature: device.State.Temperature,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbErr(err)
|
return dbErr(err)
|
||||||
@@ -381,7 +379,6 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
|
|||||||
Kelvin: state.Kelvin,
|
Kelvin: state.Kelvin,
|
||||||
},
|
},
|
||||||
Intensity: state.Intensity,
|
Intensity: state.Intensity,
|
||||||
Temperature: state.Temperature,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,6 @@ 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")
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ type DriverKind string
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
DTHue DriverKind = "Hue"
|
DTHue DriverKind = "Hue"
|
||||||
DTHue2 DriverKind = "Hue2"
|
|
||||||
DTNanoLeaf DriverKind = "Nanoleaf"
|
DTNanoLeaf DriverKind = "Nanoleaf"
|
||||||
DTLIFX DriverKind = "LIFX"
|
DTLIFX DriverKind = "LIFX"
|
||||||
DTMill DriverKind = "Mill"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ValidDriverKinds = []DriverKind{
|
var ValidDriverKinds = []DriverKind{
|
||||||
|
|||||||
+39
-83
@@ -8,14 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ColorValue struct {
|
type ColorValue struct {
|
||||||
Hue float64 `json:"h,omitempty"` // 0..360
|
Hue float64 `json:"h,omitempty"` // 0..360
|
||||||
Saturation float64 `json:"s,omitempty"` // 0..=1
|
Saturation float64 `json:"s,omitempty"` // 0..=1
|
||||||
Kelvin int `json:"kelvin,omitempty"`
|
Kelvin int `json:"kelvin,omitempty"`
|
||||||
XY *ColorXY `json:"xy,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ColorValue) IsEmpty() bool {
|
|
||||||
return c.XY == nil && c.Kelvin == 0 && c.Saturation == 0 && c.Hue == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorValue) IsHueSat() bool {
|
func (c *ColorValue) IsHueSat() bool {
|
||||||
@@ -23,27 +18,7 @@ func (c *ColorValue) IsHueSat() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorValue) IsKelvin() bool {
|
func (c *ColorValue) IsKelvin() bool {
|
||||||
return !c.IsXY() && c.Kelvin > 0
|
return c.Kelvin > 0
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ColorValue) IsXY() bool {
|
|
||||||
return c.XY != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToXY converts the color to XY if possible. If the color already is XY, it returns
|
|
||||||
// a copy of its held value. There are no guarantees of conforming to a gamut, however.
|
|
||||||
func (c *ColorValue) ToXY() (xy ColorXY, ok bool) {
|
|
||||||
if c.XY != nil {
|
|
||||||
xy = *c.XY
|
|
||||||
ok = true
|
|
||||||
} else if c.Kelvin > 0 && c.Hue < 0.001 && c.Saturation <= 0.001 {
|
|
||||||
ok = false
|
|
||||||
} else {
|
|
||||||
xy = hsToXY(c.Hue, c.Saturation)
|
|
||||||
ok = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorValue) String() string {
|
func (c *ColorValue) String() string {
|
||||||
@@ -54,75 +29,56 @@ func (c *ColorValue) String() string {
|
|||||||
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
|
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func ParseColorValue(raw string) (ColorValue, error) {
|
func ParseColorValue(raw string) (ColorValue, error) {
|
||||||
tokens := strings.SplitN(raw, ":", 2)
|
tokens := strings.SplitN(raw, ":", 2)
|
||||||
if len(tokens) != 2 {
|
if len(tokens) != 2 {
|
||||||
return ColorValue{}, ErrBadInput
|
return ColorValue{}, ErrBadInput
|
||||||
}
|
}
|
||||||
|
|
||||||
switch tokens[0] {
|
if tokens[0] == "kelvin" || tokens[0] == "k" {
|
||||||
case "kelvin", "k":
|
parsedPart, err := strconv.Atoi(tokens[1])
|
||||||
{
|
if err != nil {
|
||||||
parsedPart, err := strconv.Atoi(tokens[1])
|
return ColorValue{}, ErrBadInput
|
||||||
if err != nil {
|
|
||||||
return ColorValue{}, ErrBadInput
|
|
||||||
}
|
|
||||||
|
|
||||||
return ColorValue{Kelvin: parsedPart}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "xy":
|
return ColorValue{Kelvin: parsedPart}, nil
|
||||||
{
|
}
|
||||||
parts := strings.Split(tokens[1], ",")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return ColorValue{}, ErrUnknownColorFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
x, err1 := strconv.ParseFloat(parts[0], 64)
|
if tokens[0] == "hs" {
|
||||||
y, err2 := strconv.ParseFloat(parts[1], 64)
|
parts := strings.Split(tokens[1], ",")
|
||||||
if err1 != nil || err2 != nil {
|
if len(parts) < 2 {
|
||||||
return ColorValue{}, ErrBadInput
|
return ColorValue{}, ErrUnknownColorFormat
|
||||||
}
|
|
||||||
|
|
||||||
return ColorValue{XY: &ColorXY{X: x, Y: y}}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "hs":
|
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||||
{
|
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||||
parts := strings.Split(tokens[1], ",")
|
if err1 != nil || err2 != nil {
|
||||||
if len(parts) < 2 {
|
return ColorValue{}, ErrBadInput
|
||||||
return ColorValue{}, ErrUnknownColorFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
|
||||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
|
||||||
if err1 != nil || err2 != nil {
|
|
||||||
return ColorValue{}, ErrBadInput
|
|
||||||
}
|
|
||||||
|
|
||||||
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "hsk":
|
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
||||||
{
|
}
|
||||||
parts := strings.Split(tokens[1], ",")
|
|
||||||
if len(parts) < 3 {
|
|
||||||
return ColorValue{}, ErrUnknownColorFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
if tokens[0] == "hsk" {
|
||||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
parts := strings.Split(tokens[1], ",")
|
||||||
part3, err3 := strconv.Atoi(parts[2])
|
if len(parts) < 3 {
|
||||||
if err1 != nil || err2 != nil || err3 != nil {
|
return ColorValue{}, ErrUnknownColorFormat
|
||||||
return ColorValue{}, ErrBadInput
|
|
||||||
}
|
|
||||||
|
|
||||||
return ColorValue{
|
|
||||||
Hue: math.Mod(part1, 360),
|
|
||||||
Saturation: math.Min(math.Max(part2, 0), 1),
|
|
||||||
Kelvin: part3,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||||
|
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||||
|
part3, err3 := strconv.Atoi(parts[2])
|
||||||
|
if err1 != nil || err2 != nil || err3 != nil {
|
||||||
|
return ColorValue{}, ErrBadInput
|
||||||
|
}
|
||||||
|
|
||||||
|
return ColorValue{
|
||||||
|
Hue: math.Mod(part1, 360),
|
||||||
|
Saturation: math.Min(math.Max(part2, 0), 1),
|
||||||
|
Kelvin: part3,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ColorValue{}, ErrUnknownColorFormat
|
return ColorValue{}, ErrUnknownColorFormat
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
const eps = 0.0001
|
|
||||||
const epsSquare = eps * eps
|
|
||||||
|
|
||||||
type ColorGamut struct {
|
|
||||||
Red ColorXY `json:"red"`
|
|
||||||
Green ColorXY `json:"green"`
|
|
||||||
Blue ColorXY `json:"blue"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) side(x1, y1, x2, y2, x, y float64) float64 {
|
|
||||||
return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) naiveContains(color ColorXY) bool {
|
|
||||||
x, y := color.X, color.Y
|
|
||||||
x1, y1 := cg.Red.X, cg.Red.Y
|
|
||||||
x2, y2 := cg.Green.X, cg.Green.Y
|
|
||||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
|
||||||
|
|
||||||
checkSide1 := cg.side(x1, y1, x2, y2, x, y) < 0
|
|
||||||
checkSide2 := cg.side(x2, y2, x3, y3, x, y) < 0
|
|
||||||
checkSide3 := cg.side(x3, y3, x1, y1, x, y) < 0
|
|
||||||
|
|
||||||
return checkSide1 && checkSide2 && checkSide3
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) getBounds() (xMin, xMax, yMin, yMax float64) {
|
|
||||||
x1, y1 := cg.Red.X, cg.Red.Y
|
|
||||||
x2, y2 := cg.Green.X, cg.Green.Y
|
|
||||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
|
||||||
|
|
||||||
xMin = math.Min(x1, math.Min(x2, x3)) - eps
|
|
||||||
xMax = math.Max(x1, math.Max(x2, x3)) + eps
|
|
||||||
yMin = math.Min(y1, math.Min(y2, y3)) - eps
|
|
||||||
yMax = math.Max(y1, math.Max(y2, y3)) + eps
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) isInBounds(color ColorXY) bool {
|
|
||||||
x, y := color.X, color.Y
|
|
||||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
|
||||||
|
|
||||||
return !(x < xMin || xMax < x || y < yMin || yMax < y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 {
|
|
||||||
sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
|
|
||||||
dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1
|
|
||||||
if dotProduct < 0 {
|
|
||||||
return (x-x1)*(x-x1) + (y-y1)*(y-y1)
|
|
||||||
} else if dotProduct <= 1 {
|
|
||||||
sqLength2 := (x1-x)*(x1-x) + (y1-y)*(y1-y)
|
|
||||||
return sqLength2 - dotProduct*dotProduct*sqLength1
|
|
||||||
} else {
|
|
||||||
return (x-x2)*(x-x2) + (y-y2)*(y-y2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) atTheEdge(color ColorXY) bool {
|
|
||||||
x, y := color.X, color.Y
|
|
||||||
x1, y1 := cg.Red.X, cg.Red.Y
|
|
||||||
x2, y2 := cg.Green.X, cg.Green.Y
|
|
||||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
|
||||||
|
|
||||||
if cg.distanceSquarePointToSegment(x1, y1, x2, y2, x, y) <= epsSquare {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if cg.distanceSquarePointToSegment(x2, y2, x3, y3, x, y) <= epsSquare {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if cg.distanceSquarePointToSegment(x3, y3, x1, y1, x, y) <= epsSquare {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) Contains(color ColorXY) bool {
|
|
||||||
if cg == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *ColorGamut) Conform(color ColorXY) ColorXY {
|
|
||||||
if cg.Contains(color) {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
|
|
||||||
var best *ColorXY
|
|
||||||
|
|
||||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
|
||||||
|
|
||||||
for x := xMin; x < xMax; x += 0.001 {
|
|
||||||
for y := yMin; y < yMax; y += 0.001 {
|
|
||||||
color2 := ColorXY{X: x, Y: y}
|
|
||||||
|
|
||||||
if cg.Contains(color2) {
|
|
||||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
|
||||||
best = &color2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if best == nil {
|
|
||||||
centerX := (cg.Red.X + cg.Green.X + cg.Blue.X) / 3
|
|
||||||
centerY := (cg.Red.Y + cg.Green.Y + cg.Blue.Y) / 3
|
|
||||||
|
|
||||||
stepX := (centerX - color.X) / 5000
|
|
||||||
stepY := (centerY - color.Y) / 5000
|
|
||||||
|
|
||||||
for !cg.Contains(color) {
|
|
||||||
color.X += stepX
|
|
||||||
color.Y += stepY
|
|
||||||
}
|
|
||||||
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
|
|
||||||
for x := best.X - 0.001; x < best.X+0.001; x += 0.0002 {
|
|
||||||
for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 {
|
|
||||||
color2 := ColorXY{X: x, Y: y}
|
|
||||||
|
|
||||||
if cg.atTheEdge(color2) {
|
|
||||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
|
||||||
best = &color2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00003 {
|
|
||||||
for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 {
|
|
||||||
color2 := ColorXY{X: x, Y: y}
|
|
||||||
|
|
||||||
if cg.atTheEdge(color2) {
|
|
||||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
|
||||||
best = &color2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return *best
|
|
||||||
}
|
|
||||||
|
|
||||||
type ColorXY struct {
|
|
||||||
X float64 `json:"x"`
|
|
||||||
Y float64 `json:"y"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xy ColorXY) DistanceTo(other ColorXY) float64 {
|
|
||||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xy ColorXY) Round() ColorXY {
|
|
||||||
return ColorXY{
|
|
||||||
X: math.Round(xy.X*10000) / 10000,
|
|
||||||
Y: math.Round(xy.Y*10000) / 10000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hsToXY(hue, sat float64) ColorXY {
|
|
||||||
c := colorful.Hsv(hue, sat, 1)
|
|
||||||
red255, green255, blue255 := c.RGB255()
|
|
||||||
red := float64(red255) / 255.0
|
|
||||||
green := float64(green255) / 255.0
|
|
||||||
blue := float64(blue255) / 255.0
|
|
||||||
|
|
||||||
return rgbToXY(red, green, blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rgbToXY(red float64, green float64, blue float64) ColorXY {
|
|
||||||
x := red*0.649926 + green*0.103455 + blue*0.197109
|
|
||||||
y := red*0.234327 + green*0.743075 + blue*0.022598
|
|
||||||
z := green*0.053077 + blue*1.035763
|
|
||||||
|
|
||||||
return ColorXY{
|
|
||||||
X: x / (x + y + z),
|
|
||||||
Y: y / (x + y + z),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func screenRGBToXY(red, green, blue float64) ColorXY {
|
|
||||||
for _, component := range []*float64{&red, &green, &blue} {
|
|
||||||
if *component > 0.04045 {
|
|
||||||
*component = math.Pow((*component+0.055)/(1.055), 2.4)
|
|
||||||
} else {
|
|
||||||
*component /= 12.92
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgbToXY(red, green, blue)
|
|
||||||
}
|
|
||||||
+10
-17
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -37,7 +38,7 @@ type DeviceState struct {
|
|||||||
Power bool `json:"power"`
|
Power bool `json:"power"`
|
||||||
Color ColorValue `json:"color,omitempty"`
|
Color ColorValue `json:"color,omitempty"`
|
||||||
Intensity float64 `json:"intensity,omitempty"`
|
Intensity float64 `json:"intensity,omitempty"`
|
||||||
Temperature int `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceScene struct {
|
type DeviceScene struct {
|
||||||
@@ -86,7 +87,6 @@ var (
|
|||||||
DCColorHS DeviceCapability = "ColorHS"
|
DCColorHS DeviceCapability = "ColorHS"
|
||||||
DCColorHSK DeviceCapability = "ColorHSK"
|
DCColorHSK DeviceCapability = "ColorHSK"
|
||||||
DCColorKelvin DeviceCapability = "ColorKelvin"
|
DCColorKelvin DeviceCapability = "ColorKelvin"
|
||||||
DCColorXY DeviceCapability = "ColorXY"
|
|
||||||
DCButtons DeviceCapability = "Buttons"
|
DCButtons DeviceCapability = "Buttons"
|
||||||
DCPresence DeviceCapability = "Presence"
|
DCPresence DeviceCapability = "Presence"
|
||||||
DCIntensity DeviceCapability = "Intensity"
|
DCIntensity DeviceCapability = "Intensity"
|
||||||
@@ -156,17 +156,6 @@ func (d *Device) HasTag(tags ...string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) IsOnlySensor() bool {
|
|
||||||
return !d.HasCapability(
|
|
||||||
DCPower,
|
|
||||||
DCColorHS,
|
|
||||||
DCColorHSK,
|
|
||||||
DCColorKelvin,
|
|
||||||
DCIntensity,
|
|
||||||
DCTemperatureControl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Device) HasCapability(capabilities ...DeviceCapability) bool {
|
func (d *Device) HasCapability(capabilities ...DeviceCapability) bool {
|
||||||
for _, c := range d.Capabilities {
|
for _, c := range d.Capabilities {
|
||||||
for _, c2 := range capabilities {
|
for _, c2 := range capabilities {
|
||||||
@@ -199,10 +188,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +198,14 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
|
|||||||
intensity := device.State.Intensity * *s.Intensity
|
intensity := device.State.Intensity * *s.Intensity
|
||||||
n.Intensity = &intensity
|
n.Intensity = &intensity
|
||||||
}
|
}
|
||||||
|
if s.Color != nil {
|
||||||
|
c, err := ParseColorValue(*s.Color)
|
||||||
|
if err == nil {
|
||||||
|
c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360)
|
||||||
|
c.Saturation *= device.State.Color.Saturation
|
||||||
|
c.Kelvin += device.State.Color.Kelvin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -10,13 +10,9 @@ type DriverProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Driver interface {
|
type Driver interface {
|
||||||
SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]Bridge, error)
|
SearchBridge(ctx context.Context, address string, dryRun bool) ([]Bridge, error)
|
||||||
SearchDevices(ctx context.Context, bridge Bridge, timeout time.Duration) ([]Device, error)
|
SearchDevices(ctx context.Context, bridge Bridge, timeout time.Duration) ([]Device, error)
|
||||||
ListDevices(ctx context.Context, bridge Bridge) ([]Device, error)
|
ListDevices(ctx context.Context, bridge Bridge) ([]Device, error)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ var ErrIncorrectToken = errors.New("driver is not accepting authentication infor
|
|||||||
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
|
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
|
||||||
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 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")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var (
|
|||||||
ENBridgeDisconnected = "BridgeDisconnected"
|
ENBridgeDisconnected = "BridgeDisconnected"
|
||||||
ENButtonPressed = "ButtonPressed"
|
ENButtonPressed = "ButtonPressed"
|
||||||
ENSensorPresenceStarted = "SensorPresenceStarted"
|
ENSensorPresenceStarted = "SensorPresenceStarted"
|
||||||
|
ENSensorPresenceEnding = "SensorPresenceEnding"
|
||||||
ENSensorPresenceEnded = "SensorPresenceEnded"
|
ENSensorPresenceEnded = "SensorPresenceEnded"
|
||||||
ENSensorTemperature = "SensorTemperature"
|
ENSensorTemperature = "SensorTemperature"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -168,8 +168,12 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
|
|||||||
return false, true
|
return false, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.matches(strconv.Itoa(device.State.Temperature)), false
|
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
|
||||||
case "scene":
|
case "scene":
|
||||||
|
if len(device.SceneAssignments) == 0 {
|
||||||
|
return c.matches("-1"), false
|
||||||
|
}
|
||||||
|
|
||||||
sceneId := -1
|
sceneId := -1
|
||||||
for _, assignment := range device.SceneAssignments {
|
for _, assignment := range device.SceneAssignments {
|
||||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||||
|
|||||||
+10
-73
@@ -61,38 +61,24 @@ const (
|
|||||||
SEGradient SceneEffect = "Gradient"
|
SEGradient SceneEffect = "Gradient"
|
||||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||||
SETransition SceneEffect = "Transition"
|
SETransition SceneEffect = "Transition"
|
||||||
SEMotion SceneEffect = "Motion"
|
|
||||||
SETemperature SceneEffect = "Temperature"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneRole struct {
|
type SceneRole struct {
|
||||||
Effect SceneEffect `json:"effect"`
|
Effect SceneEffect `json:"effect"`
|
||||||
MotionSeconds float64 `json:"motionSeconds"`
|
PowerMode ScenePowerMode `json:"powerMode"`
|
||||||
MinTemperature int `json:"minTemperature"`
|
TargetKind ReferenceKind `json:"targetKind"`
|
||||||
MaxTemperature int `json:"maxTemperature"`
|
TargetValue string `json:"targetValue"`
|
||||||
PowerMode ScenePowerMode `json:"powerMode"`
|
Interpolate bool `json:"interpolate"`
|
||||||
TargetKind ReferenceKind `json:"targetKind"`
|
Relative bool `json:"relative"`
|
||||||
TargetValue string `json:"targetValue"`
|
Order string `json:"order"`
|
||||||
Interpolate bool `json:"interpolate"`
|
States []NewDeviceState `json:"states"`
|
||||||
Relative bool `json:"relative"`
|
|
||||||
Order string `json:"order"`
|
|
||||||
States []NewDeviceState `json:"states"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneRunContext struct {
|
type SceneRunContext struct {
|
||||||
CurrentTime time.Time
|
|
||||||
Index int
|
Index int
|
||||||
Length int
|
Length int
|
||||||
IntervalNumber int64
|
IntervalNumber int64
|
||||||
IntervalMax int64
|
IntervalMax int64
|
||||||
Sensors []SceneSensor
|
|
||||||
}
|
|
||||||
|
|
||||||
type SceneSensor struct {
|
|
||||||
ID int
|
|
||||||
UpdateTime time.Time
|
|
||||||
Temperature *float64
|
|
||||||
Presence *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SceneRunContext) PositionFacShifted() float64 {
|
func (d *SceneRunContext) PositionFacShifted() float64 {
|
||||||
@@ -134,7 +120,7 @@ func (r *SceneRole) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch r.Effect {
|
switch r.Effect {
|
||||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature:
|
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||||
default:
|
default:
|
||||||
return ErrSceneRoleUnknownEffect
|
return ErrSceneRoleUnknownEffect
|
||||||
}
|
}
|
||||||
@@ -192,53 +178,6 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
|||||||
newState = r.State(c.PositionFacShifted())
|
newState = r.State(c.PositionFacShifted())
|
||||||
case SETransition:
|
case SETransition:
|
||||||
newState = r.State(c.IntervalFac())
|
newState = r.State(c.IntervalFac())
|
||||||
case SEMotion:
|
|
||||||
presence := false
|
|
||||||
absenceTime := time.Time{}
|
|
||||||
|
|
||||||
for _, sensors := range c.Sensors {
|
|
||||||
if sensors.Presence == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if *sensors.Presence {
|
|
||||||
presence = true
|
|
||||||
} else {
|
|
||||||
if sensors.UpdateTime.After(absenceTime) {
|
|
||||||
absenceTime = sensors.UpdateTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if presence {
|
|
||||||
newState = r.State(0.0)
|
|
||||||
} else {
|
|
||||||
fac := c.CurrentTime.Sub(absenceTime).Seconds() / r.MotionSeconds
|
|
||||||
if fac < 0 {
|
|
||||||
fac = 0
|
|
||||||
} else if fac > 1 {
|
|
||||||
fac = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
newState = r.State(fac)
|
|
||||||
}
|
|
||||||
case SETemperature:
|
|
||||||
avg := 0.0
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
for _, sensor := range c.Sensors {
|
|
||||||
if sensor.Temperature == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
avg = *sensor.Temperature
|
|
||||||
count = 1
|
|
||||||
} else {
|
|
||||||
avg = ((avg * float64(count)) + *sensor.Temperature) / float64(count+1)
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Relative {
|
if r.Relative {
|
||||||
@@ -249,7 +188,7 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
|||||||
case SPDevice:
|
case SPDevice:
|
||||||
newState.Power = nil
|
newState.Power = nil
|
||||||
case SPScene:
|
case SPScene:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
case SPBoth:
|
case SPBoth:
|
||||||
if newState.Power != nil {
|
if newState.Power != nil {
|
||||||
powerIntersection := *newState.Power && device.State.Power
|
powerIntersection := *newState.Power && device.State.Power
|
||||||
@@ -299,7 +238,6 @@ 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"`
|
||||||
@@ -307,7 +245,6 @@ 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
|
||||||
|
|||||||
+3
-5
@@ -9,8 +9,6 @@ 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 {
|
||||||
@@ -31,6 +29,7 @@ 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
|
||||||
@@ -83,12 +82,11 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CurrentTimeOfDay() TimeOfDay {
|
func CurrentTimeOfDay() TimeOfDay {
|
||||||
return TimeOfDayFromDate(time.Now().In(TimeOfDayTimeZone))
|
return TimeOfDayFromDate(time.Now().Local())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeOfDayFromDate(date time.Time) TimeOfDay {
|
func TimeOfDayFromDate(date time.Time) TimeOfDay {
|
||||||
localDate := date.In(TimeOfDayTimeZone)
|
return NewTimeOfDay(date.Hour(), date.Minute(), date.Second())
|
||||||
return NewTimeOfDay(localDate.Hour(), localDate.Minute(), localDate.Second())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {
|
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
ALTER TABLE device_state
|
|
||||||
ADD COLUMN temperature INT NOT NULL DEFAULT 0;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
ALTER TABLE device_state
|
|
||||||
DROP COLUMN temperature;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
CREATE UNIQUE INDEX scene_name ON scene (name);
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP INDEX IF EXISTS scene_name;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
Reference in New Issue
Block a user