package models import ( "context" "math" "math/rand" "sort" "strings" "time" ) type Scene struct { ID int `json:"id"` Name string `json:"name"` IntervalMS int64 `json:"interval"` Roles []SceneRole `json:"roles"` } func (s *Scene) Validate() error { if s.IntervalMS < 0 { return ErrSceneInvalidInterval } if len(s.Roles) == 0 { return ErrSceneNoRoles } for _, role := range s.Roles { err := role.Validate() if err != nil { return err } } return nil } func (s *Scene) Role(device *Device) *SceneRole { index := s.RoleIndex(device) if index == -1 { return nil } return &s.Roles[index] } func (s *Scene) RoleIndex(device *Device) int { for i, role := range s.Roles { if role.TargetKind.Matches(device, role.TargetValue) { return i } } return -1 } type SceneEffect string const ( SEStatic SceneEffect = "Plain" SERandom SceneEffect = "Random" SEGradient SceneEffect = "Gradient" SEWalkingGradient SceneEffect = "WalkingGradient" SETransition SceneEffect = "Transition" ) type SceneRole struct { Effect SceneEffect `json:"effect"` PowerMode ScenePowerMode `json:"powerMode"` TargetKind ReferenceKind `json:"targetKind"` TargetValue string `json:"targetValue"` Interpolate bool `json:"interpolate"` Order string `json:"order"` States []NewDeviceState `json:"states"` } type SceneRunContext struct { Index int Length int IntervalNumber int64 IntervalMax int64 } func (d *SceneRunContext) PositionFacShifted() float64 { shiftFac := float64((int64(d.Index)+d.IntervalNumber)%int64(d.Length)) * (1 / float64(d.Length)) return math.Mod(d.PositionFac()+shiftFac, 1) } func (d *SceneRunContext) PositionFac() float64 { return float64(d.Index) / float64(d.Length) } func (d *SceneRunContext) IntervalFac() float64 { fac := float64(d.IntervalNumber) / float64(d.IntervalMax) if fac > 1 { return 1 } else if fac < 0 { return 0 } else { return fac } } func (r *SceneRole) Validate() error { if len(r.States) == 0 { return ErrSceneRoleNoStates } switch r.TargetKind { case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll: default: return ErrBadInput } switch r.PowerMode { case SPScene, SPDevice: default: return ErrSceneRoleUnknownPowerMode } switch r.Effect { case SEStatic, SERandom, SEGradient, SETransition: default: return ErrSceneRoleUnknownEffect } switch r.Order { case "", "-name", "name", "+name", "-id", "id", "+id": default: return ErrSceneRoleUnsupportedOrdering } return nil } func (r *SceneRole) ApplyOrder(devices []Device) { if r.Order == "" { return } desc := strings.HasPrefix(r.Order, "-") orderKey := r.Order if desc || strings.HasPrefix(r.Order, "+") { orderKey = orderKey[1:] } switch orderKey { case "id": sort.Slice(devices, func(i, j int) bool { if desc { return devices[i].ID > devices[j].ID } return devices[i].ID < devices[j].ID }) case "name": sort.Slice(devices, func(i, j int) bool { if desc { return strings.Compare(devices[i].Name, devices[j].Name) < 0 } return strings.Compare(devices[i].Name, devices[j].Name) > 0 }) } } func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState) { switch r.Effect { case SEStatic: newState = r.States[0] case SERandom: newState = r.State(rand.Float64()) case SEGradient: newState = r.State(c.PositionFac()) case SEWalkingGradient: newState = r.State(c.PositionFacShifted()) case SETransition: newState = r.State(c.IntervalFac()) } switch r.PowerMode { case SPDevice: newState.Power = nil case SPScene: // Do nothing } return } func (r *SceneRole) State(fac float64) NewDeviceState { if len(r.States) == 0 { return NewDeviceState{} } else if len(r.States) == 1 { return r.States[0] } if r.Interpolate { if len(r.States) == 2 { return r.States[0].Interpolate(r.States[1], fac) } else { pos := fac * float64(len(r.States)-1) start := math.Floor(pos) localFac := pos - start return r.States[int(start)].Interpolate(r.States[int(start)+1], localFac) } } else { index := int(fac * float64(len(r.States))) if index == len(r.States) { index = len(r.States) - 1 } return r.States[index] } } type ScenePowerMode string const ( SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off. SPScene ScenePowerMode = "Scene" // Scene state decide power. ) // DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene // instance is created for the group. type DeviceSceneAssignment struct { SceneID int `json:"sceneId"` Group string `json:"group"` StartTime time.Time `json:"start"` DurationMS int64 `json:"durationMs"` } type SceneRepository interface { Find(ctx context.Context, id int) (*Scene, error) FetchAll(ctx context.Context) ([]Scene, error) Save(ctx context.Context, bridge *Scene) error Delete(ctx context.Context, bridge *Scene) error }