Browse Source

scene system so far.

pull/1/head
Gisle Aune 3 years ago
parent
commit
44716c84d1
  1. 7
      app/api/util.go
  2. 36
      app/services/scene/manager.go
  3. 87
      app/services/scene/scene.go
  4. 49
      models/device.go
  5. 7
      models/errors.go
  6. 204
      models/scene.go
  7. 55
      models/shared.go

7
app/api/util.go

@ -15,6 +15,13 @@ var errorMap = map[error]int{
models.ErrBadColor: 400, models.ErrBadColor: 400,
models.ErrInternal: 500, models.ErrInternal: 500,
models.ErrUnknownColorFormat: 400, models.ErrUnknownColorFormat: 400,
models.ErrSceneInvalidInterval: 400,
models.ErrSceneNoRoles: 400,
models.ErrSceneRoleNoStates: 400,
models.ErrSceneRoleUnsupportedOrdering: 422,
models.ErrSceneRoleUnknownEffect: 422,
models.ErrSceneRoleUnknownPowerMode: 422,
} }
type response struct { type response struct {

36
app/services/scene/manager.go

@ -0,0 +1,36 @@
package scene
import (
"git.aiterp.net/lucifer/new-server/models"
"sync"
)
type Manager struct {
mu sync.Mutex
assignments map[int]*Scene
scenes []*Scene
}
func (mgr *Manager) UpdateDevice(device *models.Device) {
}
func (mgr *Manager) GetUnassigned(devices []models.Device) []models.Device {
mgr.mu.Lock()
defer mgr.mu.Unlock()
res := make([]models.Device, 0, len(devices))
for _, device := range devices {
if _, ok := mgr.assignments[device.ID]; !ok {
res = append(res, device)
}
}
return res
}
var globalManager = &Manager{}
func Get() *Manager {
return globalManager
}

87
app/services/scene/scene.go

@ -0,0 +1,87 @@
package scene
import (
"git.aiterp.net/lucifer/new-server/models"
"time"
)
type Scene struct {
startTime time.Time
duration time.Duration
tag string
data models.Scene
roleList [][]models.Device
deviceMap map[int]*models.Device
roleMap map[int]int
changes map[int]models.NewDeviceState
}
func (s *Scene) UpdateScene(scene models.Scene) {
s.data = scene
s.roleMap = make(map[int]int)
for _, device := range s.deviceMap {
s.moveDevice(*device)
}
}
func (s *Scene) UpsertDevice(device models.Device) {
s.deviceMap[device.ID] = &device
s.moveDevice(device)
}
func (s *Scene) RemoveDevice(device models.Device) {
delete(s.deviceMap, device.ID)
delete(s.roleMap, device.ID)
}
func (s *Scene) runRole(index int) {
}
func (s *Scene) moveDevice(device models.Device, run bool) {
oldRole, hasOldRole := s.roleMap[device.ID]
newRole := s.data.RoleIndex(&device)
if hasOldRole && oldRole == newRole {
return
}
if hasOldRole {
for i, device2 := range s.roleList[oldRole] {
if device2.ID == device.ID {
s.roleList[oldRole] = append(s.roleList[oldRole][:i], s.roleList[oldRole][i+1:]...)
break
}
}
s.runRole(oldRole)
}
s.roleMap[device.ID] = newRole
s.runRole(newRole)
}
func (s *Scene) intervalNumber() int64 {
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
return int64(time.Since(s.startTime) / intervalDuration)
}
func (s *Scene) intervalFac() float64 {
return clamp(float64(time.Since(s.startTime)) / float64(s.duration))
}
func (s *Scene) positionFac(i, len int) float64 {
return clamp(float64(i) / float64(len))
}
func clamp(fac float64) float64 {
if fac > 1.00 {
return 1.00
} else if fac < 0.00 {
return 0.00
} else {
return fac
}
}

49
models/device.go

@ -3,6 +3,7 @@ package models
import ( import (
"context" "context"
"strings" "strings"
"time"
) )
type Device struct { type Device struct {
@ -37,6 +38,13 @@ type DeviceState struct {
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature"`
} }
type DeviceScene struct {
SceneID int `json:"sceneId"`
Time time.Time `json:"time"`
DurationMS int64 `json:"duration"`
Tag string `json:"tag"`
}
type NewDeviceState struct { type NewDeviceState struct {
Power *bool `json:"power"` Power *bool `json:"power"`
Color *string `json:"color"` Color *string `json:"color"`
@ -170,3 +178,44 @@ func (d *Device) SetState(newState NewDeviceState) error {
return nil return nil
} }
func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDeviceState {
n := NewDeviceState{}
if s.Power != nil && other.Power != nil {
if fac >= 0.5 {
n.Power = other.Power
} else {
n.Power = s.Power
}
}
if s.Color != nil && other.Color != nil {
sc, err := ParseColorValue(*s.Color)
oc, err2 := ParseColorValue(*other.Color)
if err == nil && err2 == nil {
rc := ColorValue{}
rc.Hue = interpolateFloat(sc.Hue, oc.Hue, fac)
rc.Saturation = interpolateFloat(sc.Saturation, oc.Saturation, fac)
rc.Kelvin = interpolateInt(sc.Kelvin, oc.Kelvin, fac)
rcStr := rc.String()
n.Color = &rcStr
}
}
if s.Intensity != nil && other.Intensity != nil {
n.Intensity = new(float64)
*n.Intensity = interpolateFloat(*s.Intensity, *other.Intensity, fac)
}
return n
}
func interpolateFloat(a, b, fac float64) float64 {
return (a * (1 - fac)) + (b * fac)
}
func interpolateInt(a, b int, fac float64) int {
return int((float64(a) * (1 - fac)) + (float64(b) * fac))
}

7
models/errors.go

@ -22,3 +22,10 @@ var ErrInvalidPacketSize = errors.New("invalid packet size")
var ErrReadTimeout = errors.New("read timeout") var ErrReadTimeout = errors.New("read timeout")
var ErrUnrecognizedPacketType = errors.New("packet type not recognized") var ErrUnrecognizedPacketType = errors.New("packet type not recognized")
var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running") var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running")
var ErrSceneInvalidInterval = errors.New("scene interval must be 0 (=disabled) or greater")
var ErrSceneNoRoles = errors.New("scene cannot have zero rules")
var ErrSceneRoleNoStates = errors.New("scene rule has no states")
var ErrSceneRoleUnsupportedOrdering = errors.New("scene rule has an unsupported ordering")
var ErrSceneRoleUnknownEffect = errors.New("scene rule has an unknown effect")
var ErrSceneRoleUnknownPowerMode = errors.New("scene rule has an unknown power mode")

204
models/scene.go

@ -0,0 +1,204 @@
package models
import (
"math"
"math/rand"
"sort"
"strings"
)
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"
SETransition SceneEffect = "Transition"
)
type SceneRole struct {
Effect SceneEffect `json:"effect"`
PowerMode ScenePowerMode `json:"overridePower"`
TargetKind ReferenceKind `json:"targetKind"`
TargetValue string `json:"targetValue"`
Interpolate bool `json:"interpolate"`
Order string `json:"order"`
States []NewDeviceState `json:"states"`
}
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, SPOverride:
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, intervalFac float64, positionFac float64) (newState NewDeviceState, deviceChanged bool) {
switch r.Effect {
case SEStatic:
newState = r.States[0]
case SERandom:
newState = r.State(rand.Float64())
case SEGradient:
newState = r.State(positionFac)
case SETransition:
newState = r.State(intervalFac)
}
switch r.PowerMode {
case SPDevice:
newState.Power = nil
case SPScene:
// Do nothing
case SPOverride:
if newState.Power != nil && device.State.Power != *newState.Power {
device.State.Power = *newState.Power
deviceChanged = true
}
}
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.
SPOverride ScenePowerMode = "Override" // Same as above, but Scene state set Device state' power. This is good for "wake up" scenes.
)
type SceneAssignment struct {
Scene *Scene
Device *Device
Tag string
}

55
models/shared.go

@ -1,5 +1,10 @@
package models package models
import (
"strconv"
"strings"
)
type ReferenceKind string type ReferenceKind string
var ( var (
@ -9,3 +14,53 @@ var (
RKAll ReferenceKind = "All" RKAll ReferenceKind = "All"
RKName ReferenceKind = "Name" RKName ReferenceKind = "Name"
) )
func (rk ReferenceKind) Matches(device *Device, value string) bool {
switch rk {
case RKName:
if strings.HasPrefix(value, "*") {
return strings.HasSuffix(device.Name, value[1:])
} else if strings.HasSuffix(value, "*") {
return strings.HasPrefix(device.Name, value[:len(value)-1])
} else {
return device.Name == value
}
case RKDeviceID:
idStr := strconv.Itoa(device.ID)
for _, idStr2 := range strings.Split(value, ",") {
if idStr == idStr2 {
return true
}
}
case RKBridgeID:
idStr := strconv.Itoa(device.BridgeID)
for _, idStr2 := range strings.Split(value, ",") {
if idStr == idStr2 {
return true
}
}
case RKTag:
hadAny := false
for _, tag := range strings.Split(value, ",") {
if strings.HasPrefix(tag, "-") {
if device.HasTag(tag[1:]) {
return false
}
} else if strings.HasPrefix(tag, "+") {
if !device.HasTag(tag[1:]) {
return false
}
} else {
if device.HasTag(tag) {
hadAny = true
}
}
}
return hadAny
case RKAll:
return true
}
return false
}
Loading…
Cancel
Save