add rest of hue driver functionality.
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/internal/drivers"
|
||||
"git.aiterp.net/lucifer/new-server/internal/drivers/hue"
|
||||
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"sync"
|
||||
@@ -17,6 +18,7 @@ func DriverProvider() models.DriverProvider {
|
||||
if dp == nil {
|
||||
dp = drivers.DriverMap{
|
||||
models.DTNanoLeaf: &nanoleaf.Driver{},
|
||||
models.DTHue: &hue.Driver{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
@@ -63,8 +64,6 @@ func main() {
|
||||
devices[i].ID = i + 1
|
||||
}
|
||||
|
||||
_ = driver.Publish(context.Background(), bridge, devices)
|
||||
|
||||
ch := config.EventChannel
|
||||
go func() {
|
||||
err := driver.Run(context.Background(), bridge, ch)
|
||||
@@ -86,6 +85,11 @@ func main() {
|
||||
text, _ := reader.ReadString('\n')
|
||||
text = strings.Trim(text, "\t \r\n")
|
||||
|
||||
if text == "json" {
|
||||
j, _ := json.MarshalIndent(devices, "", " ")
|
||||
fmt.Println(string(j))
|
||||
}
|
||||
|
||||
tokens := strings.Split(text, " ")
|
||||
if len(tokens) < 4 {
|
||||
continue
|
||||
@@ -122,15 +126,15 @@ func main() {
|
||||
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)
|
||||
}
|
||||
|
||||
updatedDevices = append(updatedDevices, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,5 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pressly/goose v2.7.0+incompatible
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
)
|
||||
|
||||
@@ -59,6 +59,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrO
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Bridge struct {
|
||||
mu sync.Mutex
|
||||
host string
|
||||
token string
|
||||
externalID int
|
||||
lightStates []*hueLightState
|
||||
sensorStates []*hueSensorState
|
||||
}
|
||||
|
||||
func (b *Bridge) Refresh(ctx context.Context) error {
|
||||
lightMap, err := b.getLights(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
for index, light := range lightMap {
|
||||
var state *hueLightState
|
||||
for _, existingState := range b.lightStates {
|
||||
if existingState.index == index {
|
||||
state = existingState
|
||||
}
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
state = &hueLightState{
|
||||
index: index,
|
||||
uniqueID: light.Uniqueid,
|
||||
externalID: -1,
|
||||
info: light,
|
||||
}
|
||||
|
||||
b.lightStates = append(b.lightStates, state)
|
||||
} else {
|
||||
if light.Uniqueid != state.uniqueID {
|
||||
state.uniqueID = light.Uniqueid
|
||||
state.externalID = -1
|
||||
}
|
||||
}
|
||||
|
||||
state.CheckStaleness(light.State)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
sensorMap, err := b.getSensors(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
for index, sensor := range sensorMap {
|
||||
var state *hueSensorState
|
||||
for _, existingState := range b.sensorStates {
|
||||
if existingState.index == index {
|
||||
state = existingState
|
||||
}
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
state = &hueSensorState{
|
||||
index: index,
|
||||
uniqueID: sensor.UniqueID,
|
||||
externalID: -1,
|
||||
}
|
||||
|
||||
b.sensorStates = append(b.sensorStates, state)
|
||||
} else {
|
||||
if sensor.UniqueID != state.uniqueID {
|
||||
state.uniqueID = sensor.UniqueID
|
||||
state.externalID = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) SyncStale(ctx context.Context) error {
|
||||
indices := make([]int, 0, 4)
|
||||
inputs := make([]LightStateInput, 0, 4)
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
b.mu.Lock()
|
||||
for _, state := range b.lightStates {
|
||||
if !state.stale {
|
||||
continue
|
||||
}
|
||||
|
||||
indices = append(indices, state.index)
|
||||
inputs = append(inputs, state.input)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if len(inputs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, input := range inputs {
|
||||
index := indices[i]
|
||||
|
||||
eg.Go(func() error { return b.putLightState(ctx, index, input) })
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
|
||||
sensorMap, err := b.getSensors(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []models.Event
|
||||
|
||||
b.mu.Lock()
|
||||
for idx, sensorData := range sensorMap {
|
||||
for _, state := range b.sensorStates {
|
||||
if idx == state.index {
|
||||
event := state.Update(sensorData)
|
||||
if event != nil {
|
||||
events = append(events, *event)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) StartDiscovery(ctx context.Context, model string) error {
|
||||
return b.post(ctx, model, nil, nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateInput) error {
|
||||
return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
|
||||
}
|
||||
|
||||
func (b *Bridge) getToken(ctx context.Context) (string, error) {
|
||||
result := make([]CreateUserResponse, 0, 1)
|
||||
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result) == 0 || result[0].Error != nil {
|
||||
return "", errLinkButtonNotPressed
|
||||
}
|
||||
if result[0].Success == nil {
|
||||
return "", models.ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
return result[0].Success.Username, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) getLights(ctx context.Context) (map[int]LightData, error) {
|
||||
result := make(map[int]LightData, 16)
|
||||
err := b.get(ctx, "lights", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
|
||||
result := make(map[int]SensorData, 16)
|
||||
err := b.get(ctx, "sensors", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
|
||||
if b.token != "" {
|
||||
resource = b.token + "/" + resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.token != "" {
|
||||
resource = b.token + "/" + resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (b *Bridge) put(ctx context.Context, resource string, body interface{}, target interface{}) error {
|
||||
rb, err := reqBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.token != "" {
|
||||
resource = b.token + "/" + resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type DiscoveryEntry struct {
|
||||
Id string `json:"id"`
|
||||
InternalIPAddress string `json:"internalipaddress"`
|
||||
}
|
||||
|
||||
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 Overview struct {
|
||||
Lights map[int]LightData `json:"lights"`
|
||||
Config BridgeConfig `json:"config"`
|
||||
Sensors map[int]SensorData `json:"sensors"`
|
||||
}
|
||||
|
||||
type BridgeConfig struct {
|
||||
Name string `json:"name"`
|
||||
ZigbeeChannel int `json:"zigbeechannel"`
|
||||
BridgeID string `json:"bridgeid"`
|
||||
Mac string `json:"mac"`
|
||||
DHCP bool `json:"dhcp"`
|
||||
IPAddress string `json:"ipaddress"`
|
||||
NetMask string `json:"netmask"`
|
||||
Gateway string `json:"gateway"`
|
||||
ProxyAddress string `json:"proxyaddress"`
|
||||
ProxyPort int `json:"proxyport"`
|
||||
UTC string `json:"UTC"`
|
||||
LocalTime string `json:"localtime"`
|
||||
TimeZone string `json:"timezone"`
|
||||
ModelID string `json:"modelid"`
|
||||
DataStoreVersion string `json:"datastoreversion"`
|
||||
SWVersion string `json:"swversion"`
|
||||
APIVersion string `json:"apiversion"`
|
||||
LinkButton bool `json:"linkbutton"`
|
||||
PortalServices bool `json:"portalservices"`
|
||||
PortalConnection string `json:"portalconnection"`
|
||||
FactoryNew bool `json:"factorynew"`
|
||||
StarterKitID string `json:"starterkitid"`
|
||||
Whitelist map[string]WhitelistEntry `json:"whitelist"`
|
||||
}
|
||||
|
||||
type LightState struct {
|
||||
On bool `json:"on"`
|
||||
Bri int `json:"bri"`
|
||||
Hue int `json:"hue"`
|
||||
Sat int `json:"sat"`
|
||||
Effect string `json:"effect"`
|
||||
XY []float64 `json:"xy"`
|
||||
CT int `json:"ct"`
|
||||
Alert string `json:"alert"`
|
||||
ColorMode string `json:"colormode"`
|
||||
Mode string `json:"mode"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
type LightStateInput struct {
|
||||
On *bool `json:"on,omitempty"`
|
||||
Bri *int `json:"bri,omitempty"`
|
||||
Hue *int `json:"hue,omitempty"`
|
||||
Sat *int `json:"sat,omitempty"`
|
||||
Effect *string `json:"effect,omitempty"`
|
||||
XY *[2]float64 `json:"xy,omitempty"`
|
||||
CT *int `json:"ct,omitempty"`
|
||||
Alert *string `json:"alert,omitempty"`
|
||||
TransitionTime *int `json:"transitiontime,omitempty"`
|
||||
}
|
||||
|
||||
type LightData struct {
|
||||
State LightState `json:"state"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Modelid string `json:"modelid"`
|
||||
Manufacturername string `json:"manufacturername"`
|
||||
Productname string `json:"productname"`
|
||||
Capabilities struct {
|
||||
Certified bool `json:"certified"`
|
||||
Control struct {
|
||||
Mindimlevel int `json:"mindimlevel"`
|
||||
Maxlumen int `json:"maxlumen"`
|
||||
Colorgamuttype string `json:"colorgamuttype"`
|
||||
Colorgamut [][]float64 `json:"colorgamut"`
|
||||
CT struct {
|
||||
Min int `json:"min"`
|
||||
Max int `json:"max"`
|
||||
} `json:"ct"`
|
||||
} `json:"control"`
|
||||
Streaming struct {
|
||||
Renderer bool `json:"renderer"`
|
||||
Proxy bool `json:"proxy"`
|
||||
} `json:"streaming"`
|
||||
} `json:"capabilities"`
|
||||
Config struct {
|
||||
Archetype string `json:"archetype"`
|
||||
Function string `json:"function"`
|
||||
Direction string `json:"direction"`
|
||||
Startup struct {
|
||||
Mode string `json:"mode"`
|
||||
Configured bool `json:"configured"`
|
||||
} `json:"startup"`
|
||||
} `json:"config"`
|
||||
Swupdate struct {
|
||||
State string `json:"state"`
|
||||
Lastinstall string `json:"lastinstall"`
|
||||
} `json:"swupdate"`
|
||||
Uniqueid string `json:"uniqueid"`
|
||||
Swversion string `json:"swversion"`
|
||||
Swconfigid string `json:"swconfigid"`
|
||||
Productid string `json:"productid"`
|
||||
}
|
||||
|
||||
type SensorData struct {
|
||||
State struct {
|
||||
Daylight interface{} `json:"daylight"`
|
||||
ButtonEvent int `json:"buttonevent"`
|
||||
LastUpdated string `json:"lastupdated"`
|
||||
Presence bool `json:"presence"`
|
||||
} `json:"state"`
|
||||
Config struct {
|
||||
On bool `json:"on"`
|
||||
Configured bool `json:"configured"`
|
||||
Sunriseoffset int `json:"sunriseoffset"`
|
||||
Sunsetoffset int `json:"sunsetoffset"`
|
||||
} `json:"config"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Modelid string `json:"modelid"`
|
||||
Manufacturername string `json:"manufacturername"`
|
||||
Productname string `json:"productname"`
|
||||
Swversion string `json:"swversion"`
|
||||
UniqueID string `json:"uniqueid"`
|
||||
}
|
||||
|
||||
type WhitelistEntry struct {
|
||||
LastUseDate string `json:"last use date"`
|
||||
CreateDate string `json:"create date"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
var buttonNames = []string{"On", "DimUp", "DimDown", "Off"}
|
||||
|
||||
var errLinkButtonNotPressed = errors.New("link button not pressed")
|
||||
@@ -0,0 +1,399 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
mu sync.Mutex
|
||||
bridges []*Bridge
|
||||
}
|
||||
|
||||
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
|
||||
if address == "" {
|
||||
if !dryRun {
|
||||
return nil, models.ErrAddressOnlyDryRunnable
|
||||
}
|
||||
|
||||
res, err := http.Get("https://discovery.meethue.com")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
entries := make([]DiscoveryEntry, 0, 8)
|
||||
err = json.NewDecoder(res.Body).Decode(&entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridges := make([]models.Bridge, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
bridges = append(bridges, models.Bridge{
|
||||
ID: -1,
|
||||
Name: entry.Id,
|
||||
Driver: models.DTHue,
|
||||
Address: entry.InternalIPAddress,
|
||||
Token: "",
|
||||
})
|
||||
}
|
||||
|
||||
return bridges, nil
|
||||
}
|
||||
|
||||
deviceInfo := BridgeDeviceInfo{}
|
||||
res, err := http.Get(fmt.Sprintf("http://%s/description.xml", address))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
err = xml.NewDecoder(res.Body).Decode(&deviceInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge := models.Bridge{
|
||||
ID: -1,
|
||||
Name: deviceInfo.Device.FriendlyName,
|
||||
Driver: models.DTHue,
|
||||
Address: address,
|
||||
Token: "",
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
b := &Bridge{host: address}
|
||||
|
||||
timeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
token, err := b.getToken(timeout)
|
||||
if err != nil {
|
||||
if err == errLinkButtonNotPressed {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge.Token = token
|
||||
b.token = token
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return []models.Bridge{bridge}, 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
|
||||
}
|
||||
|
||||
before, err := d.ListDevices(ctx, bridge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if timeout.Seconds() < 10 {
|
||||
timeout = time.Second * 10
|
||||
}
|
||||
halfTime := timeout / 2
|
||||
|
||||
err = b.StartDiscovery(ctx, "sensors")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case <-time.After(halfTime):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
err = b.StartDiscovery(ctx, "lights")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case <-time.After(halfTime):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
err = b.Refresh(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
after, err := d.ListDevices(ctx, bridge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intersection := make([]models.Device, 0, 4)
|
||||
for _, device := range after {
|
||||
found := false
|
||||
for _, device2 := range before {
|
||||
if device2.InternalID == device.InternalID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
intersection = append(intersection, device)
|
||||
}
|
||||
}
|
||||
|
||||
return intersection, 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
|
||||
}
|
||||
|
||||
devices := make([]models.Device, 0, 8)
|
||||
|
||||
lightMap, err := b.getLights(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, lightInfo := range lightMap {
|
||||
device := models.Device{
|
||||
ID: -1,
|
||||
BridgeID: b.externalID,
|
||||
InternalID: lightInfo.Uniqueid,
|
||||
Icon: "lightbulb",
|
||||
Name: lightInfo.Name,
|
||||
Capabilities: []models.DeviceCapability{
|
||||
models.DCPower,
|
||||
},
|
||||
ButtonNames: nil,
|
||||
DriverProperties: map[string]interface{}{
|
||||
"modelId": lightInfo.Modelid,
|
||||
"productName": lightInfo.Productname,
|
||||
"swVersion": lightInfo.Swversion,
|
||||
"hueLightType": lightInfo.Type,
|
||||
},
|
||||
UserProperties: nil,
|
||||
State: models.DeviceState{},
|
||||
Tags: nil,
|
||||
}
|
||||
|
||||
hasDimming := false
|
||||
hasCT := false
|
||||
hasColor := false
|
||||
|
||||
switch lightInfo.Type {
|
||||
case "On/off light":
|
||||
// Always take DCPower for granted anyway.
|
||||
case "Dimmable light":
|
||||
hasDimming = true
|
||||
case "Color temperature light":
|
||||
hasDimming = true
|
||||
hasCT = true
|
||||
case "Color light":
|
||||
hasDimming = true
|
||||
hasColor = true
|
||||
case "Extended color light":
|
||||
hasDimming = true
|
||||
hasColor = true
|
||||
hasCT = true
|
||||
}
|
||||
|
||||
ctrl := lightInfo.Capabilities.Control
|
||||
if hasDimming {
|
||||
device.Capabilities = append(device.Capabilities, models.DCIntensity)
|
||||
}
|
||||
if hasCT {
|
||||
device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
|
||||
device.DriverProperties["minKelvin"] = 1000000 / ctrl.CT.Max
|
||||
device.DriverProperties["maxKelvin"] = 1000000 / ctrl.CT.Min
|
||||
}
|
||||
if hasColor {
|
||||
device.Capabilities = append(device.Capabilities, models.DCColorHS)
|
||||
device.DriverProperties["gamutType"] = ctrl.Colorgamuttype
|
||||
device.DriverProperties["gamutData"] = ctrl.Colorgamut
|
||||
}
|
||||
device.DriverProperties["maxLumen"] = strconv.Itoa(ctrl.Maxlumen)
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
sensorMap, err := b.getSensors(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sensorInfo := range sensorMap {
|
||||
device := models.Device{
|
||||
ID: -1,
|
||||
BridgeID: b.externalID,
|
||||
InternalID: sensorInfo.UniqueID,
|
||||
Name: sensorInfo.Name,
|
||||
Capabilities: []models.DeviceCapability{},
|
||||
ButtonNames: []string{},
|
||||
DriverProperties: map[string]interface{}{
|
||||
"modelId": sensorInfo.Modelid,
|
||||
"productName": sensorInfo.Productname,
|
||||
"swVersion": sensorInfo.Swversion,
|
||||
"hueLightType": sensorInfo.Type,
|
||||
},
|
||||
UserProperties: nil,
|
||||
State: models.DeviceState{},
|
||||
Tags: nil,
|
||||
}
|
||||
|
||||
switch sensorInfo.Type {
|
||||
case "ZLLSwitch":
|
||||
device.Capabilities = append(device.Capabilities, models.DCButtons)
|
||||
device.ButtonNames = append(buttonNames[:0:0], buttonNames...)
|
||||
device.Icon = "lightswitch"
|
||||
case "ZLLPresence":
|
||||
device.Capabilities = append(device.Capabilities, models.DCPresence)
|
||||
device.Icon = "sensor"
|
||||
case "Daylight":
|
||||
continue
|
||||
}
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = b.Refresh(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
for _, device := range devices {
|
||||
for _, state := range b.lightStates {
|
||||
if device.InternalID == state.uniqueID {
|
||||
state.externalID = device.ID
|
||||
state.Update(device.State)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, state := range b.sensorStates {
|
||||
if device.InternalID == state.uniqueID {
|
||||
state.externalID = device.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
return b.SyncStale(ctx)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fastTicker := time.NewTicker(time.Second / 10)
|
||||
slowTicker := time.NewTicker(time.Second / 3)
|
||||
selectedTicker := fastTicker
|
||||
ticksUntilRefresh := 0
|
||||
ticksSinceChange := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-selectedTicker.C:
|
||||
if ticksUntilRefresh <= 0 {
|
||||
err := b.Refresh(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.SyncStale(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticksUntilRefresh = 60
|
||||
}
|
||||
|
||||
events, err := b.SyncSensors(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
ch <- event
|
||||
ticksSinceChange = 0
|
||||
}
|
||||
|
||||
if ticksSinceChange > 30 {
|
||||
selectedTicker = slowTicker
|
||||
} else if ticksSinceChange == 0 {
|
||||
selectedTicker = fastTicker
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
ticksUntilRefresh -= 1
|
||||
ticksSinceChange += 1
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
token: info.Token,
|
||||
externalID: info.ID,
|
||||
}
|
||||
|
||||
// If this call succeeds, then the token is ok.
|
||||
lightMap, err := bridge.getLights(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Found %d lights on bridge %d", len(lightMap), bridge.externalID)
|
||||
|
||||
// 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,179 @@
|
||||
package hue
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type hueLightState struct {
|
||||
index int
|
||||
uniqueID string
|
||||
externalID int
|
||||
info LightData
|
||||
input LightStateInput
|
||||
|
||||
stale bool
|
||||
}
|
||||
|
||||
func (s *hueLightState) Update(state models.DeviceState) {
|
||||
input := LightStateInput{}
|
||||
if state.Power {
|
||||
input.On = ptrBool(true)
|
||||
if state.Color.IsKelvin() {
|
||||
input.CT = ptrInt(1000000 / state.Color.Kelvin)
|
||||
if s.input.CT == nil || *s.input.CT != *input.CT {
|
||||
s.stale = true
|
||||
}
|
||||
} else {
|
||||
input.Hue = ptrInt(int(state.Color.Hue*(65536/360)) % 65536)
|
||||
if s.input.Hue == nil || *s.input.Hue != *input.Hue {
|
||||
s.stale = true
|
||||
}
|
||||
|
||||
input.Sat = ptrInt(int(state.Color.Hue * 255))
|
||||
if *input.Sat > 254 {
|
||||
*input.Sat = 254
|
||||
}
|
||||
if *input.Sat < 0 {
|
||||
*input.Sat = 0
|
||||
}
|
||||
if s.input.Sat == nil || *s.input.Sat != *input.Sat {
|
||||
s.stale = true
|
||||
}
|
||||
}
|
||||
|
||||
input.Bri = ptrInt(int(state.Intensity * 255))
|
||||
if *input.Bri > 254 {
|
||||
*input.Bri = 254
|
||||
} else if *input.Bri < 0 {
|
||||
*input.Bri = 0
|
||||
}
|
||||
|
||||
if s.input.Bri == nil || *s.input.Bri != *input.Bri {
|
||||
s.stale = true
|
||||
}
|
||||
} else {
|
||||
input.On = ptrBool(false)
|
||||
}
|
||||
|
||||
if s.input.On == nil || *s.input.On != *input.On {
|
||||
s.stale = true
|
||||
}
|
||||
|
||||
input.TransitionTime = ptrInt(1)
|
||||
|
||||
s.input = input
|
||||
}
|
||||
|
||||
func (s *hueLightState) CheckStaleness(state LightState) {
|
||||
if state.ColorMode == "xy" {
|
||||
s.stale = true
|
||||
if s.input.CT == nil && s.input.Hue == nil {
|
||||
s.input.Hue = ptrInt(state.Hue)
|
||||
s.input.Sat = ptrInt(state.Sat)
|
||||
s.input.Bri = ptrInt(state.Bri)
|
||||
}
|
||||
return
|
||||
} else if state.ColorMode == "ct" {
|
||||
if s.input.CT == nil || state.CT != *s.input.CT {
|
||||
s.stale = true
|
||||
}
|
||||
} else {
|
||||
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat == *s.input.Sat {
|
||||
s.stale = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type hueSensorState struct {
|
||||
index int
|
||||
externalID int
|
||||
uniqueID string
|
||||
prevData *SensorData
|
||||
prevTime time.Time
|
||||
}
|
||||
|
||||
func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
||||
stateTime, err := time.ParseInLocation("2006-01-02T15:04:05", newData.State.LastUpdated, time.UTC)
|
||||
if err != nil {
|
||||
// Invalid time is probably "none".
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
state.prevData = &newData
|
||||
state.prevTime = stateTime
|
||||
}()
|
||||
|
||||
if state.uniqueID != newData.UniqueID {
|
||||
state.uniqueID = newData.UniqueID
|
||||
}
|
||||
|
||||
if state.prevData != nil && newData.Type != state.prevData.Type {
|
||||
return nil
|
||||
}
|
||||
if time.Since(stateTime) > time.Second*3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch newData.Type {
|
||||
case "ZLLSwitch":
|
||||
{
|
||||
pe := state.prevData.State.ButtonEvent
|
||||
ce := newData.State.ButtonEvent
|
||||
|
||||
td := stateTime.Sub(state.prevTime) >= time.Second
|
||||
pIdx := (pe / 1000) - 1
|
||||
cIdx := (ce / 1000) - 1
|
||||
pBtn := pe % 1000 / 2 // 0 = pressed, 1 = released
|
||||
cBtn := ce % 1000 / 2 // 0 = pressed, 1 = released
|
||||
|
||||
if pIdx == cIdx && cBtn == 1 && pBtn == 0 {
|
||||
// Do not allow 4002 after 4000.
|
||||
// 4000 after 4002 is fine, though.
|
||||
break
|
||||
}
|
||||
|
||||
if cBtn != pBtn || cIdx != pIdx || td {
|
||||
return &models.Event{
|
||||
Name: models.ENButtonPressed,
|
||||
Payload: map[string]string{
|
||||
"buttonIndex": strconv.Itoa(cIdx),
|
||||
"buttonName": buttonNames[cIdx],
|
||||
"deviceId": strconv.Itoa(state.externalID),
|
||||
"hueButtonEvent": strconv.Itoa(ce),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
case "ZLLPresence":
|
||||
{
|
||||
// TODO: Test this!
|
||||
if state.prevData != nil && state.prevData.State.Presence != newData.State.Presence {
|
||||
name := models.ENSensorPresenceStarted
|
||||
if !newData.State.Presence {
|
||||
name = models.ENSensorPresenceEnded
|
||||
}
|
||||
|
||||
return &models.Event{
|
||||
Name: name,
|
||||
Payload: map[string]string{
|
||||
"deviceId": strconv.Itoa(state.externalID),
|
||||
"deviceInternalId": newData.UniqueID,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ptrBool(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func ptrInt(v int) *int {
|
||||
return &v
|
||||
}
|
||||
@@ -50,12 +50,12 @@ func (b *bridge) Devices() []models.Device {
|
||||
models.DCButtons,
|
||||
},
|
||||
ButtonNames: []string{"Touch"},
|
||||
DriverProperties: map[string]string{
|
||||
"x": strconv.Itoa(panel.X),
|
||||
"y": strconv.Itoa(panel.Y),
|
||||
"o": strconv.Itoa(panel.O),
|
||||
DriverProperties: map[string]interface{}{
|
||||
"x": panel.X,
|
||||
"y": panel.Y,
|
||||
"o": panel.O,
|
||||
"shapeType": shapeTypeMap[panel.ShapeType],
|
||||
"shapeWidth": strconv.Itoa(shapeWidthMap[panel.ShapeType]),
|
||||
"shapeWidth": shapeWidthMap[panel.ShapeType],
|
||||
},
|
||||
UserProperties: nil,
|
||||
State: models.DeviceState{
|
||||
@@ -393,7 +393,7 @@ func (b *bridge) runTouchListener(ctx context.Context, host, apiKey string, ch c
|
||||
}
|
||||
|
||||
event := models.Event{
|
||||
Name: "ButtonPressed",
|
||||
Name: models.ENButtonPressed,
|
||||
Payload: map[string]string{
|
||||
"buttonIndex": "0",
|
||||
"buttonName": "Touch",
|
||||
|
||||
+12
-11
@@ -6,17 +6,17 @@ import (
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
ID int `json:"id"`
|
||||
BridgeID int `json:"bridgeID"`
|
||||
InternalID string `json:"internalId"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Capabilities []DeviceCapability `json:"capabilities"`
|
||||
ButtonNames []string `json:"buttonNames"`
|
||||
DriverProperties map[string]string `json:"driverProperties"`
|
||||
UserProperties map[string]string `json:"userProperties"`
|
||||
State DeviceState `json:"state"`
|
||||
Tags []string `json:"tags"`
|
||||
ID int `json:"id"`
|
||||
BridgeID int `json:"bridgeID"`
|
||||
InternalID string `json:"internalId"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Capabilities []DeviceCapability `json:"capabilities"`
|
||||
ButtonNames []string `json:"buttonNames"`
|
||||
DriverProperties map[string]interface{} `json:"driverProperties"`
|
||||
UserProperties map[string]string `json:"userProperties"`
|
||||
State DeviceState `json:"state"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// DeviceState contains optional state values that
|
||||
@@ -52,6 +52,7 @@ var (
|
||||
DCColorHS DeviceCapability = "ColorHS"
|
||||
DCColorKelvin DeviceCapability = "ColorKelvin"
|
||||
DCButtons DeviceCapability = "Buttons"
|
||||
DCPresence DeviceCapability = "Presence"
|
||||
DCIntensity DeviceCapability = "Intensity"
|
||||
DCTemperature DeviceCapability = "Temperature"
|
||||
)
|
||||
|
||||
+5
-2
@@ -20,8 +20,11 @@ func (e *Event) HasPayload(key string) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
ENBridgeConnected = "BridgeConnected"
|
||||
ENBridgeDisconnected = "BridgeDisconnected"
|
||||
ENBridgeConnected = "BridgeConnected"
|
||||
ENBridgeDisconnected = "BridgeDisconnected"
|
||||
ENButtonPressed = "ButtonPressed"
|
||||
ENSensorPresenceStarted = "SensorPresenceStart"
|
||||
ENSensorPresenceEnded = "SensorPresenceEnd"
|
||||
)
|
||||
|
||||
func BridgeConnectedEvent(bridge Bridge) Event {
|
||||
|
||||
Reference in New Issue
Block a user