@blueharford/scrypted-spatial-awareness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/CLAUDE.md +168 -0
- package/README.md +152 -0
- package/dist/main.nodejs.js +3 -0
- package/dist/main.nodejs.js.LICENSE.txt +1 -0
- package/dist/main.nodejs.js.map +1 -0
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +37376 -0
- package/out/main.nodejs.js.map +1 -0
- package/out/plugin.zip +0 -0
- package/package.json +59 -0
- package/src/alerts/alert-manager.ts +347 -0
- package/src/core/object-correlator.ts +376 -0
- package/src/core/tracking-engine.ts +367 -0
- package/src/devices/global-tracker-sensor.ts +191 -0
- package/src/devices/tracking-zone.ts +245 -0
- package/src/integrations/mqtt-publisher.ts +320 -0
- package/src/main.ts +690 -0
- package/src/models/alert.ts +229 -0
- package/src/models/topology.ts +168 -0
- package/src/models/tracked-object.ts +226 -0
- package/src/state/tracking-state.ts +285 -0
- package/src/ui/editor.html +1051 -0
- package/src/utils/id-generator.ts +36 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracking State Management
|
|
3
|
+
* Stores and manages all tracked objects across cameras
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
TrackedObject,
|
|
8
|
+
GlobalTrackingId,
|
|
9
|
+
ObjectSighting,
|
|
10
|
+
createTrackedObject,
|
|
11
|
+
addSighting,
|
|
12
|
+
addJourneySegment,
|
|
13
|
+
JourneySegment,
|
|
14
|
+
} from '../models/tracked-object';
|
|
15
|
+
|
|
16
|
+
type StateChangeCallback = (objects: TrackedObject[]) => void;
|
|
17
|
+
|
|
18
|
+
export class TrackingState {
|
|
19
|
+
private objects: Map<GlobalTrackingId, TrackedObject> = new Map();
|
|
20
|
+
private objectsByCamera: Map<string, Set<GlobalTrackingId>> = new Map();
|
|
21
|
+
private changeCallbacks: StateChangeCallback[] = [];
|
|
22
|
+
private storage: Storage;
|
|
23
|
+
private console: Console;
|
|
24
|
+
|
|
25
|
+
constructor(storage: Storage, console: Console) {
|
|
26
|
+
this.storage = storage;
|
|
27
|
+
this.console = console;
|
|
28
|
+
this.loadPersistedState();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Create a new tracked object from initial sighting */
|
|
32
|
+
createObject(
|
|
33
|
+
globalId: GlobalTrackingId,
|
|
34
|
+
sighting: ObjectSighting,
|
|
35
|
+
isEntryPoint: boolean
|
|
36
|
+
): TrackedObject {
|
|
37
|
+
const tracked = createTrackedObject(globalId, sighting, isEntryPoint);
|
|
38
|
+
this.upsertObject(tracked);
|
|
39
|
+
return tracked;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Add or update a tracked object */
|
|
43
|
+
upsertObject(object: TrackedObject): void {
|
|
44
|
+
const existing = this.objects.get(object.globalId);
|
|
45
|
+
|
|
46
|
+
// Update camera index - remove from old cameras
|
|
47
|
+
if (existing) {
|
|
48
|
+
for (const cameraId of existing.activeOnCameras) {
|
|
49
|
+
this.objectsByCamera.get(cameraId)?.delete(object.globalId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Add to new cameras
|
|
54
|
+
for (const cameraId of object.activeOnCameras) {
|
|
55
|
+
if (!this.objectsByCamera.has(cameraId)) {
|
|
56
|
+
this.objectsByCamera.set(cameraId, new Set());
|
|
57
|
+
}
|
|
58
|
+
this.objectsByCamera.get(cameraId)!.add(object.globalId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.objects.set(object.globalId, object);
|
|
62
|
+
this.notifyChange();
|
|
63
|
+
this.persistState();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Add a new sighting to an existing tracked object */
|
|
67
|
+
addSighting(globalId: GlobalTrackingId, sighting: ObjectSighting): boolean {
|
|
68
|
+
const tracked = this.objects.get(globalId);
|
|
69
|
+
if (!tracked) return false;
|
|
70
|
+
|
|
71
|
+
addSighting(tracked, sighting);
|
|
72
|
+
|
|
73
|
+
// Update camera index
|
|
74
|
+
if (!this.objectsByCamera.has(sighting.cameraId)) {
|
|
75
|
+
this.objectsByCamera.set(sighting.cameraId, new Set());
|
|
76
|
+
}
|
|
77
|
+
this.objectsByCamera.get(sighting.cameraId)!.add(globalId);
|
|
78
|
+
|
|
79
|
+
this.notifyChange();
|
|
80
|
+
this.persistState();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Add a journey segment (cross-camera transition) */
|
|
85
|
+
addJourney(globalId: GlobalTrackingId, segment: JourneySegment): boolean {
|
|
86
|
+
const tracked = this.objects.get(globalId);
|
|
87
|
+
if (!tracked) return false;
|
|
88
|
+
|
|
89
|
+
addJourneySegment(tracked, segment);
|
|
90
|
+
|
|
91
|
+
// Update camera index
|
|
92
|
+
this.objectsByCamera.get(segment.fromCameraId)?.delete(globalId);
|
|
93
|
+
if (!this.objectsByCamera.has(segment.toCameraId)) {
|
|
94
|
+
this.objectsByCamera.set(segment.toCameraId, new Set());
|
|
95
|
+
}
|
|
96
|
+
this.objectsByCamera.get(segment.toCameraId)!.add(globalId);
|
|
97
|
+
|
|
98
|
+
this.notifyChange();
|
|
99
|
+
this.persistState();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get object by global ID */
|
|
104
|
+
getObject(globalId: GlobalTrackingId): TrackedObject | undefined {
|
|
105
|
+
return this.objects.get(globalId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get all active objects (active or pending state) */
|
|
109
|
+
getActiveObjects(): TrackedObject[] {
|
|
110
|
+
return Array.from(this.objects.values())
|
|
111
|
+
.filter(obj => obj.state === 'active' || obj.state === 'pending');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Get objects currently visible on a specific camera */
|
|
115
|
+
getObjectsOnCamera(cameraId: string): TrackedObject[] {
|
|
116
|
+
const ids = this.objectsByCamera.get(cameraId) || new Set();
|
|
117
|
+
return Array.from(ids)
|
|
118
|
+
.map(id => this.objects.get(id))
|
|
119
|
+
.filter((obj): obj is TrackedObject => !!obj && obj.state === 'active');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Get all objects (including exited and lost) */
|
|
123
|
+
getAllObjects(): TrackedObject[] {
|
|
124
|
+
return Array.from(this.objects.values());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Get count of active objects */
|
|
128
|
+
getActiveCount(): number {
|
|
129
|
+
return this.getActiveObjects().length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Get journey for an object */
|
|
133
|
+
getJourney(globalId: GlobalTrackingId): JourneySegment[] | undefined {
|
|
134
|
+
return this.objects.get(globalId)?.journey;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Mark object as having exited the property */
|
|
138
|
+
markExited(globalId: GlobalTrackingId, exitCameraId: string, exitCameraName?: string): void {
|
|
139
|
+
const obj = this.objects.get(globalId);
|
|
140
|
+
if (obj) {
|
|
141
|
+
obj.state = 'exited';
|
|
142
|
+
obj.hasExited = true;
|
|
143
|
+
obj.exitCamera = exitCameraId;
|
|
144
|
+
obj.exitCameraName = exitCameraName;
|
|
145
|
+
obj.activeOnCameras = [];
|
|
146
|
+
|
|
147
|
+
// Update camera index
|
|
148
|
+
for (const [cameraId, set] of this.objectsByCamera.entries()) {
|
|
149
|
+
set.delete(globalId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.notifyChange();
|
|
153
|
+
this.persistState();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Mark object as lost (not seen for too long) */
|
|
158
|
+
markLost(globalId: GlobalTrackingId): void {
|
|
159
|
+
const obj = this.objects.get(globalId);
|
|
160
|
+
if (obj) {
|
|
161
|
+
obj.state = 'lost';
|
|
162
|
+
obj.activeOnCameras = [];
|
|
163
|
+
|
|
164
|
+
// Update camera index
|
|
165
|
+
for (const [cameraId, set] of this.objectsByCamera.entries()) {
|
|
166
|
+
set.delete(globalId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.notifyChange();
|
|
170
|
+
this.persistState();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Update object to pending state (waiting for correlation) */
|
|
175
|
+
markPending(globalId: GlobalTrackingId): void {
|
|
176
|
+
const obj = this.objects.get(globalId);
|
|
177
|
+
if (obj && obj.state === 'active') {
|
|
178
|
+
obj.state = 'pending';
|
|
179
|
+
this.notifyChange();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Reactivate a pending object */
|
|
184
|
+
reactivate(globalId: GlobalTrackingId): void {
|
|
185
|
+
const obj = this.objects.get(globalId);
|
|
186
|
+
if (obj && obj.state === 'pending') {
|
|
187
|
+
obj.state = 'active';
|
|
188
|
+
this.notifyChange();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Register callback for state changes */
|
|
193
|
+
onStateChange(callback: StateChangeCallback): void {
|
|
194
|
+
this.changeCallbacks.push(callback);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Remove state change callback */
|
|
198
|
+
offStateChange(callback: StateChangeCallback): void {
|
|
199
|
+
const index = this.changeCallbacks.indexOf(callback);
|
|
200
|
+
if (index !== -1) {
|
|
201
|
+
this.changeCallbacks.splice(index, 1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private notifyChange(): void {
|
|
206
|
+
const objects = this.getAllObjects();
|
|
207
|
+
for (const callback of this.changeCallbacks) {
|
|
208
|
+
try {
|
|
209
|
+
callback(objects);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
this.console.error('State change callback error:', e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private persistState(): void {
|
|
217
|
+
try {
|
|
218
|
+
// Only persist active, pending, and recent objects
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const toPersist = Array.from(this.objects.values())
|
|
221
|
+
.filter(obj =>
|
|
222
|
+
obj.state === 'active' ||
|
|
223
|
+
obj.state === 'pending' ||
|
|
224
|
+
(now - obj.lastSeen < 3600000) // Last hour
|
|
225
|
+
);
|
|
226
|
+
this.storage.setItem('tracked-objects', JSON.stringify(toPersist));
|
|
227
|
+
} catch (e) {
|
|
228
|
+
this.console.error('Failed to persist tracking state:', e);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private loadPersistedState(): void {
|
|
233
|
+
try {
|
|
234
|
+
const json = this.storage.getItem('tracked-objects');
|
|
235
|
+
if (json) {
|
|
236
|
+
const objects = JSON.parse(json) as TrackedObject[];
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
|
|
239
|
+
for (const obj of objects) {
|
|
240
|
+
// Mark old active objects as lost
|
|
241
|
+
if (obj.state === 'active' && now - obj.lastSeen > 300000) {
|
|
242
|
+
obj.state = 'lost';
|
|
243
|
+
obj.activeOnCameras = [];
|
|
244
|
+
}
|
|
245
|
+
this.objects.set(obj.globalId, obj);
|
|
246
|
+
|
|
247
|
+
// Rebuild camera index for active objects
|
|
248
|
+
for (const cameraId of obj.activeOnCameras) {
|
|
249
|
+
if (!this.objectsByCamera.has(cameraId)) {
|
|
250
|
+
this.objectsByCamera.set(cameraId, new Set());
|
|
251
|
+
}
|
|
252
|
+
this.objectsByCamera.get(cameraId)!.add(obj.globalId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.console.log(`Loaded ${objects.length} persisted tracked objects`);
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
this.console.error('Failed to load persisted tracking state:', e);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Clean up old objects beyond retention period */
|
|
264
|
+
cleanup(maxAge: number = 86400000): void {
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
let removed = 0;
|
|
267
|
+
|
|
268
|
+
for (const [id, obj] of this.objects.entries()) {
|
|
269
|
+
if (now - obj.lastSeen > maxAge && obj.state !== 'active') {
|
|
270
|
+
this.objects.delete(id);
|
|
271
|
+
removed++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (removed > 0) {
|
|
276
|
+
this.console.log(`Cleaned up ${removed} old tracked objects`);
|
|
277
|
+
this.persistState();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Generate a unique global tracking ID */
|
|
282
|
+
generateId(): GlobalTrackingId {
|
|
283
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
284
|
+
}
|
|
285
|
+
}
|