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.

340 lines
7.6 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
1 year ago
2 years ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
1 year ago
1 year ago
2 years ago
2 years ago
1 year ago
  1. package effectenforcer
  2. import (
  3. lucifer3 "git.aiterp.net/lucifer3/server"
  4. "git.aiterp.net/lucifer3/server/commands"
  5. "git.aiterp.net/lucifer3/server/device"
  6. "git.aiterp.net/lucifer3/server/events"
  7. "git.aiterp.net/lucifer3/server/internal/color"
  8. "git.aiterp.net/lucifer3/server/internal/gentools"
  9. "git.aiterp.net/lucifer3/server/services/variables"
  10. "github.com/google/uuid"
  11. "strings"
  12. "sync"
  13. "sync/atomic"
  14. "time"
  15. )
  16. func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
  17. s := &effectEnforcer{
  18. resolver: resolver,
  19. sceneMap: sceneMap,
  20. list: make([]*effectEnforcerRun, 0, 16),
  21. index: make(map[string]*effectEnforcerRun, 8),
  22. supportFlags: make(map[string]device.SupportFlags),
  23. colorFlags: make(map[string]device.ColorFlags),
  24. }
  25. return s
  26. }
  27. type effectEnforcer struct {
  28. mu sync.Mutex
  29. resolver device.Resolver
  30. sceneMap device.SceneMap
  31. supportFlags map[string]device.SupportFlags
  32. colorFlags map[string]device.ColorFlags
  33. variables variables.Variables
  34. started uint32
  35. list []*effectEnforcerRun
  36. index map[string]*effectEnforcerRun
  37. }
  38. func (s *effectEnforcer) Active() bool {
  39. return true
  40. }
  41. func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) {
  42. switch event := event.(type) {
  43. case events.HardwareState:
  44. s.mu.Lock()
  45. colorFlags := s.colorFlags
  46. supportFlags := s.supportFlags
  47. s.mu.Unlock()
  48. colorFlags = gentools.CopyMap(colorFlags)
  49. supportFlags = gentools.CopyMap(supportFlags)
  50. colorFlags[event.ID] = event.ColorFlags
  51. supportFlags[event.ID] = event.SupportFlags
  52. s.mu.Lock()
  53. s.colorFlags = colorFlags
  54. s.supportFlags = supportFlags
  55. s.mu.Unlock()
  56. case events.DeviceReady:
  57. // If the device is managed by the effect enforcer, cause the effect to be
  58. // re-ran.
  59. s.mu.Lock()
  60. if run, ok := s.index[event.ID]; ok {
  61. run.due = time.Now()
  62. }
  63. s.mu.Unlock()
  64. case variables.PropertyPatch:
  65. s.mu.Lock()
  66. s.variables = s.variables.WithPropertyPatch(event)
  67. s.mu.Unlock()
  68. case events.AliasAdded:
  69. s.triggerVariableEffects()
  70. case events.AliasRemoved:
  71. s.triggerVariableEffects()
  72. case events.MotionSensed:
  73. s.triggerVariableEffects()
  74. case events.TemperatureChanged:
  75. s.triggerVariableEffects()
  76. }
  77. }
  78. func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
  79. switch command := command.(type) {
  80. case commands.Assign:
  81. pointers := s.resolver.Resolve(command.Match)
  82. allowedIDs := make([]string, 0, len(pointers))
  83. for _, ptr := range pointers {
  84. if s.sceneMap.SceneID(ptr.ID) == nil {
  85. allowedIDs = append(allowedIDs, ptr.ID)
  86. }
  87. }
  88. if len(pointers) == 0 {
  89. if command.ID != nil {
  90. bus.RunEvent(events.AssignmentRemoved{ID: *command.ID})
  91. } else {
  92. bus.RunEvent(events.Log{
  93. Level: "info",
  94. Code: "assignment_matched_none",
  95. Message: "Assignment to \"" + command.Match + "\" matched no devices.",
  96. })
  97. }
  98. return
  99. }
  100. id := uuid.New()
  101. if command.ID != nil {
  102. id = *command.ID
  103. }
  104. bus.RunEvent(events.AssignmentCreated{
  105. ID: id,
  106. Match: command.Match,
  107. Effect: command.Effect,
  108. })
  109. s.mu.Lock()
  110. // Create a new run
  111. newRun := &effectEnforcerRun{
  112. match: command.Match,
  113. id: id,
  114. due: time.Now(),
  115. ids: allowedIDs,
  116. effect: command.Effect,
  117. }
  118. s.list = append(s.list, newRun)
  119. // Switch over the indices.
  120. for _, id := range allowedIDs {
  121. if oldRun := s.index[id]; oldRun != nil {
  122. oldRun.remove(id)
  123. }
  124. s.index[id] = newRun
  125. }
  126. s.mu.Unlock()
  127. if atomic.CompareAndSwapUint32(&s.started, 0, 1) {
  128. go s.runLoop(bus)
  129. }
  130. for _, deviceID := range allowedIDs {
  131. bus.RunEvent(events.DeviceAssigned{
  132. DeviceID: deviceID,
  133. AssignmentID: gentools.Ptr(id),
  134. })
  135. }
  136. }
  137. }
  138. func (s *effectEnforcer) triggerVariableEffects() {
  139. now := time.Now()
  140. s.mu.Lock()
  141. for _, run := range s.list {
  142. if _, ok := run.effect.(lucifer3.VariableEffect); ok {
  143. run.due = now
  144. }
  145. }
  146. s.mu.Unlock()
  147. }
  148. func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
  149. deleteList := make([]int, 0, 8)
  150. batch := make(commands.SetStateBatch, 64)
  151. deleteIDs := make([]uuid.UUID, 0)
  152. for now := range time.NewTicker(time.Millisecond * 50).C {
  153. deleteIDs = deleteIDs[:0]
  154. s.mu.Lock()
  155. colorFlags := s.colorFlags
  156. supportFlags := s.supportFlags
  157. for i, run := range s.list {
  158. if run.dead {
  159. deleteIDs = append(deleteIDs, run.id)
  160. deleteList = append(deleteList, i-len(deleteList))
  161. continue
  162. }
  163. if run.due.IsZero() || now.Before(run.due) {
  164. continue
  165. }
  166. for j, id := range run.ids {
  167. if id == "" {
  168. continue
  169. }
  170. state := run.effect.State(j, len(run.ids), run.round)
  171. if vEff, ok := run.effect.(lucifer3.VariableEffect); ok {
  172. variableName := strings.Split(vEff.VariableName(), ".")
  173. if len(variableName) == 0 {
  174. variableName = append(variableName, "avg")
  175. }
  176. var value *variables.AvgMinMax
  177. switch variableName[0] {
  178. case "temperature":
  179. if value2, ok := s.variables.Temperature[run.match]; ok {
  180. value = &value2
  181. }
  182. case "motion":
  183. if value2, ok := s.variables.Motion[run.match]; ok {
  184. value = &value2
  185. }
  186. }
  187. if value != nil {
  188. switch variableName[1] {
  189. case "min":
  190. state = vEff.VariableState(j, len(run.ids), value.Min)
  191. case "max":
  192. state = vEff.VariableState(j, len(run.ids), value.Max)
  193. case "avg":
  194. state = vEff.VariableState(j, len(run.ids), value.Avg)
  195. }
  196. }
  197. }
  198. batch[id] = state
  199. }
  200. if freq := run.effect.Frequency(); freq > 0 {
  201. if freq < 100*time.Millisecond {
  202. run.due = now
  203. } else {
  204. run.due = run.due.Add(freq)
  205. }
  206. run.round += 1
  207. } else {
  208. run.due = time.Time{}
  209. }
  210. }
  211. if len(deleteList) > 0 {
  212. for _, i := range deleteList {
  213. s.list = append(s.list[:i], s.list[i+1:]...)
  214. }
  215. deleteList = deleteList[:0]
  216. }
  217. s.mu.Unlock()
  218. for _, id := range deleteIDs {
  219. bus.RunEvent(events.AssignmentRemoved{ID: id})
  220. }
  221. if len(batch) > 0 {
  222. for id, state := range batch {
  223. sf := supportFlags[id]
  224. if state.Power != nil && !sf.HasAny(device.SFlagPower) {
  225. state.Power = nil
  226. }
  227. if state.Temperature != nil && !sf.HasAny(device.SFlagTemperature) {
  228. state.Temperature = nil
  229. }
  230. if state.Intensity != nil && !sf.HasAny(device.SFlagIntensity) {
  231. state.Intensity = nil
  232. }
  233. if state.Color != nil && !sf.HasAny(device.SFlagColor) {
  234. state.Color = nil
  235. } else if state.Color != nil {
  236. cf := colorFlags[id]
  237. invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
  238. (state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
  239. (state.Color.RGB != nil && !cf.HasAll(device.CFlagRGB)) ||
  240. (state.Color.HS != nil && !cf.HasAll(device.CFlagHS))
  241. if invalid {
  242. var converted color.Color
  243. var ok bool
  244. switch {
  245. case cf.HasAny(device.CFlagXY):
  246. converted, ok = state.Color.ToXY()
  247. case cf.HasAny(device.CFlagRGB):
  248. converted, ok = state.Color.ToRGB()
  249. case cf.HasAny(device.CFlagHS):
  250. converted, ok = state.Color.ToHS()
  251. }
  252. if !ok {
  253. state.Color = nil
  254. } else {
  255. state.Color = &converted
  256. }
  257. }
  258. }
  259. batch[id] = state
  260. }
  261. bus.RunCommand(batch)
  262. batch = make(commands.SetStateBatch, 64)
  263. }
  264. }
  265. }
  266. type effectEnforcerRun struct {
  267. id uuid.UUID
  268. match string
  269. due time.Time
  270. ids []string
  271. effect lucifer3.Effect
  272. dead bool
  273. round int
  274. }
  275. // remove takes out an id from the effect, and returns whether the effect is empty.
  276. func (r *effectEnforcerRun) remove(id string) {
  277. anyLeft := false
  278. for i, id2 := range r.ids {
  279. if id2 == id {
  280. r.ids[i] = ""
  281. } else if id2 != "" {
  282. anyLeft = true
  283. }
  284. }
  285. if !anyLeft {
  286. r.dead = true
  287. }
  288. return
  289. }