hue hue hue
This commit is contained in:
@@ -65,16 +65,16 @@ func (b *EventBus) JoinPrivileged(service ActiveService) {
|
||||
}
|
||||
|
||||
func (b *EventBus) RunCommand(command Command) {
|
||||
if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetState") {
|
||||
if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetStates") {
|
||||
if setStates := atomic.LoadInt32(&b.setStates); setStates > 0 {
|
||||
fmt.Println("[INFO]", setStates, "SetState commands hidden.")
|
||||
fmt.Println("[INFO]", setStates, "SetStates commands hidden.")
|
||||
atomic.AddInt32(&b.setStates, -setStates)
|
||||
}
|
||||
|
||||
fmt.Println("[COMMAND]", cd)
|
||||
} else {
|
||||
if atomic.AddInt32(&b.setStates, 1) >= 1000 {
|
||||
fmt.Println("[INFO] 100 SetState commands hidden.")
|
||||
fmt.Println("[INFO] 100 SetStates commands hidden.")
|
||||
atomic.AddInt32(&b.setStates, -1000)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,12 @@ func (b *EventBus) RunEvent(event Event) {
|
||||
b.send(serviceMessage{event: event})
|
||||
}
|
||||
|
||||
func (b *EventBus) RunEvents(events []Event) {
|
||||
for _, event := range events {
|
||||
b.RunEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *EventBus) send(message serviceMessage) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
+3
-51
@@ -3,11 +3,8 @@ package main
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
"git.aiterp.net/lucifer3/server/services/hue"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"time"
|
||||
)
|
||||
@@ -22,54 +19,9 @@ func main() {
|
||||
bus.JoinPrivileged(sceneMap)
|
||||
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
|
||||
bus.Join(nanoleaf.NewService())
|
||||
bus.Join(hue.NewService())
|
||||
|
||||
bus.JoinCallback(func(event lucifer3.Event) bool {
|
||||
switch event.(type) {
|
||||
case events.DeviceReady:
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:2d0c", Alias: "lucifer:name:Hex 5"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:542f", Alias: "lucifer:name:Hex 6"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:e760", Alias: "lucifer:name:Hex 7"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:207a", Alias: "lucifer:name:Hex 8"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:df9a", Alias: "lucifer:name:Hex 9"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:cdd5", Alias: "lucifer:name:Hex 4"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:4597", Alias: "lucifer:name:Hex 3"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:82cb", Alias: "lucifer:name:Hex 2"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:09fd", Alias: "lucifer:name:Hex 1"})
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: "nanoleaf:10.80.1.14:*",
|
||||
Effect: effects.Pattern{
|
||||
States: []device.State{
|
||||
{Power: p(true), Intensity: p(1.0), Color: p(color.MustParse("xy:0.22,0.18"))},
|
||||
{Power: p(true), Intensity: p(0.4), Color: p(color.MustParse("xy:0.22,0.18"))},
|
||||
{Power: p(true), Intensity: p(0.5), Color: p(color.MustParse("xy:0.22,0.18"))},
|
||||
},
|
||||
AnimationMS: 200,
|
||||
},
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:*", Alias: "lucifer:tag:Magic Lamps"})
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: "lucifer:tag:Magic Lamps",
|
||||
Effect: effects.Pattern{
|
||||
States: []device.State{
|
||||
{Power: p(true), Intensity: p(1.0), Color: p(color.MustParse("hs:0,0.9"))},
|
||||
{Power: p(true), Intensity: p(0.7), Color: p(color.MustParse("hs:100,0.8"))},
|
||||
},
|
||||
AnimationMS: 500,
|
||||
},
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
bus.RunCommand(commands.ConnectDevice{
|
||||
ID: "nanoleaf:10.80.1.14",
|
||||
APIKey: "QRj5xcQxAQMQsjK4gvaprdhOwr1sCIcj",
|
||||
})
|
||||
bus.RunCommand(commands.ConnectDevice{ID: "hue:10.80.1.5", APIKey: "0-Ch5MKQtYnXrA3b8jvE4408mS3tHo9Vn57Zv8pt"})
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ func (c SetState) Matches(driver string) (sub string, ok bool) {
|
||||
}
|
||||
|
||||
func (c SetState) CommandDescription() string {
|
||||
return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State)
|
||||
return fmt.Sprintf("SetStates(%s, %s)", c.ID, c.State)
|
||||
}
|
||||
|
||||
type SetStateBatch map[string]device.State
|
||||
|
||||
+11
-11
@@ -32,43 +32,43 @@ func (f SupportFlags) HasAll(d SupportFlags) bool {
|
||||
return bits.OnesCount32(uint32(f&d)) == bits.OnesCount32(uint32(d))
|
||||
}
|
||||
|
||||
// ColorFlag is primarily to detect warm-white lights, as XY/RGB/HS/HSK can convert without trouble.
|
||||
type ColorFlag uint32
|
||||
// ColorFlags is primarily to detect warm-white lights, as XY/RGB/HS/HSK can convert without trouble.
|
||||
type ColorFlags uint32
|
||||
|
||||
const (
|
||||
CFlagXY ColorFlag = 1 << iota
|
||||
CFlagXY ColorFlags = 1 << iota
|
||||
CFlagRGB
|
||||
CFlagHS
|
||||
CFlagHSK
|
||||
CFlagKelvin
|
||||
)
|
||||
|
||||
var colorFlags = []ColorFlag{CFlagXY, CFlagRGB, CFlagHS, CFlagHSK, CFlagKelvin}
|
||||
var colorFlags = []ColorFlags{CFlagXY, CFlagRGB, CFlagHS, CFlagHSK, CFlagKelvin}
|
||||
|
||||
func (f ColorFlag) String() string {
|
||||
func (f ColorFlags) String() string {
|
||||
return gentools.FlagString(f, colorFlags, []string{"XY", "RGB", "HS", "HSK", "K"})
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsColor() bool {
|
||||
func (f ColorFlags) IsColor() bool {
|
||||
return f.HasAny(CFlagXY | CFlagRGB | CFlagHS | CFlagHSK)
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsWarmWhite() bool {
|
||||
func (f ColorFlags) IsWarmWhite() bool {
|
||||
return f&CFlagKelvin == CFlagKelvin
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsColorOnly() bool {
|
||||
func (f ColorFlags) IsColorOnly() bool {
|
||||
return f.IsColor() && !f.IsWarmWhite()
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsWarmWhiteOnly() bool {
|
||||
func (f ColorFlags) IsWarmWhiteOnly() bool {
|
||||
return f.IsWarmWhite() && !f.IsColor()
|
||||
}
|
||||
|
||||
func (f ColorFlag) HasAny(d ColorFlag) bool {
|
||||
func (f ColorFlags) HasAny(d ColorFlags) bool {
|
||||
return (f & d) != 0
|
||||
}
|
||||
|
||||
func (f ColorFlag) HasAll(d ColorFlag) bool {
|
||||
func (f ColorFlags) HasAll(d ColorFlags) bool {
|
||||
return bits.OnesCount32(uint32(f&d)) == bits.OnesCount32(uint32(d))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
)
|
||||
|
||||
// Info is
|
||||
type Info struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
InternalName string `json:"internalName,omitempty"`
|
||||
Aliases []string `json:"aliases"`
|
||||
SupportFlags SupportFlags `json:"supportFlags"`
|
||||
ColorFlags ColorFlags `json:"colorFlags"`
|
||||
Buttons []string `json:"buttons"`
|
||||
DesiredState *State `json:"desiredState"`
|
||||
HardwareState *State `json:"hardwareState"`
|
||||
ColorGamut *color.Gamut `json:"colorGamut,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package effects
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
)
|
||||
|
||||
type serializedEffect struct {
|
||||
Manual *Manual `json:"manual,omitempty"`
|
||||
Gradient *Gradient `json:"gradient,omitempty"`
|
||||
Pattern *Pattern `json:"pattern,omitempty"`
|
||||
Random *Random `json:"random,omitempty"`
|
||||
}
|
||||
|
||||
type Serializable struct {
|
||||
lucifer3.Effect
|
||||
}
|
||||
|
||||
func (s *Serializable) UnmarshalJSON(raw []byte) error {
|
||||
value := serializedEffect{}
|
||||
err := json.Unmarshal(raw, &value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case value.Manual != nil:
|
||||
s.Effect = *value.Manual
|
||||
case value.Gradient != nil:
|
||||
s.Effect = *value.Gradient
|
||||
case value.Pattern != nil:
|
||||
s.Effect = *value.Pattern
|
||||
case value.Random != nil:
|
||||
s.Effect = *value.Random
|
||||
default:
|
||||
return errors.New("unsupported effect")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Serializable) MarshalJSON() ([]byte, error) {
|
||||
switch effect := s.Effect.(type) {
|
||||
case Manual:
|
||||
return json.Marshal(serializedEffect{Manual: &effect})
|
||||
case Gradient:
|
||||
return json.Marshal(serializedEffect{Gradient: &effect})
|
||||
case Pattern:
|
||||
return json.Marshal(serializedEffect{Pattern: &effect})
|
||||
case Random:
|
||||
return json.Marshal(serializedEffect{Random: &effect})
|
||||
default:
|
||||
panic(s.Effect.EffectDescription() + "is not understood by serializer")
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -3,6 +3,7 @@ package events
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
)
|
||||
|
||||
type DeviceConnected struct {
|
||||
@@ -23,13 +24,16 @@ func (e DeviceDisconnected) EventDescription() string {
|
||||
}
|
||||
|
||||
type HardwareState struct {
|
||||
ID string `json:"internalId"`
|
||||
InternalName string `json:"internalName"`
|
||||
SupportFlags device.SupportFlags `json:"deviceFlags"`
|
||||
ColorFlags device.ColorFlag `json:"colorFlags"`
|
||||
Buttons []string `json:"buttons"`
|
||||
State device.State `json:"state"`
|
||||
Unreachable bool `json:"unreachable"`
|
||||
ID string `json:"internalId"`
|
||||
InternalName string `json:"internalName"`
|
||||
SupportFlags device.SupportFlags `json:"supportFlags"`
|
||||
ColorFlags device.ColorFlags `json:"colorFlags"`
|
||||
ColorGamut *color.Gamut `json:"colorGamut,omitempty"`
|
||||
TemperatureRange *[2]int `json:"temperatureRange,omitempty"`
|
||||
Buttons []string `json:"buttons"`
|
||||
State device.State `json:"state"`
|
||||
BatteryPercentage *int `json:"batteryPercentage"`
|
||||
Unreachable bool `json:"unreachable"`
|
||||
}
|
||||
|
||||
func (e HardwareState) EventDescription() string {
|
||||
@@ -38,6 +42,8 @@ func (e HardwareState) EventDescription() string {
|
||||
)
|
||||
}
|
||||
|
||||
// HardwareMetadata contains things that has no bearing on the functionality of
|
||||
// lucifer, but may be interesting to have in the GUI.
|
||||
type HardwareMetadata struct {
|
||||
ID string `json:"id"`
|
||||
X int `json:"x,omitempty"`
|
||||
|
||||
@@ -9,9 +9,10 @@ const eps = 0.0001
|
||||
const epsSquare = eps * eps
|
||||
|
||||
type Gamut struct {
|
||||
Red XY `json:"red"`
|
||||
Green XY `json:"green"`
|
||||
Blue XY `json:"blue"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Red XY `json:"red"`
|
||||
Green XY `json:"green"`
|
||||
Blue XY `json:"blue"`
|
||||
}
|
||||
|
||||
func (cg *Gamut) side(x1, y1, x2, y2, x, y float64) float64 {
|
||||
@@ -171,7 +172,8 @@ func (xy XY) ToHS() HueSat {
|
||||
}
|
||||
|
||||
func (xy XY) DistanceTo(other XY) float64 {
|
||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
return math.Abs(xy.X-other.X) + math.Abs(xy.Y-other.Y)
|
||||
//return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
func (xy XY) Round() XY {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func CompactIDList(ids []string) []string {
|
||||
ids = append(ids[:0:0], ids...)
|
||||
sort.Strings(ids)
|
||||
|
||||
currentGroup := ""
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package gentools
|
||||
|
||||
func CopyMap[K comparable, V any](m map[K]V) map[K]V {
|
||||
m2 := make(map[K]V, len(m))
|
||||
for k, v := range m {
|
||||
m2[k] = v
|
||||
}
|
||||
|
||||
return m2
|
||||
}
|
||||
|
||||
func OneItemMap[K comparable, V any](key K, value V) map[K]V {
|
||||
m := make(map[K]V, 1)
|
||||
m[key] = value
|
||||
return m
|
||||
}
|
||||
@@ -5,6 +5,16 @@ func Ptr[T any](t T) *T {
|
||||
}
|
||||
|
||||
func ShallowCopy[T any](t *T) *T {
|
||||
if t == nil {
|
||||
return t
|
||||
}
|
||||
|
||||
tCopy := *t
|
||||
return &tCopy
|
||||
}
|
||||
|
||||
func ShallowCopyTo[T any](dst **T, src *T) {
|
||||
if src != nil {
|
||||
*dst = ShallowCopy(src)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,3 +22,21 @@ Outer:
|
||||
*arr = append(*arr, v)
|
||||
}
|
||||
}
|
||||
|
||||
func Map[T any, U any](arr []T, cb func(T) U) []U {
|
||||
arr2 := make([]U, len(arr))
|
||||
for i, v := range arr {
|
||||
arr2[i] = cb(v)
|
||||
}
|
||||
|
||||
return arr2
|
||||
}
|
||||
|
||||
func Flatten[T any](arr [][]T) []T {
|
||||
arr2 := make([]T, 0, 128)
|
||||
for _, sub := range arr {
|
||||
arr2 = sub
|
||||
}
|
||||
|
||||
return arr2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewBridge(host string, client *Client) *Bridge {
|
||||
return &Bridge{
|
||||
client: client,
|
||||
host: host,
|
||||
ctx: context.Background(),
|
||||
cancel: func() {},
|
||||
resources: map[string]*ResourceData{},
|
||||
activeStates: map[string]device.State{},
|
||||
desiredStates: map[string]device.State{},
|
||||
hasSeen: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
type Bridge struct {
|
||||
mu sync.Mutex
|
||||
|
||||
client *Client
|
||||
host string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
resources map[string]*ResourceData
|
||||
activeStates map[string]device.State
|
||||
desiredStates map[string]device.State
|
||||
hasSeen map[string]bool
|
||||
|
||||
lastDiscoverCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (b *Bridge) SearchDevices(timeout time.Duration) error {
|
||||
discoverCtx, cancel := context.WithCancel(b.ctx)
|
||||
|
||||
b.mu.Lock()
|
||||
if b.lastDiscoverCancel != nil {
|
||||
b.lastDiscoverCancel()
|
||||
}
|
||||
b.lastDiscoverCancel = cancel
|
||||
b.mu.Unlock()
|
||||
|
||||
if timeout <= time.Second*10 {
|
||||
timeout = time.Second * 10
|
||||
}
|
||||
|
||||
// Spend half the time waiting for devices
|
||||
// TODO: Wait for v2 endpoint
|
||||
ctx, cancel := context.WithTimeout(discoverCtx, timeout/2)
|
||||
defer cancel()
|
||||
err := b.client.LegacyDiscover(ctx, "sensors")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-ctx.Done()
|
||||
if discoverCtx.Err() != nil {
|
||||
return discoverCtx.Err()
|
||||
}
|
||||
|
||||
// Spend half the time waiting for lights
|
||||
// TODO: Wait for v2 endpoint
|
||||
ctx, cancel = context.WithTimeout(discoverCtx, timeout/2)
|
||||
defer cancel()
|
||||
err = b.client.LegacyDiscover(ctx, "sensors")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-ctx.Done()
|
||||
if discoverCtx.Err() != nil {
|
||||
return discoverCtx.Err()
|
||||
}
|
||||
|
||||
// Let the main loop get the new light.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
||||
ctx, cancel := context.WithTimeout(b.ctx, time.Second*15)
|
||||
defer cancel()
|
||||
|
||||
allResources, err := b.client.AllResources(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := make(map[string]*ResourceData, len(allResources))
|
||||
for i := range allResources {
|
||||
resources[allResources[i].ID] = &allResources[i]
|
||||
}
|
||||
|
||||
newEvents := make([]lucifer3.Event, 0, 0)
|
||||
b.mu.Lock()
|
||||
for id, res := range resources {
|
||||
if res.Type == "device" {
|
||||
hwState, hwEvent := res.GenerateEvent(b.host, resources)
|
||||
if !b.hasSeen[id] {
|
||||
newEvents = append(newEvents, hwState, hwEvent, events.DeviceReady{ID: hwState.ID})
|
||||
b.hasSeen[id] = true
|
||||
} else {
|
||||
newEvents = append(newEvents, hwState)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.resources = resources
|
||||
b.mu.Unlock()
|
||||
|
||||
return newEvents, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event, shouldRefresh bool) {
|
||||
b.mu.Lock()
|
||||
mapCopy := gentools.CopyMap(b.resources)
|
||||
b.mu.Unlock()
|
||||
|
||||
for _, resource := range resources {
|
||||
if mapCopy[resource.ID] != nil {
|
||||
mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
|
||||
} else {
|
||||
log.Println(resource.ID, resource.Type, "not seen!")
|
||||
shouldRefresh = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
if resource.Owner != nil && resource.Owner.Kind == "device" {
|
||||
if parent, ok := mapCopy[resource.Owner.ID]; ok {
|
||||
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
|
||||
events = append(events, hwState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.resources = mapCopy
|
||||
b.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Bridge) SetStates(patch map[string]device.State) {
|
||||
b.mu.Lock()
|
||||
newStates := gentools.CopyMap(b.desiredStates)
|
||||
resources := b.resources
|
||||
b.mu.Unlock()
|
||||
|
||||
prefix := "hue:" + b.host + ":"
|
||||
for id, state := range patch {
|
||||
if !strings.HasPrefix(id, prefix) {
|
||||
continue
|
||||
}
|
||||
id = id[len(prefix):]
|
||||
|
||||
resource := resources[id]
|
||||
if resource == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
newStates[id] = resource.FixState(state, resources)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.desiredStates = newStates
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
||||
hwEvents, err := b.RefreshAll()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bus.RunEvents(hwEvents)
|
||||
|
||||
sse := b.client.SSE(ctx)
|
||||
step := time.NewTicker(time.Second * 30)
|
||||
defer step.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case updates, ok := <-sse:
|
||||
{
|
||||
if !ok {
|
||||
return errors.New("SSE lost connection")
|
||||
}
|
||||
|
||||
newEvents, shouldUpdate := b.ApplyPatches(
|
||||
gentools.Flatten(gentools.Map(updates, func(update SSEUpdate) []ResourceData {
|
||||
return update.Data
|
||||
})),
|
||||
)
|
||||
|
||||
bus.RunEvents(newEvents)
|
||||
|
||||
if shouldUpdate {
|
||||
hwEvents, err := b.RefreshAll()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bus.RunEvents(hwEvents)
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
{
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewClient(host, token string) *Client {
|
||||
ch := make(chan struct{}, 5)
|
||||
for i := 0; i < 2; i++ {
|
||||
ch <- struct{}{}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
host: host,
|
||||
token: token,
|
||||
ch: ch,
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
host string
|
||||
token string
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
func (c *Client) Register(ctx context.Context) (string, error) {
|
||||
result := make([]CreateUserResponse, 0, 1)
|
||||
err := c.post(ctx, "api/", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result) == 0 || result[0].Error != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
return c.Register(ctx)
|
||||
}
|
||||
}
|
||||
if result[0].Success == nil {
|
||||
return "", lucifer3.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
c.token = result[0].Success.Username
|
||||
|
||||
return result[0].Success.Username, nil
|
||||
}
|
||||
|
||||
func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) {
|
||||
res := struct {
|
||||
Error interface{}
|
||||
Data []ResourceData
|
||||
}{}
|
||||
|
||||
err := c.get(ctx, "clip/v2/resource", &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) {
|
||||
res := struct {
|
||||
Error interface{}
|
||||
Data []ResourceData
|
||||
}{}
|
||||
|
||||
err := c.get(ctx, "clip/v2/resource/"+kind, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error {
|
||||
return c.put(ctx, link.Path(), update, nil)
|
||||
}
|
||||
|
||||
func (c *Client) LegacyDiscover(ctx context.Context, kind string) error {
|
||||
return c.legacyPost(ctx, kind, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) SSE(ctx context.Context) <-chan []SSEUpdate {
|
||||
ch := make(chan []SSEUpdate, 4)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{
|
||||
"Accept": "text/event-stream",
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("SSE Connect error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
br := bufio.NewReader(reader)
|
||||
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Println("SSE Read error:", err)
|
||||
return
|
||||
}
|
||||
line = strings.Trim(line, " \t\r\n")
|
||||
kv := strings.SplitN(line, ": ", 2)
|
||||
if len(kv) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch kv[0] {
|
||||
case "data":
|
||||
var data []SSEUpdate
|
||||
err := json.Unmarshal([]byte(kv[1]), &data)
|
||||
if err != nil {
|
||||
log.Println("Parsing SSE event failed:", err)
|
||||
log.Println(" json:", kv[1])
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
ch <- data
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-c.ch:
|
||||
defer func() {
|
||||
c.ch <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req.Header.Set("hue-application-key", c.token)
|
||||
|
||||
client := httpClient
|
||||
if headers["Accept"] == "text/event-stream" {
|
||||
client = sseClient
|
||||
}
|
||||
|
||||
res, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
_ = res.Body.Close()
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, path string, target interface{}) error {
|
||||
body, err := c.getReader(ctx, path, map[string]string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer body.Close()
|
||||
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
return json.NewDecoder(body).Decode(&target)
|
||||
}
|
||||
|
||||
func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.ch:
|
||||
defer func() {
|
||||
c.ch <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("hue-application-key", c.token)
|
||||
|
||||
res, err := httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(&target)
|
||||
}
|
||||
|
||||
func (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.ch:
|
||||
defer func() {
|
||||
c.ch <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.token != "" {
|
||||
req.Header.Set("hue-application-key", c.token)
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(&target)
|
||||
}
|
||||
|
||||
func (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.ch:
|
||||
defer func() {
|
||||
c.ch <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.token != "" {
|
||||
resource = c.token + "/" + resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(target)
|
||||
}
|
||||
|
||||
func reqBody(body interface{}) (io.Reader, error) {
|
||||
if body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch v := body.(type) {
|
||||
case []byte:
|
||||
return bytes.NewReader(v), nil
|
||||
case string:
|
||||
return strings.NewReader(v), nil
|
||||
case io.Reader:
|
||||
return v, nil
|
||||
default:
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(jsonData), nil
|
||||
}
|
||||
}
|
||||
|
||||
var sseClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Hour * 1000000,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 5,
|
||||
MaxIdleConnsPerHost: 1,
|
||||
IdleConnTimeout: 0,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
Timeout: time.Hour * 1000000,
|
||||
}
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 256,
|
||||
MaxIdleConnsPerHost: 16,
|
||||
IdleConnTimeout: 10 * time.Minute,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
Timeout: time.Minute,
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceData struct {
|
||||
ID string `json:"id"`
|
||||
LegacyID string `json:"id_v1"`
|
||||
Metadata DeviceMetadata `json:"metadata"`
|
||||
Type string `json:"type"`
|
||||
|
||||
ProductData DeviceProductData `json:"product_data"`
|
||||
Services []ResourceLink `json:"services"`
|
||||
}
|
||||
|
||||
type DeviceMetadata struct {
|
||||
Archetype string `json:"archetype"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type DeviceProductData struct {
|
||||
Certified bool `json:"certified"`
|
||||
ManufacturerName string `json:"manufacturer_name"`
|
||||
ModelID string `json:"model_id"`
|
||||
ProductArchetype string `json:"product_archetype"`
|
||||
ProductName string `json:"product_name"`
|
||||
SoftwareVersion string `json:"software_version"`
|
||||
}
|
||||
|
||||
type SSEUpdate struct {
|
||||
CreationTime time.Time `json:"creationTime"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Data []ResourceData `json:"data"`
|
||||
}
|
||||
|
||||
type ResourceData struct {
|
||||
ID string `json:"id"`
|
||||
LegacyID string `json:"id_v1"`
|
||||
Metadata DeviceMetadata `json:"metadata"`
|
||||
Type string `json:"type"`
|
||||
|
||||
Mode *string `json:"mode"`
|
||||
|
||||
Owner *ResourceLink `json:"owner"`
|
||||
ProductData *DeviceProductData `json:"product_data"`
|
||||
Services []ResourceLink `json:"services"`
|
||||
Button *SensorButton `json:"button"`
|
||||
Power *LightPower `json:"on"`
|
||||
Color *LightColor `json:"color"`
|
||||
ColorTemperature *LightCT `json:"color_temperature"`
|
||||
Dimming *LightDimming `json:"dimming"`
|
||||
Dynamics *LightDynamics `json:"dynamics"`
|
||||
Alert *LightAlert `json:"alert"`
|
||||
PowerState *PowerState `json:"power_state"`
|
||||
Temperature *SensorTemperature `json:"temperature"`
|
||||
Motion *SensorMotion `json:"motion"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
func (res *ResourceData) ServiceID(kind string) *string {
|
||||
for _, ptr := range res.Services {
|
||||
if ptr.Kind == kind {
|
||||
return &ptr.ID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (res *ResourceData) ServiceIndex(kind string, id string) int {
|
||||
idx := 0
|
||||
for _, link := range res.Services {
|
||||
if link.ID == id {
|
||||
return idx
|
||||
} else if link.Kind == kind {
|
||||
idx += 1
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (res *ResourceData) FixState(state device.State, resources map[string]*ResourceData) device.State {
|
||||
fixedState := device.State{}
|
||||
if lightID := res.ServiceID("light"); lightID != nil {
|
||||
if light := resources[*lightID]; light != nil {
|
||||
if state.Color != nil {
|
||||
if state.Color.IsKelvin() {
|
||||
if light.ColorTemperature != nil {
|
||||
mirek := 1000000 / *state.Color.K
|
||||
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
|
||||
mirek = light.ColorTemperature.MirekSchema.MirekMinimum
|
||||
}
|
||||
if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
|
||||
mirek = light.ColorTemperature.MirekSchema.MirekMaximum
|
||||
}
|
||||
|
||||
fixedState.Color = &color.Color{K: gentools.Ptr(1000000 / mirek)}
|
||||
}
|
||||
} else {
|
||||
if light.Color != nil {
|
||||
if col, ok := state.Color.ToXY(); ok {
|
||||
col.XY = gentools.Ptr(light.Color.Gamut.Conform(*col.XY))
|
||||
fixedState.Color = &col
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.Intensity != nil && light.Dimming != nil {
|
||||
fixedState.Intensity = gentools.ShallowCopy(state.Intensity)
|
||||
}
|
||||
|
||||
if state.Power != nil && light.Power != nil {
|
||||
fixedState.Power = gentools.ShallowCopy(state.Power)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixedState
|
||||
}
|
||||
|
||||
func (res *ResourceData) WithUpdate(update ResourceUpdate) *ResourceData {
|
||||
resCopy := *res
|
||||
|
||||
if update.Name != nil {
|
||||
resCopy.Metadata.Name = *update.Name
|
||||
}
|
||||
if update.Power != nil {
|
||||
cp := *resCopy.Power
|
||||
resCopy.Power = &cp
|
||||
resCopy.Power.On = *update.Power
|
||||
}
|
||||
if update.ColorXY != nil {
|
||||
cp := *resCopy.Color
|
||||
resCopy.Color = &cp
|
||||
resCopy.Color.XY = *update.ColorXY
|
||||
|
||||
if resCopy.ColorTemperature != nil {
|
||||
cp := *resCopy.ColorTemperature
|
||||
resCopy.ColorTemperature = &cp
|
||||
resCopy.ColorTemperature.Mirek = nil
|
||||
}
|
||||
}
|
||||
if update.Mirek != nil {
|
||||
cp := *resCopy.ColorTemperature
|
||||
resCopy.ColorTemperature = &cp
|
||||
mirek := *update.Mirek
|
||||
resCopy.ColorTemperature.Mirek = &mirek
|
||||
}
|
||||
if update.Brightness != nil {
|
||||
cp := *resCopy.Dimming
|
||||
resCopy.Dimming = &cp
|
||||
resCopy.Dimming.Brightness = *update.Brightness
|
||||
}
|
||||
|
||||
return &resCopy
|
||||
}
|
||||
|
||||
func (res *ResourceData) WithPatch(patch ResourceData) *ResourceData {
|
||||
res2 := *res
|
||||
|
||||
if patch.Color != nil {
|
||||
res2.Color = gentools.ShallowCopy(res2.Color)
|
||||
res2.Color.XY = patch.Color.XY
|
||||
}
|
||||
if patch.ColorTemperature != nil {
|
||||
res2.ColorTemperature = gentools.ShallowCopy(res2.ColorTemperature)
|
||||
res2.ColorTemperature.Mirek = gentools.ShallowCopy(patch.ColorTemperature.Mirek)
|
||||
}
|
||||
|
||||
gentools.ShallowCopyTo(&res2.ProductData, patch.ProductData)
|
||||
gentools.ShallowCopyTo(&res2.Button, patch.Button)
|
||||
gentools.ShallowCopyTo(&res2.Power, patch.Power)
|
||||
gentools.ShallowCopyTo(&res2.Color, patch.Color)
|
||||
gentools.ShallowCopyTo(&res2.ColorTemperature, patch.ColorTemperature)
|
||||
gentools.ShallowCopyTo(&res2.Dimming, patch.Dimming)
|
||||
gentools.ShallowCopyTo(&res2.Dynamics, patch.Dynamics)
|
||||
gentools.ShallowCopyTo(&res2.Alert, patch.Alert)
|
||||
gentools.ShallowCopyTo(&res2.PowerState, patch.PowerState)
|
||||
gentools.ShallowCopyTo(&res2.Temperature, patch.Temperature)
|
||||
gentools.ShallowCopyTo(&res2.Motion, patch.Motion)
|
||||
gentools.ShallowCopyTo(&res2.Status, patch.Status)
|
||||
|
||||
return &res2
|
||||
}
|
||||
|
||||
func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*ResourceData) (events.HardwareState, events.HardwareMetadata) {
|
||||
hwState := events.HardwareState{
|
||||
ID: fmt.Sprintf("hue:%s:%s", hostname, res.ID),
|
||||
InternalName: res.Metadata.Name,
|
||||
State: res.GenerateState(resources),
|
||||
Unreachable: false,
|
||||
}
|
||||
hwMeta := events.HardwareMetadata{
|
||||
ID: hwState.ID,
|
||||
}
|
||||
buttonCount := 0
|
||||
|
||||
if res.ProductData != nil {
|
||||
hwMeta.FirmwareVersion = res.ProductData.SoftwareVersion
|
||||
}
|
||||
|
||||
for _, ptr := range res.Services {
|
||||
svc := resources[ptr.ID]
|
||||
if svc == nil {
|
||||
continue // I've never seen this happen, but safety first.
|
||||
}
|
||||
|
||||
switch ptr.Kind {
|
||||
case "device_power":
|
||||
hwState.BatteryPercentage = gentools.Ptr(int(svc.PowerState.BatteryLevel))
|
||||
case "button":
|
||||
buttonCount += 1
|
||||
hwState.SupportFlags |= device.SFlagSensorButtons
|
||||
case "motion":
|
||||
hwState.SupportFlags |= device.SFlagSensorPresence
|
||||
case "temperature":
|
||||
hwState.SupportFlags |= device.SFlagSensorTemperature
|
||||
case "light":
|
||||
if svc.Power != nil {
|
||||
hwState.SupportFlags |= device.SFlagPower
|
||||
}
|
||||
if svc.Dimming != nil {
|
||||
hwState.SupportFlags |= device.SFlagIntensity
|
||||
}
|
||||
if svc.ColorTemperature != nil {
|
||||
hwState.SupportFlags |= device.SFlagColor
|
||||
hwState.ColorFlags |= device.CFlagKelvin
|
||||
hwState.TemperatureRange = &[2]int{
|
||||
1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
|
||||
1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
|
||||
}
|
||||
}
|
||||
if svc.Color != nil {
|
||||
hwState.SupportFlags |= device.SFlagColor
|
||||
hwState.ColorFlags |= device.CFlagXY
|
||||
hwState.ColorGamut = gentools.Ptr(svc.Color.Gamut)
|
||||
hwState.ColorGamut.Label = svc.Color.GamutType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if buttonCount == 4 {
|
||||
hwState.Buttons = []string{"On", "DimUp", "DimDown", "Off"}
|
||||
} else if buttonCount == 1 {
|
||||
hwState.Buttons = []string{"Button"}
|
||||
} else {
|
||||
for n := 1; n <= buttonCount; n++ {
|
||||
hwState.Buttons = append(hwState.Buttons, fmt.Sprint("Button", n))
|
||||
}
|
||||
}
|
||||
|
||||
return hwState, hwMeta
|
||||
}
|
||||
|
||||
func (res *ResourceData) GenerateState(resources map[string]*ResourceData) device.State {
|
||||
state := device.State{}
|
||||
for _, ptr := range res.Services {
|
||||
switch ptr.Kind {
|
||||
case "light":
|
||||
light := resources[ptr.ID]
|
||||
if light == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if light.Power != nil {
|
||||
state.Power = gentools.Ptr(light.Power.On)
|
||||
}
|
||||
if light.Dimming != nil {
|
||||
state.Intensity = gentools.Ptr(light.Dimming.Brightness / 100.0)
|
||||
}
|
||||
if light.ColorTemperature != nil {
|
||||
if light.ColorTemperature.Mirek != nil {
|
||||
state.Color = &color.Color{K: gentools.Ptr(1000000 / *light.ColorTemperature.Mirek)}
|
||||
}
|
||||
}
|
||||
if light.Color != nil {
|
||||
if state.Color == nil || state.Color.IsEmpty() {
|
||||
state.Color = &color.Color{XY: &light.Color.XY}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
type SensorButton struct {
|
||||
LastEvent string `json:"last_event"`
|
||||
}
|
||||
|
||||
type SensorMotion struct {
|
||||
Motion bool `json:"motion"`
|
||||
Valid bool `json:"motion_valid"`
|
||||
}
|
||||
|
||||
type SensorTemperature struct {
|
||||
Temperature float64 `json:"temperature"`
|
||||
Valid bool `json:"temperature_valid"`
|
||||
}
|
||||
|
||||
type PowerState struct {
|
||||
BatteryState string `json:"battery_state"`
|
||||
BatteryLevel float64 `json:"battery_level"`
|
||||
}
|
||||
|
||||
type LightPower struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
|
||||
type LightDimming struct {
|
||||
Brightness float64 `json:"brightness"`
|
||||
}
|
||||
|
||||
type LightColor struct {
|
||||
Gamut color.Gamut `json:"gamut"`
|
||||
GamutType string `json:"gamut_type"`
|
||||
XY color.XY `json:"xy"`
|
||||
}
|
||||
|
||||
type LightCT struct {
|
||||
Mirek *int `json:"mirek"`
|
||||
MirekSchema LightCTMirekSchema `json:"mirek_schema"`
|
||||
MirekValid bool `json:"mirek_valid"`
|
||||
}
|
||||
|
||||
type LightCTMirekSchema struct {
|
||||
MirekMaximum int `json:"mirek_maximum"`
|
||||
MirekMinimum int `json:"mirek_minimum"`
|
||||
}
|
||||
|
||||
type LightDynamics struct {
|
||||
Speed float64 `json:"speed"`
|
||||
SpeedValid bool `json:"speed_valid"`
|
||||
Status string `json:"status"`
|
||||
StatusValues []string `json:"status_values"`
|
||||
}
|
||||
|
||||
type LightAlert struct {
|
||||
ActionValues []string `json:"action_values"`
|
||||
}
|
||||
|
||||
type ResourceUpdate struct {
|
||||
Name *string
|
||||
Power *bool
|
||||
ColorXY *color.XY
|
||||
Brightness *float64
|
||||
Mirek *int
|
||||
TransitionDuration *time.Duration
|
||||
}
|
||||
|
||||
func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
|
||||
chunks := make([]string, 0, 4)
|
||||
if r.Name != nil {
|
||||
s, _ := json.Marshal(*r.Name)
|
||||
chunks = append(chunks, fmt.Sprintf(`"metadata":{"name":%s}`, string(s)))
|
||||
}
|
||||
if r.Power != nil {
|
||||
chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power))
|
||||
}
|
||||
if r.ColorXY != nil {
|
||||
chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y))
|
||||
}
|
||||
if r.Brightness != nil {
|
||||
chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness))
|
||||
}
|
||||
if r.Mirek != nil {
|
||||
chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek))
|
||||
}
|
||||
if r.TransitionDuration != nil {
|
||||
chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds()))
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil
|
||||
}
|
||||
|
||||
type ResourceLink struct {
|
||||
ID string `json:"rid"`
|
||||
Kind string `json:"rtype"`
|
||||
}
|
||||
|
||||
func (rl *ResourceLink) Path() string {
|
||||
return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID)
|
||||
}
|
||||
|
||||
type CreateUserInput struct {
|
||||
DeviceType string `json:"devicetype"`
|
||||
}
|
||||
|
||||
type CreateUserResponse struct {
|
||||
Success *struct {
|
||||
Username string `json:"username"`
|
||||
} `json:"success"`
|
||||
Error *struct {
|
||||
Type int `json:"type"`
|
||||
Address string `json:"address"`
|
||||
Description string `json:"description"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type DiscoveryEntry struct {
|
||||
Id string `json:"id"`
|
||||
InternalIPAddress string `json:"internalipaddress"`
|
||||
}
|
||||
|
||||
type BridgeDeviceInfo struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Text string `xml:",chardata"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
SpecVersion struct {
|
||||
Text string `xml:",chardata"`
|
||||
Major string `xml:"major"`
|
||||
Minor string `xml:"minor"`
|
||||
} `xml:"specVersion"`
|
||||
URLBase string `xml:"URLBase"`
|
||||
Device struct {
|
||||
Text string `xml:",chardata"`
|
||||
DeviceType string `xml:"deviceType"`
|
||||
FriendlyName string `xml:"friendlyName"`
|
||||
Manufacturer string `xml:"manufacturer"`
|
||||
ManufacturerURL string `xml:"manufacturerURL"`
|
||||
ModelDescription string `xml:"modelDescription"`
|
||||
ModelName string `xml:"modelName"`
|
||||
ModelNumber string `xml:"modelNumber"`
|
||||
ModelURL string `xml:"modelURL"`
|
||||
SerialNumber string `xml:"serialNumber"`
|
||||
UDN string `xml:"UDN"`
|
||||
PresentationURL string `xml:"presentationURL"`
|
||||
IconList struct {
|
||||
Text string `xml:",chardata"`
|
||||
Icon struct {
|
||||
Text string `xml:",chardata"`
|
||||
Mimetype string `xml:"mimetype"`
|
||||
Height string `xml:"height"`
|
||||
Width string `xml:"width"`
|
||||
Depth string `xml:"depth"`
|
||||
URL string `xml:"url"`
|
||||
} `xml:"icon"`
|
||||
} `xml:"iconList"`
|
||||
} `xml:"device"`
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewService() lucifer3.ActiveService {
|
||||
return &service{
|
||||
bridges: map[string]*Bridge{},
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
mu sync.Mutex
|
||||
|
||||
bridges map[string]*Bridge
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
switch command := command.(type) {
|
||||
case commands.PairDevice:
|
||||
{
|
||||
if hostname, ok := command.Matches("hue"); ok {
|
||||
go func() {
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
client := NewClient(hostname, "")
|
||||
|
||||
token, err := client.Register(timeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bus.RunEvent(events.DeviceAccepted{
|
||||
ID: command.ID,
|
||||
APIKey: token,
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
case commands.SetStateBatch:
|
||||
for _, bridge := range s.bridges {
|
||||
bridge.SetStates(command)
|
||||
}
|
||||
|
||||
case commands.SetState:
|
||||
if sub, ok := command.Matches("hue"); ok {
|
||||
if s.bridges[sub] != nil {
|
||||
s.bridges[sub].SetStates(gentools.OneItemMap(command.ID, command.State))
|
||||
}
|
||||
}
|
||||
|
||||
case commands.ConnectDevice:
|
||||
if sub, ok := command.Matches("hue"); ok {
|
||||
if s.bridges[sub] != nil {
|
||||
s.bridges[sub].cancel()
|
||||
delete(s.bridges, sub)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
client := NewClient(sub, command.APIKey)
|
||||
|
||||
bridge := NewBridge(sub, client)
|
||||
bridge.ctx = ctx
|
||||
bridge.cancel = cancel
|
||||
s.bridges[sub] = bridge
|
||||
|
||||
go func() {
|
||||
for bridge.ctx.Err() == nil {
|
||||
ctx2, cancel2 := context.WithCancel(ctx)
|
||||
|
||||
err := bridge.Run(ctx2, bus)
|
||||
cancel2()
|
||||
if err != nil {
|
||||
bus.RunEvent(events.DeviceFailed{
|
||||
ID: command.ID,
|
||||
Error: fmt.Sprintf("Run failed: %s", err),
|
||||
})
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 5):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) {}
|
||||
Reference in New Issue
Block a user