/* nodejs-poolController. An application to control pool equipment. Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022. Russell Goldin, tagyoureit. russ.goldin@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import * as fs from "fs"; import * as path from "path"; import * as express from "express"; import * as extend from 'extend'; import * as multer from 'multer'; import { sys, LightGroup, ControllerType, Pump, Valve, Body, General, Circuit, ICircuit, Feature, CircuitGroup, CustomNameCollection, Schedule, Chlorinator, Heater, Screenlogic } from "../../../controller/Equipment"; import { config } from "../../../config/Config"; import { logger } from "../../../logger/Logger"; import { utils } from "../../../controller/Constants"; import { ServiceProcessError } from "../../../controller/Errors"; import { state } from "../../../controller/State"; import { stopPacketCaptureAsync, startPacketCapture } from '../../../app'; import { conn } from "../../../controller/comms/Comms"; import { webApp, BackupFile, RestoreFile } from "../../Server"; import { release } from "os"; import { ScreenLogicComms, sl } from "../../../controller/comms/ScreenLogic"; import { screenlogic } from "node-screenlogic"; export class ConfigRoute { private static securitySessions: Map = new Map(); private static isOcpWriteSecurityEnforced(): boolean { return sys.controllerType === 'intellicenter' && sys.security.enabled; } private static getClientKey(req: express.Request): string { const forwarded = (req.headers['x-forwarded-for'] || '') as string; const forwardedIp = forwarded.split(',')[0].trim(); const connIp = ((req.connection as any) || {}).remoteAddress || ''; return forwardedIp || req.ip || connIp || 'unknown'; } private static getSecuritySession(req: express.Request): any { const key = ConfigRoute.getClientKey(req); return ConfigRoute.securitySessions.get(key); } private static clearSecuritySession(req: express.Request): any { const key = ConfigRoute.getClientKey(req); ConfigRoute.securitySessions.delete(key); return { isAuthenticated: false, isAdmin: false, roleId: 0, roleName: '', ip: key }; } private static createSecuritySession(req: express.Request, role: any): any { const key = ConfigRoute.getClientKey(req); const session = { isAuthenticated: true, isAdmin: role.id === 1 || typeof role.name === 'string' && role.name.toLowerCase().indexOf('admin') >= 0, roleId: role.id || 0, roleName: role.name || '', permissionsMask: role.permissionsMask || 0, timeout: role.timeout || 5, ip: key, updatedAt: new Date().toISOString() }; ConfigRoute.securitySessions.set(key, session); return session; } private static getRoleForPin(pin: string): any { const normalizedPin = (pin || '').toString().replace(/\D/g, ''); if (normalizedPin.length === 0) return undefined; const roles = sys.security.roles.toArray(); for (let i = 0; i < roles.length; i++) { const rolePin = ((roles[i] as any).pin || '').toString().replace(/\D/g, ''); if (rolePin.length > 0 && rolePin === normalizedPin) return roles[i]; } return undefined; } private static validateWriteAccess(req: express.Request): { allowed: boolean; reason?: string; session?: any } { if (!ConfigRoute.isOcpWriteSecurityEnforced()) return { allowed: true }; if (!sys.security.enabled) return { allowed: true }; const session = ConfigRoute.getSecuritySession(req); if (typeof session === 'undefined' || !session.isAuthenticated) { return { allowed: false, reason: 'Security is enabled; log in with a PIN to change configuration.' }; } if (session.permissionsMask === 0 && !session.isAdmin) { return { allowed: false, reason: 'Guest sessions are read-only.' }; } return { allowed: true, session: session }; } private static getSessionResponse(req: express.Request): any { const session = ConfigRoute.getSecuritySession(req); const guestEnabled = sys.security.guestEnabled; let guestPermissionsMask = 0; if (guestEnabled) { const roles = sys.security.roles.toArray(); const guest = roles.find((r: any) => r.id === 9 || (typeof r.name === 'string' && r.name.toLowerCase() === 'guest')); if (guest) guestPermissionsMask = (guest as any).permissionsMask || 0; } return { enabled: sys.security.enabled, guestEnabled: guestEnabled, guestPermissionsMask: guestPermissionsMask, session: typeof session === 'undefined' ? { isAuthenticated: false, isAdmin: false, roleId: 0, roleName: '', permissionsMask: 0, timeout: 5, ip: ConfigRoute.getClientKey(req) } : session }; } public static initRoutes(app: express.Application) { app.use('/config', (req, res, next) => { const method = (req.method || '').toUpperCase(); if (method !== 'PUT' && method !== 'POST' && method !== 'DELETE') return next(); const reqPath = req.path || req.url || ''; if ( reqPath.startsWith('/security/login') || reqPath.startsWith('/security/logout') || reqPath.startsWith('/security/session') ) { return next(); } const access = ConfigRoute.validateWriteAccess(req); if (access.allowed) return next(); return res.status(403).send({ error: 'FORBIDDEN', message: access.reason, security: ConfigRoute.getSessionResponse(req) }); }); app.get('/config/body/:body/heatModes', (req, res) => { return res.status(200).send(sys.bodies.getItemById(parseInt(req.params.body, 10)).getHeatModes()); }); app.get('/v2/config/body/:body/heatModes', (req, res) => { return res.status(200).send(sys.board.bodies.getHeatModesV2(parseInt(req.params.body, 10))); }); app.get('/config/circuit/names', (req, res) => { let circuitNames = sys.board.circuits.getCircuitNames(); return res.status(200).send(circuitNames); }); app.get('/config/circuit/references', (req, res) => { let circuits = typeof req.query.circuits === 'undefined' || utils.makeBool(req.query.circuits); let features = typeof req.query.features === 'undefined' || utils.makeBool(req.query.features); let groups = typeof req.query.features === 'undefined' || utils.makeBool(req.query.groups); let virtual = typeof req.query.virtual === 'undefined' || utils.makeBool(req.query.virtual); return res.status(200).send(sys.board.circuits.getCircuitReferences(circuits, features, virtual, groups)); }); /******* CONFIGURATION PICK LISTS/REFERENCES and VALIDATION PARAMETERS *********/ /// Returns an object that contains the general options for setting up the panel. app.get('/config/options/general', (req, res) => { let opts = { countries: sys.board.valueMaps.countries.toArray(), tempUnits: sys.board.valueMaps.tempUnits.toArray(), timeZones: sys.board.valueMaps.timeZones.toArray(), clockSources: sys.board.valueMaps.clockSources.toArray(), clockModes: sys.board.valueMaps.clockModes.toArray(), pool: sys.general.get(true), sensors: sys.board.system.getSensors(), systemUnits: sys.board.valueMaps.systemUnits.toArray() }; return res.status(200).send(opts); }); app.get('/config/options/security', (req, res) => { let sec = sys.security.get(true); let roles = (sec.roles || []).filter((r: any) => { if (r.id === 1) return true; if (!sys.security.enabled) return false; if (r.id === 9 && !sys.security.guestEnabled) return false; return true; }); return res.status(200).send({ security: { ...sec, roles: roles }, session: ConfigRoute.getSessionResponse(req).session }); }); app.get('/config/options/remotes', (req, res) => { let circuits = sys.board.circuits.getCircuitReferences(true, true, false, true); let remoteVirtuals = [ { id: 237, name: 'Heat Boost' }, { id: 238, name: 'Heat Enable' }, { id: 239, name: 'Pump Speed +' }, { id: 240, name: 'Pump Speed -' }, { id: 253, name: 'Pool Heat Enable' }, { id: 254, name: 'All Lights On' }, { id: 255, name: 'All Lights Off' } ]; for (let i = 0; i < remoteVirtuals.length; i++) { circuits.push({ id: remoteVirtuals[i].id, name: remoteVirtuals[i].name, equipmentType: 'virtual' }); } circuits.sort((a, b) => a.id - b.id); let opts = { maxRemotes: sys.equipment.maxRemotes, remoteTypes: sys.board.valueMaps.remoteTypes.toArray(), circuits: circuits, pumps: sys.pumps.get().filter(p => p.isActive), bodies: sys.bodies.get().map((b, i) => ({ val: i, desc: b.name })), remotes: sys.remotes.get() }; return res.status(200).send(opts); }); app.put('/config/remote', async (req, res, next) => { try { res.status(200).send(await sys.board.remotes.setRemoteAsync(req.body)); } catch (err) { next(err); } }); app.put('/config/alerts', async (req, res, next) => { try { res.status(200).send(await (sys.board as any).alerts.setAlertNotificationsAsync(req.body)); } catch (err) { next(err); } }); app.get('/config/options/alerts', (req, res) => { return res.status(200).send({ alerts: sys.alerts.get(true), definitions: typeof (sys.board as any).getAlertDefinitions === 'function' ? (sys.board as any).getAlertDefinitions() : {}, poolOptions: { cooldownDelay: sys.general.options.cooldownDelay, heaterStartDelay: sys.general.options.heaterStartDelay, valveDelayTime: sys.general.options.valveDelayTime, manualPriority: sys.general.options.manualPriority }, runtime: { chemControllers: state.chemControllers.getExtended(), chemDosers: state.chemDosers.getExtended() } }); }); app.get('/config/options/rs485', async (req, res, next) => { try { let opts = { ports: [], local: [], screenlogic: {} } let cfg = config.getSection('controller'); for (let section in cfg) { if (section.startsWith('comms')) { let cport = extend(true, { enabled: false, netConnect: false, mock: false }, cfg[section]); let port = conn.findPortById(cport.portId || 0); if (typeof cport.type === 'undefined'){ cport.type = cport.netConnect ? 'netConnect' : cport.mock ? 'mock' : 'local' } if (typeof port !== 'undefined') cport.stats = port.stats; if (port.portId === 0 && port.type === 'screenlogic') { cport.screenlogic.stats = sl.stats; } opts.ports.push(cport); } // if (section.startsWith('screenlogic')){ // let screenlogic = cfg[section]; // screenlogic.types = [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }]; // screenlogic.stats = sl.stats; // opts.screenlogic = screenlogic; // } } opts.local = await conn.getLocalPortsAsync() || []; return res.status(200).send(opts); } catch (err) { next(err); } }); // app.get('/config/options/screenlogic', async (req, res, next) => { // try { // let cfg = config.getSection('controller.screenlogic'); // let data = { // cfg, // types: [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }] // } // return res.status(200).send(data); // } catch (err) { next(err); } // }); app.get('/config/options/screenlogic/search', async (req, res, next) => { try { let localUnits = await ScreenLogicComms.searchAsync(); return res.status(200).send(localUnits); } catch (err) { next(err); } }); app.get('/config/options/circuits', async (req, res, next) => { try { let opts = { maxCircuits: sys.equipment.maxCircuits, equipmentIds: sys.equipment.equipmentIds.circuits, invalidIds: sys.board.equipmentIds.invalidIds.get(), equipmentNames: sys.board.circuits.getCircuitNames(), functions: sys.board.circuits.getCircuitFunctions(), circuits: sys.circuits.get(), controllerType: sys.controllerType, servers: await sys.ncp.getREMServers() }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/circuitGroups', (req, res) => { let opts = { maxCircuitGroups: sys.equipment.maxCircuitGroups, equipmentNames: sys.board.circuits.getCircuitNames(), circuits: sys.board.circuits.getCircuitReferences(true, true, false), circuitGroups: sys.circuitGroups.get(), circuitStates: sys.board.valueMaps.groupCircuitStates.toArray() }; return res.status(200).send(opts); }); app.get('/config/options/lightGroups', (req, res) => { let opts = { maxLightGroups: sys.equipment.maxLightGroups, equipmentNames: sys.board.circuits.getCircuitNames(), themes: sys.board.circuits.getLightThemes(), colors: sys.board.valueMaps.lightColors.toArray(), circuits: sys.board.circuits.getLightReferences(), lightGroups: sys.lightGroups.get(), functions: sys.board.circuits.getCircuitFunctions() }; return res.status(200).send(opts); }); app.get('/config/options/features', (req, res) => { let opts = { maxFeatures: sys.equipment.maxFeatures, invalidIds: sys.board.equipmentIds.invalidIds.get(), equipmentIds: sys.equipment.equipmentIds.features, equipmentNames: sys.board.circuits.getCircuitNames(), functions: sys.board.features.getFeatureFunctions(), features: sys.features.get() }; return res.status(200).send(opts); }); app.get('/config/options/bodies', (req, res) => { let opts = { maxBodies: sys.equipment.maxBodies, bodyTypes: sys.board.valueMaps.bodies.toArray(), bodies: sys.bodies.get(), capacityUnits: sys.board.valueMaps.volumeUnits.toArray() }; return res.status(200).send(opts); }); app.get('/config/options/valves', async (req, res, next) => { try { let opts = { maxValves: sys.equipment.maxValves, valveTypes: sys.board.valueMaps.valveTypes.toArray(), circuits: sys.board.circuits.getCircuitReferences(true, true, true), valves: sys.valves.get(), servers: await sys.ncp.getREMServers() }; opts.circuits.unshift({ id: 256, name: 'Unassigned', type: 0, equipmentType: 'circuit' }); return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/pumps', async (req, res, next) => { try { let opts: any = { maxPumps: sys.equipment.maxPumps, pumpUnits: sys.board.valueMaps.pumpUnits.toArray(), pumpTypes: sys.board.valueMaps.pumpTypes.toArray(), models: { ss: sys.board.valueMaps.pumpSSModels.toArray(), ds: sys.board.valueMaps.pumpDSModels.toArray(), vs: sys.board.valueMaps.pumpVSModels.toArray(), vf: sys.board.valueMaps.pumpVSModels.toArray(), vsf: sys.board.valueMaps.pumpVSFModels.toArray(), vssvrs: sys.board.valueMaps.pumpVSSVRSModels.toArray() }, circuits: sys.board.circuits.getCircuitReferences(true, true, true, true, true), bodies: sys.board.valueMaps.pumpBodies.toArray(), pumps: sys.pumps.get(), servers: await sys.ncp.getREMServers(), rs485ports: await conn.listInstalledPorts() }; // RKS: Why do we need the circuit names? We have the circuits. Is this so // that we can name the pump. I thought that *Touch uses the pump type as the name // plus a number. // RG: because I need the name/val of the Not Used circuit for displaying pumpCircuits that are // empty. EG A pump circuit can be not used even if all the circuits are used. if (sys.controllerType !== ControllerType.IntelliCenter) { opts.circuitNames = sys.board.circuits.getCircuitNames().filter(c => c.name === 'notused'); } return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/schedules', async (req, res, next) => { try { let opts = { maxSchedules: sys.equipment.maxSchedules, tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units), scheduleTimeTypes: sys.board.valueMaps.scheduleTimeTypes.toArray(), scheduleTypes: sys.board.valueMaps.scheduleTypes.toArray(), scheduleDays: sys.board.valueMaps.scheduleDays.toArray(), heatSources: sys.board.valueMaps.heatSources.toArray(), circuits: sys.board.circuits.getCircuitReferences(true, true, false, true), schedules: sys.schedules.get(), clockMode: sys.general.options.clockMode || 12, displayTypes: sys.board.valueMaps.scheduleDisplayTypes.toArray(), bodies: [], eggTimers: sys.eggTimers.get() // needed for *Touch to not overwrite real schedules }; // Now get all the body heat sources. for (let i = 0; i < sys.bodies.length; i++) { let body = sys.bodies.getItemByIndex(i); opts.bodies.push({ id: body.id, circuit: body.circuit, name: body.name, alias: body.alias, heatSources: sys.board.bodies.getHeatSources(body.id) }); } return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/heaters', async (req, res, next) => { try { // Ensure heat mode/source valueMaps reflect the *current board* before returning picklists. // Without this, startup can expose generic defaults until the first status/config packets arrive. sys.board.heaters.updateHeaterServices(); let opts = { tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units), bodies: sys.board.bodies.getBodyAssociations(), maxHeaters: sys.equipment.maxHeaters, heaters: sys.heaters.get(), heaterTypes: sys.board.valueMaps.heaterTypes.toArray(), equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(), // Align with `/config/body/:id/heatModes` (body picklist). This ensures any board-specific // filtering (e.g. IntelliCenter v3 preferred-mode suppression) is reflected consistently. // Future improvement should return valid modes per body. heatModes: sys.board.bodies.getHeatModes(1), coolDownDelay: sys.general.options.cooldownDelay, servers: [], rs485ports: await conn.listInstalledPorts() }; // We only need the servers data when the controller is a Nixie controller. We don't need to // wait for this information if we are dealing with an OCP. if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers(); return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/v2/config/options/heaters', async (req, res, next) => { try { sys.board.heaters.updateHeaterServices(); let bodyHeatModes = {}; for (let i = 0; i < sys.bodies.length; i++) { let body = sys.bodies.getItemByIndex(i); bodyHeatModes[body.id] = sys.board.bodies.getHeatModesV2(body.id); } let opts = { tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units), bodies: sys.board.bodies.getBodyAssociations(), maxHeaters: sys.equipment.maxHeaters, heaters: sys.heaters.get(), heaterTypes: sys.board.valueMaps.heaterTypes.toArray(), equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(), bodyHeatModes: bodyHeatModes, coolDownDelay: sys.general.options.cooldownDelay, servers: [], rs485ports: await conn.listInstalledPorts() }; if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers(); return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/customNames', (req, res) => { let opts = { maxCustomNames: sys.equipment.maxCustomNames, customNames: sys.customNames.get() }; return res.status(200).send(opts); }); app.get('/config/options/chemControllers', async (req, res, next) => { try { let remServers = await sys.ncp.getREMServers(); let alarms = { flow: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 1].includes(el.val)), pH: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 2, 4].includes(el.val)), orp: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 8, 16].includes(el.val)), pHTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 32].includes(el.val)), orpTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 64].includes(el.val)), probeFault: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 128].includes(el.val)) } let warnings = { waterChemistry: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 1, 2].includes(el.val)), pHLockout: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 1].includes(el.val)), pHDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 2].includes(el.val)), orpDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 4].includes(el.val)), invalidSetup: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 8].includes(el.val)), chlorinatorCommsError: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 16].includes(el.val)), } let opts = { types: sys.board.valueMaps.chemControllerTypes.toArray(), bodies: sys.board.bodies.getBodyAssociations(), tempUnits: sys.board.valueMaps.tempUnits.toArray(), status: sys.board.valueMaps.chemControllerStatus.toArray(), pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(), phSupplyTypes: sys.board.valueMaps.phSupplyTypes.toArray(), volumeUnits: sys.board.valueMaps.volumeUnits.toArray(), dosingMethods: sys.board.valueMaps.chemDosingMethods.toArray(), chlorDosingMethods: sys.board.valueMaps.chemChlorDosingMethods.toArray(), orpProbeTypes: sys.board.valueMaps.chemORPProbeTypes.toArray(), phProbeTypes: sys.board.valueMaps.chemPhProbeTypes.toArray(), flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(), acidTypes: sys.board.valueMaps.acidTypes.toArray(), remServers, dosingStatus: sys.board.valueMaps.chemControllerDosingStatus.toArray(), siCalcTypes: sys.board.valueMaps.siCalcTypes.toArray(), alarms, warnings, // waterFlow: sys.board.valueMaps.chemControllerWaterFlow.toArray(), // remove controllers: sys.chemControllers.get(), maxChemControllers: sys.equipment.maxChemControllers, intellichemStandaloneSupported: sys.controllerType === ControllerType.Nixie, doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(), chlorinators: sys.chlorinators.get(), }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/chemDosers', async (req, res, next) => { try { let remServers = await sys.ncp.getREMServers(); let opts = { bodies: sys.board.bodies.getBodyAssociations(), tempUnits: sys.board.valueMaps.tempUnits.toArray(), status: sys.board.valueMaps.chemDoserStatus.toArray(), pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(), volumeUnits: sys.board.valueMaps.volumeUnits.toArray(), flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(), remServers, dosingStatus: sys.board.valueMaps.chemDoserDosingStatus.toArray(), dosers: sys.chemDosers.get(), doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(), maxChemDosers: sys.equipment.maxChemDosers }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/rem', async (req, res, next) => { try { let opts = { servers: await sys.ncp.getREMServers() } return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/controllerType', async (req, res, next) => { try { let opts = { controllerType: sys.controllerType, type: state.controllerState, equipment: sys.equipment.get(), controllerTypes: sys.getAvailableControllerTypes() } return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/anslq25ControllerType', async (req, res, next) => { try { let opts = { // controllerType: typeof sys.anslq25.controllerType === 'undefined' ? '' : sys.anslq25.controllerType, // model: typeof sys.anslq25.model === 'undefined' ? '' : sys.anslq25.model, // equipment: sys.equipment.get(), ...sys.anslq25.get(true), controllerTypes: sys.getAvailableControllerTypes(['easytouch', 'intellitouch', 'intellicenter']), rs485ports: await conn.listInstalledPorts() } return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/chlorinators', async (req, res, next) => { try { let opts = { types: sys.board.valueMaps.chlorinatorType.toArray(), bodies: sys.board.bodies.getBodyAssociations(), chlorinators: sys.chlorinators.get(), maxChlorinators: sys.equipment.maxChlorinators, models: sys.board.valueMaps.chlorinatorModel.toArray(), equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(), rs485ports: await conn.listInstalledPorts() }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/config/options/dateTime', (req, res) => { let opts = { dow: sys.board.system.getDOW() } return res.status(200).send(opts); }); app.get('/app/options/logger', (req, res) => { let opts = { logger: config.getSection('log') } return res.status(200).send(opts); }); app.get('/app/all/', (req, res) => { let opts = config.getSection(); return res.status(200).send(opts); }); app.get('/config/options/tempSensors', (req, res) => { let opts = { tempUnits: sys.board.valueMaps.tempUnits.toArray(), sensors: sys.board.system.getSensors() }; return res.status(200).send(opts); }); // ISSUE-075 #6 / ISSUE-080: expose the IntelliCenter cover configuration surface so // dashPanel (or any API consumer) can render the Controllers โ†’ Covers page. // // Response shape: // { // maxCovers: 2 (IntelliCenter hard-cap: A/D Cover Module part 522039 supports 2), // bodyOptions: [{ val, name }, ...], // Pool / Spa valueMap rows // availableCircuits: [...getCircuitReferences], // picker list for "Affected Circuits" // covers: [ { id, name, isActive, body, normallyOn, chlorActive, chlorOutput, // chlorOutputMax, circuits: [ids], ... } ] // } // // `chlorOutputMax` is per-body (Pool 0-50, Spa 0-10) โ€” the OCP enforces this; mirroring // it in the API lets dashPanel cap its slider without a second round-trip. app.get('/config/options/covers', (req, res) => { const poolBodyId = sys.board.valueMaps.bodies.getValue('pool'); const spaBodyId = sys.board.valueMaps.bodies.getValue('spa'); const covers = sys.covers.get().map((c: any) => { const bodyVal = sys.board.valueMaps.bodies.encode(c.body); const chlorOutputMax = bodyVal === spaBodyId ? 10 : 50; return Object.assign({}, c, { chlorOutputMax }); }); const opts = { maxCovers: 2, bodyOptions: [ { val: poolBodyId, name: 'Pool' }, { val: spaBodyId, name: 'Spa' } ], availableCircuits: sys.board.circuits.getCircuitReferences(true, true, false, false), covers }; return res.status(200).send(opts); }); app.get('/config/options/filters', async (req, res, next) => { try { let opts = { types: sys.board.valueMaps.filterTypes.toArray(), bodies: sys.board.bodies.getBodyAssociations(), filters: sys.filters.get(), areaUnits: sys.board.valueMaps.areaUnits.toArray(), pressureUnits: sys.board.valueMaps.pressureUnits.toArray(), circuits: sys.board.circuits.getCircuitReferences(true, true, true, false), servers: [] }; if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers(); return res.status(200).send(opts); } catch (err) { next(err); } }); /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/ /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/ app.put('/config/rem', async (req, res, next) => { try { // RSG: this is problematic because we now enable multiple rem type interfaces that may not be called REM. // This is now also a dupe of PUT /app/interface and should be consolidated // config.setSection('web.interfaces.rem', req.body); config.setInterface(req.body); } catch (err) { next(err); } }) app.put('/config/tempSensors', async (req, res, next) => { try { await sys.board.system.setTempSensorsAsync(req.body); let opts = { tempUnits: sys.board.valueMaps.tempUnits.toArray(), sensors: sys.board.system.getSensors() }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.put('/config/filter', async (req, res, next) => { try { let sfilter = await sys.board.filters.setFilterAsync(req.body); return res.status(200).send(sfilter.get(true)); } catch (err) { next(err); } }); app.put('/config/controllerType', async (req, res, next) => { try { let controller = await sys.board.setControllerType(req.body); return res.status(200).send(controller.get(true)); } catch (err) { next(err); } }); app.put('/config/anslq25ControllerType', async (req, res, next) => { try { // sys.anslq25ControllerType await sys.anslq25Board.setAnslq25Async(req.body); return res.status(200).send(sys.anslq25.get(true)); } catch (err) { next(err); } }); // Virtual Equipment (wire-level slave simulators: pumps, etc.) // These are NOT in poolConfig/state; they're a separate runtime feature // persisted in data/virtualEquipment.json and controlled purely via REST. app.get('/config/virtualEquipment', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(200).send({ pumps: [] }); return res.status(200).send(sys.virtualEquipment.getSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/pump', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const pump = await sys.virtualEquipment.upsertPumpAsync(req.body || {}); return res.status(200).send(pump.toSnapshot()); } catch (err) { next(err); } }); app.delete('/config/virtualEquipment/pump/:address', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); await sys.virtualEquipment.deletePumpAsync(address); return res.status(200).send(sys.virtualEquipment.getSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/pump/:address/reenable', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); const pump = await sys.virtualEquipment.reenablePumpAsync(address); if (!pump) return res.status(404).send({ error: `no virtual pump at address ${address}` }); return res.status(200).send(pump.toSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/chlorinator', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const chlor = await sys.virtualEquipment.upsertChlorinatorAsync(req.body || {}); return res.status(200).send(chlor.toSnapshot()); } catch (err) { next(err); } }); app.delete('/config/virtualEquipment/chlorinator/:address', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); await sys.virtualEquipment.deleteChlorinatorAsync(address); return res.status(200).send(sys.virtualEquipment.getSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/chlorinator/:address/reenable', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); const chlor = await sys.virtualEquipment.reenableChlorinatorAsync(address); if (!chlor) return res.status(404).send({ error: `no virtual chlorinator at address ${address}` }); return res.status(200).send(chlor.toSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/intellichem', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const ic = await sys.virtualEquipment.upsertIntelliChemAsync(req.body || {}); return res.status(200).send(ic.toSnapshot()); } catch (err) { next(err); } }); app.delete('/config/virtualEquipment/intellichem/:address', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); await sys.virtualEquipment.deleteIntelliChemAsync(address); return res.status(200).send(sys.virtualEquipment.getSnapshot()); } catch (err) { next(err); } }); app.put('/config/virtualEquipment/intellichem/:address/reenable', async (req, res, next) => { try { if (!sys.virtualEquipment) return res.status(503).send({ error: 'VirtualEquipment not initialized' }); const address = parseInt(req.params.address, 10); if (!Number.isFinite(address)) return res.status(400).send({ error: 'invalid address' }); const ic = await sys.virtualEquipment.reenableIntelliChemAsync(address); if (!ic) return res.status(404).send({ error: `no virtual intellichem at address ${address}` }); return res.status(200).send(ic.toSnapshot()); } catch (err) { next(err); } }); app.delete('/config/filter', async (req, res, next) => { try { let sfilter = await sys.board.filters.deleteFilterAsync(req.body); return res.status(200).send(sfilter.get(true)); } catch (err) { next(err); } }); app.put('/config/general', async (req, res, next) => { // Change the options for the pool. try { let rc = await sys.board.system.setGeneralAsync(req.body); let opts = { countries: sys.board.valueMaps.countries.toArray(), tempUnits: sys.board.valueMaps.tempUnits.toArray(), timeZones: sys.board.valueMaps.timeZones.toArray(), clockSources: sys.board.valueMaps.clockSources.toArray(), clockModes: sys.board.valueMaps.clockModes.toArray(), pool: sys.general.get(true), sensors: sys.board.system.getSensors() }; return res.status(200).send(opts); } catch (err) { next(err); } }); app.put('/config/valve', async (req, res, next) => { // Update a valve. try { let valve = await sys.board.valves.setValveAsync(req.body); return res.status(200).send((valve).get(true)); } catch (err) { next(err); } }); app.delete('/config/valve', async (req, res, next) => { // Update a valve. try { let valve = await sys.board.valves.deleteValveAsync(req.body); return res.status(200).send((valve).get(true)); } catch (err) { next(err); } }); app.put('/config/body', async (req, res, next) => { // Change the body attributes. try { let body = await sys.board.bodies.setBodyAsync(req.body); return res.status(200).send((body).get(true)); } catch (err) { next(err); } }); app.put('/config/circuit', async (req, res, next) => { // add/update a circuit try { let circuit = await sys.board.circuits.setCircuitAsync(req.body); return res.status(200).send((circuit).get(true)); } catch (err) { next(err); } }); app.delete('/config/circuit', async (req, res, next) => { // delete a circuit try { let circuit = await sys.board.circuits.deleteCircuitAsync(req.body); return res.status(200).send((circuit).get(true)); } catch (err) { next(err); } }); app.put('/config/feature', async (req, res, next) => { // add/update a feature try { let feature = await sys.board.features.setFeatureAsync(req.body); return res.status(200).send((feature).get(true)); } catch (err) { next(err); } }); app.delete('/config/feature', async (req, res, next) => { // delete a feature try { let feature = await sys.board.features.deleteFeatureAsync(req.body); return res.status(200).send((feature).get(true)); } catch (err) { next(err); } }); app.put('/config/circuitGroup', async (req, res, next) => { // add/update a circuitGroup try { let group = await sys.board.circuits.setCircuitGroupAsync(req.body); return res.status(200).send((group).get(true)); } catch (err) { next(err); } }); app.delete('/config/circuitGroup', async (req, res, next) => { try { let group = await sys.board.circuits.deleteCircuitGroupAsync(req.body); return res.status(200).send((group).get(true)); } catch (err) { next(err); } }); app.put('/config/lightGroup', async (req, res, next) => { try { let group = await sys.board.circuits.setLightGroupAsync(req.body); return res.status(200).send((group).get(true)); } catch (err) { next(err); } }); app.delete('/config/lightGroup', async (req, res, next) => { try { let group = await sys.board.circuits.deleteLightGroupAsync(req.body); return res.status(200).send((group).get(true)); } catch (err) { next(err); } }); app.put('/config/pump', async (req, res, next) => { // Change the pump attributes. This will add the pump if it doesn't exist, set // any affiliated circuits and maintain all attribututes of the pump. // RSG: Caveat - you have to send none or all of the pump circuits or any not included be deleted. try { let pump = await sys.board.pumps.setPumpAsync(req.body); return res.status(200).send((pump).get(true)); } catch (err) { next(err); } }); app.put('/config/pumpCircuit', async (req, res, next) => { try { let pmpId = parseInt(req.body.pumpId, 10); let circId = parseInt(req.body.circuitId, 10); let pmp: Pump; if (isNaN(pmpId)) { let pmpAddress = parseInt(req.body.address, 10); if (!isNaN(pmpAddress)) pmp = sys.pumps.find(x => x.address === pmpAddress); } else pmp = sys.pumps.find(x => x.id === pmpId); if (typeof pmp === 'undefined') throw new ServiceProcessError(`Pump not found`, '/config/pumpCircuit', 'Set circuit speed'); let data = pmp.get(true); let c = typeof data.circuits !== 'undefined' && typeof data.circuits.find !== 'undefined' ? data.circuits.find(x => x.circuit === circId) : undefined; if (typeof c === 'undefined') throw new ServiceProcessError(`Circuit not found`, '/config/pumpCircuit', 'Set circuit speed'); if (typeof req.body.speed !== 'undefined') { let speed = parseInt(req.body.speed, 10); if (isNaN(speed)) throw new ServiceProcessError(`Invalid circuit speed supplied`, '/config/pumpCircuit', 'Set circuit speed'); c.speed = speed; } else if (typeof req.body.flow !== 'undefined') { let flow = parseInt(req.body.flow, 10); if (isNaN(flow)) throw new ServiceProcessError(`Invalid circuit flow supplied`, '/config/pumpCircuit', 'Set circuit flow'); c.flow = flow; } else { throw new ServiceProcessError(`You must supply a target flow or speed`, '/config/pumpCircuit', 'Set circuit flow'); } await sys.board.pumps.setPumpAsync(data); return res.status(200).send((pmp).get(true)); } catch (err) { next(err); } }); // RKS: 05-20-22 This is a remnant of the old web ui. It is not called and the setType method needed to go away. //app.delete('/config/pump/:pumpId', async (req, res, next) => { // try { // let pump = sys.pumps.getItemById(parseInt(req.params.pumpId, 10)); // await sys.board.pumps.deletePumpAsync() // if (pump.type === 0) { // return res.status(500).send(`Pump ${pump.id} not active`); // } // pump.setType(0); // return res.status(200).send('OK'); // } catch (err) { next(err); } //}); app.delete('/config/pump', async (req, res, next) => { try { let pump = await sys.board.pumps.deletePumpAsync(req.body); return res.status(200).send((pump).get(true)); } catch (err) { next(err); } }); app.put('/config/customNames', async (req, res, next) => { try { let names = await sys.board.system.setCustomNamesAsync(req.body); return res.status(200).send(names.get()); } catch (err) { next(err); } }); app.put('/config/customName', async (req, res, next) => { try { let name = await sys.board.system.setCustomNameAsync(req.body); return res.status(200).send(name.get(true)); } catch (err) { next(err); } }); app.get('/config/schedule/:id', (req, res) => { let schedId = parseInt(req.params.id || '0', 10); let sched = sys.schedules.getItemById(schedId).get(true); return res.status(200).send(sched); }); app.put('/config/schedule', async (req, res, next) => { try { let sched = await sys.board.schedules.setScheduleAsync(req.body); return res.status(200).send((sched as Schedule).get(true)); } catch (err) { next(err); } }); app.delete('/config/schedule', async (req, res, next) => { try { let sched = await sys.board.schedules.deleteScheduleAsync(req.body); return res.status(200).send((sched as Schedule).get(true)); } catch (err) { //console.log(`Error deleting schedule... ${err}`); next(err); } }); app.put('/config/chlorinator', async (req, res, next) => { try { let chlor = await sys.board.chlorinator.setChlorAsync(req.body); return res.status(200).send(sys.chlorinators.getItemById(chlor.id).get(true)); } catch (err) { next(err); } }); app.delete('/config/chlorinator', async (req, res, next) => { try { let chlor = await sys.board.chlorinator.deleteChlorAsync(req.body); return res.status(200).send(chlor.get(true)); } catch (err) { next(err); } }); app.put('/config/heater', async (req, res, next) => { try { let heater = await sys.board.heaters.setHeaterAsync(req.body); return res.status(200).send(sys.heaters.getItemById(heater.id).get(true)); } catch (err) { next(err); } }); app.delete('/config/heater', async (req, res, next) => { try { let heater = await sys.board.heaters.deleteHeaterAsync(req.body); return res.status(200).send((heater as Heater).get(true)); } catch (err) { next(err); } }); // ISSUE-075 #7 / ISSUE-080: cover config write path. Body / normallyOn / chlorActive / // chlorOutput / circuits โ€” name is read-only (OCP exposes no rename UI for covers; // see .plan/v3.008/covers-packet-reference.md ยง4.1). The board implementation is // responsible for enforcing the Pool 0-50 / Spa 0-10 output cap and the 1-cover-per-body // Pentair constraint. app.put('/config/cover', async (req, res, next) => { try { let cover = await sys.board.covers.setCoverAsync(req.body); return res.status(200).send(sys.covers.getItemById(cover.id).get(true)); } catch (err) { next(err); } }); /***** END OF ENDPOINTS FOR MODIFYINC THE OUTDOOR CONTROL PANEL SETTINGS *****/ app.get('/config/circuits/names', (req, res) => { let circuitNames = sys.board.circuits.getCircuitNames(); return res.status(200).send(circuitNames); }); app.get('/config/circuit/functions', (req, res) => { let circuitFunctions = sys.board.circuits.getCircuitFunctions(); return res.status(200).send(circuitFunctions); }); app.get('/config/features/functions', (req, res) => { let featureFunctions = sys.board.features.getFeatureFunctions(); return res.status(200).send(featureFunctions); }); app.get('/config/circuit/:id', (req, res) => { // todo: need getInterfaceById.get() in case features are requested here // todo: it seems to make sense to combine with /state/circuit/:id as they both have similiar/overlapping info return res.status(200).send(sys.circuits.getItemById(parseInt(req.params.id, 10)).get()); }); app.get('/config/circuit/:id/lightThemes', (req, res) => { let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10)); let themes = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightThemes(circuit.type) : []; return res.status(200).send(themes); }); app.get('/config/circuit/:id/lightCommands', (req, res) => { let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10)); let commands = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightCommands(circuit.type) : []; return res.status(200).send(commands); }); app.get('/config/chlorinator/:id', (req, res) => { return res.status(200).send(sys.chlorinators.getItemById(parseInt(req.params.id, 10)).get()); }); app.get('/config/chlorinators/search', async (req, res, next) => { // Change the options for the pool. try { //await sys.board.virtualChlorinatorController.search(); return res.status(200).send(sys.chlorinators.getItemById(1).get()); } catch (err) { next(err); } }); app.put('/config/dateTime', async (req, res, next) => { try { let time = await sys.updateControllerDateTimeAsync(req.body); return res.status(200).send(time); } catch (err) { next(err); } }); app.get('/config/lightGroups/themes', (req, res) => { // RSG: is this and /config/circuit/:id/lightThemes both needed? let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10)); return res.status(200).send(grp.getLightThemes()); }); app.get('/config/lightGroups/commands', (req, res) => { let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10)); return res.status(200).send(grp.getLightCommands()); }); app.get('/config/lightGroup/:id', (req, res) => { // if (sys.controllerType === ControllerType.IntelliCenter) { let grp = sys.lightGroups.getItemById(parseInt(req.params.id, 10)); return res.status(200).send(grp.getExtended()); // } // else // return res.status(200).send(sys.intellibrite.getExtended()); }); app.get('/config/lightGroup/colors', (req, res) => { return res.status(200).send(sys.board.valueMaps.lightColors.toArray()); }); app.put('/config/lightGroup/:id/setColors', async (req, res, next) => { try { let id = parseInt(req.params.id, 10); let grp = extend(true, { id: id }, req.body); await sys.board.circuits.setLightGroupAttribsAsync(grp); return res.status(200).send(sys.lightGroups.getItemById(id).getExtended()); } catch (err) { next(err); } }); app.get('/config/intellibrite/themes', (req, res) => { return res.status(200).send(sys.board.circuits.getLightThemes(16)); }); app.get('/config/circuitGroup/:id', (req, res) => { let grp = sys.circuitGroups.getItemById(parseInt(req.params.id, 10)); return res.status(200).send(grp.getExtended()); }); /* app.get('/config/chemController/search', async (req, res, next) => { // Change the options for the pool. try { let result = await sys.board.virtualChemControllers.search(); return res.status(200).send(result); } catch (err) { next(err); } }); */ app.put('/config/chemController', async (req, res, next) => { try { let chem = await sys.board.chemControllers.setChemControllerAsync(req.body); return res.status(200).send(chem.get()); } catch (err) { next(err); } }); app.put('/config/chemDoser', async (req, res, next) => { try { let doser = await sys.board.chemDosers.setChemDoserAsync(req.body); return res.status(200).send(doser.get()); } catch (err) { next(err); } }); app.put('/config/chemController/calibrateDose', async (req, res, next) => { try { let schem = await sys.board.chemControllers.calibrateDoseAsync(req.body); return res.status(200).send(schem.getExtended()); } catch (err) { next(err); } }); app.put('/config/chemDoser/calibrateDose', async (req, res, next) => { try { let schem = await sys.board.chemDosers.calibrateDoseAsync(req.body); return res.status(200).send(schem.getExtended()); } catch (err) { next(err); } }); app.put('/config/chemController/feed', async (req, res, next) => { try { let chem = await sys.board.chemControllers.setChemControllerAsync(req.body); return res.status(200).send(chem.get()); } catch (err) { next(err); } }); app.delete('/config/chemController', async (req, res, next) => { try { let chem = await sys.board.chemControllers.deleteChemControllerAsync(req.body); return res.status(200).send(chem.get()); } catch (err) { next(err); } }); app.delete('/config/chemDoser', async (req, res, next) => { try { let doser = await sys.board.chemDosers.deleteChemDoserAsync(req.body); return res.status(200).send(doser.get()); } catch (err) { next(err); } }); /* app.get('/config/intellibrite', (req, res) => { return res.status(200).send(sys.intellibrite.getExtended()); }); app.get('/config/intellibrite/colors', (req, res) => { return res.status(200).send(sys.board.valueMaps.lightColors.toArray()); }); app.put('/config/intellibrite/setColors', (req, res) => { let grp = extend(true, { id: 0 }, req.body); sys.board.circuits.setIntelliBriteColors(new LightGroup(grp)); return res.status(200).send('OK'); }); */ app.get('/config/security/session', (req, res) => { return res.status(200).send(ConfigRoute.getSessionResponse(req)); }); app.put('/config/security/login', (req, res) => { if (!sys.security.enabled) { return res.status(409).send({ error: 'SECURITY_DISABLED', message: 'Panel security is disabled.', security: ConfigRoute.getSessionResponse(req) }); } const role = ConfigRoute.getRoleForPin(((req.body || {}).pin || '').toString()); if (typeof role === 'undefined') { return res.status(401).send({ error: 'INVALID_PIN', message: 'PIN does not match a configured security role.' }); } return res.status(200).send({ enabled: sys.security.enabled, session: ConfigRoute.createSecuritySession(req, role) }); }); app.put('/config/security/logout', (req, res) => { return res.status(200).send({ enabled: sys.security.enabled, session: ConfigRoute.clearSecuritySession(req) }); }); app.put('/config/security/role', async (req, res, next) => { try { let result = await (sys.board as any).setSecurityRoleAsync(req.body); return res.status(200).send(result); } catch (err) { next(err); } }); app.get('/config', (req, res) => { return res.status(200).send(sys.getSection('all')); }); app.get('/config/:section', (req, res) => { return res.status(200).send(sys.getSection(req.params.section)); }); /******* ENDPOINTS FOR MANAGING THE poolController APPLICATION *********/ app.put('/app/logger/setOptions', (req, res) => { logger.setOptions(req.body); return res.status(200).send(logger.options); }); app.put('/app/logger/clearMessages', (req, res) => { logger.clearMessages(); return res.status(200).send('OK'); }); app.get('/app/messages/broadcast/actions', (req, res) => { return res.status(200).send(sys.board.valueMaps.msgBroadcastActions.toArray()); }); app.put('/app/config/reload', (req, res) => { sys.board.reloadConfig(); return res.status(200).send('OK'); }); app.put('/app/interface', async (req, res, next) => { try { let iface = await webApp.updateServerInterface(req.body); return res.status(200).send(iface); } catch (err) { next(err); } }); app.put('/app/rs485Port', async (req, res, next) => { try { let port = await conn.setPortAsync(req.body); return res.status(200).send(port); } catch (err) { next(err); } }); // app.put('/app/screenlogic', async (req, res, next) => { // try { // let screenlogic = await sl.setScreenlogicAsync(req.body); // return res.status(200).send(screenlogic); // } // catch (err) { next(err); } // }); app.delete('/app/rs485Port', async (req, res, next) => { try { let port = await conn.deleteAuxPort(req.body); return res.status(200).send(port); } catch (err) { next(err); } }); app.get('/app/config/startPacketCapture', async (req, res, next) => { try { await startPacketCapture(true); return res.status(200).send('OK'); } catch (err) { next(err); } }); app.get('/app/config/startPacketCaptureWithoutReset', async (req, res, next) => { try { await startPacketCapture(false); return res.status(200).send('OK'); } catch (err) { next(err); } }); app.get('/app/config/stopPacketCapture', async (req, res, next) => { try { let file = await stopPacketCaptureAsync(); if (typeof file !== 'string' || file.length === 0 || !fs.existsSync(file)) { logger.warn(`stopPacketCapture did not produce a valid backup file path`); return res.status(409).send('Packet capture is not active or no capture file is available.'); } return res.download(file); } catch (err) { next(err); } }); app.get('/app/config/:section', (req, res) => { return res.status(200).send(config.getSection(req.params.section)); }); app.get('/app/config/options/backup', async (req, res, next) => { try { let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] }); let servers = await sys.ncp.getREMServers(); if (typeof servers !== 'undefined') { // Just in case somebody deletes the backup section and doesn't put it back properly. for (let i = 0; i < servers.length; i++) { let srv = servers[i]; if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false, host: srv.interface.options.host }); } for (let i = opts.servers.length - 1; i >= 0; i--) { let srv = opts.servers[i]; if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1); } } if (typeof opts.servers === 'undefined') opts.servers = []; return res.status(200).send(opts); } catch (err) { next(err); } }); app.get('/app/config/options/restore', async (req, res, next) => { try { let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [], backupFiles: [] }); let servers = await sys.ncp.getREMServers(); if (typeof servers !== 'undefined') { for (let i = 0; i < servers.length; i++) { let srv = servers[i]; if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false }); } for (let i = opts.servers.length - 1; i >= 0; i--) { let srv = opts.servers[i]; if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1); } } if (typeof opts.servers === 'undefined') opts.servers = []; opts.backupFiles = await webApp.readBackupFiles(); return res.status(200).send(opts); } catch (err) { next(err); } }); app.put('/app/config/options/backup', async (req, res, next) => { try { config.setSection('controller.backups', req.body); let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] }); webApp.autoBackup = utils.makeBool(opts.automatic); await webApp.checkAutoBackup(); return res.status(200).send(opts); } catch (err) { next(err); } }); app.put('/app/config/createBackup', async (req, res, next) => { try { let ret = await webApp.backupServer(req.body); res.download(ret.filePath); } catch (err) { next(err); } }); app.delete('/app/backup/file', async (req, res, next) => { try { let opts = req.body; fs.unlinkSync(opts.filePath); return res.status(200).send(opts); } catch (err) { next(err); } }); app.post('/app/backup/file', async (req, res, next) => { try { let file = multer({ limits: { fileSize: 1000000 }, storage: multer.memoryStorage() }).single('backupFile'); file(req, res, async (err) => { try { if (err) { next(err); } else { // Validate the incoming data and save it off only if it is valid. let bf = await BackupFile.fromBuffer(req.file.originalname, req.file.buffer); if (typeof bf === 'undefined') { err = new ServiceProcessError(`Invalid backup file: ${req.file.originalname}`, 'POST: app/backup/file', 'extractBackupOptions'); next(err); } else { if (fs.existsSync(bf.filePath)) return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/backup/file', 'writeFile')); else { try { fs.writeFileSync(bf.filePath, new Uint8Array(req.file.buffer)); } catch (e) { logger.error(`Error writing backup file ${e.message}`); } } return res.status(200).send(bf); } } } catch (e) { err = new ServiceProcessError(`Error uploading file: ${e.message}`, 'POST: app/backup/file', 'uploadFile'); next(err); logger.error(`Error uploading file ${e.message}`); } }); } catch (err) { next(err); } }); app.put('/app/restore/validate', async (req, res, next) => { try { // Validate all the restore options. let opts = req.body; let ctx = await webApp.validateRestore(opts); return res.status(200).send(ctx); } catch (err) { next(err); } }); app.put('/app/restore/file', async (req, res, next) => { try { let opts = req.body; let results = await webApp.restoreServers(opts); return res.status(200).send(results); } catch (err) { next(err); } }); app.put('/app/anslq25', async(req, res, next) => { try { await sys.anslq25Board.setAnslq25Async(req.body); return res.status(200).send(sys.anslq25.get(true)); } catch (err) { next(err); } }); app.delete('/app/anslq25', async(req, res, next) => { try { await sys.anslq25Board.deleteAnslq25Async(req.body); return res.status(200).send(sys.anslq25.get(true)); } catch (err) { next(err); } }); } }