You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
452 lines
12 KiB
452 lines
12 KiB
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"`
|
|
}
|