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.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 switch res.ProductData.ProductArchetype { case "candle_bulb": hwMeta.Icon = "hue_lightbulb_e14" case "sultan_bulb": hwMeta.Icon = "hue_lightbulb_e27" case "hue_play": hwMeta.Icon = "hue_playbar" case "hue_signe", "hue_go": hwMeta.Icon = res.ProductData.ProductArchetype case "unknown_archetype": switch res.ProductData.ProductName { case "Hue motion sensor": hwMeta.Icon = "hue_motionsensor" case "Hue dimmer switch": hwMeta.Icon = "hue_dimmerswitch" } } 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 "zigbee_connectivity": switch *svc.Status { case "connectivity_issue": hwState.Unreachable = true } 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 if svc.ColorTemperature.MirekSchema.MirekMinimum != 0 { hwState.ColorKelvinRange = &[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) Split() []ResourceUpdate { updates := make([]ResourceUpdate, 0, 2) if r.Name != nil { updates = append(updates, ResourceUpdate{Name: r.Name, TransitionDuration: r.TransitionDuration}) } if r.Power != nil { updates = append(updates, ResourceUpdate{Power: r.Power, TransitionDuration: r.TransitionDuration}) } if r.ColorXY != nil { updates = append(updates, ResourceUpdate{ColorXY: r.ColorXY, TransitionDuration: r.TransitionDuration}) } if r.Brightness != nil { updates = append(updates, ResourceUpdate{Brightness: r.Brightness, TransitionDuration: r.TransitionDuration}) } if r.Mirek != nil { updates = append(updates, ResourceUpdate{Mirek: r.Mirek, TransitionDuration: r.TransitionDuration}) } return updates } 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"` }