Browse Source

add rest of hue driver functionality.

pull/1/head
Gisle Aune 4 years ago
parent
commit
2aa2757828
  1. 2
      app/config/driver.go
  2. 12
      cmd/bridgetest/main.go
  3. 1
      go.mod
  4. 2
      go.sum
  5. 286
      internal/drivers/hue/bridge.go
  6. 196
      internal/drivers/hue/data.go
  7. 399
      internal/drivers/hue/driver.go
  8. 179
      internal/drivers/hue/state.go
  9. 12
      internal/drivers/nanoleaf/bridge.go
  10. 23
      models/device.go
  11. 7
      models/event.go

2
app/config/driver.go

@ -2,6 +2,7 @@ package config
import ( import (
"git.aiterp.net/lucifer/new-server/internal/drivers" "git.aiterp.net/lucifer/new-server/internal/drivers"
"git.aiterp.net/lucifer/new-server/internal/drivers/hue"
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf" "git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"sync" "sync"
@ -17,6 +18,7 @@ func DriverProvider() models.DriverProvider {
if dp == nil { if dp == nil {
dp = drivers.DriverMap{ dp = drivers.DriverMap{
models.DTNanoLeaf: &nanoleaf.Driver{}, models.DTNanoLeaf: &nanoleaf.Driver{},
models.DTHue: &hue.Driver{},
} }
} }

12
cmd/bridgetest/main.go

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
@ -63,8 +64,6 @@ func main() {
devices[i].ID = i + 1 devices[i].ID = i + 1
} }
_ = driver.Publish(context.Background(), bridge, devices)
ch := config.EventChannel ch := config.EventChannel
go func() { go func() {
err := driver.Run(context.Background(), bridge, ch) err := driver.Run(context.Background(), bridge, ch)
@ -86,6 +85,11 @@ func main() {
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')
text = strings.Trim(text, "\t  \r\n") text = strings.Trim(text, "\t  \r\n")
if text == "json" {
j, _ := json.MarshalIndent(devices, "", " ")
fmt.Println(string(j))
}
tokens := strings.Split(text, " ") tokens := strings.Split(text, " ")
if len(tokens) < 4 { if len(tokens) < 4 {
continue continue
@ -122,15 +126,15 @@ func main() {
for _, id := range ids { for _, id := range ids {
if id == -1 || id == device.ID { if id == -1 || id == device.ID {
if (color.IsKelvin() && device.HasCapability(models.DCColorKelvin)) || (color.IsHueSat() && device.HasCapability(models.DCColorHS)) { if (color.IsKelvin() && device.HasCapability(models.DCColorKelvin)) || (color.IsHueSat() && device.HasCapability(models.DCColorHS)) {
device.State.Color = color
if device.HasCapability(models.DCPower) { if device.HasCapability(models.DCPower) {
device.State.Power = power device.State.Power = power
} }
if device.HasCapability(models.DCIntensity) { if device.HasCapability(models.DCIntensity) {
device.State.Intensity = intensity device.State.Intensity = intensity
} }
updatedDevices = append(updatedDevices, device)
} }
updatedDevices = append(updatedDevices, device)
} }
} }
} }

1
go.mod

@ -12,4 +12,5 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pressly/goose v2.7.0+incompatible github.com/pressly/goose v2.7.0+incompatible
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
) )

2
go.sum

@ -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/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

286
internal/drivers/hue/bridge.go

@ -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
}
}

196
internal/drivers/hue/data.go

@ -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")

399
internal/drivers/hue/driver.go

@ -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
}

179
internal/drivers/hue/state.go

@ -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
}

12
internal/drivers/nanoleaf/bridge.go

@ -50,12 +50,12 @@ func (b *bridge) Devices() []models.Device {
models.DCButtons, models.DCButtons,
}, },
ButtonNames: []string{"Touch"}, 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], "shapeType": shapeTypeMap[panel.ShapeType],
"shapeWidth": strconv.Itoa(shapeWidthMap[panel.ShapeType]),
"shapeWidth": shapeWidthMap[panel.ShapeType],
}, },
UserProperties: nil, UserProperties: nil,
State: models.DeviceState{ State: models.DeviceState{
@ -393,7 +393,7 @@ func (b *bridge) runTouchListener(ctx context.Context, host, apiKey string, ch c
} }
event := models.Event{ event := models.Event{
Name: "ButtonPressed",
Name: models.ENButtonPressed,
Payload: map[string]string{ Payload: map[string]string{
"buttonIndex": "0", "buttonIndex": "0",
"buttonName": "Touch", "buttonName": "Touch",

23
models/device.go

@ -6,17 +6,17 @@ import (
) )
type Device struct { 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 // DeviceState contains optional state values that
@ -52,6 +52,7 @@ var (
DCColorHS DeviceCapability = "ColorHS" DCColorHS DeviceCapability = "ColorHS"
DCColorKelvin DeviceCapability = "ColorKelvin" DCColorKelvin DeviceCapability = "ColorKelvin"
DCButtons DeviceCapability = "Buttons" DCButtons DeviceCapability = "Buttons"
DCPresence DeviceCapability = "Presence"
DCIntensity DeviceCapability = "Intensity" DCIntensity DeviceCapability = "Intensity"
DCTemperature DeviceCapability = "Temperature" DCTemperature DeviceCapability = "Temperature"
) )

7
models/event.go

@ -20,8 +20,11 @@ func (e *Event) HasPayload(key string) bool {
} }
var ( var (
ENBridgeConnected = "BridgeConnected"
ENBridgeDisconnected = "BridgeDisconnected"
ENBridgeConnected = "BridgeConnected"
ENBridgeDisconnected = "BridgeDisconnected"
ENButtonPressed = "ButtonPressed"
ENSensorPresenceStarted = "SensorPresenceStart"
ENSensorPresenceEnded = "SensorPresenceEnd"
) )
func BridgeConnectedEvent(bridge Bridge) Event { func BridgeConnectedEvent(bridge Bridge) Event {

Loading…
Cancel
Save