|
| 1 | +# |
| 2 | +# Matter_EventHandler.be - suppport for Matter Events |
| 3 | +# |
| 4 | +# Copyright (C) 2023 Stephan Hadinger & Theo Arends |
| 5 | +# |
| 6 | +# This program is free software: you can redistribute it and/or modify |
| 7 | +# it under the terms of the GNU General Public License as published by |
| 8 | +# the Free Software Foundation, either version 3 of the License, or |
| 9 | +# (at your option) any later version. |
| 10 | +# |
| 11 | +# This program is distributed in the hope that it will be useful, |
| 12 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +# GNU General Public License for more details. |
| 15 | +# |
| 16 | +# You should have received a copy of the GNU General Public License |
| 17 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 18 | +# |
| 19 | + |
| 20 | +import matter |
| 21 | + |
| 22 | +#@ solidify:Matter_EventHandler,weak |
| 23 | +#@ solidify:Matter_EventQueued,weak |
| 24 | + |
| 25 | +################################################################################# |
| 26 | +# Matter_Event_Queued class |
| 27 | +# |
| 28 | +# This class encapsulates any element within teh event queue |
| 29 | +# Takes a `Matter_EventDataIB` |
| 30 | +# |
| 31 | +# Invariants: |
| 32 | +# `priority` is guaranteed to be in range 0..2 |
| 33 | +################################################################################# |
| 34 | +class Matter_EventQueued |
| 35 | + # common elements |
| 36 | + var endpoint |
| 37 | + var cluster |
| 38 | + var event_id |
| 39 | + var is_urgent |
| 40 | + var priority |
| 41 | + var event_no |
| 42 | + var raw_tlv # content encoded as full TLV |
| 43 | + |
| 44 | + def init(event_ib) |
| 45 | + self.endpoint = event_ib.path.endpoint |
| 46 | + self.cluster = event_ib.path.cluster |
| 47 | + self.event_id = event_ib.path.event |
| 48 | + self.is_urgent = event_ib.path.is_urgent |
| 49 | + self.priority = event_ib.priority |
| 50 | + if (self.priority < 0) self.priority = 0 end |
| 51 | + if (self.priority > matter.EVENT_CRITICAL) self.priority = matter.EVENT_CRITICAL end |
| 52 | + self.event_no = int64.toint64(event_ib.event_number) # int64 |
| 53 | + self.raw_tlv = event_ib.to_TLV().tlv2raw() # bytes() |
| 54 | + end |
| 55 | +end |
| 56 | +matter.EventQueued = Matter_EventQueued |
| 57 | + |
| 58 | +# elements are made of `Matter_EventDataIB` |
| 59 | +# var path # |
| 60 | + # var node # u64 as bytes |
| 61 | + # var endpoint # u16 |
| 62 | + # var cluster # u32 |
| 63 | + # var event # u32 |
| 64 | + # var is_urgent # bool |
| 65 | +# var event_number # u64 as bytes |
| 66 | +# var priority # u8 |
| 67 | +# # one of |
| 68 | +# var epoch_timestamp # u64 |
| 69 | +# var system_timestamp # u64 |
| 70 | +# var delta_epoch_timestamp # u64 |
| 71 | +# var delta_system_timestamp # u64 |
| 72 | +# # data |
| 73 | +# var data # any TLV |
| 74 | + |
| 75 | +# EVENT_DEBUG=0 |
| 76 | +# EVENT_INFO=1 |
| 77 | +# EVENT_CRITICAL=2 |
| 78 | + |
| 79 | +################################################################################# |
| 80 | +# Matter_IM class |
| 81 | +################################################################################# |
| 82 | +class Matter_EventHandler |
| 83 | + static var EVENT_NO_INCR = 1000 # counter increased when persisting |
| 84 | + static var EVENT_NO_FILENAME = "_matter_event_no" |
| 85 | + static var EVENT_QUEUE_SIZE_MAX = 10 # each queue is 10 elements depth |
| 86 | + |
| 87 | + # circular buffers |
| 88 | + var queue_debug # queue of events for level DEBUG |
| 89 | + var queue_info # queue of events for level INFO |
| 90 | + var queue_critical # queue of events for level CRITICAL |
| 91 | + |
| 92 | + var device # link back to matter_device top object |
| 93 | + # Events |
| 94 | + var counter_event_no # event number, monotonically increasing even after restarts |
| 95 | + var counter_event_no_persisted # the nest number persisted for after the restart |
| 96 | + |
| 97 | + def init(device) |
| 98 | + self.device = device |
| 99 | + self.queue_debug = [] |
| 100 | + self.queue_info = [] |
| 101 | + self.queue_critical = [] |
| 102 | + self.load_event_no_persisted() # initializes self.counter_event_no and self.counter_event_no_persisted |
| 103 | + end |
| 104 | + |
| 105 | + ##################################################################### |
| 106 | + # load_event_no_persisted |
| 107 | + # |
| 108 | + # Load the next acceptable event_no from `persist` and persist it |
| 109 | + # with a predefined gap `self.EVENT_NO_INCR` (default 1000) |
| 110 | + def load_event_no_persisted() |
| 111 | + import persist |
| 112 | + var event_no_str = str(persist.find(self.EVENT_NO_FILENAME, "0")) |
| 113 | + self.counter_event_no = int64.fromstring(event_no_str) |
| 114 | + self.counter_event_no_persisted = self.counter_event_no.add(self.EVENT_NO_INCR) |
| 115 | + # save back next slot |
| 116 | + persist.setmember(self.EVENT_NO_FILENAME, self.counter_event_no_persisted.tostring()) |
| 117 | + persist.save() |
| 118 | + end |
| 119 | + |
| 120 | + ##################################################################### |
| 121 | + # Enqueue event |
| 122 | + # |
| 123 | + # Takes `Matter_EventDataIB` |
| 124 | + ##################################################################### |
| 125 | + def queue_event(ev_ib) |
| 126 | + var ev_queued = matter.EventQueued(ev_ib) |
| 127 | + |
| 128 | + var cur_prio = ev_queued.priority |
| 129 | + |
| 130 | + # we reuse the same logic as connectedhomeip |
| 131 | + # https://github.com/project-chip/connectedhomeip/blob/master/src/app/EventManagement.h |
| 132 | + # |
| 133 | + # Here is a copy of the original comment: |
| 134 | + |
| 135 | + #--------------------------------------------------------------------------------- |
| 136 | + * A newly generated event will be placed in the lowest-priority (in practice |
| 137 | + * DEBUG) buffer, the one associated with the first LogStorageResource. If |
| 138 | + * there is no space in that buffer, space will be created by evicting the |
| 139 | + * oldest event currently in that buffer, until enough space is available. |
| 140 | + * |
| 141 | + * When an event is evicted from a buffer, there are two possibilities: |
| 142 | + * |
| 143 | + * 1) If the next LogStorageResource has a priority that is no higher than the |
| 144 | + * event's priority, the event will be moved to that LogStorageResource's |
| 145 | + * buffer. This may in turn require events to be evicted from that buffer. |
| 146 | + * 2) If the next LogStorageResource has a priority that is higher than the |
| 147 | + * event's priority, then the event is just dropped. |
| 148 | + * |
| 149 | + * This means that LogStorageResources at a given priority level are reserved |
| 150 | + * for events of that priority level or higher priority. |
| 151 | + * |
| 152 | + * As a simple example, assume there are only two priority levels, DEBUG and |
| 153 | + * CRITICAL, and two LogStorageResources with those priorities. In that case, |
| 154 | + * old CRITICAL events will not start getting dropped until both buffers are |
| 155 | + * full, while old DEBUG events will start getting dropped once the DEBUG |
| 156 | + * LogStorageResource buffer is full. |
| 157 | + ---------------------------------------------------------------------------------# |
| 158 | + |
| 159 | + # first step, always add to DEBUG queue |
| 160 | + self.queue_debug.push(ev_queued) |
| 161 | + # if DEBUG queue is full |
| 162 | + if size(self.queue_debug) > self.EVENT_QUEUE_SIZE_MAX |
| 163 | + # remove first (oldest element) |
| 164 | + var ev_debug_removed = self.queue_debug.pop(0) |
| 165 | + if ev_debug_removed.priority > matter.EVENT_DEBUG |
| 166 | + # if removed item is higher than DEBUG, push to INFO queue |
| 167 | + self.queue_info.push(ev_debug_removed) |
| 168 | + # if INFO queue is full |
| 169 | + if size(self.queue_info) > self.EVENT_QUEUE_SIZE_MAX |
| 170 | + # remove first (oldest element) |
| 171 | + var ev_info_removed = self.queue_info.pop(0) |
| 172 | + if ev_info_removed.priority > matter.EVENT_INFO |
| 173 | + # if removed item is higher than INFO, push to CRITICAL queue |
| 174 | + self.queue_critical.push(ev_info_removed) |
| 175 | + # if CRITICAL queue is full |
| 176 | + if size(self.queue_critical) > self.EVENT_QUEUE_SIZE_MAX |
| 177 | + # remove first (oldest element) |
| 178 | + var ev_critical_removed = self.queue_critical.pop(0) |
| 179 | + end |
| 180 | + end |
| 181 | + end |
| 182 | + end |
| 183 | + end |
| 184 | + end |
| 185 | + |
| 186 | + ##################################################################### |
| 187 | + # Events handling |
| 188 | + ##################################################################### |
| 189 | + # Get next event number |
| 190 | + def get_next_event_no() |
| 191 | + self.counter_event_no = self.counter_event_no.add(1) |
| 192 | + if self.counter_event_no >= self.counter_event_no_persisted |
| 193 | + self.load_event_no_persisted() # force an increment like done during boot |
| 194 | + end |
| 195 | + return self.counter_event_no |
| 196 | + end |
| 197 | + |
| 198 | + |
| 199 | + ##################################################################### |
| 200 | + # Dump events for debugging |
| 201 | + ##################################################################### |
| 202 | + def dump() |
| 203 | + tasmota.log(f"MTR: Events queues sizes: critical {size(self.queue_critical)}, info {size(self.queue_info)}, debug {size(self.queue_debug)}", 2) |
| 204 | + var cnt = [0, 0, 0] # counters |
| 205 | + for ev: self.queue_debug cnt[ev.priority] += 1 end |
| 206 | + for ev: self.queue_info cnt[ev.priority] += 1 end |
| 207 | + for ev: self.queue_critical cnt[ev.priority] += 1 end |
| 208 | + tasmota.log(f"MTR: Events by types: critical {cnt[2]}, info {cnt[1]}, debug {cnt[0]}", 2) |
| 209 | + end |
| 210 | + |
| 211 | +end |
| 212 | +matter.EventHandler = Matter_EventHandler |
| 213 | + |
| 214 | +#- |
| 215 | +
|
| 216 | +# Unit tests |
| 217 | +
|
| 218 | +
|
| 219 | +-# |
| 220 | + |
0 commit comments