Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cedd0d1d28 | ||
|
|
7471102413 | ||
|
|
bf4d000455 | ||
|
|
6947fc3461 | ||
|
|
600c90a614 | ||
|
|
a08685c3cd | ||
|
|
15536c6078 | ||
|
|
2bb2154e5b | ||
|
|
8c2ef2dac3 | ||
|
|
f4e584938b | ||
|
|
8c9b47aa1d | ||
|
|
5bd7a7f6b6 | ||
|
|
88bd4dc073 | ||
|
|
6443e0ecd3 | ||
|
|
147231e4fd | ||
|
|
b191455753 |
@@ -4,15 +4,6 @@ kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: go-build
|
||||
image: golang:1.17
|
||||
commands:
|
||||
- go test -v ./...
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: go-test
|
||||
image: golang:1.17
|
||||
commands:
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func Bridges(r gin.IRoutes) {
|
||||
return []models.Bridge{bridge}, nil
|
||||
}
|
||||
|
||||
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun)
|
||||
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.Token, body.DryRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+66
-13
@@ -7,6 +7,7 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -137,12 +138,10 @@ func Devices(r gin.IRoutes) {
|
||||
|
||||
config.PublishChannel <- devices
|
||||
|
||||
go func() {
|
||||
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||
if err != nil {
|
||||
log.Println("Failed to save devices states")
|
||||
}
|
||||
}()
|
||||
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||
if err != nil {
|
||||
log.Println("Failed to save devices states")
|
||||
}
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
@@ -219,14 +218,28 @@ func Devices(r gin.IRoutes) {
|
||||
return []models.Device{}, nil
|
||||
}
|
||||
|
||||
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var scene *models.Scene
|
||||
if body.SceneName != "" {
|
||||
scene, err = config.SceneRepository().FindName(ctxOf(c), body.SceneName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
body.SceneID = scene.ID
|
||||
} else {
|
||||
scene, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
body.SceneName = scene.Name
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
|
||||
pushMode := c.Query("push") == "true"
|
||||
for i := range devices {
|
||||
@@ -267,6 +280,46 @@ func Devices(r gin.IRoutes) {
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.DELETE("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
|
||||
id, err := strconv.Atoi(c.Param("fetch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
device, err := config.DeviceRepository().Find(ctxOf(c), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Query("forgetmenot") != "true" {
|
||||
bridge, err := config.BridgeRepository().Find(ctxOf(c), device.BridgeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driver, err := config.DriverProvider().Provide(bridge.Driver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forgettableDriver, ok := driver.(models.ForgettableDriver)
|
||||
if !ok {
|
||||
return nil, models.ErrCannotForget
|
||||
}
|
||||
err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = config.DeviceRepository().Delete(ctxOf(c), device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}))
|
||||
}
|
||||
|
||||
func withSceneState(devices []models.Device) []models.Device {
|
||||
|
||||
+14
-2
@@ -5,15 +5,27 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func Scenes(r gin.IRoutes) {
|
||||
nonNumericRegex := regexp.MustCompile("[a-zA-Z ]")
|
||||
|
||||
findScene := func(c *gin.Context) (*models.Scene, error) {
|
||||
param := c.Param("id")
|
||||
if nonNumericRegex.MatchString(param) {
|
||||
return config.SceneRepository().FindName(ctxOf(c), param)
|
||||
} else {
|
||||
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
}
|
||||
}
|
||||
|
||||
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().FetchAll(ctxOf(c))
|
||||
}))
|
||||
|
||||
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
return findScene(c)
|
||||
}))
|
||||
|
||||
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -45,7 +57,7 @@ func Scenes(r gin.IRoutes) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
scene, err := findScene(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"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/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/models"
|
||||
"sync"
|
||||
@@ -18,6 +19,7 @@ func DriverProvider() models.DriverProvider {
|
||||
models.DTNanoLeaf: &nanoleaf.Driver{},
|
||||
models.DTHue: &hue.Driver{},
|
||||
models.DTLIFX: &lifx.Driver{},
|
||||
models.DTMill: &mill.Driver{},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+18
-1
@@ -1,17 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"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 MySqlPort() int { return intEnv("LUCIFER_MYSQL_PORT") }
|
||||
func MySqlSchema() string { return strEnv("LUCIFER_MYSQL_SCHEMA") }
|
||||
func MySqlUsername() string { return strEnv("LUCIFER_MYSQL_USERNAME") }
|
||||
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 {
|
||||
env, ok := os.LookupEnv(key)
|
||||
|
||||
+4
-1
@@ -7,13 +7,14 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/app/services"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StartServer() {
|
||||
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second * 10)
|
||||
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
err := publisher.Initialize(setupCtx)
|
||||
@@ -38,5 +39,7 @@ func StartServer() {
|
||||
api.EventHandlers(apiGin.Group("/event-handlers"))
|
||||
api.Scenes(apiGin.Group("/scenes"))
|
||||
|
||||
models.TimeOfDayTimeZone = config.Location()
|
||||
|
||||
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
||||
}
|
||||
|
||||
+25
-2
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"math"
|
||||
@@ -66,7 +67,6 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
||||
err := handleSpecial(event)
|
||||
if err != nil {
|
||||
log.Printf("Special event handler error (%s): %v", event.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
paramStrings := make([]string, 0, 8)
|
||||
@@ -220,7 +220,30 @@ func handleSpecial(event models.Event) error {
|
||||
}
|
||||
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package handlerfactory
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FactoryConfig struct {
|
||||
Mode FCMOde `yaml:"mode"`
|
||||
Target string `yaml:"target"`
|
||||
SceneNames []string `yaml:"scenes"`
|
||||
TriggerDeviceID string `yaml:"trigger_device_id"`
|
||||
After []After `yaml:"after"`
|
||||
From string `yaml:"from"`
|
||||
To string `yaml:"to"`
|
||||
}
|
||||
|
||||
type FCMOde string
|
||||
|
||||
var (
|
||||
SceneDimmer FCMOde = "SceneDimmer"
|
||||
MotionSensor FCMOde = "MotionSensor"
|
||||
)
|
||||
|
||||
type After struct {
|
||||
Minutes int `yaml:"minutes"`
|
||||
NewState models.NewDeviceState `yaml:"new_state"`
|
||||
}
|
||||
|
||||
func ParseConfigs(reader io.Reader) ([]FactoryConfig, error) {
|
||||
readAll, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
splits := strings.Split(string(readAll), "\n---\n")
|
||||
configs := make([]FactoryConfig, 0, 8)
|
||||
for _, split := range splits {
|
||||
var factoryConfig FactoryConfig
|
||||
err := yaml.NewDecoder(strings.NewReader(split)).Decode(&factoryConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configs = append(configs, factoryConfig)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (f *FactoryConfig) FromTime() models.TimeOfDay {
|
||||
tod, _ := models.ParseTimeOfDay(f.From)
|
||||
return tod
|
||||
}
|
||||
|
||||
func (f *FactoryConfig) ToTime() models.TimeOfDay {
|
||||
tod, _ := models.ParseTimeOfDay(f.To)
|
||||
return tod
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package handlerfactory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
)
|
||||
|
||||
func CreateHandlers(
|
||||
config FactoryConfig,
|
||||
scenes []models.Scene,
|
||||
existing []models.EventHandler,
|
||||
) ([]models.EventHandler, []models.EventHandler) {
|
||||
switch config.Mode {
|
||||
case SceneDimmer:
|
||||
return sceneDimmer(config, scenes, existing)
|
||||
case MotionSensor:
|
||||
return motionSensor(config, existing)
|
||||
default:
|
||||
return []models.EventHandler{}, []models.EventHandler{}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneDimmer(
|
||||
config FactoryConfig,
|
||||
scenes []models.Scene,
|
||||
existing []models.EventHandler,
|
||||
) ([]models.EventHandler, []models.EventHandler) {
|
||||
kind, refValue := models.ParseFetchString(config.Target)
|
||||
|
||||
toRemove := make([]models.EventHandler, 0, 8)
|
||||
for _, old := range existing {
|
||||
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
|
||||
toRemove = append(toRemove, old)
|
||||
}
|
||||
}
|
||||
|
||||
resolvedScenes := make([]models.Scene, 0, len(config.SceneNames))
|
||||
for _, name := range config.SceneNames {
|
||||
for _, scene := range scenes {
|
||||
if name == scene.Name {
|
||||
resolvedScenes = append(resolvedScenes, scene)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(scenes) < 2 {
|
||||
return []models.EventHandler{}, []models.EventHandler{}
|
||||
}
|
||||
|
||||
handlers := make([]models.EventHandler, 0, len(scenes)+4)
|
||||
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "On"},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetPower: &myTrue},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "Off"},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetPower: &myFalse},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
|
||||
for i, scene := range resolvedScenes {
|
||||
if i > 0 {
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "DimDown"},
|
||||
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||
SceneID: resolvedScenes[i-1].ID,
|
||||
Group: refValue,
|
||||
DurationMS: 0,
|
||||
}},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
}
|
||||
|
||||
if i < len(resolvedScenes)-1 {
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "DimUp"},
|
||||
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||
SceneID: resolvedScenes[i+1].ID,
|
||||
Group: refValue,
|
||||
DurationMS: 0,
|
||||
}},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "DimDown"},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 90,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||
SceneID: resolvedScenes[len(resolvedScenes)-1].ID,
|
||||
Group: refValue,
|
||||
DurationMS: 0,
|
||||
}},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "ButtonPressed",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"buttonName": {EQ: "DimUp"},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 90,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
|
||||
SceneID: resolvedScenes[0].ID,
|
||||
Group: refValue,
|
||||
DurationMS: 0,
|
||||
}},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
|
||||
return optimizeLists(handlers, toRemove)
|
||||
}
|
||||
|
||||
func motionSensor(
|
||||
config FactoryConfig,
|
||||
existing []models.EventHandler,
|
||||
) ([]models.EventHandler, []models.EventHandler) {
|
||||
kind, refValue := models.ParseFetchString(config.Target)
|
||||
|
||||
toRemove := make([]models.EventHandler, 0, 8)
|
||||
for _, old := range existing {
|
||||
if old.From != config.FromTime() || old.To != config.ToTime() {
|
||||
continue
|
||||
}
|
||||
|
||||
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
|
||||
toRemove = append(toRemove, old)
|
||||
}
|
||||
}
|
||||
|
||||
handlers := make([]models.EventHandler, 0, len(config.After))
|
||||
for _, after := range config.After {
|
||||
if after.Minutes == 0 {
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "SensorPresenceStarted",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{
|
||||
SetPower: after.NewState.Power,
|
||||
SetColor: after.NewState.Color,
|
||||
SetIntensity: after.NewState.Intensity,
|
||||
SetTemperature: after.NewState.Temperature,
|
||||
},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
} else {
|
||||
handlers = append(handlers, models.EventHandler{
|
||||
EventName: "SensorPresenceEnded",
|
||||
Conditions: map[string]models.EventCondition{
|
||||
"deviceId": {EQ: config.TriggerDeviceID},
|
||||
"minutesElapsed": {EQ: fmt.Sprintf("%d", after.Minutes)},
|
||||
},
|
||||
OneShot: false,
|
||||
Priority: 100,
|
||||
TargetKind: kind,
|
||||
TargetValue: refValue,
|
||||
Actions: models.EventAction{
|
||||
SetPower: after.NewState.Power,
|
||||
SetColor: after.NewState.Color,
|
||||
SetIntensity: after.NewState.Intensity,
|
||||
SetTemperature: after.NewState.Temperature,
|
||||
},
|
||||
From: config.FromTime(),
|
||||
To: config.ToTime(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return optimizeLists(handlers, toRemove)
|
||||
}
|
||||
|
||||
var myTrue = true
|
||||
var myFalse = false
|
||||
@@ -0,0 +1,72 @@
|
||||
package handlerfactory
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
)
|
||||
|
||||
func optimizeLists(
|
||||
add []models.EventHandler,
|
||||
del []models.EventHandler,
|
||||
) ([]models.EventHandler, []models.EventHandler) {
|
||||
newAdd := make([]models.EventHandler, 0, len(add))
|
||||
newDel := make([]models.EventHandler, 0, len(del))
|
||||
|
||||
OptimizeLoop1:
|
||||
for _, addItem := range add {
|
||||
for _, delItem := range del {
|
||||
if checkDuplicate(&addItem, &delItem) {
|
||||
continue OptimizeLoop1
|
||||
}
|
||||
}
|
||||
|
||||
newAdd = append(newAdd, addItem)
|
||||
}
|
||||
|
||||
OptimizeLoop2:
|
||||
for _, delItem := range del {
|
||||
for _, addItem := range add {
|
||||
if checkDuplicate(&addItem, &delItem) {
|
||||
continue OptimizeLoop2
|
||||
}
|
||||
}
|
||||
|
||||
newDel = append(newDel, delItem)
|
||||
}
|
||||
|
||||
return newAdd, newDel
|
||||
}
|
||||
|
||||
func checkDuplicate(a *models.EventHandler, b *models.EventHandler) bool {
|
||||
check := true
|
||||
check = check && a.EventName == b.EventName
|
||||
check = check && len(a.Conditions) == len(b.Conditions)
|
||||
for k := range a.Conditions {
|
||||
check = check && a.Conditions[k] == b.Conditions[k]
|
||||
}
|
||||
check = check && a.OneShot == b.OneShot
|
||||
check = check && a.Priority == b.Priority
|
||||
check = check && a.TargetKind == b.TargetKind
|
||||
check = check && a.TargetValue == b.TargetValue
|
||||
check = check && a.From == b.From
|
||||
check = check && a.To == b.To
|
||||
|
||||
aPower := a.Actions.SetPower
|
||||
bPower := b.Actions.SetPower
|
||||
powerEq := (aPower == nil && bPower == nil) || (aPower != nil && bPower != nil && *aPower == *bPower)
|
||||
|
||||
aColor := a.Actions.SetColor
|
||||
bColor := b.Actions.SetColor
|
||||
colorEq := (aColor == nil && bColor == nil) || (aColor != nil && bColor != nil && *aColor == *bColor)
|
||||
|
||||
aInt := a.Actions.SetIntensity
|
||||
bInt := b.Actions.SetIntensity
|
||||
intEq := (aInt == nil && bInt == nil) || (aInt != nil && bInt != nil && *aInt == *bInt)
|
||||
|
||||
aTemp := a.Actions.SetTemperature
|
||||
bTemp := b.Actions.SetTemperature
|
||||
tempEq := (aTemp == nil && bTemp == nil) || (aTemp != nil && bTemp != nil && *aTemp == *bTemp)
|
||||
|
||||
check = check && powerEq && colorEq && intEq && tempEq
|
||||
|
||||
return check
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type Publisher struct {
|
||||
started map[int]bool
|
||||
pending map[int][]models.Device
|
||||
waiting map[int]chan struct{}
|
||||
sensorCache map[int]models.SceneSensor
|
||||
}
|
||||
|
||||
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
|
||||
@@ -42,6 +43,15 @@ func (p *Publisher) UpdateScene(data models.Scene) {
|
||||
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 {
|
||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
||||
if err != nil {
|
||||
@@ -90,6 +100,10 @@ func (p *Publisher) Publish(devices ...models.Device) {
|
||||
|
||||
if p.sceneAssignment[device.ID] != nil {
|
||||
p.sceneAssignment[device.ID].UpsertDevice(device)
|
||||
|
||||
if cache, ok := p.sensorCache[device.ID]; ok {
|
||||
p.sceneAssignment[device.ID].UpdateSensor(cache)
|
||||
}
|
||||
} else {
|
||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
||||
if p.waiting[device.BridgeID] != nil {
|
||||
@@ -291,6 +305,7 @@ var publisher = Publisher{
|
||||
started: make(map[int]bool),
|
||||
pending: make(map[int][]models.Device),
|
||||
waiting: make(map[int]chan struct{}),
|
||||
sensorCache: make(map[int]models.SceneSensor),
|
||||
}
|
||||
|
||||
func Global() *Publisher {
|
||||
|
||||
@@ -13,11 +13,25 @@ type Scene struct {
|
||||
roleMap map[int]int
|
||||
roleList map[int][]models.Device
|
||||
lastStates map[int]models.DeviceState
|
||||
sensors []models.SceneSensor
|
||||
|
||||
due bool
|
||||
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.
|
||||
func (s *Scene) UpdateScene(data models.Scene) {
|
||||
devices := make([]models.Device, 0, 16)
|
||||
@@ -86,6 +100,13 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
||||
|
||||
delete(s.roleMap, 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 {
|
||||
@@ -130,6 +151,7 @@ func (s *Scene) Run() []models.Device {
|
||||
return []models.Device{}
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
intervalNumber := int64(0)
|
||||
intervalMax := int64(1)
|
||||
if s.data.IntervalMS > 0 {
|
||||
@@ -149,14 +171,31 @@ func (s *Scene) Run() []models.Device {
|
||||
continue
|
||||
}
|
||||
|
||||
sensorCount := 0
|
||||
for _, device := range list {
|
||||
if device.IsOnlySensor() {
|
||||
sensorCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
role := s.data.Roles[i]
|
||||
|
||||
jSkip := 0
|
||||
|
||||
for j, device := range list {
|
||||
if device.IsOnlySensor() {
|
||||
updatedDevices = append(updatedDevices, device)
|
||||
jSkip += 1
|
||||
continue
|
||||
}
|
||||
|
||||
newState := role.ApplyEffect(&device, models.SceneRunContext{
|
||||
Index: j,
|
||||
Length: len(list),
|
||||
CurrentTime: currentTime,
|
||||
Index: j - jSkip,
|
||||
Length: len(list) - sensorCount,
|
||||
IntervalNumber: intervalNumber,
|
||||
IntervalMax: intervalMax,
|
||||
Sensors: s.sensors,
|
||||
})
|
||||
|
||||
err := device.SetState(newState)
|
||||
|
||||
@@ -33,7 +33,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Find bridge
|
||||
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair)
|
||||
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, *flagToken, !*flagPair)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to search bridge:", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/app/client"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/handlerfactory"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func factoryCmd(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
) {
|
||||
cmd := parseCommand(os.Args[2:])
|
||||
|
||||
switch cmd.Name {
|
||||
case "run":
|
||||
fileName := cmd.Params.Get(0).String()
|
||||
if fileName == nil {
|
||||
log.Fatalln("Missing filename")
|
||||
}
|
||||
|
||||
scenes, err := c.GetScenes(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
handlers, err := c.GetHandlers(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
file, err := os.Open(*fileName)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to open file:", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
configs, err := handlerfactory.ParseConfigs(file)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse file:", err)
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
toAdd, toRemove := handlerfactory.CreateHandlers(config, scenes, handlers)
|
||||
|
||||
for _, removing := range toRemove {
|
||||
_, err := c.DeleteHandler(ctx, removing.ID)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
log.Printf("Removed handler %d", removing.ID)
|
||||
}
|
||||
|
||||
for _, adding := range toAdd {
|
||||
res, err := c.AddHandler(ctx, adding)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
log.Printf("Added handler %d", res.ID)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
if cmd.Name != "help" {
|
||||
log.Println("Unknown command:", cmd.Name)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||
}
|
||||
}
|
||||
+26
-9
@@ -76,17 +76,33 @@ func handlerCmd(
|
||||
|
||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
||||
case "delete":
|
||||
id := cmd.Params.Get(0).Int()
|
||||
if id == nil {
|
||||
log.Fatalln("ID missing")
|
||||
}
|
||||
if cmd.Params.Get(0).StringOr("_not") == "all" {
|
||||
handlers, err := c.GetHandlers(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
handler, err := c.DeleteHandler(ctx, *id)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
for _, handler := range handlers {
|
||||
_, err = c.DeleteHandler(ctx, handler.ID)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
|
||||
log.Printf("Deleted %d handlers", len(handlers))
|
||||
} else {
|
||||
id := cmd.Params.Get(0).Int()
|
||||
if id == nil {
|
||||
log.Fatalln("ID missing")
|
||||
}
|
||||
|
||||
handler, err := c.DeleteHandler(ctx, *id)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
|
||||
}
|
||||
default:
|
||||
if cmd.Name != "help" {
|
||||
log.Println("Unknown command:", cmd.Name)
|
||||
@@ -157,6 +173,7 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
|
||||
model.Actions.SetPower = nds.Power
|
||||
model.Actions.SetColor = nds.Color
|
||||
model.Actions.SetIntensity = nds.Intensity
|
||||
model.Actions.SetTemperature = nds.Temperature
|
||||
|
||||
// Add intensity
|
||||
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
||||
|
||||
@@ -32,6 +32,38 @@ EVENT HANDLER COMMANDS
|
||||
<-fire> <-fire.payload.*>
|
||||
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
|
||||
run <event name> <*=S>
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func WriteDeviceInfo(w io.Writer, devices []models.Device, fields []string) {
|
||||
if len(fields) == 1 && fields[0] == "all" {
|
||||
fields = allFields
|
||||
} else {
|
||||
fields = append(fields[:0:0], fields...)
|
||||
for i, field := range fields {
|
||||
if aliased, ok := fieldAliases[field]; ok {
|
||||
fields[i] = aliased
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length := 0
|
||||
for _, field := range fields {
|
||||
if mlFields[field] {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(field) > length {
|
||||
length = len(field)
|
||||
}
|
||||
}
|
||||
|
||||
for i, device := range devices {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if formatter, ok := fieldFormatters[field]; ok {
|
||||
_, _ = fmt.Fprintf(w, "%-*s %s\n", length+1, field+":", formatter(&device))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "%-*s (unknown field)\n", length+1, field+":")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var allFields = []string{
|
||||
"id", "name", "icon", "bridgeId", "tags", "buttonNames",
|
||||
"power", "color", "intensity", "temperature",
|
||||
"internalId", "sceneAssignment", "userProperties", "driverProperties",
|
||||
}
|
||||
|
||||
var mlFields = map[string]bool{
|
||||
"sceneAssignment": true,
|
||||
"userProperties": true,
|
||||
"driverProperties": true,
|
||||
}
|
||||
|
||||
var fieldFormatters = map[string]func(d *models.Device) string{
|
||||
"id": func(d *models.Device) string { return fmt.Sprint(d.ID) },
|
||||
"internalId": func(d *models.Device) string { return d.InternalID },
|
||||
"name": func(d *models.Device) string { return d.Name },
|
||||
"icon": func(d *models.Device) string { return d.Icon },
|
||||
"bridgeId": func(d *models.Device) string { return fmt.Sprint(d.BridgeID) },
|
||||
"tags": func(d *models.Device) string { return strings.Join(d.Tags, ", ") },
|
||||
"buttonNames": func(d *models.Device) string { return strings.Join(d.ButtonNames, ", ") },
|
||||
"power": func(d *models.Device) string { return fmt.Sprint(d.State.Power) },
|
||||
"color": func(d *models.Device) string { return d.State.Color.String() },
|
||||
"intensity": func(d *models.Device) string { return fmt.Sprint(d.State.Intensity) },
|
||||
"temperature": func(d *models.Device) string { return fmt.Sprint(d.State.Temperature) },
|
||||
|
||||
"sceneAssignment": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(64)
|
||||
for _, scene := range d.SceneAssignments {
|
||||
_, _ = fmt.Fprintf(str, "\n - id: %d\n", scene.SceneID)
|
||||
_, _ = fmt.Fprintf(str, " duration: %d\n", scene.DurationMS)
|
||||
_, _ = fmt.Fprintf(str, " group: %s\n", scene.Group)
|
||||
_, _ = fmt.Fprintf(str, " startTime: %s", scene.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if len(d.SceneAssignments) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"userProperties": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(64)
|
||||
str.WriteString("\n")
|
||||
keys := make([]string, 0, len(d.UserProperties))
|
||||
for key := range d.UserProperties {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
_, _ = fmt.Fprintf(str, " %s: %s\n", key, d.UserProperties[key])
|
||||
}
|
||||
if len(d.UserProperties) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"driverProperties": func(d *models.Device) string {
|
||||
str := &strings.Builder{}
|
||||
str.Grow(256)
|
||||
str.WriteString("\n")
|
||||
keys := make([]string, 0, len(d.DriverProperties))
|
||||
for key := range d.DriverProperties {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
j, _ := json.Marshal(d.DriverProperties[key])
|
||||
_, _ = fmt.Fprintf(str, " %s: %s\n", key, string(j))
|
||||
}
|
||||
if len(d.DriverProperties) == 0 {
|
||||
str.Reset()
|
||||
str.WriteString("(none)")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
},
|
||||
|
||||
"capabilities": func(d *models.Device) string {
|
||||
capStr := make([]string, len(d.Capabilities))
|
||||
for i := range d.Capabilities {
|
||||
capStr[i] = string(d.Capabilities[i])
|
||||
}
|
||||
return strings.Join(capStr, ", ")
|
||||
},
|
||||
}
|
||||
|
||||
var fieldAliases = map[string]string{
|
||||
"bridge": "bridgeId",
|
||||
"buttons": "buttonNames",
|
||||
"internal": "internalId",
|
||||
"caps": "capabilities",
|
||||
"cap": "capabilities",
|
||||
"userprops": "userProperties",
|
||||
"uprops": "userProperties",
|
||||
"driverprops": "driverProperties",
|
||||
"dprops": "driverProperties",
|
||||
"internalid": "internalId",
|
||||
"bridgeid": "bridgeId",
|
||||
"buttonnames": "buttonNames",
|
||||
"scene": "sceneAssignment",
|
||||
}
|
||||
+7
-5
@@ -37,8 +37,8 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if cmd.Params.Get("info").StringOr("false") == "true" {
|
||||
WriteDeviceInfoTable(os.Stdout, devices)
|
||||
if info := cmd.Params.Get("info").String(); info != nil {
|
||||
WriteDeviceInfo(os.Stdout, devices, strings.Split(*info, ","))
|
||||
} else {
|
||||
WriteDeviceStateTable(os.Stdout, devices)
|
||||
}
|
||||
@@ -82,8 +82,8 @@ func main() {
|
||||
case "update":
|
||||
{
|
||||
devices, err := c.PutDevice(ctx, cmd.Params.Get(0).StringOr("all"), models.DeviceUpdate{
|
||||
Name: cmd.Params.Get("name").String(),
|
||||
Icon: cmd.Params.Get("icon").String(),
|
||||
Name: cmd.Params.Get("name").String(),
|
||||
Icon: cmd.Params.Get("icon").String(),
|
||||
UserProperties: cmd.Params.Subset("prop.").StringPtrMap(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -93,6 +93,9 @@ func main() {
|
||||
WriteDeviceInfoTable(os.Stdout, devices)
|
||||
}
|
||||
|
||||
case "factory":
|
||||
factoryCmd(ctx, c)
|
||||
|
||||
case "handler":
|
||||
handlerCmd(ctx, c)
|
||||
|
||||
@@ -126,4 +129,3 @@ func main() {
|
||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-5
@@ -19,6 +19,16 @@ func sceneCmd(
|
||||
cmd := parseCommand(os.Args[2:])
|
||||
|
||||
switch cmd.Name {
|
||||
case "list":
|
||||
{
|
||||
scenes, err := c.GetScenes(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load scenes:", err)
|
||||
}
|
||||
|
||||
WriteSceneTable(os.Stdout, scenes)
|
||||
}
|
||||
|
||||
case "create", "update":
|
||||
{
|
||||
fileName := cmd.Params.Get(0).String()
|
||||
@@ -83,15 +93,22 @@ func sceneCmd(
|
||||
{
|
||||
fetch := cmd.Params.Get(0).String()
|
||||
id := cmd.Params.Get(1).Int()
|
||||
if fetch == nil || id == nil {
|
||||
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
|
||||
name := cmd.Params.Get(1).String()
|
||||
if fetch == nil || (id == nil && name == nil) {
|
||||
log.Println("Usage: lucy scene assign <fetch> <id|name> <group=S> <duration=I>")
|
||||
}
|
||||
|
||||
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
|
||||
SceneID: *id,
|
||||
assignment := models.DeviceSceneAssignment{
|
||||
Group: cmd.Params.Get("group").StringOr(*fetch),
|
||||
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
|
||||
})
|
||||
}
|
||||
if id != nil {
|
||||
assignment.SceneID = *id
|
||||
} else {
|
||||
assignment.SceneName = *name
|
||||
}
|
||||
|
||||
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", assignment)
|
||||
if err != nil {
|
||||
log.Println("Could not assign devices:", err)
|
||||
return
|
||||
|
||||
+19
-1
@@ -44,7 +44,7 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
|
||||
|
||||
temperatureString := ""
|
||||
if v.HasCapability(models.DCTemperatureControl, models.DCTemperatureSensor) {
|
||||
temperatureString = strconv.FormatFloat(v.State.Temperature, 'f', -1, 64)
|
||||
temperatureString = fmt.Sprintf("%d", v.State.Temperature)
|
||||
}
|
||||
|
||||
intensityString := ""
|
||||
@@ -94,6 +94,24 @@ func WriteDeviceInfoTable(w io.Writer, devices []models.Device) {
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func WriteSceneTable(w io.Writer, scenes []models.Scene) {
|
||||
table := tablewriter.NewWriter(w)
|
||||
table.SetHeader([]string{"ID", "NAME", "ROLES", "INTERVAL"})
|
||||
|
||||
table.SetReflowDuringAutoWrap(false)
|
||||
|
||||
for _, v := range scenes {
|
||||
table.Append([]string{
|
||||
fmt.Sprint(v.ID),
|
||||
v.Name,
|
||||
fmt.Sprint(len(v.Roles)),
|
||||
fmt.Sprint(v.IntervalMS),
|
||||
})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
|
||||
table := tablewriter.NewWriter(w)
|
||||
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -22,6 +23,7 @@ type Bridge struct {
|
||||
externalID int
|
||||
lightStates []*hueLightState
|
||||
sensorStates []*hueSensorState
|
||||
quarantine map[string]time.Time
|
||||
syncingPublish uint32
|
||||
}
|
||||
|
||||
@@ -33,6 +35,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
||||
|
||||
b.mu.Lock()
|
||||
for index, light := range lightMap {
|
||||
if time.Now().Before(b.quarantine[light.Uniqueid]) {
|
||||
continue
|
||||
}
|
||||
|
||||
var state *hueLightState
|
||||
for _, existingState := range b.lightStates {
|
||||
if existingState.index == index {
|
||||
@@ -67,6 +73,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
||||
|
||||
b.mu.Lock()
|
||||
for index, sensor := range sensorMap {
|
||||
if time.Now().Before(b.quarantine[sensor.UniqueID]) {
|
||||
continue
|
||||
}
|
||||
|
||||
var state *hueSensorState
|
||||
for _, existingState := range b.sensorStates {
|
||||
if existingState.index == index {
|
||||
@@ -136,6 +146,44 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
|
||||
// Find index
|
||||
b.mu.Lock()
|
||||
found := false
|
||||
index := -1
|
||||
for i, ls := range b.lightStates {
|
||||
if ls.uniqueID == device.InternalID {
|
||||
found = true
|
||||
index = i
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if !found {
|
||||
return models.ErrNotFound
|
||||
}
|
||||
|
||||
// Delete light from bridge
|
||||
err := b.deleteLight(ctx, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway.
|
||||
b.mu.Lock()
|
||||
for i, ls := range b.lightStates {
|
||||
if ls.uniqueID == device.InternalID {
|
||||
b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...)
|
||||
}
|
||||
}
|
||||
if b.quarantine == nil {
|
||||
b.quarantine = make(map[string]time.Time, 1)
|
||||
}
|
||||
b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30)
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
||||
sensorMap, err := b.getSensors(ctx)
|
||||
if err != nil {
|
||||
@@ -170,6 +218,14 @@ func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateI
|
||||
return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightStateInput) error {
|
||||
return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) deleteLight(ctx context.Context, index int) error {
|
||||
return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) getToken(ctx context.Context) (string, error) {
|
||||
result := make([]CreateUserResponse, 0, 1)
|
||||
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
||||
@@ -207,6 +263,33 @@ func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) getGroups(ctx context.Context) (map[int]GroupData, error) {
|
||||
result := make(map[int]GroupData, 16)
|
||||
err := b.get(ctx, "groups", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
|
||||
var res []struct {
|
||||
Success struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"success"`
|
||||
}
|
||||
err := b.post(ctx, "groups", input, &res)
|
||||
|
||||
id, _ := strconv.Atoi(res[0].Success.ID)
|
||||
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
|
||||
return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
|
||||
if b.token != "" {
|
||||
resource = b.token + "/" + resource
|
||||
@@ -226,6 +309,29 @@ func (b *Bridge) get(ctx context.Context, resource string, target interface{}) e
|
||||
return json.NewDecoder(res.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (b *Bridge) delete(ctx context.Context, resource string, target interface{}) error {
|
||||
if b.token != "" {
|
||||
resource = b.token + "/" + resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,8 +3,14 @@ package hue
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type GroupData struct {
|
||||
Name string `json:"name"`
|
||||
Lights []string `json:"lights"`
|
||||
}
|
||||
|
||||
type DiscoveryEntry struct {
|
||||
Id string `json:"id"`
|
||||
InternalIPAddress string `json:"internalipaddress"`
|
||||
@@ -83,6 +89,58 @@ type LightStateInput struct {
|
||||
TransitionTime *int `json:"transitiontime,omitempty"`
|
||||
}
|
||||
|
||||
func (input *LightStateInput) Equal(other LightStateInput) bool {
|
||||
if (input.On != nil) != (other.On != nil) {
|
||||
return false
|
||||
} else if input.On != nil && *input.On != *other.On {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.Bri != nil) != (other.Bri != nil) {
|
||||
return false
|
||||
} else if input.Bri != nil && *input.Bri != *other.Bri {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.Hue != nil) != (other.Hue != nil) {
|
||||
return false
|
||||
} else if input.Hue != nil && *input.Hue != *other.Hue {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.Sat != nil) != (other.Sat != nil) {
|
||||
return false
|
||||
} else if input.Sat != nil && *input.Sat != *other.Sat {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.Effect != nil) != (other.Effect != nil) {
|
||||
return false
|
||||
} else if input.Effect != nil && *input.Effect != *other.Effect {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.XY != nil) != (other.XY != nil) {
|
||||
return false
|
||||
} else if input.XY != nil && *input.XY != *other.XY {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.CT != nil) != (other.CT != nil) {
|
||||
return false
|
||||
} else if input.CT != nil && *input.CT != *other.CT {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.Alert != nil) != (other.Alert != nil) {
|
||||
return false
|
||||
} else if input.Alert != nil && *input.Alert != *other.Alert {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type LightData struct {
|
||||
State LightState `json:"state"`
|
||||
Type string `json:"type"`
|
||||
@@ -192,6 +250,36 @@ type BridgeDeviceInfo struct {
|
||||
} `xml:"device"`
|
||||
}
|
||||
|
||||
type syncGroup struct {
|
||||
GroupIndex int
|
||||
State LightStateInput
|
||||
Indexes []int
|
||||
ArrayIndexes []int
|
||||
}
|
||||
|
||||
func (sg *syncGroup) Matches(group *GroupData) bool {
|
||||
if len(sg.Indexes) != len(group.Lights) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, idx := range sg.Indexes {
|
||||
found := false
|
||||
for _, idx2 := range group.Lights {
|
||||
n := strconv.Itoa(idx)
|
||||
if n == idx2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var buttonNames = []string{"On", "DimUp", "DimDown", "Off"}
|
||||
|
||||
var errLinkButtonNotPressed = errors.New("link button not pressed")
|
||||
|
||||
@@ -19,7 +19,7 @@ type Driver struct {
|
||||
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 !dryRun {
|
||||
return nil, models.ErrAddressOnlyDryRunnable
|
||||
@@ -322,8 +322,8 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
|
||||
return err
|
||||
}
|
||||
|
||||
fastTicker := time.NewTicker(time.Second / 10)
|
||||
slowTicker := time.NewTicker(time.Second / 3)
|
||||
fastTicker := time.NewTicker(time.Second / 5)
|
||||
slowTicker := time.NewTicker(time.Second / 2)
|
||||
selectedTicker := fastTicker
|
||||
ticksUntilRefresh := 0
|
||||
ticksSinceChange := 0
|
||||
@@ -372,6 +372,15 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) ForgetDevice(ctx context.Context, bridge models.Bridge, device models.Device) error {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.ForgetDevice(ctx, device)
|
||||
}
|
||||
|
||||
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
|
||||
d.mu.Lock()
|
||||
for _, bridge := range d.bridges {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -16,12 +17,37 @@ type hueLightState struct {
|
||||
stale bool
|
||||
}
|
||||
|
||||
func (s *hueLightState) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Index int `json:"index"`
|
||||
UniqueID string `json:"uniqueId"`
|
||||
ExternalID int `json:"externalId"`
|
||||
Info LightData `json:"info"`
|
||||
Input LightStateInput `json:"input"`
|
||||
Stale bool `json:"stale"`
|
||||
}{
|
||||
Index: s.index,
|
||||
UniqueID: s.uniqueID,
|
||||
ExternalID: s.externalID,
|
||||
Info: s.info,
|
||||
Input: s.input,
|
||||
Stale: s.stale,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *hueLightState) Update(state models.DeviceState) {
|
||||
input := LightStateInput{}
|
||||
if state.Power {
|
||||
input.On = ptrBool(true)
|
||||
if state.Color.IsKelvin() {
|
||||
input.CT = ptrInt(1000000 / state.Color.Kelvin)
|
||||
if *input.CT < s.info.Capabilities.Control.CT.Min {
|
||||
*input.CT = s.info.Capabilities.Control.CT.Min
|
||||
}
|
||||
if *input.CT > s.info.Capabilities.Control.CT.Max {
|
||||
*input.CT = s.info.Capabilities.Control.CT.Max
|
||||
}
|
||||
|
||||
if s.input.CT == nil || *s.input.CT != *input.CT {
|
||||
s.stale = true
|
||||
}
|
||||
@@ -67,6 +93,13 @@ func (s *hueLightState) Update(state models.DeviceState) {
|
||||
}
|
||||
|
||||
func (s *hueLightState) CheckStaleness(state LightState) {
|
||||
if s.input.On == nil || state.On != *s.input.On {
|
||||
s.stale = true
|
||||
}
|
||||
if !state.On {
|
||||
return
|
||||
}
|
||||
|
||||
if state.ColorMode == "xy" {
|
||||
s.stale = true
|
||||
if s.input.CT == nil && s.input.Hue == nil {
|
||||
@@ -80,7 +113,7 @@ func (s *hueLightState) CheckStaleness(state LightState) {
|
||||
s.stale = true
|
||||
}
|
||||
} else {
|
||||
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat == *s.input.Sat {
|
||||
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat != *s.input.Sat {
|
||||
s.stale = true
|
||||
}
|
||||
}
|
||||
@@ -186,6 +219,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
||||
"deviceInternalId": newData.UniqueID,
|
||||
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
|
||||
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
|
||||
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -202,6 +236,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
||||
"temperature": strconv.FormatFloat(float64(newData.State.Temperature)/100, 'f', 2, 64),
|
||||
"deviceId": strconv.Itoa(state.externalID),
|
||||
"deviceInternalId": newData.UniqueID,
|
||||
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Driver struct {
|
||||
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 == "" {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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
|
||||
// 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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -23,12 +23,13 @@ type deviceRecord struct {
|
||||
}
|
||||
|
||||
type deviceStateRecord struct {
|
||||
DeviceID int `db:"device_id"`
|
||||
Hue float64 `db:"hue"`
|
||||
Saturation float64 `db:"saturation"`
|
||||
Kelvin int `db:"kelvin"`
|
||||
Power bool `db:"power"`
|
||||
Intensity float64 `db:"intensity"`
|
||||
DeviceID int `db:"device_id"`
|
||||
Hue float64 `db:"hue"`
|
||||
Saturation float64 `db:"saturation"`
|
||||
Kelvin int `db:"kelvin"`
|
||||
Power bool `db:"power"`
|
||||
Intensity float64 `db:"intensity"`
|
||||
Temperature int `db:"temperature"`
|
||||
}
|
||||
|
||||
type devicePropertyRecord struct {
|
||||
@@ -227,15 +228,16 @@ func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices
|
||||
|
||||
if mode == 0 || mode&models.SMState != 0 {
|
||||
_, err = tx.NamedExecContext(ctx, `
|
||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, temperature)
|
||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :temperature)
|
||||
`, deviceStateRecord{
|
||||
DeviceID: record.ID,
|
||||
Hue: device.State.Color.Hue,
|
||||
Saturation: device.State.Color.Saturation,
|
||||
Kelvin: device.State.Color.Kelvin,
|
||||
Power: device.State.Power,
|
||||
Intensity: device.State.Intensity,
|
||||
DeviceID: record.ID,
|
||||
Hue: device.State.Color.Hue,
|
||||
Saturation: device.State.Color.Saturation,
|
||||
Kelvin: device.State.Color.Kelvin,
|
||||
Power: device.State.Power,
|
||||
Intensity: device.State.Intensity,
|
||||
Temperature: device.State.Temperature,
|
||||
})
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
@@ -379,6 +381,7 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
|
||||
Kelvin: state.Kelvin,
|
||||
},
|
||||
Intensity: state.Intensity,
|
||||
Temperature: state.Temperature,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,16 @@ func (r *SceneRepo) Find(ctx context.Context, id int) (*models.Scene, error) {
|
||||
return r.populateOne(&scene)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) FindName(ctx context.Context, name string) (*models.Scene, error) {
|
||||
var scene sceneRecord
|
||||
err := r.DBX.GetContext(ctx, &scene, "SELECT * FROM scene WHERE name = ?", name)
|
||||
if err != nil {
|
||||
return nil, dbErr(err)
|
||||
}
|
||||
|
||||
return r.populateOne(&scene)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
|
||||
scenes := make([]sceneRecord, 0, 8)
|
||||
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
||||
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
DTHue DriverKind = "Hue"
|
||||
DTNanoLeaf DriverKind = "Nanoleaf"
|
||||
DTLIFX DriverKind = "LIFX"
|
||||
DTMill DriverKind = "Mill"
|
||||
)
|
||||
|
||||
var ValidDriverKinds = []DriverKind{
|
||||
|
||||
+16
-10
@@ -2,7 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -38,7 +37,7 @@ type DeviceState struct {
|
||||
Power bool `json:"power"`
|
||||
Color ColorValue `json:"color,omitempty"`
|
||||
Intensity float64 `json:"intensity,omitempty"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Temperature int `json:"temperature"`
|
||||
}
|
||||
|
||||
type DeviceScene struct {
|
||||
@@ -156,6 +155,17 @@ func (d *Device) HasTag(tags ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Device) IsOnlySensor() bool {
|
||||
return !d.HasCapability(
|
||||
DCPower,
|
||||
DCColorHS,
|
||||
DCColorHSK,
|
||||
DCColorKelvin,
|
||||
DCIntensity,
|
||||
DCTemperatureControl,
|
||||
)
|
||||
}
|
||||
|
||||
func (d *Device) HasCapability(capabilities ...DeviceCapability) bool {
|
||||
for _, c := range d.Capabilities {
|
||||
for _, c2 := range capabilities {
|
||||
@@ -188,6 +198,10 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
||||
d.State.Intensity = *newState.Intensity
|
||||
}
|
||||
|
||||
if newState.Temperature != nil && d.HasCapability(DCTemperatureControl) {
|
||||
d.State.Temperature = *newState.Temperature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -198,14 +212,6 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
|
||||
intensity := device.State.Intensity * *s.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
|
||||
}
|
||||
|
||||
+5
-1
@@ -10,9 +10,13 @@ type DriverProvider interface {
|
||||
}
|
||||
|
||||
type Driver interface {
|
||||
SearchBridge(ctx context.Context, address string, dryRun bool) ([]Bridge, error)
|
||||
SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]Bridge, error)
|
||||
SearchDevices(ctx context.Context, bridge Bridge, timeout time.Duration) ([]Device, error)
|
||||
ListDevices(ctx context.Context, bridge Bridge) ([]Device, error)
|
||||
Publish(ctx context.Context, bridge Bridge, devices []Device) error
|
||||
Run(ctx context.Context, bridge Bridge, ch chan<- Event) error
|
||||
}
|
||||
|
||||
type ForgettableDriver interface {
|
||||
ForgetDevice(ctx context.Context, bridge Bridge, device Device) error
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ var ErrIncorrectToken = errors.New("driver is not accepting authentication infor
|
||||
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
|
||||
var ErrBridgeSearchFailed = errors.New("bridge search failed")
|
||||
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
|
||||
var ErrCannotForwardRequest = errors.New("driver is not able to forward requests")
|
||||
var ErrCannotForget = errors.New("forget not supported on this bridge type")
|
||||
|
||||
var ErrInvalidAddress = errors.New("invalid mac address")
|
||||
var ErrPayloadTooShort = errors.New("payload too short")
|
||||
|
||||
@@ -24,7 +24,6 @@ var (
|
||||
ENBridgeDisconnected = "BridgeDisconnected"
|
||||
ENButtonPressed = "ButtonPressed"
|
||||
ENSensorPresenceStarted = "SensorPresenceStarted"
|
||||
ENSensorPresenceEnding = "SensorPresenceEnding"
|
||||
ENSensorPresenceEnded = "SensorPresenceEnded"
|
||||
ENSensorTemperature = "SensorTemperature"
|
||||
)
|
||||
|
||||
@@ -168,12 +168,8 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
|
||||
return false, true
|
||||
}
|
||||
|
||||
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
|
||||
return c.matches(strconv.Itoa(device.State.Temperature)), false
|
||||
case "scene":
|
||||
if len(device.SceneAssignments) == 0 {
|
||||
return false, false
|
||||
}
|
||||
|
||||
sceneId := -1
|
||||
for _, assignment := range device.SceneAssignments {
|
||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||
|
||||
+73
-10
@@ -61,24 +61,38 @@ const (
|
||||
SEGradient SceneEffect = "Gradient"
|
||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||
SETransition SceneEffect = "Transition"
|
||||
SEMotion SceneEffect = "Motion"
|
||||
SETemperature SceneEffect = "Temperature"
|
||||
)
|
||||
|
||||
type SceneRole struct {
|
||||
Effect SceneEffect `json:"effect"`
|
||||
PowerMode ScenePowerMode `json:"powerMode"`
|
||||
TargetKind ReferenceKind `json:"targetKind"`
|
||||
TargetValue string `json:"targetValue"`
|
||||
Interpolate bool `json:"interpolate"`
|
||||
Relative bool `json:"relative"`
|
||||
Order string `json:"order"`
|
||||
States []NewDeviceState `json:"states"`
|
||||
Effect SceneEffect `json:"effect"`
|
||||
MotionSeconds float64 `json:"motionSeconds"`
|
||||
MinTemperature int `json:"minTemperature"`
|
||||
MaxTemperature int `json:"maxTemperature"`
|
||||
PowerMode ScenePowerMode `json:"powerMode"`
|
||||
TargetKind ReferenceKind `json:"targetKind"`
|
||||
TargetValue string `json:"targetValue"`
|
||||
Interpolate bool `json:"interpolate"`
|
||||
Relative bool `json:"relative"`
|
||||
Order string `json:"order"`
|
||||
States []NewDeviceState `json:"states"`
|
||||
}
|
||||
|
||||
type SceneRunContext struct {
|
||||
CurrentTime time.Time
|
||||
Index int
|
||||
Length int
|
||||
IntervalNumber int64
|
||||
IntervalMax int64
|
||||
Sensors []SceneSensor
|
||||
}
|
||||
|
||||
type SceneSensor struct {
|
||||
ID int
|
||||
UpdateTime time.Time
|
||||
Temperature *float64
|
||||
Presence *bool
|
||||
}
|
||||
|
||||
func (d *SceneRunContext) PositionFacShifted() float64 {
|
||||
@@ -120,7 +134,7 @@ func (r *SceneRole) Validate() error {
|
||||
}
|
||||
|
||||
switch r.Effect {
|
||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature:
|
||||
default:
|
||||
return ErrSceneRoleUnknownEffect
|
||||
}
|
||||
@@ -178,6 +192,53 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
||||
newState = r.State(c.PositionFacShifted())
|
||||
case SETransition:
|
||||
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 {
|
||||
@@ -188,7 +249,7 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
||||
case SPDevice:
|
||||
newState.Power = nil
|
||||
case SPScene:
|
||||
// Do nothing
|
||||
// Do nothing
|
||||
case SPBoth:
|
||||
if newState.Power != nil {
|
||||
powerIntersection := *newState.Power && device.State.Power
|
||||
@@ -238,6 +299,7 @@ const (
|
||||
// instance is created for the group.
|
||||
type DeviceSceneAssignment struct {
|
||||
SceneID int `json:"sceneId"`
|
||||
SceneName string `json:"sceneName"`
|
||||
Group string `json:"group"`
|
||||
StartTime time.Time `json:"start"`
|
||||
DurationMS int64 `json:"durationMs"`
|
||||
@@ -245,6 +307,7 @@ type DeviceSceneAssignment struct {
|
||||
|
||||
type SceneRepository interface {
|
||||
Find(ctx context.Context, id int) (*Scene, error)
|
||||
FindName(ctx context.Context, name string) (*Scene, error)
|
||||
FetchAll(ctx context.Context) ([]Scene, error)
|
||||
Save(ctx context.Context, bridge *Scene) error
|
||||
Delete(ctx context.Context, bridge *Scene) error
|
||||
|
||||
+5
-3
@@ -9,6 +9,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var TimeOfDayTimeZone = time.UTC
|
||||
|
||||
type TimeOfDay int
|
||||
|
||||
func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
|
||||
@@ -29,7 +31,6 @@ func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
t2, err := ParseTimeOfDay(str)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -82,11 +83,12 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool {
|
||||
}
|
||||
|
||||
func CurrentTimeOfDay() TimeOfDay {
|
||||
return TimeOfDayFromDate(time.Now().Local())
|
||||
return TimeOfDayFromDate(time.Now().In(TimeOfDayTimeZone))
|
||||
}
|
||||
|
||||
func TimeOfDayFromDate(date time.Time) TimeOfDay {
|
||||
return NewTimeOfDay(date.Hour(), date.Minute(), date.Second())
|
||||
localDate := date.In(TimeOfDayTimeZone)
|
||||
return NewTimeOfDay(localDate.Hour(), localDate.Minute(), localDate.Second())
|
||||
}
|
||||
|
||||
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- +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
|
||||
Reference in New Issue
Block a user