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.

389 lines
8.9 KiB

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