You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
5.1 KiB
245 lines
5.1 KiB
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"
|
|
)
|
|
|
|
type SceneRole struct {
|
|
Effect SceneEffect `json:"effect"`
|
|
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 {
|
|
Index int
|
|
Length int
|
|
IntervalNumber int64
|
|
IntervalMax int64
|
|
}
|
|
|
|
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:
|
|
default:
|
|
return ErrSceneRoleUnknownPowerMode
|
|
}
|
|
|
|
switch r.Effect {
|
|
case SEStatic, SERandom, SEGradient, SEWalkingGradient, 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())
|
|
}
|
|
|
|
if r.Relative {
|
|
newState = newState.RelativeTo(*device)
|
|
}
|
|
|
|
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
|
|
}
|