add nanoleaf service
This commit is contained in:
@@ -96,7 +96,7 @@ func (b *EventBus) send(message serviceMessage) {
|
||||
listener.mu.Unlock()
|
||||
}
|
||||
|
||||
for i := range deleteList {
|
||||
for _, i := range deleteList {
|
||||
b.listeners = append(b.listeners[:i], b.listeners[i+1:]...)
|
||||
}
|
||||
|
||||
|
||||
+39
-70
@@ -1,16 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
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/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
"log"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,76 +21,47 @@ func main() {
|
||||
bus.JoinPrivileged(resolver)
|
||||
bus.JoinPrivileged(sceneMap)
|
||||
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
|
||||
bus.Join(nanoleaf.NewService())
|
||||
|
||||
bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"})
|
||||
|
||||
numbers := []int{5, 2, 3, 1, 4}
|
||||
for i, id := range []string{"e28c", "67db", "f744", "d057", "73c1"} {
|
||||
bus.RunEvent(events.HardwareState{
|
||||
ID: "nanoleaf:10.80.1.11:" + id,
|
||||
InternalName: fmt.Sprintf("Hexagon %d", numbers[i]),
|
||||
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
|
||||
ColorFlags: device.CFlagRGB,
|
||||
State: device.State{},
|
||||
})
|
||||
}
|
||||
|
||||
bus.RunCommand(commands.ReplaceScene{
|
||||
Match: "lucifer:name:Hex*",
|
||||
SceneName: "Evening",
|
||||
})
|
||||
|
||||
bus.RunCommand(commands.AddAlias{
|
||||
Match: "nanoleaf:10.80.1.{11,7,16,5}:*",
|
||||
Alias: "lucifer:tag:Magic Lamps",
|
||||
})
|
||||
|
||||
for i, id := range []string{"40e5", "dead", "beef", "cafe", "1337"} {
|
||||
bus.RunEvent(events.HardwareState{
|
||||
ID: "nanoleaf:10.80.1.11:" + id,
|
||||
InternalName: fmt.Sprintf("Hexagon %d", 6+i),
|
||||
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
|
||||
ColorFlags: device.CFlagRGB,
|
||||
State: device.State{},
|
||||
})
|
||||
}
|
||||
|
||||
bus.RunEvent(events.ExternalEvent{
|
||||
Kind: "weather",
|
||||
Values: map[string]string{
|
||||
"location": "Brekstad",
|
||||
"temperature_celsius": "21.00",
|
||||
"precipitation_mm": "3.21",
|
||||
},
|
||||
})
|
||||
|
||||
c1 := gentools.Ptr(color.MustParse("rgb:#ff0000"))
|
||||
c2 := gentools.Ptr(color.MustParse("rgb:#00ff00"))
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: "**:Hexagon *",
|
||||
Effect: effects.Gradient{
|
||||
States: []device.State{
|
||||
{
|
||||
Power: gentools.Ptr(true),
|
||||
Color: c1,
|
||||
Intensity: gentools.Ptr(1.0),
|
||||
bus.JoinCallback(func(event lucifer3.Event) bool {
|
||||
switch event.(type) {
|
||||
case events.DevicesReady:
|
||||
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 4"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:e760", Alias: "lucifer:name:Hex 3"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:207a", Alias: "lucifer:name:Hex 2"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:df9a", Alias: "lucifer:name:Hex 1"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:cdd5", Alias: "lucifer:name:Hex 6"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:4597", Alias: "lucifer:name:Hex 7"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:82cb", Alias: "lucifer:name:Hex 8"})
|
||||
bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:09fd", Alias: "lucifer:name:Hex 9"})
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: "nanoleaf:10.80.1.14:*",
|
||||
Effect: effects.Gradient{
|
||||
States: []device.State{
|
||||
{Power: p(true), Intensity: p(0.3), 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"))},
|
||||
},
|
||||
Interpolate: true,
|
||||
AnimationMS: 1000,
|
||||
Reverse: false,
|
||||
},
|
||||
{
|
||||
Power: gentools.Ptr(true),
|
||||
Color: c2,
|
||||
Intensity: gentools.Ptr(0.7),
|
||||
},
|
||||
},
|
||||
AnimationMS: 1000,
|
||||
Interpolate: true,
|
||||
},
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
log.Println("Search \"**:Hexagon {1,5,6}\"")
|
||||
for _, dev := range resolver.Resolve("lucifer:name:Hexagon {1,5,6}") {
|
||||
log.Println("- ID:", dev.ID)
|
||||
log.Println(" Aliases:", dev.Aliases)
|
||||
}
|
||||
bus.RunCommand(commands.ConnectDevice{
|
||||
ID: "nanoleaf:10.80.1.14",
|
||||
APIKey: "",
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 15)
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
|
||||
func p[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/internal/formattools"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PairDevice struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (c PairDevice) CommandDescription() string {
|
||||
return fmt.Sprintf("PairDevice(%s)", c.ID)
|
||||
}
|
||||
|
||||
func (c PairDevice) Matches(driver string) (sub string, ok bool) {
|
||||
split := strings.SplitN(c.ID, ":", 2)
|
||||
if split[0] != driver {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return split[1], true
|
||||
}
|
||||
|
||||
type ConnectDevice struct {
|
||||
ID string `json:"id"`
|
||||
APIKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
func (c ConnectDevice) Matches(driver string) (sub string, ok bool) {
|
||||
split := strings.SplitN(c.ID, ":", 2)
|
||||
if split[0] != driver {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return split[1], true
|
||||
}
|
||||
|
||||
func (c ConnectDevice) CommandDescription() string {
|
||||
return fmt.Sprintf("ConnectDevice(%s, %s)", c.ID, formattools.Asterisks(c.APIKey))
|
||||
}
|
||||
|
||||
type SearchDevices struct {
|
||||
ID string `json:"id"`
|
||||
Hint string `json:"hint"`
|
||||
}
|
||||
|
||||
func (c SearchDevices) Matches(driver string) (string, bool) {
|
||||
split := strings.SplitN(c.ID, ":", 2)
|
||||
if split[0] != driver {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return split[1], true
|
||||
}
|
||||
|
||||
func (c SearchDevices) CommandDescription() string {
|
||||
return fmt.Sprintf("SearchDevices(%s, %#+v)", c.ID, c.Hint)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SetState struct {
|
||||
@@ -10,6 +11,14 @@ type SetState struct {
|
||||
State device.State
|
||||
}
|
||||
|
||||
func (c SetState) Matches(driver string) (sub string, ok bool) {
|
||||
if strings.HasPrefix(c.ID, driver) && strings.HasPrefix(c.ID[len(driver):], ":") {
|
||||
return strings.SplitN(c.ID[len(driver)+1:], ":", 2)[0], true
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (c SetState) CommandDescription() string {
|
||||
return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State)
|
||||
}
|
||||
|
||||
+8
-4
@@ -17,16 +17,20 @@ type State struct {
|
||||
func (s State) String() string {
|
||||
parts := make([]string, 0, 4)
|
||||
if s.Power != nil {
|
||||
parts = append(parts, fmt.Sprintf("power:%t", *s.Power))
|
||||
if *s.Power {
|
||||
parts = append(parts, "on")
|
||||
} else {
|
||||
parts = append(parts, "off")
|
||||
}
|
||||
}
|
||||
if s.Temperature != nil {
|
||||
parts = append(parts, fmt.Sprintf("temperature:%f", *s.Temperature))
|
||||
parts = append(parts, fmt.Sprintf("%f°C", *s.Temperature))
|
||||
}
|
||||
if s.Intensity != nil {
|
||||
parts = append(parts, fmt.Sprintf("intensity:%.2f", *s.Intensity))
|
||||
parts = append(parts, fmt.Sprintf("%.1f%%", *s.Intensity*100))
|
||||
}
|
||||
if s.Color != nil {
|
||||
parts = append(parts, fmt.Sprintf("color:%s", s.Color.String()))
|
||||
parts = append(parts, s.Color.String())
|
||||
}
|
||||
|
||||
return fmt.Sprint("(", strings.Join(parts, ", "), ")")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package lucifer3
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrUnexpectedResponse = errors.New("unexpected response from device")
|
||||
var ErrIncorrectToken = errors.New("api token not accepted by device")
|
||||
@@ -0,0 +1,38 @@
|
||||
package events
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ButtonPressed struct {
|
||||
ID string `json:"id"`
|
||||
SwipedID string `json:"swipedId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (e ButtonPressed) EventDescription() string {
|
||||
if e.SwipedID != "" {
|
||||
return fmt.Sprintf("ButtonPressed(name:%s, swipe:%s->%s)", e.Name, e.SwipedID, e.ID)
|
||||
} else {
|
||||
return fmt.Sprintf("ButtonPressed(name:%s, id:%s)", e.Name, e.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (e ButtonPressed) TriggerKind() string {
|
||||
return "ButtonPressed:" + e.Name
|
||||
}
|
||||
|
||||
func (e ButtonPressed) TriggerValue(key string) (string, bool) {
|
||||
switch key {
|
||||
case "name":
|
||||
return e.Name, true
|
||||
case "id":
|
||||
return e.ID, true
|
||||
case "swipedFromId", "swipedId":
|
||||
if e.SwipedID != "" {
|
||||
return e.SwipedID, true
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package events
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Connected struct {
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
func (e Connected) EventDescription() string {
|
||||
return fmt.Sprintf("Connect(prefix:%s)", e.Prefix)
|
||||
}
|
||||
|
||||
type Disconnected struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (e Disconnected) EventDescription() string {
|
||||
return fmt.Sprintf("Disconnected(prefix:%s, reason:%s)", e.Prefix, e.Reason)
|
||||
}
|
||||
+76
-6
@@ -5,6 +5,23 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
)
|
||||
|
||||
type DeviceConnected struct {
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
func (e DeviceConnected) EventDescription() string {
|
||||
return fmt.Sprintf("DeviceConnected(prefix:%s)", e.Prefix)
|
||||
}
|
||||
|
||||
type DeviceDisconnected struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (e DeviceDisconnected) EventDescription() string {
|
||||
return fmt.Sprintf("DeviceDisconnected(prefix:%s, reason:%s)", e.Prefix, e.Reason)
|
||||
}
|
||||
|
||||
type HardwareState struct {
|
||||
ID string `json:"internalId"`
|
||||
InternalName string `json:"internalName"`
|
||||
@@ -14,16 +31,69 @@ type HardwareState struct {
|
||||
State device.State `json:"state"`
|
||||
}
|
||||
|
||||
func (d HardwareState) EventDescription() string {
|
||||
func (e HardwareState) EventDescription() string {
|
||||
return fmt.Sprintf("HardwareState(id:%s, iname:%#+v, sflags:%s, cflags:%s, buttons:%v, state:%s)",
|
||||
d.ID, d.InternalName, d.SupportFlags, d.ColorFlags, d.Buttons, d.State,
|
||||
e.ID, e.InternalName, e.SupportFlags, e.ColorFlags, e.Buttons, e.State,
|
||||
)
|
||||
}
|
||||
|
||||
type Unreachable struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
type HardwareMetadata struct {
|
||||
ID string `json:"id"`
|
||||
X int `json:"x,omitempty"`
|
||||
Y int `json:"y,omitempty"`
|
||||
O int `json:"o,omitempty"`
|
||||
ShapeType string `json:"shapeType,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
SerialNumber string `json:"serialNumber,omitempty"`
|
||||
FirmwareVersion string `json:"firmwareVersion,omitempty"`
|
||||
}
|
||||
|
||||
func (d Unreachable) EventDescription() string {
|
||||
return fmt.Sprintf("Unreachable(id:%s)", d.DeviceID)
|
||||
func (e HardwareMetadata) EventDescription() string {
|
||||
return fmt.Sprintf("HardwareMetadata(id:%s, icon:%s, ...)", e.ID, e.Icon)
|
||||
}
|
||||
|
||||
// DevicesReady is triggered to indicate that all hardware states have been pushed to the bus ahead of this event.
|
||||
type DevicesReady struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (d DevicesReady) EventDescription() string {
|
||||
return fmt.Sprintf("DevicesReady(id:%s)", d.ID)
|
||||
}
|
||||
|
||||
type DevicesUnreachable struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (d DevicesUnreachable) EventDescription() string {
|
||||
return fmt.Sprintf("DevicesUnreachable(id:%s)", d.ID)
|
||||
}
|
||||
|
||||
type DeviceFailed struct {
|
||||
ID string `json:"id"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (e DeviceFailed) EventDescription() string {
|
||||
return fmt.Sprintf("DeviceFailed(id:%s, err:%s)", e.ID, e.Error)
|
||||
}
|
||||
|
||||
type DeviceAvailable struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e DeviceAvailable) EventDescription() string {
|
||||
return fmt.Sprintf("DeviceAvailable(id:%s, name:%s)")
|
||||
}
|
||||
|
||||
type DeviceAccepted struct {
|
||||
ID string `json:"id"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Extras map[string]string `json:"extras"`
|
||||
}
|
||||
|
||||
func (e DeviceAccepted) EventDescription() string {
|
||||
// TODO: Use formattools.Asterisks
|
||||
return fmt.Sprintf("DeviceAccepted(id:%s, apiKey:%s)", e.ID, e.APIKey)
|
||||
}
|
||||
|
||||
@@ -35,3 +35,11 @@ func (rgb RGB) ToXY() XY {
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
|
||||
func (rgb RGB) Normalize() (RGB, float64) {
|
||||
hue, sat, value := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
hs := HueSat{Hue: hue, Sat: sat}
|
||||
newRGB := hs.ToRGB()
|
||||
|
||||
return newRGB, value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package formattools
|
||||
|
||||
const firmament = "****************..."
|
||||
|
||||
func Asterisks(s string) string {
|
||||
if len(s) < 16 {
|
||||
return firmament[:len(s)]
|
||||
} else {
|
||||
return firmament
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"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"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type bridge struct {
|
||||
mu sync.Mutex
|
||||
host string
|
||||
apiKey string
|
||||
panels []*panel
|
||||
panelIDMap map[uint16]string
|
||||
}
|
||||
|
||||
func (b *bridge) HardwareEvents() []lucifer3.Event {
|
||||
results := make([]lucifer3.Event, 0, len(b.panels)*2)
|
||||
for i, panel := range b.panels {
|
||||
// Find normalized RGB and intensity
|
||||
rgb := color.RGB{
|
||||
Red: float64(panel.ColorRGBA[0]) / 255.0,
|
||||
Green: float64(panel.ColorRGBA[1]) / 255.0,
|
||||
Blue: float64(panel.ColorRGBA[2]) / 255.0,
|
||||
}
|
||||
normalized, intensity := rgb.Normalize()
|
||||
col := color.Color{RGB: &normalized}
|
||||
|
||||
shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType]
|
||||
if !shapeTypeOK {
|
||||
shapeType = "Unknown"
|
||||
}
|
||||
|
||||
shapeIcon, shapeIconOK := shapeIconMap[panel.ShapeType]
|
||||
if !shapeIconOK {
|
||||
shapeIcon = "lightbulb"
|
||||
}
|
||||
|
||||
state := device.State{}
|
||||
if panel.ColorRGBA != [4]byte{0, 0, 0, 0} {
|
||||
state = device.State{
|
||||
Power: gentools.Ptr(panel.ColorRGBA[3] == 0),
|
||||
Color: &col,
|
||||
Intensity: &intensity,
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, events.HardwareState{
|
||||
ID: panel.FullID,
|
||||
InternalName: fmt.Sprintf("%s %d (%s)", shapeType, i+1, strings.SplitN(panel.FullID, ":", 3)[2]),
|
||||
SupportFlags: device.SFlagPower | device.SFlagIntensity | device.SFlagColor,
|
||||
ColorFlags: device.CFlagRGB,
|
||||
Buttons: []string{"Touch"},
|
||||
State: state,
|
||||
}, events.HardwareMetadata{
|
||||
ID: panel.FullID,
|
||||
X: panel.X,
|
||||
Y: panel.Y,
|
||||
ShapeType: shapeType,
|
||||
Icon: shapeIcon,
|
||||
})
|
||||
}
|
||||
|
||||
return append(results, events.DevicesReady{
|
||||
ID: fmt.Sprintf("nanoleaf:%s", b.host),
|
||||
})
|
||||
}
|
||||
|
||||
func (b *bridge) Refresh(ctx context.Context) (bool, error) {
|
||||
overview, err := b.Overview(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
b.mu.Lock()
|
||||
PanelLoop:
|
||||
for _, panelInfo := range overview.PanelLayout.Data.PositionData {
|
||||
if shapeTypeMap[panelInfo.ShapeType] == "Shapes Controller" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, existingPanel := range b.panels {
|
||||
if existingPanel.ID == panelInfo.PanelID {
|
||||
existingPanel.O = panelInfo.O
|
||||
existingPanel.X = panelInfo.X
|
||||
existingPanel.Y = panelInfo.Y
|
||||
existingPanel.ShapeType = panelInfo.ShapeType
|
||||
|
||||
continue PanelLoop
|
||||
}
|
||||
}
|
||||
|
||||
changed = true
|
||||
|
||||
idBytes := [2]byte{0, 0}
|
||||
binary.BigEndian.PutUint16(idBytes[:], panelInfo.PanelID)
|
||||
hexID := hex.EncodeToString(idBytes[:])
|
||||
fullID := fmt.Sprintf("nanoleaf:%s:%s", b.host, hexID)
|
||||
|
||||
b.panelIDMap[panelInfo.PanelID] = fullID
|
||||
b.panels = append(b.panels, &panel{
|
||||
ID: panelInfo.PanelID,
|
||||
FullID: fullID,
|
||||
ColorRGBA: [4]byte{0, 0, 0, 0},
|
||||
TransitionAt: time.Time{},
|
||||
O: panelInfo.O,
|
||||
X: panelInfo.X,
|
||||
Y: panelInfo.Y,
|
||||
ShapeType: panelInfo.ShapeType,
|
||||
Stale: true,
|
||||
SlowUpdates: 5,
|
||||
})
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (b *bridge) Overview(ctx context.Context) (*Overview, error) {
|
||||
req, err := http.NewRequest("GET", b.URL(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
switch res.StatusCode {
|
||||
case 400, 403, 500, 503:
|
||||
return nil, lucifer3.ErrUnexpectedResponse
|
||||
case 401:
|
||||
return nil, lucifer3.ErrIncorrectToken
|
||||
}
|
||||
|
||||
overview := Overview{}
|
||||
err = json.NewDecoder(res.Body).Decode(&overview)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &overview, nil
|
||||
}
|
||||
|
||||
func (b *bridge) URL(resource ...string) string {
|
||||
return fmt.Sprintf("http://%s:16021/api/v1/%s/%s", b.host, b.apiKey, strings.Join(resource, "/"))
|
||||
}
|
||||
|
||||
func (b *bridge) Update(id string, change device.State) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
transitionTime := time.Now().Add(time.Millisecond * 255)
|
||||
|
||||
for _, panel := range b.panels {
|
||||
if panel.FullID == id {
|
||||
if change.Intensity != nil {
|
||||
panel.Intensity = *change.Intensity
|
||||
}
|
||||
|
||||
if change.Color != nil {
|
||||
rgbColor, ok := change.Color.ToRGB()
|
||||
if !ok {
|
||||
newColor := [4]byte{255, 255, 255, 255}
|
||||
if newColor != panel.ColorRGBA {
|
||||
panel.update(newColor, transitionTime)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
rgb := rgbColor.RGB.AtIntensity(panel.Intensity)
|
||||
red := byte(rgb.Red * 255.0001)
|
||||
green := byte(rgb.Green * 255.0001)
|
||||
blue := byte(rgb.Blue * 255.0001)
|
||||
newColor := [4]byte{red, green, blue, 255}
|
||||
if newColor != panel.ColorRGBA {
|
||||
panel.update(newColor, time.Now().Add(time.Millisecond*220))
|
||||
}
|
||||
}
|
||||
|
||||
if change.Power != nil {
|
||||
panel.On = *change.Power
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bridge) Run(ctx context.Context, bus *lucifer3.EventBus) error {
|
||||
err := b.updateEffect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
|
||||
IP: net.ParseIP(b.host),
|
||||
Port: 60222,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Notify connections and disconnections
|
||||
bus.RunEvent(events.DeviceConnected{
|
||||
Prefix: fmt.Sprintf("nanoleaf:%s", b.host),
|
||||
})
|
||||
|
||||
_, err = b.Refresh(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start touch listener. This one should go down together with this one, though, so it needs a new context.
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go b.runTouchListener(ctx2, bus)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 5)
|
||||
hadErrors := false
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctx2.Done():
|
||||
return
|
||||
}
|
||||
|
||||
reqTimeout, cancel := context.WithTimeout(ctx2, time.Second*4)
|
||||
err := b.updateEffect(reqTimeout)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Println("Failed to update effects:", err, "This error is non-fatal, and it will be retried shortly.")
|
||||
hadErrors = true
|
||||
} else if hadErrors {
|
||||
b.mu.Lock()
|
||||
for _, panel := range b.panels {
|
||||
panel.Stale = true
|
||||
panel.SlowUpdates = 3
|
||||
panel.TicksUntilSlowUpdate = 10
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
hadErrors = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
defer ticker.Stop()
|
||||
|
||||
strikes := 0
|
||||
|
||||
for _, event := range b.HardwareEvents() {
|
||||
bus.RunEvent(event)
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
panelUpdate := make(PanelUpdate, 2)
|
||||
|
||||
b.mu.Lock()
|
||||
for _, panel := range b.panels {
|
||||
if !panel.Stale {
|
||||
if panel.SlowUpdates > 0 {
|
||||
panel.TicksUntilSlowUpdate -= 1
|
||||
if panel.TicksUntilSlowUpdate > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
panel.TicksUntilSlowUpdate = 10
|
||||
panel.SlowUpdates -= 1
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
panel.Stale = false
|
||||
}
|
||||
|
||||
panelUpdate.Add(panel.message())
|
||||
if panelUpdate.Len() > 150 {
|
||||
break
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if panelUpdate.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := conn.Write(panelUpdate)
|
||||
if err != nil {
|
||||
strikes++
|
||||
if strikes >= 3 {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
strikes = 0
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bridge) updateEffect(ctx context.Context) error {
|
||||
overview, err := b.Overview(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if overview.Effects.Select == "*Dynamic*" && overview.State.ColorMode == "effect" {
|
||||
return nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", b.URL("effects"), bytes.NewReader(httpMessage))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 204 {
|
||||
return lucifer3.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
for _, panel := range b.panels {
|
||||
panel.Stale = true
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bridge) runTouchListener(ctx context.Context, bus *lucifer3.EventBus) {
|
||||
cooldownID := ""
|
||||
cooldownUntil := time.Now()
|
||||
|
||||
message := make(PanelEventMessage, 65536)
|
||||
reqCloser := io.Closer(nil)
|
||||
|
||||
for {
|
||||
// Set up touch event receiver
|
||||
touchListener, err := net.ListenUDP("udp4", &net.UDPAddr{
|
||||
Port: 0,
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Socket error:", err)
|
||||
goto teardownAndRetry
|
||||
}
|
||||
|
||||
{
|
||||
// Create touch event sender on the remote end.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:16021/api/v1/%s/events?id=4", b.host, b.apiKey), nil)
|
||||
if err != nil {
|
||||
log.Println("HTTP error:", err)
|
||||
goto teardownAndRetry
|
||||
}
|
||||
req.Header["TouchEventsPort"] = []string{strconv.Itoa(touchListener.LocalAddr().(*net.UDPAddr).Port)}
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
log.Println("HTTP error:", err)
|
||||
goto teardownAndRetry
|
||||
}
|
||||
|
||||
// Discard all data coming over http.
|
||||
reqCloser = res.Body
|
||||
go io.Copy(ioutil.Discard, res.Body)
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
goto teardownAndRetry
|
||||
}
|
||||
|
||||
// Check in with the context every so often
|
||||
_ = touchListener.SetReadDeadline(time.Now().Add(time.Second))
|
||||
n, _, err := touchListener.ReadFromUDP(message)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
} else if ctx.Err() == nil {
|
||||
log.Println("UDP error:", err)
|
||||
}
|
||||
|
||||
goto teardownAndRetry
|
||||
}
|
||||
|
||||
if !message[:n].ValidateLength() {
|
||||
log.Println("Bad message length field")
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < message.Count(); i += 1 {
|
||||
b.mu.Lock()
|
||||
id, hasID := b.panelIDMap[message.PanelID(i)]
|
||||
swipedFromID := b.panelIDMap[message.SwipedFromPanelID(i)]
|
||||
b.mu.Unlock()
|
||||
|
||||
if !hasID || (id == cooldownID && time.Now().Before(cooldownUntil)) {
|
||||
continue
|
||||
}
|
||||
|
||||
bus.RunEvent(events.ButtonPressed{
|
||||
ID: id,
|
||||
SwipedID: swipedFromID,
|
||||
Name: "Touch",
|
||||
})
|
||||
|
||||
cooldownID = id
|
||||
cooldownUntil = time.Now().Add(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teardownAndRetry:
|
||||
if touchListener != nil {
|
||||
_ = touchListener.Close()
|
||||
}
|
||||
|
||||
if reqCloser != nil {
|
||||
_ = reqCloser.Close()
|
||||
reqCloser = nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 3):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EffectInfo struct {
|
||||
EffectsList []string `json:"effectsList"`
|
||||
Select string `json:"select"`
|
||||
}
|
||||
|
||||
type PanelLayout struct {
|
||||
GlobalOrientation GlobalOrientation `json:"globalOrientation"`
|
||||
Data PanelLayoutData `json:"layout"`
|
||||
}
|
||||
|
||||
type GlobalOrientation struct {
|
||||
Value int `json:"value"`
|
||||
Max int `json:"max"`
|
||||
Min int `json:"min"`
|
||||
}
|
||||
|
||||
type PanelLayoutData struct {
|
||||
NumPanels int `json:"numPanels"`
|
||||
SideLength int `json:"sideLength"`
|
||||
PositionData []PositionData `json:"positionData"`
|
||||
}
|
||||
|
||||
type PositionData struct {
|
||||
PanelID uint16 `json:"panelId"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
O int `json:"o"`
|
||||
ShapeType int `json:"shapeType"`
|
||||
}
|
||||
|
||||
type StateBool struct {
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
type StateInt struct {
|
||||
Value int `json:"value"`
|
||||
Max int `json:"max"`
|
||||
Min int `json:"min"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Brightness StateInt `json:"brightness"`
|
||||
ColorMode string `json:"colorMode"`
|
||||
Ct StateInt `json:"ct"`
|
||||
Hue StateInt `json:"hue"`
|
||||
On StateBool `json:"on"`
|
||||
Sat StateInt `json:"sat"`
|
||||
}
|
||||
|
||||
type Overview struct {
|
||||
Name string `json:"name"`
|
||||
SerialNumber string `json:"serialNo"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
FirmwareVersion string `json:"firmwareVersion"`
|
||||
HardwareVersion string `json:"hardwareVersion"`
|
||||
Model string `json:"model"`
|
||||
Effects EffectInfo `json:"effects"`
|
||||
PanelLayout PanelLayout `json:"panelLayout"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
HardwareVersion string `json:"hardwareVersion"`
|
||||
FirmwareVersion string `json:"firmwareVersion"`
|
||||
BootloaderVersion string `json:"bootloaderVersion"`
|
||||
ModelNumber string `json:"modelNumber"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Token string `json:"auth_token"`
|
||||
}
|
||||
|
||||
type PanelUpdate []byte
|
||||
|
||||
func (u *PanelUpdate) Add(message [8]byte) {
|
||||
if len(*u) < 2 {
|
||||
*u = make([]byte, 2, 10)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(*u, binary.BigEndian.Uint16(*u)+1)
|
||||
|
||||
*u = append(*u, message[:]...)
|
||||
}
|
||||
|
||||
func (u *PanelUpdate) Len() int {
|
||||
if len(*u) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(binary.BigEndian.Uint16(*u))
|
||||
}
|
||||
|
||||
type PanelEventMessage []byte
|
||||
|
||||
func (remote PanelEventMessage) Count() int {
|
||||
return int(binary.BigEndian.Uint16(remote[0:]))
|
||||
}
|
||||
|
||||
func (remote PanelEventMessage) ValidateLength() bool {
|
||||
return len(remote) >= (2 + remote.Count()*5)
|
||||
}
|
||||
|
||||
func (remote PanelEventMessage) PanelID(idx int) uint16 {
|
||||
return binary.BigEndian.Uint16(remote[2+(idx*5):])
|
||||
}
|
||||
|
||||
func (remote PanelEventMessage) TouchType(idx int) int {
|
||||
value := int(remote[2+(idx*5)])
|
||||
return (value & 0b11100000) >> 5
|
||||
}
|
||||
|
||||
func (remote PanelEventMessage) TouchStrength(idx int) int {
|
||||
value := int(remote[2+(idx*5)])
|
||||
return (value & 0b00011110) >> 1
|
||||
}
|
||||
|
||||
func (remote PanelEventMessage) SwipedFromPanelID(idx int) uint16 {
|
||||
return binary.BigEndian.Uint16(remote[2+(idx*5)+3:])
|
||||
}
|
||||
|
||||
var shapeTypeMap = map[int]string{
|
||||
0: "Legacy Triangle",
|
||||
1: "Rhythm",
|
||||
2: "Square",
|
||||
3: "Control Square Master",
|
||||
4: "Control Square Passive",
|
||||
7: "Hexagon",
|
||||
8: "Triangle",
|
||||
9: "Mini Triangle",
|
||||
12: "Shapes Controller",
|
||||
}
|
||||
|
||||
var shapeIconMap = map[int]string{
|
||||
0: "triangle",
|
||||
1: "rhythm",
|
||||
2: "Square",
|
||||
3: "square",
|
||||
4: "square",
|
||||
7: "hexagon",
|
||||
8: "triangle",
|
||||
9: "triangle-small",
|
||||
12: "hexagon",
|
||||
}
|
||||
|
||||
var shapeWidthMap = map[int]int{
|
||||
0: 150,
|
||||
1: -1,
|
||||
2: 100,
|
||||
3: 100,
|
||||
4: 100,
|
||||
7: 67,
|
||||
8: 134,
|
||||
9: 67,
|
||||
12: -1,
|
||||
}
|
||||
|
||||
var httpMessage = []byte(`{ "write": { "command": "display", "animType": "extControl", "extControlVersion": "v2" }}`)
|
||||
|
||||
type panel struct {
|
||||
ID uint16
|
||||
FullID string
|
||||
|
||||
On bool
|
||||
Intensity float64
|
||||
ColorRGBA [4]byte
|
||||
TransitionAt time.Time
|
||||
|
||||
O int
|
||||
X int
|
||||
Y int
|
||||
ShapeType int
|
||||
|
||||
Stale bool
|
||||
SlowUpdates int
|
||||
TicksUntilSlowUpdate int
|
||||
}
|
||||
|
||||
func (p *panel) message() (message [8]byte) {
|
||||
transitionTime := p.TransitionAt.Sub(time.Now()).Round(time.Millisecond * 100)
|
||||
if transitionTime > maxTransitionTime {
|
||||
transitionTime = maxTransitionTime
|
||||
} else if transitionTime < 0 {
|
||||
transitionTime = 0
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(message[0:], p.ID)
|
||||
copy(message[2:], p.ColorRGBA[:])
|
||||
binary.BigEndian.PutUint16(message[6:], uint16(transitionTime/(time.Millisecond*100)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *panel) update(colorRGBA [4]byte, transitionAt time.Time) {
|
||||
if p.ColorRGBA != colorRGBA {
|
||||
p.ColorRGBA = colorRGBA
|
||||
p.Stale = true
|
||||
p.SlowUpdates = 3
|
||||
p.TicksUntilSlowUpdate = 10
|
||||
}
|
||||
p.TransitionAt = transitionAt
|
||||
}
|
||||
|
||||
type panelUpdate []byte
|
||||
|
||||
func (u *panelUpdate) Add(message [8]byte) {
|
||||
if len(*u) < 2 {
|
||||
*u = make([]byte, 2, 10)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(*u, binary.BigEndian.Uint16(*u)+1)
|
||||
|
||||
*u = append(*u, message[:]...)
|
||||
}
|
||||
|
||||
func (u *panelUpdate) Len() int {
|
||||
if len(*u) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(binary.BigEndian.Uint16(*u))
|
||||
}
|
||||
|
||||
const maxTransitionTime = time.Minute * 109
|
||||
@@ -0,0 +1,55 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Discover(ctx context.Context, ip string, register bool) (string, *DeviceInfo, error) {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s/device_info", ip))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
deviceInfo := DeviceInfo{}
|
||||
err = json.NewDecoder(res.Body).Decode(&deviceInfo)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if deviceInfo.ModelNumber == "" {
|
||||
return "", nil, lucifer3.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
token := ""
|
||||
if register {
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:16021/api/v1/new/", ip), nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
tokenResponse := TokenResponse{}
|
||||
err = json.NewDecoder(res.Body).Decode(&tokenResponse)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
token = tokenResponse.Token
|
||||
}
|
||||
|
||||
return token, &deviceInfo, nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewService() lucifer3.ActiveService {
|
||||
return &service{
|
||||
bridges: make(map[string]*bridge),
|
||||
cancels: make(map[string]context.CancelFunc),
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
bridges map[string]*bridge
|
||||
cancels map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(*lucifer3.EventBus, lucifer3.Event) {}
|
||||
|
||||
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
switch command := command.(type) {
|
||||
case commands.SetState:
|
||||
if sub, ok := command.Matches("nanoleaf"); ok && s.bridges[sub] != nil {
|
||||
s.bridges[sub].Update(command.ID, command.State)
|
||||
}
|
||||
|
||||
case commands.SearchDevices:
|
||||
if sub, ok := command.Matches("nanoleaf"); ok {
|
||||
if s.bridges[sub] != nil {
|
||||
go func(bridge *bridge) {
|
||||
changed, err := bridge.Refresh(context.Background())
|
||||
if err != nil {
|
||||
bus.RunEvent(events.DeviceFailed{
|
||||
ID: command.ID,
|
||||
Error: fmt.Sprintf("Search failed: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if changed {
|
||||
for _, event := range bridge.HardwareEvents() {
|
||||
bus.RunEvent(event)
|
||||
}
|
||||
}
|
||||
}(s.bridges[sub])
|
||||
}
|
||||
}
|
||||
|
||||
case commands.PairDevice:
|
||||
if address, ok := command.Matches("nanoleaf"); ok {
|
||||
go func() {
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
apiKey, info, err := Discover(timeout, address, true)
|
||||
if err != nil {
|
||||
bus.RunEvent(events.DeviceFailed{
|
||||
ID: command.ID,
|
||||
Error: fmt.Sprintf("Pairing failed: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
bus.RunEvent(events.DeviceAccepted{
|
||||
ID: command.ID,
|
||||
APIKey: apiKey,
|
||||
Extras: nil,
|
||||
})
|
||||
bus.RunEvent(events.HardwareMetadata{
|
||||
ID: command.ID,
|
||||
Icon: "bridge",
|
||||
SerialNumber: info.SerialNumber,
|
||||
FirmwareVersion: strings.Join([]string{
|
||||
info.FirmwareVersion, info.BootloaderVersion, info.HardwareVersion,
|
||||
}, "; "),
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
case commands.ConnectDevice:
|
||||
if sub, ok := command.Matches("nanoleaf"); ok {
|
||||
if s.bridges[sub] != nil {
|
||||
s.cancels[sub]()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
s.bridges[sub] = &bridge{
|
||||
host: sub,
|
||||
apiKey: command.APIKey,
|
||||
panels: make([]*panel, 0, 64),
|
||||
panelIDMap: make(map[uint16]string),
|
||||
}
|
||||
s.cancels[sub] = cancel
|
||||
|
||||
go func() {
|
||||
for ctx.Err() == nil {
|
||||
ctx2, cancel2 := context.WithCancel(ctx)
|
||||
|
||||
err := s.bridges[sub].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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user