|
|
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 = "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 ErrSceneRoleNoStates }
switch r.TargetKind { case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll: default: return ErrBadInput }
switch r.PowerMode { case SPScene, SPDevice, SPBoth: default: return ErrSceneRoleUnknownPowerMode }
switch r.Effect { case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature: 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()) 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 }
|