nodejs-poolcontroller 8.4.0 → 8.4.1
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/.github/workflows/ghcr-publish.yml +1 -1
- package/157_issues.md +101 -0
- package/AGENTS.md +17 -1
- package/README.md +13 -2
- package/controller/Equipment.ts +49 -0
- package/controller/State.ts +8 -0
- package/controller/boards/AquaLinkBoard.ts +174 -2
- package/controller/boards/EasyTouchBoard.ts +44 -0
- package/controller/boards/IntelliCenterBoard.ts +360 -172
- package/controller/boards/NixieBoard.ts +7 -4
- package/controller/boards/SunTouchBoard.ts +1 -0
- package/controller/boards/SystemBoard.ts +39 -4
- package/controller/comms/Comms.ts +9 -3
- package/controller/comms/messages/Messages.ts +218 -24
- package/controller/comms/messages/config/EquipmentMessage.ts +34 -0
- package/controller/comms/messages/config/ExternalMessage.ts +1051 -989
- package/controller/comms/messages/config/GeneralMessage.ts +65 -0
- package/controller/comms/messages/config/OptionsMessage.ts +15 -2
- package/controller/comms/messages/config/PumpMessage.ts +427 -421
- package/controller/comms/messages/config/SecurityMessage.ts +37 -13
- package/controller/comms/messages/status/EquipmentStateMessage.ts +0 -218
- package/controller/comms/messages/status/HeaterStateMessage.ts +27 -15
- package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
- package/controller/comms/messages/status/VersionMessage.ts +67 -18
- package/controller/nixie/chemistry/ChemController.ts +65 -33
- package/controller/nixie/heaters/Heater.ts +10 -1
- package/controller/nixie/pumps/Pump.ts +145 -2
- package/docker-compose.yml +1 -0
- package/logger/Logger.ts +75 -64
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/web/Server.ts +3 -1
- package/web/services/config/Config.ts +150 -1
- package/web/services/state/State.ts +21 -0
- package/web/services/state/StateSocket.ts +28 -0
package/logger/Logger.ts
CHANGED
|
@@ -32,7 +32,7 @@ class Logger {
|
|
|
32
32
|
if (!fs.existsSync(path.join(process.cwd(), '/logs'))) fs.mkdirSync(path.join(process.cwd(), '/logs'));
|
|
33
33
|
this.pktPath = path.join(process.cwd(), '/logs', this.getPacketPath());
|
|
34
34
|
this.captureForReplayBaseDir = path.join(process.cwd(), '/logs/', this.getLogTimestamp());
|
|
35
|
-
|
|
35
|
+
this.captureForReplayPath = this.captureForReplayBaseDir;
|
|
36
36
|
this.pkts = [];
|
|
37
37
|
this.slMessages = [];
|
|
38
38
|
}
|
|
@@ -49,6 +49,7 @@ class Logger {
|
|
|
49
49
|
private pktTimer: NodeJS.Timeout;
|
|
50
50
|
private currentTimestamp: string;
|
|
51
51
|
private _captureInProgress: boolean = false;
|
|
52
|
+
public get captureInProgress(): boolean { return this._captureInProgress; }
|
|
52
53
|
private getPacketPath(): string {
|
|
53
54
|
// changed this to remove spaces from the name
|
|
54
55
|
return 'packetLog(' + this.getLogTimestamp() + ').log';
|
|
@@ -279,7 +280,7 @@ class Logger {
|
|
|
279
280
|
logger.info(`Starting Replay Capture.`);
|
|
280
281
|
// start new replay directory
|
|
281
282
|
|
|
282
|
-
if (!fs.existsSync(this.
|
|
283
|
+
if (!fs.existsSync(this.captureForReplayBaseDir)) fs.mkdirSync(this.captureForReplayBaseDir, { recursive: true });
|
|
283
284
|
|
|
284
285
|
// Create logs subdirectory for additional log files
|
|
285
286
|
let logsSubDir = path.join(this.captureForReplayBaseDir, 'logs');
|
|
@@ -375,74 +376,84 @@ class Logger {
|
|
|
375
376
|
this.transports.console.level = 'silly';
|
|
376
377
|
}
|
|
377
378
|
public async stopCaptureForReplayAsync(remLogs?: any[]):Promise<string> {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
379
|
+
try {
|
|
380
|
+
if (!this._captureInProgress) {
|
|
381
|
+
logger.warn(`stopCaptureForReplayAsync called with no active capture session; creating backup without capture logs`);
|
|
382
|
+
}
|
|
383
|
+
// Get REM server configurations from config
|
|
384
|
+
let configData = config.getSection();
|
|
385
|
+
let remServers = [];
|
|
386
|
+
if (configData.web && configData.web.interfaces) {
|
|
387
|
+
for (let interfaceName in configData.web.interfaces) {
|
|
388
|
+
let interfaceConfig = configData.web.interfaces[interfaceName];
|
|
389
|
+
if (interfaceConfig.type === 'rem' && interfaceConfig.enabled) {
|
|
390
|
+
remServers.push({
|
|
391
|
+
name: interfaceConfig.name || interfaceName,
|
|
392
|
+
uuid: interfaceConfig.uuid,
|
|
393
|
+
host: interfaceConfig.options?.host || '',
|
|
394
|
+
backup: true
|
|
395
|
+
});
|
|
394
396
|
}
|
|
395
397
|
}
|
|
398
|
+
}
|
|
396
399
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
zip.file(`njsPC/logs/${
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
400
|
+
// Use the existing backup logic to create the base backup.
|
|
401
|
+
const ts = this.currentTimestamp || this.getLogTimestamp();
|
|
402
|
+
let backupOptions = {
|
|
403
|
+
njsPC: true,
|
|
404
|
+
servers: remServers,
|
|
405
|
+
name: `Packet Capture ${ts}`,
|
|
406
|
+
automatic: false
|
|
407
|
+
};
|
|
408
|
+
let backupFile = await webApp.backupServer(backupOptions);
|
|
409
|
+
// Add packet capture logs to the existing backup zip
|
|
410
|
+
let jszip = require("jszip");
|
|
411
|
+
let zip = await jszip.loadAsync(fs.readFileSync(backupFile.filePath));
|
|
412
|
+
|
|
413
|
+
// Add packet capture logs to the njsPC/logs directory if present.
|
|
414
|
+
if (typeof logger.pktPath === 'string' && logger.pktPath.length > 0 && fs.existsSync(logger.pktPath)) {
|
|
415
|
+
zip.file(`njsPC/logs/${path.basename(logger.pktPath)}`, fs.readFileSync(logger.pktPath));
|
|
416
|
+
} else {
|
|
417
|
+
logger.warn(`Packet capture log file unavailable during stopCaptureForReplayAsync; skipping packet log attachment`);
|
|
418
|
+
}
|
|
419
|
+
if (typeof this.consoleToFilePath === 'string' && this.consoleToFilePath.length > 0 && fs.existsSync(this.consoleToFilePath)) {
|
|
420
|
+
zip.file(`njsPC/logs/${path.basename(this.consoleToFilePath)}`, fs.readFileSync(this.consoleToFilePath));
|
|
421
|
+
} else {
|
|
422
|
+
logger.warn(`Console capture log file unavailable during stopCaptureForReplayAsync; skipping console log attachment`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Add REM server logs if provided.
|
|
426
|
+
if (remLogs && remLogs.length > 0) {
|
|
427
|
+
logger.info(`Adding ${remLogs.length} REM logs to backup`);
|
|
428
|
+
for (let remLog of remLogs) {
|
|
429
|
+
// Create logs directory for the REM server using the hardcoded name.
|
|
430
|
+
let logPath = `Relay Equipment Manager/logs/${remLog.logFileName}`;
|
|
431
|
+
logger.info(`Adding REM log to backup: ${logPath} (size: ${remLog.logData.length} characters)`);
|
|
432
|
+
zip.file(logPath, remLog.logData);
|
|
426
433
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await zip.generateAsync({type:'nodebuffer'}).then(content => {
|
|
430
|
-
fs.writeFileSync(backupFile.filePath, content);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Restore original logging configuration
|
|
434
|
-
this.cfg = config.getSection('log');
|
|
435
|
-
logger._logger.remove(this.transports.file);
|
|
436
|
-
this.transports.console.level = this.cfg.app.level;
|
|
437
|
-
this._captureInProgress = false;
|
|
438
|
-
|
|
439
|
-
resolve(backupFile.filePath);
|
|
434
|
+
} else {
|
|
435
|
+
logger.info(`No REM logs provided to add to backup`);
|
|
440
436
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
437
|
+
|
|
438
|
+
// Generate the updated zip.
|
|
439
|
+
await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
|
|
440
|
+
fs.writeFileSync(backupFile.filePath, content);
|
|
441
|
+
});
|
|
442
|
+
// Restore original logging configuration.
|
|
443
|
+
this.cfg = config.getSection('log');
|
|
444
|
+
if (typeof this.transports.file !== 'undefined') {
|
|
445
|
+
logger._logger.remove(this.transports.file);
|
|
446
|
+
this.transports.file.close();
|
|
447
|
+
this.transports.file = undefined;
|
|
444
448
|
}
|
|
445
|
-
|
|
449
|
+
this.transports.console.level = this.cfg.app.level;
|
|
450
|
+
this._captureInProgress = false;
|
|
451
|
+
return backupFile.filePath;
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
this._captureInProgress = false;
|
|
455
|
+
return Promise.reject(err instanceof Error ? err.message : `${err}`);
|
|
456
|
+
}
|
|
446
457
|
}
|
|
447
458
|
}
|
|
448
459
|
export var logger = new Logger();
|
package/package.json
CHANGED
package/tsconfig.json
CHANGED
package/web/Server.ts
CHANGED
|
@@ -645,12 +645,14 @@ export class HttpServer extends ProtoServer {
|
|
|
645
645
|
private replayInboundMessage(mdata: any) {
|
|
646
646
|
try {
|
|
647
647
|
let msg: Inbound = new Inbound();
|
|
648
|
-
msg.direction = mdata.direction;
|
|
648
|
+
if (typeof mdata.direction !== 'undefined') msg.direction = mdata.direction;
|
|
649
649
|
msg.header = mdata.header;
|
|
650
650
|
msg.payload = mdata.payload;
|
|
651
651
|
msg.preamble = mdata.preamble;
|
|
652
652
|
msg.protocol = mdata.protocol;
|
|
653
653
|
msg.term = mdata.term;
|
|
654
|
+
if (typeof mdata.portId === 'number') msg.portId = mdata.portId;
|
|
655
|
+
msg.scope = 'replay';
|
|
654
656
|
if (msg.isValid) msg.process();
|
|
655
657
|
}
|
|
656
658
|
catch (err) {
|
|
@@ -34,7 +34,99 @@ import { ScreenLogicComms, sl } from "../../../controller/comms/ScreenLogic";
|
|
|
34
34
|
import { screenlogic } from "node-screenlogic";
|
|
35
35
|
|
|
36
36
|
export class ConfigRoute {
|
|
37
|
+
private static securitySessions: Map<string, any> = new Map<string, any>();
|
|
38
|
+
private static isOcpWriteSecurityEnforced(): boolean {
|
|
39
|
+
// Intentionally hard-disabled until OCP security semantics are fully understood.
|
|
40
|
+
// This avoids accidentally locking users out of configuration writes.
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
private static getClientKey(req: express.Request): string {
|
|
44
|
+
const forwarded = (req.headers['x-forwarded-for'] || '') as string;
|
|
45
|
+
const forwardedIp = forwarded.split(',')[0].trim();
|
|
46
|
+
const connIp = ((req.connection as any) || {}).remoteAddress || '';
|
|
47
|
+
return forwardedIp || req.ip || connIp || 'unknown';
|
|
48
|
+
}
|
|
49
|
+
private static getSecuritySession(req: express.Request): any {
|
|
50
|
+
const key = ConfigRoute.getClientKey(req);
|
|
51
|
+
return ConfigRoute.securitySessions.get(key);
|
|
52
|
+
}
|
|
53
|
+
private static clearSecuritySession(req: express.Request): any {
|
|
54
|
+
const key = ConfigRoute.getClientKey(req);
|
|
55
|
+
ConfigRoute.securitySessions.delete(key);
|
|
56
|
+
return {
|
|
57
|
+
isAuthenticated: false,
|
|
58
|
+
isAdmin: false,
|
|
59
|
+
roleId: 0,
|
|
60
|
+
roleName: '',
|
|
61
|
+
ip: key
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
private static createSecuritySession(req: express.Request, role: any): any {
|
|
65
|
+
const key = ConfigRoute.getClientKey(req);
|
|
66
|
+
const session = {
|
|
67
|
+
isAuthenticated: true,
|
|
68
|
+
isAdmin: role.id === 1 || typeof role.name === 'string' && role.name.toLowerCase().indexOf('admin') >= 0,
|
|
69
|
+
roleId: role.id || 0,
|
|
70
|
+
roleName: role.name || '',
|
|
71
|
+
ip: key,
|
|
72
|
+
updatedAt: new Date().toISOString()
|
|
73
|
+
};
|
|
74
|
+
ConfigRoute.securitySessions.set(key, session);
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
private static getRoleForPin(pin: string): any {
|
|
78
|
+
const normalizedPin = (pin || '').toString().replace(/\D/g, '');
|
|
79
|
+
if (normalizedPin.length === 0) return undefined;
|
|
80
|
+
const roles = sys.security.roles.toArray();
|
|
81
|
+
for (let i = 0; i < roles.length; i++) {
|
|
82
|
+
const rolePin = ((roles[i] as any).pin || '').toString().replace(/\D/g, '');
|
|
83
|
+
if (rolePin.length > 0 && rolePin === normalizedPin) return roles[i];
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
private static validateWriteAccess(req: express.Request): { allowed: boolean; reason?: string; session?: any } {
|
|
88
|
+
if (!ConfigRoute.isOcpWriteSecurityEnforced()) return { allowed: true };
|
|
89
|
+
if (!sys.security.enabled) return { allowed: true };
|
|
90
|
+
const session = ConfigRoute.getSecuritySession(req);
|
|
91
|
+
if (typeof session === 'undefined' || !session.isAuthenticated) {
|
|
92
|
+
return { allowed: false, reason: 'Security is enabled; log in with an administrator PIN to change configuration.' };
|
|
93
|
+
}
|
|
94
|
+
if (!session.isAdmin) return { allowed: false, reason: 'Guest sessions are read-only.' };
|
|
95
|
+
return { allowed: true, session: session };
|
|
96
|
+
}
|
|
97
|
+
private static getSessionResponse(req: express.Request): any {
|
|
98
|
+
const session = ConfigRoute.getSecuritySession(req);
|
|
99
|
+
return {
|
|
100
|
+
enabled: sys.security.enabled,
|
|
101
|
+
session: typeof session === 'undefined' ? {
|
|
102
|
+
isAuthenticated: false,
|
|
103
|
+
isAdmin: false,
|
|
104
|
+
roleId: 0,
|
|
105
|
+
roleName: '',
|
|
106
|
+
ip: ConfigRoute.getClientKey(req)
|
|
107
|
+
} : session
|
|
108
|
+
};
|
|
109
|
+
}
|
|
37
110
|
public static initRoutes(app: express.Application) {
|
|
111
|
+
app.use('/config', (req, res, next) => {
|
|
112
|
+
const method = (req.method || '').toUpperCase();
|
|
113
|
+
if (method !== 'PUT' && method !== 'POST' && method !== 'DELETE') return next();
|
|
114
|
+
const reqPath = req.path || req.url || '';
|
|
115
|
+
if (
|
|
116
|
+
reqPath.startsWith('/security/login') ||
|
|
117
|
+
reqPath.startsWith('/security/logout') ||
|
|
118
|
+
reqPath.startsWith('/security/session')
|
|
119
|
+
) {
|
|
120
|
+
return next();
|
|
121
|
+
}
|
|
122
|
+
const access = ConfigRoute.validateWriteAccess(req);
|
|
123
|
+
if (access.allowed) return next();
|
|
124
|
+
return res.status(403).send({
|
|
125
|
+
error: 'FORBIDDEN',
|
|
126
|
+
message: access.reason,
|
|
127
|
+
security: ConfigRoute.getSessionResponse(req)
|
|
128
|
+
});
|
|
129
|
+
});
|
|
38
130
|
app.get('/config/body/:body/heatModes', (req, res) => {
|
|
39
131
|
return res.status(200).send(sys.bodies.getItemById(parseInt(req.params.body, 10)).getHeatModes());
|
|
40
132
|
});
|
|
@@ -65,6 +157,28 @@ export class ConfigRoute {
|
|
|
65
157
|
};
|
|
66
158
|
return res.status(200).send(opts);
|
|
67
159
|
});
|
|
160
|
+
app.get('/config/options/security', (req, res) => {
|
|
161
|
+
return res.status(200).send({
|
|
162
|
+
security: sys.security.get(true),
|
|
163
|
+
session: ConfigRoute.getSessionResponse(req).session
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
app.get('/config/options/alerts', (req, res) => {
|
|
167
|
+
return res.status(200).send({
|
|
168
|
+
alerts: sys.alerts.get(true),
|
|
169
|
+
// Existing app-side alert-related options still live in general options.
|
|
170
|
+
poolOptions: {
|
|
171
|
+
cooldownDelay: sys.general.options.cooldownDelay,
|
|
172
|
+
heaterStartDelay: sys.general.options.heaterStartDelay,
|
|
173
|
+
valveDelayTime: sys.general.options.valveDelayTime,
|
|
174
|
+
manualPriority: sys.general.options.manualPriority
|
|
175
|
+
},
|
|
176
|
+
runtime: {
|
|
177
|
+
chemControllers: state.chemControllers.getExtended(),
|
|
178
|
+
chemDosers: state.chemDosers.getExtended()
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
68
182
|
app.get('/config/options/rs485', async (req, res, next) => {
|
|
69
183
|
try {
|
|
70
184
|
let opts = { ports: [], local: [], screenlogic: {} }
|
|
@@ -245,6 +359,7 @@ export class ConfigRoute {
|
|
|
245
359
|
maxHeaters: sys.equipment.maxHeaters,
|
|
246
360
|
heaters: sys.heaters.get(),
|
|
247
361
|
heaterTypes: sys.board.valueMaps.heaterTypes.toArray(),
|
|
362
|
+
equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
|
|
248
363
|
// Align with `/config/body/:id/heatModes` (body picklist). This ensures any board-specific
|
|
249
364
|
// filtering (e.g. IntelliCenter v3 preferred-mode suppression) is reflected consistently.
|
|
250
365
|
// Future improvement should return valid modes per body.
|
|
@@ -307,6 +422,7 @@ export class ConfigRoute {
|
|
|
307
422
|
// waterFlow: sys.board.valueMaps.chemControllerWaterFlow.toArray(), // remove
|
|
308
423
|
controllers: sys.chemControllers.get(),
|
|
309
424
|
maxChemControllers: sys.equipment.maxChemControllers,
|
|
425
|
+
intellichemStandaloneSupported: sys.controllerType === ControllerType.Nixie,
|
|
310
426
|
doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
|
|
311
427
|
chlorinators: sys.chlorinators.get(),
|
|
312
428
|
};
|
|
@@ -860,6 +976,35 @@ export class ConfigRoute {
|
|
|
860
976
|
sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
|
|
861
977
|
return res.status(200).send('OK');
|
|
862
978
|
}); */
|
|
979
|
+
app.get('/config/security/session', (req, res) => {
|
|
980
|
+
return res.status(200).send(ConfigRoute.getSessionResponse(req));
|
|
981
|
+
});
|
|
982
|
+
app.put('/config/security/login', (req, res) => {
|
|
983
|
+
if (!sys.security.enabled) {
|
|
984
|
+
return res.status(409).send({
|
|
985
|
+
error: 'SECURITY_DISABLED',
|
|
986
|
+
message: 'Panel security is disabled.',
|
|
987
|
+
security: ConfigRoute.getSessionResponse(req)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const role = ConfigRoute.getRoleForPin(((req.body || {}).pin || '').toString());
|
|
991
|
+
if (typeof role === 'undefined') {
|
|
992
|
+
return res.status(401).send({
|
|
993
|
+
error: 'INVALID_PIN',
|
|
994
|
+
message: 'PIN does not match a configured security role.'
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return res.status(200).send({
|
|
998
|
+
enabled: sys.security.enabled,
|
|
999
|
+
session: ConfigRoute.createSecuritySession(req, role)
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
app.put('/config/security/logout', (req, res) => {
|
|
1003
|
+
return res.status(200).send({
|
|
1004
|
+
enabled: sys.security.enabled,
|
|
1005
|
+
session: ConfigRoute.clearSecuritySession(req)
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
863
1008
|
app.get('/config', (req, res) => {
|
|
864
1009
|
return res.status(200).send(sys.getSection('all'));
|
|
865
1010
|
});
|
|
@@ -927,7 +1072,11 @@ export class ConfigRoute {
|
|
|
927
1072
|
app.get('/app/config/stopPacketCapture', async (req, res, next) => {
|
|
928
1073
|
try {
|
|
929
1074
|
let file = await stopPacketCaptureAsync();
|
|
930
|
-
|
|
1075
|
+
if (typeof file !== 'string' || file.length === 0 || !fs.existsSync(file)) {
|
|
1076
|
+
logger.warn(`stopPacketCapture did not produce a valid backup file path`);
|
|
1077
|
+
return res.status(409).send('Packet capture is not active or no capture file is available.');
|
|
1078
|
+
}
|
|
1079
|
+
return res.download(file);
|
|
931
1080
|
}
|
|
932
1081
|
catch (err) { next(err); }
|
|
933
1082
|
});
|
|
@@ -399,6 +399,27 @@ export class StateRoute {
|
|
|
399
399
|
}
|
|
400
400
|
catch (err) { next(err); }
|
|
401
401
|
});
|
|
402
|
+
app.put('/state/light/setBrightness', async (req, res, next) => {
|
|
403
|
+
try {
|
|
404
|
+
let cstate = await sys.board.circuits.setDimmerLevelAsync(
|
|
405
|
+
parseInt(req.body.id, 10),
|
|
406
|
+
parseInt(typeof req.body.level !== 'undefined' ? req.body.level : req.body.brightness, 10)
|
|
407
|
+
);
|
|
408
|
+
return res.status(200).send(cstate.get(true));
|
|
409
|
+
}
|
|
410
|
+
catch (err) { next(err); }
|
|
411
|
+
});
|
|
412
|
+
app.put('/state/light/setColor', async (req, res, next) => {
|
|
413
|
+
try {
|
|
414
|
+
let cstate = await sys.board.circuits.setLightColorAsync(parseInt(req.body.id, 10), {
|
|
415
|
+
red: parseInt(typeof req.body.red !== 'undefined' ? req.body.red : req.body.r, 10),
|
|
416
|
+
green: parseInt(typeof req.body.green !== 'undefined' ? req.body.green : req.body.g, 10),
|
|
417
|
+
blue: parseInt(typeof req.body.blue !== 'undefined' ? req.body.blue : req.body.b, 10)
|
|
418
|
+
});
|
|
419
|
+
return res.status(200).send(cstate.get(true));
|
|
420
|
+
}
|
|
421
|
+
catch (err) { next(err); }
|
|
422
|
+
});
|
|
402
423
|
app.put('/state/feature/setState', async (req, res, next) => {
|
|
403
424
|
try {
|
|
404
425
|
let isOn = utils.makeBool(typeof req.body.isOn !== 'undefined' ? req.body.isOn : req.body.state);
|
|
@@ -300,6 +300,13 @@ export class StateSocket {
|
|
|
300
300
|
}
|
|
301
301
|
catch (err) { next(err); }
|
|
302
302
|
});
|
|
303
|
+
app.put('/state/light/setTheme', async (req, res, next) => {
|
|
304
|
+
try {
|
|
305
|
+
let theme = await state.circuits.setLightThemeAsync(parseInt(req.body.id, 10), parseInt(req.body.theme, 10));
|
|
306
|
+
return res.status(200).send(theme);
|
|
307
|
+
}
|
|
308
|
+
catch (err) { next(err); }
|
|
309
|
+
});
|
|
303
310
|
|
|
304
311
|
app.put('/state/circuit/setDimmerLevel', async (req, res, next) => {
|
|
305
312
|
try {
|
|
@@ -308,6 +315,27 @@ export class StateSocket {
|
|
|
308
315
|
}
|
|
309
316
|
catch (err) { next(err); }
|
|
310
317
|
});
|
|
318
|
+
app.put('/state/light/setBrightness', async (req, res, next) => {
|
|
319
|
+
try {
|
|
320
|
+
let circuit = await sys.board.circuits.setDimmerLevelAsync(
|
|
321
|
+
parseInt(req.body.id, 10),
|
|
322
|
+
parseInt(typeof req.body.level !== 'undefined' ? req.body.level : req.body.brightness, 10)
|
|
323
|
+
);
|
|
324
|
+
return res.status(200).send(circuit);
|
|
325
|
+
}
|
|
326
|
+
catch (err) { next(err); }
|
|
327
|
+
});
|
|
328
|
+
app.put('/state/light/setColor', async (req, res, next) => {
|
|
329
|
+
try {
|
|
330
|
+
let circuit = await sys.board.circuits.setLightColorAsync(parseInt(req.body.id, 10), {
|
|
331
|
+
red: parseInt(typeof req.body.red !== 'undefined' ? req.body.red : req.body.r, 10),
|
|
332
|
+
green: parseInt(typeof req.body.green !== 'undefined' ? req.body.green : req.body.g, 10),
|
|
333
|
+
blue: parseInt(typeof req.body.blue !== 'undefined' ? req.body.blue : req.body.b, 10)
|
|
334
|
+
});
|
|
335
|
+
return res.status(200).send(circuit);
|
|
336
|
+
}
|
|
337
|
+
catch (err) { next(err); }
|
|
338
|
+
});
|
|
311
339
|
app.put('/state/feature/setState', async (req, res, next) => {
|
|
312
340
|
try {
|
|
313
341
|
await state.features.setFeatureStateAsync(req.body.id, req.body.state);
|