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.

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