ħ€»ªŋœnß ª®€ þħ€ ”€ßþªŋœnß.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var flagDriver = flag.String("driver", "Nanoleaf", "The bridge driver to use")
|
||||
var flagAddress = flag.String("address", "127.0.0.1", "The bridge's address")
|
||||
var flagToken = flag.String("token", "", "The bridge's access token / api key / login")
|
||||
var flagPair = flag.Bool("pair", false, "Try to pair with the bridge.")
|
||||
var flagSearch = flag.Bool("search", false, "Search for devices first.")
|
||||
var flagSearchTimeout = flag.Duration("search-timeout", time.Second*3, "Timeout for device search.")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// TODO: Select driver
|
||||
driver := nanoleaf.Driver{}
|
||||
|
||||
// Find bridge
|
||||
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to search bridge:", err)
|
||||
}
|
||||
if len(bridges) == 0 {
|
||||
log.Fatalln("No bridges found")
|
||||
}
|
||||
bridge := bridges[0]
|
||||
if !*flagPair {
|
||||
bridge.Token = *flagToken
|
||||
} else {
|
||||
log.Println("New token:", bridge.Token)
|
||||
}
|
||||
|
||||
// List devices
|
||||
var devices []models.Device
|
||||
if *flagSearch {
|
||||
devices, err = driver.SearchDevices(context.Background(), bridge, *flagSearchTimeout)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to search devices:", err)
|
||||
}
|
||||
} else {
|
||||
devices, err = driver.ListDevices(context.Background(), bridge)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to list devices:", err)
|
||||
}
|
||||
}
|
||||
for i := range devices {
|
||||
devices[i].ID = i + 1
|
||||
}
|
||||
|
||||
_ = driver.Publish(context.Background(), bridge, devices)
|
||||
|
||||
ch := make(chan models.Event)
|
||||
go func() {
|
||||
err := driver.Run(context.Background(), bridge, ch)
|
||||
if err != nil {
|
||||
log.Fatalln("Run bridge stopped:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Format: [id1,id2,...] [on|off] [color] [intensity]")
|
||||
for _, device := range devices {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Device: %d - %s %+v\n", device.ID, device.InternalID, device.Capabilities)
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Format: [id1,id2,...] [on|off] [color]")
|
||||
|
||||
for {
|
||||
text, _ := reader.ReadString('\n')
|
||||
text = strings.Trim(text, "\t \r\n")
|
||||
|
||||
tokens := strings.Split(text, " ")
|
||||
if len(tokens) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
color, err := models.ParseColorValue(tokens[2])
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Invalid color:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
intensity, _ := strconv.ParseFloat(tokens[3], 64)
|
||||
|
||||
power := strings.ToLower(tokens[1]) == "on"
|
||||
|
||||
idsStr := strings.Split(tokens[0], ",")
|
||||
ids := make([]int, 0, len(idsStr))
|
||||
for _, idStr := range idsStr {
|
||||
if idStr == "*" {
|
||||
ids = append(ids[:0], -1)
|
||||
break
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
updatedDevices := devices[:0:0]
|
||||
for _, device := range devices {
|
||||
for _, id := range ids {
|
||||
if id == -1 || id == device.ID {
|
||||
if (color.IsKelvin() && device.HasCapability(models.DCColorKelvin)) || (color.IsHueSat() && device.HasCapability(models.DCColorHS)) {
|
||||
device.State.Color = color
|
||||
if device.HasCapability(models.DCPower) {
|
||||
device.State.Power = power
|
||||
}
|
||||
if device.HasCapability(models.DCIntensity) {
|
||||
device.State.Intensity = intensity
|
||||
}
|
||||
updatedDevices = append(updatedDevices, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(updatedDevices) > 0 {
|
||||
err := driver.Publish(context.Background(), bridge, updatedDevices)
|
||||
if err != nil {
|
||||
log.Fatalln("Publish to bridge failed:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for event := range ch {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Event %s", event.Name)
|
||||
keys := make([]string, 0, 8)
|
||||
for key := range event.Payload {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
_, _ = fmt.Fprintf(os.Stderr, " %s=%#+v", key, event.Payload[key])
|
||||
}
|
||||
_, _ = fmt.Fprint(os.Stderr, "\n")
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,13 @@ module git.aiterp.net/lucifer/new-server
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go v1.4.5 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.10.0 // indirect
|
||||
github.com/gin-gonic/gin v1.7.1
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/lib/pq v1.10.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pressly/goose v2.7.0+incompatible
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
)
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
github.com/ClickHouse/clickhouse-go v1.4.5 h1:FfhyEnv6/BaWldyjgT2k4gDDmeNwJ9C4NbY/MXxJlXk=
|
||||
github.com/ClickHouse/clickhouse-go v1.4.5/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
|
||||
@@ -20,32 +13,25 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
|
||||
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
@@ -53,7 +39,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -68,11 +53,7 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
@@ -80,8 +61,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type bridge struct {
|
||||
mu sync.Mutex
|
||||
externalID int
|
||||
host string
|
||||
apiKey string
|
||||
panels []*panel
|
||||
panelIDMap map[uint16]int
|
||||
}
|
||||
|
||||
func (b *bridge) Devices() []models.Device {
|
||||
results := make([]models.Device, 0, len(b.panels))
|
||||
for i, panel := range b.panels {
|
||||
red := float64(panel.ColorRGBA[0]) / 255.0
|
||||
green := float64(panel.ColorRGBA[1]) / 255.0
|
||||
blue := float64(panel.ColorRGBA[2]) / 255.0
|
||||
|
||||
hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv()
|
||||
|
||||
results = append(results, models.Device{
|
||||
ID: -1,
|
||||
BridgeID: b.externalID,
|
||||
InternalID: strconv.Itoa(int(panel.ID)),
|
||||
Icon: "hexagon",
|
||||
Name: fmt.Sprintf("Hexagon %d", i),
|
||||
Capabilities: []models.DeviceCapability{
|
||||
models.DCPower,
|
||||
models.DCColorHS,
|
||||
models.DCIntensity,
|
||||
models.DCButtons,
|
||||
},
|
||||
ButtonNames: []string{"Touch"},
|
||||
DriverProperties: map[string]string{
|
||||
"x": strconv.Itoa(panel.X),
|
||||
"y": strconv.Itoa(panel.Y),
|
||||
"o": strconv.Itoa(panel.O),
|
||||
"shapeType": shapeTypeMap[panel.ShapeType],
|
||||
"shapeWidth": strconv.Itoa(shapeWidthMap[panel.ShapeType]),
|
||||
},
|
||||
UserProperties: nil,
|
||||
State: models.DeviceState{
|
||||
Power: panel.ColorRGBA[3] == 0,
|
||||
Color: models.ColorValue{
|
||||
Hue: math.Mod(hue, 360),
|
||||
Saturation: sat,
|
||||
},
|
||||
Intensity: value,
|
||||
Temperature: 0,
|
||||
},
|
||||
Tags: nil,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (b *bridge) Refresh(ctx context.Context) error {
|
||||
overview, err := b.Overview(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
PanelLoop:
|
||||
for _, panelInfo := range overview.PanelLayout.Data.PositionData {
|
||||
if panelInfo.PanelID == 0 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
b.panels = append(b.panels, &panel{
|
||||
ID: panelInfo.PanelID,
|
||||
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 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, models.ErrUnexpectedResponse
|
||||
case 401:
|
||||
return nil, models.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(devices []models.Device) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
for _, device := range devices {
|
||||
id, err := strconv.Atoi(device.InternalID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
b.panelIDMap[uint16(id)] = device.ID
|
||||
|
||||
for _, panel := range b.panels {
|
||||
if panel.ID == uint16(id) {
|
||||
if device.State.Power {
|
||||
color := colorful.Hsv(device.State.Color.Hue, device.State.Color.Saturation, device.State.Intensity)
|
||||
|
||||
red, green, blue := color.RGB255()
|
||||
newColor := [4]byte{red, green, blue, 255}
|
||||
if newColor != panel.ColorRGBA {
|
||||
panel.update(newColor, time.Now().Add(time.Millisecond*120))
|
||||
}
|
||||
} else {
|
||||
panel.update([4]byte{0, 0, 0, 0}, time.Now())
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bridge) Run(ctx context.Context, info models.Bridge, ch chan<- models.Event) error {
|
||||
err := b.updateEffect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
|
||||
IP: net.ParseIP(info.Address),
|
||||
Port: 60222,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Notify connections and disconnections
|
||||
ch <- models.Event{Name: "BridgeConnected", Payload: map[string]string{"bridgeId": strconv.Itoa(info.ID)}}
|
||||
defer func() {
|
||||
ch <- models.Event{Name: "BridgeDisconnected", Payload: map[string]string{"bridgeId": strconv.Itoa(info.ID)}}
|
||||
}()
|
||||
|
||||
// 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, info.Address, info.Token, ch)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 5)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctx2.Done():
|
||||
}
|
||||
|
||||
err := b.updateEffect(ctx)
|
||||
if err != nil {
|
||||
log.Println("Failed to update effects:", err, "This error is non-fatal, and it will be retried shortly.")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
defer ticker.Stop()
|
||||
|
||||
strikes := 0
|
||||
|
||||
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 models.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bridge) runTouchListener(ctx context.Context, host, apiKey string, ch chan<- models.Event) {
|
||||
cooldownID := 0
|
||||
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", host, 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()
|
||||
externalID, hasExternalID := b.panelIDMap[message.PanelID(i)]
|
||||
swipedFromID, hasSwipedFromID := b.panelIDMap[message.SwipedFromPanelID(i)]
|
||||
b.mu.Unlock()
|
||||
|
||||
if !hasExternalID || (externalID == cooldownID && time.Now().Before(cooldownUntil)) {
|
||||
continue
|
||||
}
|
||||
|
||||
event := models.Event{
|
||||
Name: "ButtonPressed",
|
||||
Payload: map[string]string{
|
||||
"buttonIndex": "0",
|
||||
"buttonName": "Touch",
|
||||
"deviceId": strconv.Itoa(externalID),
|
||||
},
|
||||
}
|
||||
if hasSwipedFromID {
|
||||
event.Payload["swipedFromDeviceId"] = strconv.Itoa(swipedFromID)
|
||||
}
|
||||
|
||||
ch <- event
|
||||
|
||||
cooldownID = externalID
|
||||
cooldownUntil = time.Now().Add(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teardownAndRetry:
|
||||
if touchListener != nil {
|
||||
_ = touchListener.Close()
|
||||
}
|
||||
|
||||
if reqCloser != nil {
|
||||
_ = reqCloser.Close()
|
||||
reqCloser = nil
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package nanoleaf
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
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: "Triangle",
|
||||
1: "Rhythm",
|
||||
2: "Square",
|
||||
3: "Control Square Master",
|
||||
4: "Control Square Passive",
|
||||
7: "Hexagon (Shapes)",
|
||||
8: "Triangle (Shapes)",
|
||||
9: "Mini Triangle (Shapes)",
|
||||
12: "Shapes Controller",
|
||||
}
|
||||
|
||||
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" }}`)
|
||||
@@ -0,0 +1,162 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
mu sync.Mutex
|
||||
bridges []*bridge
|
||||
}
|
||||
|
||||
// SearchBridge checks the bridge at the address. If it's not a dry-run, you must hold down the power button
|
||||
// before calling this function and wait for the pattern.
|
||||
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s/device_info", address))
|
||||
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, models.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
token := ""
|
||||
if !dryRun {
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:16021/api/v1/new/", address), 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, models.ErrBridgeSearchFailed
|
||||
}
|
||||
|
||||
tokenResponse := TokenResponse{}
|
||||
err = json.NewDecoder(res.Body).Decode(&tokenResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token = tokenResponse.Token
|
||||
}
|
||||
|
||||
return []models.Bridge{{
|
||||
ID: -1,
|
||||
Name: fmt.Sprintf("Nanoleaf Controller (MN: %s, SN: %s, HV: %s, FV: %s, BV: %s)",
|
||||
deviceInfo.ModelNumber,
|
||||
deviceInfo.SerialNumber,
|
||||
deviceInfo.HardwareVersion,
|
||||
deviceInfo.FirmwareVersion,
|
||||
deviceInfo.BootloaderVersion,
|
||||
),
|
||||
Driver: models.DTNanoLeaf,
|
||||
Address: address,
|
||||
Token: token,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (d *Driver) SearchDevices(ctx context.Context, bridge models.Bridge, timeout time.Duration) ([]models.Device, error) {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if timeout > time.Millisecond {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
ctx = timeoutCtx
|
||||
}
|
||||
|
||||
err = b.Refresh(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Devices(), nil
|
||||
}
|
||||
|
||||
func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Devices(), nil
|
||||
}
|
||||
|
||||
func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models.Event) error {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Run(ctx, bridge, ch)
|
||||
}
|
||||
|
||||
func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
|
||||
b, err := d.ensureBridge(ctx, bridge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Update(devices)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*bridge, error) {
|
||||
d.mu.Lock()
|
||||
for _, bridge := range d.bridges {
|
||||
if bridge.host == info.Address {
|
||||
d.mu.Unlock()
|
||||
return bridge, nil
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
|
||||
bridge := &bridge{
|
||||
host: info.Address,
|
||||
apiKey: info.Token,
|
||||
externalID: info.ID,
|
||||
panelIDMap: make(map[uint16]int, 9),
|
||||
}
|
||||
|
||||
// If this fails, then the authorization failed.
|
||||
err := bridge.Refresh(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To avoid a potential duplicate, try looking for it again before inserting
|
||||
d.mu.Lock()
|
||||
for _, bridge := range d.bridges {
|
||||
if bridge.host == info.Address {
|
||||
d.mu.Unlock()
|
||||
return bridge, nil
|
||||
}
|
||||
}
|
||||
d.bridges = append(d.bridges, bridge)
|
||||
d.mu.Unlock()
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package nanoleaf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
type panel struct {
|
||||
ID uint16
|
||||
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
|
||||
+17
-15
@@ -2,13 +2,14 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ColorValue struct {
|
||||
Hue int `json:"h,omitempty"`
|
||||
Saturation int `json:"s,omitempty"`
|
||||
Hue float64 `json:"h,omitempty"` // 0..360
|
||||
Saturation float64 `json:"s,omitempty"` // 0..=1
|
||||
Kelvin int `json:"kelvin,omitempty"`
|
||||
}
|
||||
|
||||
@@ -25,16 +26,17 @@ func (c *ColorValue) String() string {
|
||||
return fmt.Sprintf("kelvin:%d", c.Kelvin)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("hsv:%d,%d", c.Hue, c.Saturation)
|
||||
return fmt.Sprintf("hsv:%f,%f", c.Hue, c.Saturation)
|
||||
}
|
||||
|
||||
var kelvinRegex = regexp.MustCompile("kelvin:([0-9]+)")
|
||||
var hsRegex = regexp.MustCompile("hs:([0-9]+),([0-9]+)")
|
||||
|
||||
func ParseColorValue(raw string) (ColorValue, error) {
|
||||
if kelvinRegex.MatchString(raw) {
|
||||
part := kelvinRegex.FindString(raw)
|
||||
parsedPart, err := strconv.Atoi(part)
|
||||
tokens := strings.SplitN(raw, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
if tokens[0] == "kelvin" {
|
||||
parsedPart, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
@@ -42,19 +44,19 @@ func ParseColorValue(raw string) (ColorValue, error) {
|
||||
return ColorValue{Kelvin: parsedPart}, nil
|
||||
}
|
||||
|
||||
if hsRegex.MatchString(raw) {
|
||||
parts := kelvinRegex.FindAllString(raw, 2)
|
||||
if tokens[0] == "hs" {
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
part1, err1 := strconv.Atoi(parts[0])
|
||||
part2, err2 := strconv.Atoi(parts[1])
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{Hue: part1, Saturation: part2}, nil
|
||||
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
||||
}
|
||||
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
|
||||
+3
-3
@@ -22,13 +22,13 @@ type Device struct {
|
||||
// DeviceState contains optional state values that
|
||||
// - Power: Whether the device is powered on
|
||||
// - Color: Color value, if a color setting can be set on the device
|
||||
// - Intensity: e.g. brightness, from 0-255
|
||||
// - Intensity: e.g. brightness, range 0..=1
|
||||
// - Temperature: e.g. for thermostats
|
||||
type DeviceState struct {
|
||||
Power bool `json:"power"`
|
||||
Color ColorValue `json:"color,omitempty"`
|
||||
Intensity int `json:"intensity,omitempty"`
|
||||
Temperature int `json:"temperature"`
|
||||
Intensity float64 `json:"intensity,omitempty"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type NewDeviceState struct {
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ type DriverProvider interface {
|
||||
type Driver interface {
|
||||
SearchBridge(ctx context.Context, address string, dryRun bool) ([]Bridge, error)
|
||||
SearchDevices(ctx context.Context, bridge Bridge, timeout time.Duration) ([]Device, error)
|
||||
Consume(ctx context.Context, bridge Bridge, devices []Device, ch chan Event) (chan<- struct{}, error)
|
||||
ListDevices(ctx context.Context, bridge Bridge) ([]Device, error)
|
||||
Publish(ctx context.Context, bridge Bridge, devices []Device) error
|
||||
Run(ctx context.Context, bridge Bridge, ch chan<- Event) error
|
||||
}
|
||||
|
||||
@@ -6,3 +6,8 @@ var ErrInvalidName = errors.New("invalid name")
|
||||
var ErrBadInput = errors.New("bad input")
|
||||
var ErrBadColor = errors.New("bad color")
|
||||
var ErrUnknownColorFormat = errors.New("unknown color format")
|
||||
|
||||
var ErrMissingToken = errors.New("driver is missing authentication information")
|
||||
var ErrIncorrectToken = errors.New("driver is not accepting authentication information")
|
||||
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
|
||||
var ErrBridgeSearchFailed = errors.New("bridge search failed")
|
||||
|
||||
@@ -72,9 +72,9 @@ func (c *EventCondition) checkDevice(key string, device Device) bool {
|
||||
case "color":
|
||||
return c.matches(device.State.Color.String())
|
||||
case "intensity":
|
||||
return c.matches(strconv.Itoa(device.State.Intensity))
|
||||
return c.matches(strconv.FormatFloat(device.State.Intensity, 'f', -1, 64))
|
||||
case "temperature":
|
||||
return c.matches(strconv.Itoa(device.State.Temperature))
|
||||
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64))
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user