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.

377 lines
8.6 KiB

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