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.

240 lines
5.0 KiB

  1. package models
  2. import (
  3. "context"
  4. "math"
  5. "math/rand"
  6. "sort"
  7. "strings"
  8. "time"
  9. )
  10. type Scene struct {
  11. ID int `json:"id"`
  12. Name string `json:"name"`
  13. IntervalMS int64 `json:"interval"`
  14. Roles []SceneRole `json:"roles"`
  15. }
  16. func (s *Scene) Validate() error {
  17. if s.IntervalMS < 0 {
  18. return ErrSceneInvalidInterval
  19. }
  20. if len(s.Roles) == 0 {
  21. return ErrSceneNoRoles
  22. }
  23. for _, role := range s.Roles {
  24. err := role.Validate()
  25. if err != nil {
  26. return err
  27. }
  28. }
  29. return nil
  30. }
  31. func (s *Scene) Role(device *Device) *SceneRole {
  32. index := s.RoleIndex(device)
  33. if index == -1 {
  34. return nil
  35. }
  36. return &s.Roles[index]
  37. }
  38. func (s *Scene) RoleIndex(device *Device) int {
  39. for i, role := range s.Roles {
  40. if role.TargetKind.Matches(device, role.TargetValue) {
  41. return i
  42. }
  43. }
  44. return -1
  45. }
  46. type SceneEffect string
  47. const (
  48. SEStatic SceneEffect = "Plain"
  49. SERandom SceneEffect = "Random"
  50. SEGradient SceneEffect = "Gradient"
  51. SEWalkingGradient SceneEffect = "WalkingGradient"
  52. SETransition SceneEffect = "Transition"
  53. )
  54. type SceneRole struct {
  55. Effect SceneEffect `json:"effect"`
  56. PowerMode ScenePowerMode `json:"powerMode"`
  57. TargetKind ReferenceKind `json:"targetKind"`
  58. TargetValue string `json:"targetValue"`
  59. Interpolate bool `json:"interpolate"`
  60. Order string `json:"order"`
  61. States []NewDeviceState `json:"states"`
  62. }
  63. type SceneRunContext struct {
  64. Index int
  65. Length int
  66. IntervalNumber int64
  67. IntervalMax int64
  68. }
  69. func (d *SceneRunContext) PositionFacShifted() float64 {
  70. shiftFac := float64((int64(d.Index)+d.IntervalNumber)%int64(d.Length)) * (1 / float64(d.Length))
  71. return math.Mod(d.PositionFac()+shiftFac, 1)
  72. }
  73. func (d *SceneRunContext) PositionFac() float64 {
  74. return float64(d.Index) / float64(d.Length)
  75. }
  76. func (d *SceneRunContext) IntervalFac() float64 {
  77. fac := float64(d.IntervalNumber) / float64(d.IntervalMax)
  78. if fac > 1 {
  79. return 1
  80. } else if fac < 0 {
  81. return 0
  82. } else {
  83. return fac
  84. }
  85. }
  86. func (r *SceneRole) Validate() error {
  87. if len(r.States) == 0 {
  88. return ErrSceneRoleNoStates
  89. }
  90. switch r.TargetKind {
  91. case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll:
  92. default:
  93. return ErrBadInput
  94. }
  95. switch r.PowerMode {
  96. case SPScene, SPDevice:
  97. default:
  98. return ErrSceneRoleUnknownPowerMode
  99. }
  100. switch r.Effect {
  101. case SEStatic, SERandom, SEGradient, SETransition:
  102. default:
  103. return ErrSceneRoleUnknownEffect
  104. }
  105. switch r.Order {
  106. case "", "-name", "name", "+name", "-id", "id", "+id":
  107. default:
  108. return ErrSceneRoleUnsupportedOrdering
  109. }
  110. return nil
  111. }
  112. func (r *SceneRole) ApplyOrder(devices []Device) {
  113. if r.Order == "" {
  114. return
  115. }
  116. desc := strings.HasPrefix(r.Order, "-")
  117. orderKey := r.Order
  118. if desc || strings.HasPrefix(r.Order, "+") {
  119. orderKey = orderKey[1:]
  120. }
  121. switch orderKey {
  122. case "id":
  123. sort.Slice(devices, func(i, j int) bool {
  124. if desc {
  125. return devices[i].ID > devices[j].ID
  126. }
  127. return devices[i].ID < devices[j].ID
  128. })
  129. case "name":
  130. sort.Slice(devices, func(i, j int) bool {
  131. if desc {
  132. return strings.Compare(devices[i].Name, devices[j].Name) < 0
  133. }
  134. return strings.Compare(devices[i].Name, devices[j].Name) > 0
  135. })
  136. }
  137. }
  138. func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState) {
  139. switch r.Effect {
  140. case SEStatic:
  141. newState = r.States[0]
  142. case SERandom:
  143. newState = r.State(rand.Float64())
  144. case SEGradient:
  145. newState = r.State(c.PositionFac())
  146. case SEWalkingGradient:
  147. newState = r.State(c.PositionFacShifted())
  148. case SETransition:
  149. newState = r.State(c.IntervalFac())
  150. }
  151. switch r.PowerMode {
  152. case SPDevice:
  153. newState.Power = nil
  154. case SPScene:
  155. // Do nothing
  156. }
  157. return
  158. }
  159. func (r *SceneRole) State(fac float64) NewDeviceState {
  160. if len(r.States) == 0 {
  161. return NewDeviceState{}
  162. } else if len(r.States) == 1 {
  163. return r.States[0]
  164. }
  165. if r.Interpolate {
  166. if len(r.States) == 2 {
  167. return r.States[0].Interpolate(r.States[1], fac)
  168. } else {
  169. pos := fac * float64(len(r.States)-1)
  170. start := math.Floor(pos)
  171. localFac := pos - start
  172. return r.States[int(start)].Interpolate(r.States[int(start)+1], localFac)
  173. }
  174. } else {
  175. index := int(fac * float64(len(r.States)))
  176. if index == len(r.States) {
  177. index = len(r.States) - 1
  178. }
  179. return r.States[index]
  180. }
  181. }
  182. type ScenePowerMode string
  183. const (
  184. SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
  185. SPScene ScenePowerMode = "Scene" // Scene state decide power.
  186. )
  187. // DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
  188. // instance is created for the group.
  189. type DeviceSceneAssignment struct {
  190. SceneID int `json:"sceneId"`
  191. Group string `json:"group"`
  192. StartTime time.Time `json:"start"`
  193. DurationMS int64 `json:"durationMs"`
  194. }
  195. type SceneRepository interface {
  196. Find(ctx context.Context, id int) (*Scene, error)
  197. FetchAll(ctx context.Context) ([]Scene, error)
  198. Save(ctx context.Context, bridge *Scene) error
  199. Delete(ctx context.Context, bridge *Scene) error
  200. }