package models import ( "context" "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/internal/lerrors" "strings" "time" ) 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]interface{} `json:"driverProperties"` UserProperties map[string]string `json:"userProperties"` SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"` SceneState *DeviceState `json:"sceneState"` State DeviceState `json:"state"` Tags []string `json:"tags"` } type DeviceUpdate struct { Icon *string `json:"icon"` Name *string `json:"name"` UserProperties map[string]*string `json:"userProperties"` } // DeviceState contains optional state values that // - Power: Whether the device is powered on // - Color: Color value, if a color setting can be set on the device // - Intensity: e.g. brightness, range 0..=1 // - Temperature: e.g. for thermostats type DeviceState struct { Power bool `json:"power"` Color color.Color `json:"color,omitempty"` Intensity float64 `json:"intensity,omitempty"` Temperature int `json:"temperature"` } type DeviceScene struct { SceneID int `json:"sceneId"` Time time.Time `json:"time"` DurationMS int64 `json:"duration"` Tag string `json:"tag"` } type NewDeviceState struct { Power *bool `json:"power"` Color *string `json:"color"` Intensity *float64 `json:"intensity"` Temperature *int `json:"temperature"` } type DeviceCapability string type SaveMode int const ( SMState SaveMode = 1 SMProperties SaveMode = 2 SMTags SaveMode = 4 ) type DeviceRepository interface { Find(ctx context.Context, id int) (*Device, error) FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error) Save(ctx context.Context, device *Device, mode SaveMode) error SaveMany(ctx context.Context, mode SaveMode, devices []Device) error Delete(ctx context.Context, device *Device) error } func DeviceCapabilitiesToStrings(caps []DeviceCapability) []string { res := make([]string, 0, len(caps)) for _, cap := range caps { res = append(res, string(cap)) } return res } var ( DCPower DeviceCapability = "Power" DCColorHS DeviceCapability = "HueSat" DCColorHSK DeviceCapability = "ColorHSK" DCColorKelvin DeviceCapability = "ColorKelvin" DCColorXY DeviceCapability = "XY" DCColorRGB DeviceCapability = "RGB" DCButtons DeviceCapability = "Buttons" DCPresence DeviceCapability = "Presence" DCIntensity DeviceCapability = "Intensity" DCTemperatureControl DeviceCapability = "TemperatureControl" DCTemperatureSensor DeviceCapability = "TemperatureSensor" ) var Capabilities = []DeviceCapability{ DCPower, DCColorHS, DCColorKelvin, DCButtons, DCPresence, DCIntensity, DCTemperatureControl, DCTemperatureSensor, } func (d *Device) ApplyUpdate(update DeviceUpdate) { if update.Name != nil { d.Name = *update.Name } if update.Icon != nil { d.Icon = *update.Icon } if d.UserProperties == nil { d.UserProperties = make(map[string]string) } for key, value := range update.UserProperties { if value != nil { d.UserProperties[key] = *value } else { delete(d.UserProperties, key) } } } func (d *Device) Validate() error { d.Name = strings.Trim(d.Name, " \t\n ") if d.Name == "" { return lerrors.ErrInvalidName } newCaps := make([]DeviceCapability, 0, len(d.Capabilities)) for _, currCap := range d.Capabilities { for _, validCap := range Capabilities { if currCap == validCap { newCaps = append(newCaps, currCap) break } } } d.Capabilities = newCaps return nil } func (d *Device) HasTag(tags ...string) bool { for _, c := range d.Tags { for _, c2 := range tags { if c == c2 { return true } } } return false } func (d *Device) IsOnlySensor() bool { return !d.HasCapability( DCPower, DCColorHS, DCColorHSK, DCColorKelvin, DCIntensity, DCTemperatureControl, ) } func (d *Device) HasCapability(capabilities ...DeviceCapability) bool { for _, c := range d.Capabilities { for _, c2 := range capabilities { if c == c2 { return true } } } return false } func (d *Device) SetState(newState NewDeviceState) error { if newState.Power != nil && d.HasCapability(DCPower) { d.State.Power = *newState.Power } if newState.Color != nil { parsed, err := color.Parse(*newState.Color) if err != nil { return err } switch { case (parsed.RGB != nil || parsed.XY != nil || parsed.HS != nil) || d.HasCapability(DCColorHS, DCColorXY, DCColorRGB): d.State.Color = parsed case parsed.K != nil && d.HasCapability(DCColorKelvin, DCColorHSK): if !d.HasCapability(DCColorKelvin) { d.State.Color.HS = &color.HueSat{Hue: 0, Sat: 0} } d.State.Color = parsed } } if newState.Intensity != nil && d.HasCapability(DCIntensity) { d.State.Intensity = *newState.Intensity } if newState.Temperature != nil && d.HasCapability(DCTemperatureControl) { d.State.Temperature = *newState.Temperature } return nil } func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState { n := NewDeviceState{} if s.Intensity != nil { intensity := device.State.Intensity * *s.Intensity n.Intensity = &intensity } return n } func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDeviceState { n := NewDeviceState{} if s.Power != nil && other.Power != nil { if fac >= 0.5 { n.Power = other.Power } else { n.Power = s.Power } } if s.Color != nil && other.Color != nil { sc, err := color.Parse(*s.Color) oc, err2 := color.Parse(*other.Color) if err == nil && err2 == nil { rc := sc.Interpolate(oc, fac) rcStr := rc.String() n.Color = &rcStr } } if s.Intensity != nil && other.Intensity != nil { n.Intensity = new(float64) *n.Intensity = interpolateFloat(*s.Intensity, *other.Intensity, fac) } return n } func interpolateFloat(a, b, fac float64) float64 { return (a * (1 - fac)) + (b * fac) } func interpolateInt(a, b int, fac float64) int { return int((float64(a) * (1 - fac)) + (float64(b) * fac)) }