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.
315 lines
6.9 KiB
315 lines
6.9 KiB
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
|
|
}
|