hue hue hue

This commit is contained in:
2022-08-28 21:39:36 +02:00
parent d1f42d932b
commit e00fe2a4d2
16 changed files with 1300 additions and 77 deletions
+222
View File
@@ -0,0 +1,222 @@
package hue
import (
"context"
"errors"
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/gentools"
"log"
"strings"
"sync"
"time"
)
func NewBridge(host string, client *Client) *Bridge {
return &Bridge{
client: client,
host: host,
ctx: context.Background(),
cancel: func() {},
resources: map[string]*ResourceData{},
activeStates: map[string]device.State{},
desiredStates: map[string]device.State{},
hasSeen: map[string]bool{},
}
}
type Bridge struct {
mu sync.Mutex
client *Client
host string
ctx context.Context
cancel context.CancelFunc
resources map[string]*ResourceData
activeStates map[string]device.State
desiredStates map[string]device.State
hasSeen map[string]bool
lastDiscoverCancel context.CancelFunc
}
func (b *Bridge) SearchDevices(timeout time.Duration) error {
discoverCtx, cancel := context.WithCancel(b.ctx)
b.mu.Lock()
if b.lastDiscoverCancel != nil {
b.lastDiscoverCancel()
}
b.lastDiscoverCancel = cancel
b.mu.Unlock()
if timeout <= time.Second*10 {
timeout = time.Second * 10
}
// Spend half the time waiting for devices
// TODO: Wait for v2 endpoint
ctx, cancel := context.WithTimeout(discoverCtx, timeout/2)
defer cancel()
err := b.client.LegacyDiscover(ctx, "sensors")
if err != nil {
return err
}
<-ctx.Done()
if discoverCtx.Err() != nil {
return discoverCtx.Err()
}
// Spend half the time waiting for lights
// TODO: Wait for v2 endpoint
ctx, cancel = context.WithTimeout(discoverCtx, timeout/2)
defer cancel()
err = b.client.LegacyDiscover(ctx, "sensors")
if err != nil {
return err
}
<-ctx.Done()
if discoverCtx.Err() != nil {
return discoverCtx.Err()
}
// Let the main loop get the new light.
return nil
}
func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
ctx, cancel := context.WithTimeout(b.ctx, time.Second*15)
defer cancel()
allResources, err := b.client.AllResources(ctx)
if err != nil {
return nil, err
}
resources := make(map[string]*ResourceData, len(allResources))
for i := range allResources {
resources[allResources[i].ID] = &allResources[i]
}
newEvents := make([]lucifer3.Event, 0, 0)
b.mu.Lock()
for id, res := range resources {
if res.Type == "device" {
hwState, hwEvent := res.GenerateEvent(b.host, resources)
if !b.hasSeen[id] {
newEvents = append(newEvents, hwState, hwEvent, events.DeviceReady{ID: hwState.ID})
b.hasSeen[id] = true
} else {
newEvents = append(newEvents, hwState)
}
}
}
b.resources = resources
b.mu.Unlock()
return newEvents, nil
}
func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event, shouldRefresh bool) {
b.mu.Lock()
mapCopy := gentools.CopyMap(b.resources)
b.mu.Unlock()
for _, resource := range resources {
if mapCopy[resource.ID] != nil {
mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
} else {
log.Println(resource.ID, resource.Type, "not seen!")
shouldRefresh = true
}
}
for _, resource := range resources {
if resource.Owner != nil && resource.Owner.Kind == "device" {
if parent, ok := mapCopy[resource.Owner.ID]; ok {
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
events = append(events, hwState)
}
}
}
b.mu.Lock()
b.resources = mapCopy
b.mu.Unlock()
return
}
func (b *Bridge) SetStates(patch map[string]device.State) {
b.mu.Lock()
newStates := gentools.CopyMap(b.desiredStates)
resources := b.resources
b.mu.Unlock()
prefix := "hue:" + b.host + ":"
for id, state := range patch {
if !strings.HasPrefix(id, prefix) {
continue
}
id = id[len(prefix):]
resource := resources[id]
if resource == nil {
continue
}
newStates[id] = resource.FixState(state, resources)
}
b.mu.Lock()
b.desiredStates = newStates
b.mu.Unlock()
}
func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
hwEvents, err := b.RefreshAll()
if err != nil {
return nil
}
bus.RunEvents(hwEvents)
sse := b.client.SSE(ctx)
step := time.NewTicker(time.Second * 30)
defer step.Stop()
for {
select {
case updates, ok := <-sse:
{
if !ok {
return errors.New("SSE lost connection")
}
newEvents, shouldUpdate := b.ApplyPatches(
gentools.Flatten(gentools.Map(updates, func(update SSEUpdate) []ResourceData {
return update.Data
})),
)
bus.RunEvents(newEvents)
if shouldUpdate {
hwEvents, err := b.RefreshAll()
if err != nil {
return nil
}
bus.RunEvents(hwEvents)
}
}
case <-ctx.Done():
{
return nil
}
}
}
}
+358
View File
@@ -0,0 +1,358 @@
package hue
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
lucifer3 "git.aiterp.net/lucifer3/server"
"io"
"log"
"net"
"net/http"
"strings"
"time"
)
func NewClient(host, token string) *Client {
ch := make(chan struct{}, 5)
for i := 0; i < 2; i++ {
ch <- struct{}{}
}
return &Client{
host: host,
token: token,
ch: ch,
}
}
type Client struct {
host string
token string
ch chan struct{}
}
func (c *Client) Register(ctx context.Context) (string, error) {
result := make([]CreateUserResponse, 0, 1)
err := c.post(ctx, "api/", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
if err != nil {
return "", err
}
if len(result) == 0 || result[0].Error != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(time.Second):
return c.Register(ctx)
}
}
if result[0].Success == nil {
return "", lucifer3.ErrUnexpectedResponse
}
c.token = result[0].Success.Username
return result[0].Success.Username, nil
}
func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) {
res := struct {
Error interface{}
Data []ResourceData
}{}
err := c.get(ctx, "clip/v2/resource", &res)
if err != nil {
return nil, err
}
return res.Data, nil
}
func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) {
res := struct {
Error interface{}
Data []ResourceData
}{}
err := c.get(ctx, "clip/v2/resource/"+kind, &res)
if err != nil {
return nil, err
}
return res.Data, nil
}
func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error {
return c.put(ctx, link.Path(), update, nil)
}
func (c *Client) LegacyDiscover(ctx context.Context, kind string) error {
return c.legacyPost(ctx, kind, nil, nil)
}
func (c *Client) SSE(ctx context.Context) <-chan []SSEUpdate {
ch := make(chan []SSEUpdate, 4)
go func() {
defer close(ch)
reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{
"Accept": "text/event-stream",
})
if err != nil {
log.Println("SSE Connect error:", err)
return
}
defer reader.Close()
br := bufio.NewReader(reader)
for {
line, err := br.ReadString('\n')
if err != nil {
log.Println("SSE Read error:", err)
return
}
line = strings.Trim(line, "  \t\r\n")
kv := strings.SplitN(line, ": ", 2)
if len(kv) < 2 {
continue
}
switch kv[0] {
case "data":
var data []SSEUpdate
err := json.Unmarshal([]byte(kv[1]), &data)
if err != nil {
log.Println("Parsing SSE event failed:", err)
log.Println(" json:", kv[1])
return
}
if len(data) > 0 {
ch <- data
}
}
}
}()
return ch
}
func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-c.ch:
defer func() {
c.ch <- struct{}{}
}()
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil)
if err != nil {
return nil, err
}
for key, value := range headers {
req.Header.Set(key, value)
}
req.Header.Set("hue-application-key", c.token)
client := httpClient
if headers["Accept"] == "text/event-stream" {
client = sseClient
}
res, err := client.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
_ = res.Body.Close()
return nil, errors.New(res.Status)
}
return res.Body, nil
}
func (c *Client) get(ctx context.Context, path string, target interface{}) error {
body, err := c.getReader(ctx, path, map[string]string{})
if err != nil {
return err
}
defer body.Close()
if target == nil {
return nil
}
return json.NewDecoder(body).Decode(&target)
}
func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-c.ch:
defer func() {
c.ch <- struct{}{}
}()
}
rb, err := reqBody(body)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb)
if err != nil {
return err
}
req.Header.Set("hue-application-key", c.token)
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer res.Body.Close()
if target == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(&target)
}
func (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-c.ch:
defer func() {
c.ch <- struct{}{}
}()
}
rb, err := reqBody(body)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb)
if err != nil {
return err
}
if c.token != "" {
req.Header.Set("hue-application-key", c.token)
}
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer res.Body.Close()
if target == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(&target)
}
func (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-c.ch:
defer func() {
c.ch <- struct{}{}
}()
}
rb, err := reqBody(body)
if err != nil {
return err
}
if c.token != "" {
resource = c.token + "/" + resource
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb)
if err != nil {
return err
}
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer res.Body.Close()
if target == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(target)
}
func reqBody(body interface{}) (io.Reader, error) {
if body == nil {
return nil, nil
}
switch v := body.(type) {
case []byte:
return bytes.NewReader(v), nil
case string:
return strings.NewReader(v), nil
case io.Reader:
return v, nil
default:
jsonData, err := json.Marshal(v)
if err != nil {
return nil, err
}
return bytes.NewReader(jsonData), nil
}
}
var sseClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: time.Hour * 1000000,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 5,
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 0,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: time.Hour * 1000000,
}
var httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 256,
MaxIdleConnsPerHost: 16,
IdleConnTimeout: 10 * time.Minute,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: time.Minute,
}
+452
View File
@@ -0,0 +1,452 @@
package hue
import (
"encoding/json"
"encoding/xml"
"fmt"
"git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/color"
"git.aiterp.net/lucifer3/server/internal/gentools"
"strings"
"time"
)
type DeviceData struct {
ID string `json:"id"`
LegacyID string `json:"id_v1"`
Metadata DeviceMetadata `json:"metadata"`
Type string `json:"type"`
ProductData DeviceProductData `json:"product_data"`
Services []ResourceLink `json:"services"`
}
type DeviceMetadata struct {
Archetype string `json:"archetype"`
Name string `json:"name"`
}
type DeviceProductData struct {
Certified bool `json:"certified"`
ManufacturerName string `json:"manufacturer_name"`
ModelID string `json:"model_id"`
ProductArchetype string `json:"product_archetype"`
ProductName string `json:"product_name"`
SoftwareVersion string `json:"software_version"`
}
type SSEUpdate struct {
CreationTime time.Time `json:"creationTime"`
ID string `json:"id"`
Type string `json:"type"`
Data []ResourceData `json:"data"`
}
type ResourceData struct {
ID string `json:"id"`
LegacyID string `json:"id_v1"`
Metadata DeviceMetadata `json:"metadata"`
Type string `json:"type"`
Mode *string `json:"mode"`
Owner *ResourceLink `json:"owner"`
ProductData *DeviceProductData `json:"product_data"`
Services []ResourceLink `json:"services"`
Button *SensorButton `json:"button"`
Power *LightPower `json:"on"`
Color *LightColor `json:"color"`
ColorTemperature *LightCT `json:"color_temperature"`
Dimming *LightDimming `json:"dimming"`
Dynamics *LightDynamics `json:"dynamics"`
Alert *LightAlert `json:"alert"`
PowerState *PowerState `json:"power_state"`
Temperature *SensorTemperature `json:"temperature"`
Motion *SensorMotion `json:"motion"`
Status *string `json:"status"`
}
func (res *ResourceData) ServiceID(kind string) *string {
for _, ptr := range res.Services {
if ptr.Kind == kind {
return &ptr.ID
}
}
return nil
}
func (res *ResourceData) ServiceIndex(kind string, id string) int {
idx := 0
for _, link := range res.Services {
if link.ID == id {
return idx
} else if link.Kind == kind {
idx += 1
}
}
return -1
}
func (res *ResourceData) FixState(state device.State, resources map[string]*ResourceData) device.State {
fixedState := device.State{}
if lightID := res.ServiceID("light"); lightID != nil {
if light := resources[*lightID]; light != nil {
if state.Color != nil {
if state.Color.IsKelvin() {
if light.ColorTemperature != nil {
mirek := 1000000 / *state.Color.K
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
mirek = light.ColorTemperature.MirekSchema.MirekMinimum
}
if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
mirek = light.ColorTemperature.MirekSchema.MirekMaximum
}
fixedState.Color = &color.Color{K: gentools.Ptr(1000000 / mirek)}
}
} else {
if light.Color != nil {
if col, ok := state.Color.ToXY(); ok {
col.XY = gentools.Ptr(light.Color.Gamut.Conform(*col.XY))
fixedState.Color = &col
}
}
}
}
if state.Intensity != nil && light.Dimming != nil {
fixedState.Intensity = gentools.ShallowCopy(state.Intensity)
}
if state.Power != nil && light.Power != nil {
fixedState.Power = gentools.ShallowCopy(state.Power)
}
}
}
return fixedState
}
func (res *ResourceData) WithUpdate(update ResourceUpdate) *ResourceData {
resCopy := *res
if update.Name != nil {
resCopy.Metadata.Name = *update.Name
}
if update.Power != nil {
cp := *resCopy.Power
resCopy.Power = &cp
resCopy.Power.On = *update.Power
}
if update.ColorXY != nil {
cp := *resCopy.Color
resCopy.Color = &cp
resCopy.Color.XY = *update.ColorXY
if resCopy.ColorTemperature != nil {
cp := *resCopy.ColorTemperature
resCopy.ColorTemperature = &cp
resCopy.ColorTemperature.Mirek = nil
}
}
if update.Mirek != nil {
cp := *resCopy.ColorTemperature
resCopy.ColorTemperature = &cp
mirek := *update.Mirek
resCopy.ColorTemperature.Mirek = &mirek
}
if update.Brightness != nil {
cp := *resCopy.Dimming
resCopy.Dimming = &cp
resCopy.Dimming.Brightness = *update.Brightness
}
return &resCopy
}
func (res *ResourceData) WithPatch(patch ResourceData) *ResourceData {
res2 := *res
if patch.Color != nil {
res2.Color = gentools.ShallowCopy(res2.Color)
res2.Color.XY = patch.Color.XY
}
if patch.ColorTemperature != nil {
res2.ColorTemperature = gentools.ShallowCopy(res2.ColorTemperature)
res2.ColorTemperature.Mirek = gentools.ShallowCopy(patch.ColorTemperature.Mirek)
}
gentools.ShallowCopyTo(&res2.ProductData, patch.ProductData)
gentools.ShallowCopyTo(&res2.Button, patch.Button)
gentools.ShallowCopyTo(&res2.Power, patch.Power)
gentools.ShallowCopyTo(&res2.Color, patch.Color)
gentools.ShallowCopyTo(&res2.ColorTemperature, patch.ColorTemperature)
gentools.ShallowCopyTo(&res2.Dimming, patch.Dimming)
gentools.ShallowCopyTo(&res2.Dynamics, patch.Dynamics)
gentools.ShallowCopyTo(&res2.Alert, patch.Alert)
gentools.ShallowCopyTo(&res2.PowerState, patch.PowerState)
gentools.ShallowCopyTo(&res2.Temperature, patch.Temperature)
gentools.ShallowCopyTo(&res2.Motion, patch.Motion)
gentools.ShallowCopyTo(&res2.Status, patch.Status)
return &res2
}
func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*ResourceData) (events.HardwareState, events.HardwareMetadata) {
hwState := events.HardwareState{
ID: fmt.Sprintf("hue:%s:%s", hostname, res.ID),
InternalName: res.Metadata.Name,
State: res.GenerateState(resources),
Unreachable: false,
}
hwMeta := events.HardwareMetadata{
ID: hwState.ID,
}
buttonCount := 0
if res.ProductData != nil {
hwMeta.FirmwareVersion = res.ProductData.SoftwareVersion
}
for _, ptr := range res.Services {
svc := resources[ptr.ID]
if svc == nil {
continue // I've never seen this happen, but safety first.
}
switch ptr.Kind {
case "device_power":
hwState.BatteryPercentage = gentools.Ptr(int(svc.PowerState.BatteryLevel))
case "button":
buttonCount += 1
hwState.SupportFlags |= device.SFlagSensorButtons
case "motion":
hwState.SupportFlags |= device.SFlagSensorPresence
case "temperature":
hwState.SupportFlags |= device.SFlagSensorTemperature
case "light":
if svc.Power != nil {
hwState.SupportFlags |= device.SFlagPower
}
if svc.Dimming != nil {
hwState.SupportFlags |= device.SFlagIntensity
}
if svc.ColorTemperature != nil {
hwState.SupportFlags |= device.SFlagColor
hwState.ColorFlags |= device.CFlagKelvin
hwState.TemperatureRange = &[2]int{
1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
}
}
if svc.Color != nil {
hwState.SupportFlags |= device.SFlagColor
hwState.ColorFlags |= device.CFlagXY
hwState.ColorGamut = gentools.Ptr(svc.Color.Gamut)
hwState.ColorGamut.Label = svc.Color.GamutType
}
}
}
if buttonCount == 4 {
hwState.Buttons = []string{"On", "DimUp", "DimDown", "Off"}
} else if buttonCount == 1 {
hwState.Buttons = []string{"Button"}
} else {
for n := 1; n <= buttonCount; n++ {
hwState.Buttons = append(hwState.Buttons, fmt.Sprint("Button", n))
}
}
return hwState, hwMeta
}
func (res *ResourceData) GenerateState(resources map[string]*ResourceData) device.State {
state := device.State{}
for _, ptr := range res.Services {
switch ptr.Kind {
case "light":
light := resources[ptr.ID]
if light == nil {
continue
}
if light.Power != nil {
state.Power = gentools.Ptr(light.Power.On)
}
if light.Dimming != nil {
state.Intensity = gentools.Ptr(light.Dimming.Brightness / 100.0)
}
if light.ColorTemperature != nil {
if light.ColorTemperature.Mirek != nil {
state.Color = &color.Color{K: gentools.Ptr(1000000 / *light.ColorTemperature.Mirek)}
}
}
if light.Color != nil {
if state.Color == nil || state.Color.IsEmpty() {
state.Color = &color.Color{XY: &light.Color.XY}
}
}
}
}
return state
}
type SensorButton struct {
LastEvent string `json:"last_event"`
}
type SensorMotion struct {
Motion bool `json:"motion"`
Valid bool `json:"motion_valid"`
}
type SensorTemperature struct {
Temperature float64 `json:"temperature"`
Valid bool `json:"temperature_valid"`
}
type PowerState struct {
BatteryState string `json:"battery_state"`
BatteryLevel float64 `json:"battery_level"`
}
type LightPower struct {
On bool `json:"on"`
}
type LightDimming struct {
Brightness float64 `json:"brightness"`
}
type LightColor struct {
Gamut color.Gamut `json:"gamut"`
GamutType string `json:"gamut_type"`
XY color.XY `json:"xy"`
}
type LightCT struct {
Mirek *int `json:"mirek"`
MirekSchema LightCTMirekSchema `json:"mirek_schema"`
MirekValid bool `json:"mirek_valid"`
}
type LightCTMirekSchema struct {
MirekMaximum int `json:"mirek_maximum"`
MirekMinimum int `json:"mirek_minimum"`
}
type LightDynamics struct {
Speed float64 `json:"speed"`
SpeedValid bool `json:"speed_valid"`
Status string `json:"status"`
StatusValues []string `json:"status_values"`
}
type LightAlert struct {
ActionValues []string `json:"action_values"`
}
type ResourceUpdate struct {
Name *string
Power *bool
ColorXY *color.XY
Brightness *float64
Mirek *int
TransitionDuration *time.Duration
}
func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
chunks := make([]string, 0, 4)
if r.Name != nil {
s, _ := json.Marshal(*r.Name)
chunks = append(chunks, fmt.Sprintf(`"metadata":{"name":%s}`, string(s)))
}
if r.Power != nil {
chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power))
}
if r.ColorXY != nil {
chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y))
}
if r.Brightness != nil {
chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness))
}
if r.Mirek != nil {
chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek))
}
if r.TransitionDuration != nil {
chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds()))
}
return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil
}
type ResourceLink struct {
ID string `json:"rid"`
Kind string `json:"rtype"`
}
func (rl *ResourceLink) Path() string {
return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID)
}
type CreateUserInput struct {
DeviceType string `json:"devicetype"`
}
type CreateUserResponse struct {
Success *struct {
Username string `json:"username"`
} `json:"success"`
Error *struct {
Type int `json:"type"`
Address string `json:"address"`
Description string `json:"description"`
} `json:"error"`
}
type DiscoveryEntry struct {
Id string `json:"id"`
InternalIPAddress string `json:"internalipaddress"`
}
type BridgeDeviceInfo struct {
XMLName xml.Name `xml:"root"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
SpecVersion struct {
Text string `xml:",chardata"`
Major string `xml:"major"`
Minor string `xml:"minor"`
} `xml:"specVersion"`
URLBase string `xml:"URLBase"`
Device struct {
Text string `xml:",chardata"`
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
ManufacturerURL string `xml:"manufacturerURL"`
ModelDescription string `xml:"modelDescription"`
ModelName string `xml:"modelName"`
ModelNumber string `xml:"modelNumber"`
ModelURL string `xml:"modelURL"`
SerialNumber string `xml:"serialNumber"`
UDN string `xml:"UDN"`
PresentationURL string `xml:"presentationURL"`
IconList struct {
Text string `xml:",chardata"`
Icon struct {
Text string `xml:",chardata"`
Mimetype string `xml:"mimetype"`
Height string `xml:"height"`
Width string `xml:"width"`
Depth string `xml:"depth"`
URL string `xml:"url"`
} `xml:"icon"`
} `xml:"iconList"`
} `xml:"device"`
}
+105
View File
@@ -0,0 +1,105 @@
package hue
import (
"context"
"fmt"
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/commands"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/gentools"
"sync"
"time"
)
func NewService() lucifer3.ActiveService {
return &service{
bridges: map[string]*Bridge{},
}
}
type service struct {
mu sync.Mutex
bridges map[string]*Bridge
}
func (s *service) Active() bool {
return true
}
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
switch command := command.(type) {
case commands.PairDevice:
{
if hostname, ok := command.Matches("hue"); ok {
go func() {
timeout, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
client := NewClient(hostname, "")
token, err := client.Register(timeout)
if err != nil {
return
}
bus.RunEvent(events.DeviceAccepted{
ID: command.ID,
APIKey: token,
})
}()
}
}
case commands.SetStateBatch:
for _, bridge := range s.bridges {
bridge.SetStates(command)
}
case commands.SetState:
if sub, ok := command.Matches("hue"); ok {
if s.bridges[sub] != nil {
s.bridges[sub].SetStates(gentools.OneItemMap(command.ID, command.State))
}
}
case commands.ConnectDevice:
if sub, ok := command.Matches("hue"); ok {
if s.bridges[sub] != nil {
s.bridges[sub].cancel()
delete(s.bridges, sub)
}
ctx, cancel := context.WithCancel(context.Background())
client := NewClient(sub, command.APIKey)
bridge := NewBridge(sub, client)
bridge.ctx = ctx
bridge.cancel = cancel
s.bridges[sub] = bridge
go func() {
for bridge.ctx.Err() == nil {
ctx2, cancel2 := context.WithCancel(ctx)
err := bridge.Run(ctx2, bus)
cancel2()
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: command.ID,
Error: fmt.Sprintf("Run failed: %s", err),
})
}
select {
case <-time.After(time.Second * 5):
case <-ctx.Done():
return
}
}
}()
}
}
}
func (s *service) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) {}