Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9b47aa1d | ||
|
|
5bd7a7f6b6 | ||
|
|
88bd4dc073 | ||
|
|
6443e0ecd3 | ||
|
|
147231e4fd | ||
|
|
b191455753 |
@@ -4,15 +4,6 @@ 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.DryRun)
|
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.Token, body.DryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -137,12 +137,10 @@ func Devices(r gin.IRoutes) {
|
|||||||
|
|
||||||
config.PublishChannel <- devices
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
go func() {
|
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||||
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
if err != nil {
|
||||||
if err != nil {
|
log.Println("Failed to save devices states")
|
||||||
log.Println("Failed to save devices states")
|
}
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return withSceneState(devices), nil
|
return withSceneState(devices), nil
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/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"
|
||||||
@@ -18,6 +19,7 @@ func DriverProvider() models.DriverProvider {
|
|||||||
models.DTNanoLeaf: &nanoleaf.Driver{},
|
models.DTNanoLeaf: &nanoleaf.Driver{},
|
||||||
models.DTHue: &hue.Driver{},
|
models.DTHue: &hue.Driver{},
|
||||||
models.DTLIFX: &lifx.Driver{},
|
models.DTLIFX: &lifx.Driver{},
|
||||||
|
models.DTMill: &mill.Driver{},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -4,6 +4,7 @@ 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"
|
||||||
@@ -66,7 +67,6 @@ 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,7 +220,30 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ 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 {
|
||||||
@@ -42,6 +43,15 @@ 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 {
|
||||||
@@ -90,6 +100,10 @@ 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)
|
||||||
if p.waiting[device.BridgeID] != nil {
|
if p.waiting[device.BridgeID] != nil {
|
||||||
@@ -291,6 +305,7 @@ 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,11 +13,25 @@ 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)
|
||||||
@@ -86,6 +100,13 @@ 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 {
|
||||||
@@ -130,6 +151,7 @@ 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 {
|
||||||
@@ -149,14 +171,31 @@ 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{
|
||||||
Index: j,
|
CurrentTime: currentTime,
|
||||||
Length: len(list),
|
Index: j - jSkip,
|
||||||
|
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, !*flagPair)
|
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, *flagToken, !*flagPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to search bridge:", err)
|
log.Fatalln("Failed to search bridge:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
|
|||||||
model.Actions.SetPower = nds.Power
|
model.Actions.SetPower = nds.Power
|
||||||
model.Actions.SetColor = nds.Color
|
model.Actions.SetColor = nds.Color
|
||||||
model.Actions.SetIntensity = nds.Intensity
|
model.Actions.SetIntensity = nds.Intensity
|
||||||
|
model.Actions.SetTemperature = nds.Temperature
|
||||||
|
|
||||||
// Add intensity
|
// Add intensity
|
||||||
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
||||||
|
|||||||
+1
-1
@@ -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 = strconv.FormatFloat(v.State.Temperature, 'f', -1, 64)
|
temperatureString = fmt.Sprintf("%d", v.State.Temperature)
|
||||||
}
|
}
|
||||||
|
|
||||||
intensityString := ""
|
intensityString := ""
|
||||||
|
|||||||
+133
-15
@@ -6,10 +6,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -97,17 +98,17 @@ func (b *Bridge) Refresh(ctx context.Context) error {
|
|||||||
|
|
||||||
func (b *Bridge) SyncStale(ctx context.Context) error {
|
func (b *Bridge) SyncStale(ctx context.Context) error {
|
||||||
indices := make([]int, 0, 4)
|
indices := make([]int, 0, 4)
|
||||||
|
arrayIndices := make([]int, 0, 4)
|
||||||
inputs := make([]LightStateInput, 0, 4)
|
inputs := make([]LightStateInput, 0, 4)
|
||||||
|
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
for _, state := range b.lightStates {
|
for i, state := range b.lightStates {
|
||||||
if !state.stale {
|
if !state.stale {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
indices = append(indices, state.index)
|
indices = append(indices, state.index)
|
||||||
|
arrayIndices = append(arrayIndices, i)
|
||||||
inputs = append(inputs, state.input)
|
inputs = append(inputs, state.input)
|
||||||
}
|
}
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
@@ -116,24 +117,85 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, input := range inputs {
|
groups := make([]*syncGroup, 0, 4)
|
||||||
iCopy := i
|
for i := range inputs {
|
||||||
index := indices[i]
|
input := inputs[i]
|
||||||
inputCopy := input
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
found := false
|
||||||
err := b.putLightState(ctx, index, inputCopy)
|
for _, group := range groups {
|
||||||
|
if group.State.Equal(input) {
|
||||||
|
group.Indexes = append(group.Indexes, indices[i])
|
||||||
|
group.ArrayIndexes = append(group.ArrayIndexes, arrayIndices[i])
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
groups = append(groups, &syncGroup{
|
||||||
|
GroupIndex: -1,
|
||||||
|
State: input,
|
||||||
|
Indexes: []int{indices[i]},
|
||||||
|
ArrayIndexes: []int{arrayIndices[i]},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupMap, err := b.getGroups(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, data := range groupMap {
|
||||||
|
for _, group := range groups {
|
||||||
|
if group.Matches(&data) {
|
||||||
|
group.GroupIndex = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if group.GroupIndex == -1 {
|
||||||
|
data := GroupData{
|
||||||
|
Name: "lucifer_auto_group",
|
||||||
|
Lights: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, idx := range group.Indexes {
|
||||||
|
data.Lights = append(data.Lights, strconv.Itoa(idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := b.postGroup(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.lightStates[iCopy].stale = false
|
group.GroupIndex = id
|
||||||
|
}
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return eg.Wait()
|
log.Println("Updating", len(inputs), "lights on Hue bridge", b.externalID, "in", len(groups), "groups")
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
err := b.putGroupLightState(ctx, group.GroupIndex, group.State)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
for _, arrayIndex := range group.ArrayIndexes {
|
||||||
|
b.lightStates[arrayIndex].stale = false
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if groupMap[group.GroupIndex].Name == "lucifer_auto_group" {
|
||||||
|
err := b.deleteGroup(ctx, group.GroupIndex)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Could not delete temporary group", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
||||||
@@ -170,6 +232,10 @@ 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) 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)
|
||||||
@@ -207,6 +273,34 @@ 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
|
||||||
@@ -226,6 +320,30 @@ 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,8 +3,14 @@ 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"`
|
||||||
@@ -83,6 +89,58 @@ 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"`
|
||||||
@@ -192,6 +250,36 @@ 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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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"
|
||||||
@@ -16,12 +17,37 @@ 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
|
||||||
}
|
}
|
||||||
@@ -67,6 +93,13 @@ 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 {
|
||||||
@@ -80,7 +113,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +219,7 @@ 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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +236,7 @@ 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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
// 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,12 +23,13 @@ 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 {
|
||||||
@@ -227,15 +228,16 @@ 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)
|
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, temperature)
|
||||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :temperature)
|
||||||
`, 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)
|
||||||
@@ -379,6 +381,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ var (
|
|||||||
DTHue DriverKind = "Hue"
|
DTHue DriverKind = "Hue"
|
||||||
DTNanoLeaf DriverKind = "Nanoleaf"
|
DTNanoLeaf DriverKind = "Nanoleaf"
|
||||||
DTLIFX DriverKind = "LIFX"
|
DTLIFX DriverKind = "LIFX"
|
||||||
|
DTMill DriverKind = "Mill"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ValidDriverKinds = []DriverKind{
|
var ValidDriverKinds = []DriverKind{
|
||||||
|
|||||||
+16
-10
@@ -2,7 +2,6 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -38,7 +37,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 float64 `json:"temperature"`
|
Temperature int `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceScene struct {
|
type DeviceScene struct {
|
||||||
@@ -156,6 +155,17 @@ 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 {
|
||||||
@@ -188,6 +198,10 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
|||||||
d.State.Intensity = *newState.Intensity
|
d.State.Intensity = *newState.Intensity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newState.Temperature != nil && d.HasCapability(DCTemperatureControl) {
|
||||||
|
d.State.Temperature = *newState.Temperature
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,14 +212,6 @@ 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
-1
@@ -10,7 +10,7 @@ type DriverProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Driver 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)
|
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
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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 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,7 +24,6 @@ var (
|
|||||||
ENBridgeDisconnected = "BridgeDisconnected"
|
ENBridgeDisconnected = "BridgeDisconnected"
|
||||||
ENButtonPressed = "ButtonPressed"
|
ENButtonPressed = "ButtonPressed"
|
||||||
ENSensorPresenceStarted = "SensorPresenceStarted"
|
ENSensorPresenceStarted = "SensorPresenceStarted"
|
||||||
ENSensorPresenceEnding = "SensorPresenceEnding"
|
|
||||||
ENSensorPresenceEnded = "SensorPresenceEnded"
|
ENSensorPresenceEnded = "SensorPresenceEnded"
|
||||||
ENSensorTemperature = "SensorTemperature"
|
ENSensorTemperature = "SensorTemperature"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -168,12 +168,8 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
|
|||||||
return false, true
|
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":
|
case "scene":
|
||||||
if len(device.SceneAssignments) == 0 {
|
|
||||||
return false, 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
|
||||||
|
|||||||
+71
-10
@@ -61,24 +61,38 @@ 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"`
|
||||||
PowerMode ScenePowerMode `json:"powerMode"`
|
MotionSeconds float64 `json:"motionSeconds"`
|
||||||
TargetKind ReferenceKind `json:"targetKind"`
|
MinTemperature int `json:"minTemperature"`
|
||||||
TargetValue string `json:"targetValue"`
|
MaxTemperature int `json:"maxTemperature"`
|
||||||
Interpolate bool `json:"interpolate"`
|
PowerMode ScenePowerMode `json:"powerMode"`
|
||||||
Relative bool `json:"relative"`
|
TargetKind ReferenceKind `json:"targetKind"`
|
||||||
Order string `json:"order"`
|
TargetValue string `json:"targetValue"`
|
||||||
States []NewDeviceState `json:"states"`
|
Interpolate bool `json:"interpolate"`
|
||||||
|
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 {
|
||||||
@@ -120,7 +134,7 @@ func (r *SceneRole) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch r.Effect {
|
switch r.Effect {
|
||||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature:
|
||||||
default:
|
default:
|
||||||
return ErrSceneRoleUnknownEffect
|
return ErrSceneRoleUnknownEffect
|
||||||
}
|
}
|
||||||
@@ -178,6 +192,53 @@ 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 {
|
||||||
@@ -188,7 +249,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
|
||||||
|
|||||||
@@ -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