package models import ( "context" "git.aiterp.net/lucifer/new-server/internal/lerrors" "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 lerrors.ErrSceneInvalidInterval } if len(s.Roles) == 0 { return lerrors.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 = "Static" SERandom SceneEffect = "Random" SEGradient SceneEffect = "Gradient" SEWalkingGradient SceneEffect = "WalkingGradient" SETransition SceneEffect = "Transition" SEMotion SceneEffect = "Motion" SETemperature SceneEffect = "Temperature" ) type SceneRole struct { Effect SceneEffect `json:"effect"` MotionSeconds float64 `json:"motionSeconds"` MinTemperature int `json:"minTemperature"` MaxTemperature int `json:"maxTemperature"` PowerMode ScenePowerMode `json:"powerMode"` TargetKind ReferenceKind `json:"targetKind"` TargetValue string `json:"targetValue"` Interpolate bool `json:"interpolate"` Relative bool `json:"relative"` Order string `json:"order"` States []NewDeviceState `json:"states"` } type SceneRunContext struct { CurrentTime time.Time Index int Length int IntervalNumber int64 IntervalMax int64 Sensors []SceneSensor } type SceneSensor struct { ID int UpdateTime time.Time Temperature *float64 Presence *bool } func (d *SceneRunContext) PositionFacShifted() float64 { shiftFac := float64(d.IntervalNumber%int64(d.Length)) / 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 lerrors.ErrSceneRoleNoStates } switch r.TargetKind { case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll: default: return lerrors.ErrBadInput } switch r.PowerMode { case SPScene, SPDevice, SPBoth: default: return lerrors.ErrSceneRoleUnknownPowerMode } switch r.Effect { case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature: default: return lerrors.ErrSceneRoleUnknownEffect } switch r.Order { case "", "-name", "name", "+name", "-id", "id", "+id": default: return lerrors.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()) case SEMotion: presence := false absenceTime := time.Time{} for _, sensors := range c.Sensors { if sensors.Presence == nil { continue } if *sensors.Presence { presence = true } else { if sensors.UpdateTime.After(absenceTime) { absenceTime = sensors.UpdateTime } } } if presence { newState = r.State(0.0) } else { fac := c.CurrentTime.Sub(absenceTime).Seconds() / r.MotionSeconds if fac < 0 { fac = 0 } else if fac > 1 { fac = 1 } newState = r.State(fac) } case SETemperature: avg := 0.0 count := 0 for _, sensor := range c.Sensors { if sensor.Temperature == nil { continue } if count == 0 { avg = *sensor.Temperature count = 1 } else { avg = ((avg * float64(count)) + *sensor.Temperature) / float64(count+1) count += 1 } } } if r.Relative { newState = newState.RelativeTo(*device) } switch r.PowerMode { case SPDevice: newState.Power = nil case SPScene: // Do nothing case SPBoth: if newState.Power != nil { powerIntersection := *newState.Power && device.State.Power newState.Power = &powerIntersection } } 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. SPBoth ScenePowerMode = "Both" // Power is only on if both Device and Scene says it is. ) // 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"` SceneName string `json:"sceneName"` Group string `json:"group"` StartTime time.Time `json:"start"` DurationMS int64 `json:"durationMs"` } type SceneRepository interface { Find(ctx context.Context, id int) (*Scene, error) FindName(ctx context.Context, name string) (*Scene, error) FetchAll(ctx context.Context) ([]Scene, error) Save(ctx context.Context, bridge *Scene) error Delete(ctx context.Context, bridge *Scene) error }