-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathACSOrchestrator.ts
More file actions
319 lines (268 loc) · 13 KB
/
Copy pathACSOrchestrator.ts
File metadata and controls
319 lines (268 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import { ACSController } from "./ACSController.js";
import { CoreAuthToRequest, CoreConfigReport, CoreInfoOptions, CoreInfoRequest, CoreLogRequest, CoreRole, CoreStateChangeReport, CoreStatusReport, ServerCommand, ServerInfoResponse } from "./ACSFormats.js";
import * as CoreRepo from "../../repositories/Devices/CoreRepository.js";
import * as ACRepo from "../../repositories/Devices/AccessControllerRepository.js";
import * as AuditLogRepo from "../../repositories/AuditLogs/AuditLogRepository.js";
import * as DeviceLogRepo from "../../repositories/Logs/DeviceLogsRepository.js";
import * as UserRepo from "../../repositories/Users/UserRepository.js";
import * as MakerspaceRepo from "../../repositories/Makerspaces/MakerspaceRespository.js";
import * as DeviceRepo from "../../repositories/Devices/DeviceRepository.js";
import { AccessControllerState, DeviceLogSeverity } from "../../knex/tables.js";
import { AccessAttemptReason } from "../devices/accessController.js";
import { Makerspace } from "../makerspaces/makerspace.js";
import { Core } from "../devices/core.js";
import { getInstanceByAccessControllerDeviceAndChannel, getInstanceByAccessControllerID, updateInstanceHobbsTime } from "../../repositories/Equipment/EquipmentInstancesRepository.js";
export class ACSOrchestrator {
private static coreControllers: Map<number, ACSController> = new Map();
public static registerDevice(deviceID: number, controller: ACSController) {
ACSOrchestrator.coreControllers.set(deviceID, controller);
}
public static getDeviceController(deviceID: number): ACSController | undefined {
return ACSOrchestrator.coreControllers.get(deviceID);
}
public static async handleCoreStatusReport(deviceID: number, statusReport: CoreStatusReport) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) {
console.warn("status report from non-core", deviceID)
return;
}
await core.statusUpdate(statusReport.currentCardTag);
const timesToSend = [];
let shouldUpdateTimes = false
for (let i = 0; i < statusReport.channels.length; i++) {
const channel = statusReport.channels[i];
await core.updateControllerState(channel.channelID, channel.state);
const inst = await getInstanceByAccessControllerDeviceAndChannel(deviceID, channel.channelID)
timesToSend.push({ channelID: channel.channelID, hobbsTime: inst? Number(inst.hobbsTime) : 0 })
if (inst == undefined) {
continue;
}
if (inst.hobbsTime > channel.hobbsTime) {
console.warn("hobbs tme fiasco", inst, channel)
// warning: the device is wrong about how long
DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { msg: "hobbs-time-mismatch", reported: channel.hobbsTime, stored: inst.hobbsTime })
shouldUpdateTimes = true
} else {
await updateInstanceHobbsTime(inst.id, channel.hobbsTime)
}
}
if (shouldUpdateTimes) {
ACSOrchestrator.getDeviceController(deviceID)?.sendCoreCommand(core, {
hobbsTime: timesToSend
})
}
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-status-error", error: e });
}
}
public static async handleCoreStateChangeReport(deviceID: number, stateChangeReport: CoreStateChangeReport) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) { return; }
const oldCardTag = core.currentCardTag;
await core.statusUpdate(stateChangeReport.currentCardTag);
for (let i = 0; i < stateChangeReport.channels.length; i++) {
const channel = stateChangeReport.channels[i];
// TODO: put this in an AccessController.reportStateChange function, such that if we detect a state change in a regular status update,
// we can still trigger the proper effects
if (channel.fromState === AccessControllerState.UNLOCKED) {
// Leaving UNLOCKED, register an end of session message
(await ACRepo.getAccessControllersByDeviceAndChannelID(deviceID, channel.channelID))?.endSession(oldCardTag ?? "");
} else if (channel.toState === AccessControllerState.UNLOCKED) {
// Going to UNLOCKED, register start of session message
(await ACRepo.getAccessControllersByDeviceAndChannelID(deviceID, channel.channelID))?.startSession(stateChangeReport.currentCardTag)
}
await core.updateControllerState(channel.channelID, channel.toState);
}
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-state-change-report-error", error: e });
}
}
public static async handleCoreLogRequest(deviceID: number, logRequest: CoreLogRequest) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) { return; }
if (logRequest.auditLog) {
await AuditLogRepo.createAuditLog(
`Message from {device}: ${logRequest.message}`,
logRequest.category,
core.makerspaceID,
{ id: core.deviceID, label: core.name }
)
} else {
await DeviceLogRepo.createDeviceLog(
core.deviceID,
DeviceLogSeverity.LOW,
{ type: "message", message: logRequest.message }
)
}
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-log-error", error: e });
}
}
public static async handleCoreAuthToRequest(deviceID: number, authToRequest: CoreAuthToRequest) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
const user = await UserRepo.getUserByCardTagID(authToRequest.cardTagID);
if (core === undefined || user === undefined) {
await AuditLogRepo.createAuditLog(
`{user} failed to activate {device}`,
"auth",
core?.makerspaceID,
{ id: user?.id ?? -1, label: user ? `${user.firstName} ${user.lastName}` : "Unknown User" },
{ id: deviceID, label: core ? core.name : "unknown device" }
);
if (core !== undefined) {
const controllers = await core.getAccessControllers();
ACSOrchestrator.getDeviceController(deviceID)?.sendCoreAuthToResponse(core, {
channels: controllers.map((controller) => ({
channelID: controller.channelID,
state: authToRequest.state,
approved: false,
reason: AccessAttemptReason.UNKNOWN_USER
})),
cardTagID: authToRequest.cardTagID
})
}
return;
}
const attemptResult = await core.authTo(user.id, authToRequest.state, true);
ACSOrchestrator.getDeviceController(deviceID)?.sendCoreAuthToResponse(core, {
channels: attemptResult,
cardTagID: authToRequest.cardTagID
})
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-authTo-error", error: e });
}
}
public static async handleCoreConfigReport(deviceID: number, configReport: CoreConfigReport) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) { return; }
await core.updateConfiguration(configReport)
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-config-report-error", error: e });
}
}
static async getHMIInfo(core: Core) {
const acs = await core.getAccessControllers()
const welcomeFor = await core.getWelcomeMakerspace()
if (acs.length == 0 && welcomeFor == undefined) {
// this thing is unassociated, doesn't make sense to report as welcome reader or equipment
return undefined;
}
const role = welcomeFor != undefined ? CoreRole.WELCOME : CoreRole.EQUIPMENT
const makerspace = await MakerspaceRepo.getMakerspaceByID(welcomeFor?.id ?? core.makerspaceID)
const channels = await Promise.all(acs.map(async (ac) => {
const entity = await getInstanceByAccessControllerID(ac.id)
return { channelID: ac.channelID, pairedEntity: entity?.name ?? "unknown" };
}));
return {
role: role,
deviceName: core.name,
makerspace: makerspace?.name ?? "unknown",
channels: channels
}
}
public static async handleCoreInfoRequest(deviceID: number, infoRequest: CoreInfoRequest) {
try {
const getHobbsTimes = async (core: Core) => {
const acs = await core.getAccessControllers()
if (acs.length == 0) {
console.warn("warn: hobbs time request from device with no controllers ")
return undefined
}
const times = await Promise.all(acs.map(async (ac) => {
const inst = await getInstanceByAccessControllerDeviceAndChannel(acs[0].deviceID, ac.channelID);
return { channelID: ac.channelID, hobbsTime: inst ? Number(inst.hobbsTime) : 0 };
}))
return times;
};
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) { return; }
const response: ServerInfoResponse = {
time: infoRequest.fields.includes(CoreInfoOptions.TIME)
? (new Date).getTime() : undefined,
state: infoRequest.fields.includes(CoreInfoOptions.STATE)
? (await core.getAccessControllers()).map((controller) => ({ id: controller.channelID, state: controller.state })) : undefined,
hmi: infoRequest.fields.includes(CoreInfoOptions.HMI) ? await this.getHMIInfo(core) : undefined,
flags: infoRequest.fields.includes(CoreInfoOptions.FLAGS)
? {
lockWhenIdle: core.flags?.lockWhenIdle ?? false,
restartWhenUnused: core.flags?.restartWhenUnused ?? false,
welcoming: (await core.getWelcomeMakerspace()) !== undefined
} : undefined,
hobbsTime: infoRequest.fields.includes(CoreInfoOptions.HOBBS_TIME) ? await getHobbsTimes(core) : undefined
}
ACSOrchestrator.getDeviceController(core.deviceID)?.sendCoreInfoResponse(core, response);
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-info-request-error", error: e });
}
}
public static async handleSendCoreCommand(deviceID: number, command: ServerCommand) {
try {
const core = await CoreRepo.getCoreByDeviceID(deviceID);
if (core === undefined) { return; }
ACSOrchestrator.getDeviceController(deviceID)?.sendCoreCommand(core, command);
} catch (e) {
await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "send-core-command-error", error: e });
}
}
public static async handleWelcomeRequest(makerspaceID: number, deviceID: number, cardTagID: string) {
try {
const device = await DeviceRepo.getDeviceByID(deviceID);
if (device === undefined) { console.log("can't find device"); return; }
const rawSpace = await MakerspaceRepo.getMakerspaceByID(makerspaceID);
if (rawSpace === undefined) { console.log("can't find makerspace"); return; }
const user = await UserRepo.getUserByCardTagID(cardTagID);
if (user === undefined) {
ACSOrchestrator.getDeviceController(deviceID)?.sendWelcomeResponse(device, { welcomed: false, cardTagID: cardTagID });
await AuditLogRepo.createAuditLog(
`Unknown card {conceal} failed to sign in to {makerspace}`,
"welcome",
rawSpace.id,
{ id: 0, label: cardTagID },
{ id: rawSpace.id, label: rawSpace.name }
);
return;
}
const welcomeSpace = new Makerspace(rawSpace);
await welcomeSpace.welcome(user.id);
ACSOrchestrator.getDeviceController(deviceID)?.sendWelcomeResponse(device, { welcomed: true, cardTagID: cardTagID });
await AuditLogRepo.createAuditLog(
`{user} signed in to {makerspace}`,
"welcome",
rawSpace.id,
{ id: user.id, label: `${user.firstName} ${user.lastName}` },
{ id: rawSpace.id, label: rawSpace.name }
);
} catch (e) {
console.warn("ACS: Error handling welcome request: ", e)
}
}
public static async commandAllCores(command: ServerCommand): Promise<void> {
try {
const cores = ACSOrchestrator.coreControllers.keys();
for (const coreID of cores) {
const core = await CoreRepo.getCoreByDeviceID(coreID);
if (core === undefined) { continue; }
ACSOrchestrator.coreControllers.get(coreID)?.sendCoreCommand(core, command);
}
} catch (e) {
console.warn("ACS: Error commanding all cores: ", e)
}
}
public static async commandMakerspaceCores(makerspaceID: number, command: ServerCommand): Promise<void> {
try {
const cores = ACSOrchestrator.coreControllers.keys();
for (const coreID of cores) {
const core = await CoreRepo.getCoreByDeviceID(coreID);
if (core === undefined || core.makerspaceID !== makerspaceID) { continue; }
ACSOrchestrator.coreControllers.get(coreID)?.sendCoreCommand(core, command);
}
} catch (e) {
console.warn("ACS: Error commanding makerspace cores: ", e)
}
}
}