From 20ea2394f408a0a8e637b851f094cf7851ae6aa8 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 2 Sep 2024 13:16:20 +0300 Subject: [PATCH 1/5] Use async version of setInterval for the main loop Fixes "Promise returned in function argument where a void return was expected @typescript-eslint/no-misused-promises" --- package-lock.json | 10 ++++++++++ package.json | 1 + src/eachwatt.ts | 7 ++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0d7711..a004a03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@influxdata/influxdb-client": "^1.33.2", "modbus-serial": "^8.0.16", "mqtt": "^5.1.2", + "set-interval-async": "^3.0.3", "slugify": "^1.6.6", "winston": "^3.11.0", "ws": "^8.17.1", @@ -5179,6 +5180,15 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/set-interval-async": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/set-interval-async/-/set-interval-async-3.0.3.tgz", + "integrity": "sha512-o4DyBv6mko+A9cH3QKek4SAAT5UyJRkfdTi6JHii6ZCKUYFun8SwgBmQrOXd158JOwBQzA+BnO8BvT64xuCaSw==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index cf28b42..e1ec2f2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@influxdata/influxdb-client": "^1.33.2", "modbus-serial": "^8.0.16", "mqtt": "^5.1.2", + "set-interval-async": "^3.0.3", "slugify": "^1.6.6", "winston": "^3.11.0", "ws": "^8.17.1", diff --git a/src/eachwatt.ts b/src/eachwatt.ts index 02d1610..3e3e0f5 100644 --- a/src/eachwatt.ts +++ b/src/eachwatt.ts @@ -12,6 +12,7 @@ import { createLogger, LogLevel, setLogLevel } from './logger' import { setRequestTimeout as setHttpRequestTimeout } from './http/client' import { setRequestTimeout as setModbusRequestTimeout } from './modbus/client' import { applyFilters } from './filter/filter' +import { setIntervalAsync } from 'set-interval-async' // Set up a signal handler, so we can exit on Ctrl + C when run from Docker process.on('SIGINT', () => { @@ -120,13 +121,13 @@ const mainPollerFunc = async (config: Config) => { }) // Adjust request timeouts to be half that of the polling interval - const pollingInterval = config.settings.pollingInterval + const pollingInterval = config.settings.pollingInterval as number logger.info(`Polling sensors with interval ${pollingInterval} milliseconds`) - const timeoutMs = (pollingInterval as number) / 2 + const timeoutMs = pollingInterval / 2 setHttpRequestTimeout(timeoutMs) setModbusRequestTimeout(timeoutMs) // Start polling sensors await mainPollerFunc(config) - setInterval(mainPollerFunc, pollingInterval, config) + setIntervalAsync(mainPollerFunc, pollingInterval, config) })() From f3e069bb99092ed95dc345b20f4442e79fd9a86f Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 2 Sep 2024 13:16:52 +0300 Subject: [PATCH 2/5] Switch to the recommended-type-checked linter preset --- eslint.config.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 12eb3f2..9c91a01 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,7 +15,7 @@ const compat = new FlatCompat({ }); export default [ - ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), + ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended-type-checked"), { plugins: { "@typescript-eslint": typescriptEslint, @@ -25,8 +25,10 @@ export default [ globals: { ...globals.node, }, - parser: tsParser, + parserOptions: { + project: "./tsconfig.json" + }, ecmaVersion: 2021, sourceType: "module", }, From 6a12a6c7799374ca9888374748ff98480ddd509a Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 2 Sep 2024 14:05:17 +0300 Subject: [PATCH 3/5] Disable require-await, it doesn't suit us --- eslint.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9c91a01..dc49279 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,8 @@ export default [ rules: { "no-console": "error", + // We have type-hinted functions that in their dummy implementations return static data + "@typescript-eslint/require-await": "off", }, }, ]; \ No newline at end of file From a84d92c444408c06d875b59cf7f007c08779f5be Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 2 Sep 2024 14:05:51 +0300 Subject: [PATCH 4/5] Enable return-await rule It's not enabled by recommended-type-checked despite what the documentation says --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index dc49279..22e1f6b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -37,6 +37,7 @@ export default [ "no-console": "error", // We have type-hinted functions that in their dummy implementations return static data "@typescript-eslint/require-await": "off", + "@typescript-eslint/return-await": "error" }, }, ]; \ No newline at end of file From 6f6bbae23b0ecf287bb4b4c27842ba0611fdd9e8 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 2 Sep 2024 14:33:10 +0300 Subject: [PATCH 5/5] Fix various linting errors after enabling recommended-type-checked --- src/characteristics.ts | 2 +- src/circuit.ts | 2 +- src/config.ts | 2 +- src/eachwatt.ts | 2 +- src/http/client.ts | 4 ++-- src/http/server.ts | 7 +++++-- src/modbus/register.ts | 4 ++-- src/publisher.ts | 4 ++-- src/publisher/console.ts | 4 ++-- src/publisher/websocket.ts | 6 +++--- src/sensor/modbus.ts | 8 ++++---- 11 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/characteristics.ts b/src/characteristics.ts index 3a0b7b5..73625fe 100644 --- a/src/characteristics.ts +++ b/src/characteristics.ts @@ -18,5 +18,5 @@ export const pollCharacteristicsSensors = async ( promises.push(sensor.pollFunc(timestamp, c)) } - return await Promise.all(promises) + return Promise.all(promises) } diff --git a/src/circuit.ts b/src/circuit.ts index 47872a3..907063e 100644 --- a/src/circuit.ts +++ b/src/circuit.ts @@ -33,5 +33,5 @@ export const pollPowerSensors = async ( promises.push(sensor.pollFunc(timestamp, circuit, existingSensorData)) } - return await Promise.all(promises) + return Promise.all(promises) } diff --git a/src/config.ts b/src/config.ts index 54978b8..b7a5e64 100644 --- a/src/config.ts +++ b/src/config.ts @@ -189,7 +189,7 @@ export const resolveAndValidateConfig = (config: Config): Config => { circuit.sensor.pollFunc = getDummySensorData break default: - throw new Error(`Unrecognized sensor type ${circuit.sensor.type}`) + throw new Error(`Unrecognized sensor type ${circuit.sensor.type as string}`) } } diff --git a/src/eachwatt.ts b/src/eachwatt.ts index 3e3e0f5..9755901 100644 --- a/src/eachwatt.ts +++ b/src/eachwatt.ts @@ -96,7 +96,7 @@ const mainPollerFunc = async (config: Config) => { } } -;(async () => { +void (async () => { const configFile = argv.config as string if (!fs.existsSync(configFile)) { logger.error(`Configuration ${configFile} file does not exist or is not readable`) diff --git a/src/http/client.ts b/src/http/client.ts index 3bf00da..8c5a838 100644 --- a/src/http/client.ts +++ b/src/http/client.ts @@ -4,7 +4,7 @@ const logger = createLogger('http') let requestTimeout = 0 let lastTimestamp = 0 -const promiseCache = new Map() +const promiseCache = new Map>() const createRequestParams = (): RequestInit => { return { @@ -30,7 +30,7 @@ export const getDedupedResponse = async (timestamp: number, url: string): Promis const key = `${timestamp}_${url}` if (promiseCache.has(key)) { - return promiseCache.get(key) + return promiseCache.get(key)! } const request = new Request(url, createRequestParams()) diff --git a/src/http/server.ts b/src/http/server.ts index 709cada..b562cae 100644 --- a/src/http/server.ts +++ b/src/http/server.ts @@ -14,7 +14,7 @@ const mimeTypes = new Map([ ['.png', 'image/png'], ]) -export const httpRequestHandler: RequestListener = async (req: IncomingMessage, res: ServerResponse) => { +export const httpRequestHandler: RequestListener = (req: IncomingMessage, res: ServerResponse) => { const filePath = resolveFilePath(req.url) // Serve 404 if file doesn't exist @@ -28,7 +28,10 @@ export const httpRequestHandler: RequestListener = async (req: IncomingMessage, const extension = path.extname(filePath).toLowerCase() const mimeType = mimeTypes.get(extension) - await serveStaticFile(filePath, mimeType, res) + // RequestListener returns void so we must wrap awaits + void (async () => { + await serveStaticFile(filePath, mimeType, res) + })() } const resolveFilePath = (reqUrl: string | undefined): string => { diff --git a/src/modbus/register.ts b/src/modbus/register.ts index 13bbde2..880240b 100644 --- a/src/modbus/register.ts +++ b/src/modbus/register.ts @@ -67,11 +67,11 @@ export const parseRegisterDefinition = (definition: string): ModbusRegister => { } if (!isValidDataType(dataType)) { - throw new Error(`Invalid data type specified: ${dataType}`) + throw new Error(`Invalid data type specified: ${dataType as string}`) } return { - registerType: registerType as RegisterType, + registerType, address: parsedAddress, dataType, } diff --git a/src/publisher.ts b/src/publisher.ts index c44633c..3987c86 100644 --- a/src/publisher.ts +++ b/src/publisher.ts @@ -8,8 +8,8 @@ export enum PublisherType { } export interface PublisherImpl { - publishSensorData: (sensorData: PowerSensorData[]) => void - publishCharacteristicsSensorData: (sensorData: CharacteristicsSensorData[]) => void + publishSensorData: (sensorData: PowerSensorData[]) => Promise + publishCharacteristicsSensorData: (sensorData: CharacteristicsSensorData[]) => Promise } export interface Publisher { diff --git a/src/publisher/console.ts b/src/publisher/console.ts index 6c9fb9a..f1c42b9 100644 --- a/src/publisher/console.ts +++ b/src/publisher/console.ts @@ -9,13 +9,13 @@ export interface ConsolePublisher extends Publisher { const logger = createLogger('publisher.console') export class ConsolePublisherImpl implements PublisherImpl { - publishSensorData(sensorData: PowerSensorData[]): void { + async publishSensorData(sensorData: PowerSensorData[]): Promise { for (const data of sensorData) { logger.info(`${data.circuit.name}: ${data.power}W`) } } - publishCharacteristicsSensorData(sensorData: CharacteristicsSensorData[]): void { + async publishCharacteristicsSensorData(sensorData: CharacteristicsSensorData[]): Promise { for (const data of sensorData) { logger.info(`${data.characteristics.name}: ${data.voltage}V, ${data.frequency}Hz`) } diff --git a/src/publisher/websocket.ts b/src/publisher/websocket.ts index 138ad16..186a146 100644 --- a/src/publisher/websocket.ts +++ b/src/publisher/websocket.ts @@ -30,7 +30,7 @@ export class WebSocketPublisherImpl implements PublisherImpl { // Reuse the HTTP server given to us this.wss = new WebSocketServer({ server: httpServer }) - // Keep track of the last published sensor data so we can deliver it immediately (if available) to newly connected + // Keep track of the last published sensor data, so we can deliver it immediately (if available) to newly connected // clients this.lastPublishedSensorData = { characteristicsSensorData: null, @@ -62,7 +62,7 @@ export class WebSocketPublisherImpl implements PublisherImpl { }) } - publishCharacteristicsSensorData(sensorData: CharacteristicsSensorData[]): void { + async publishCharacteristicsSensorData(sensorData: CharacteristicsSensorData[]): Promise { this.broadcastMessage({ type: 'characteristicsSensorData', data: sensorData, @@ -71,7 +71,7 @@ export class WebSocketPublisherImpl implements PublisherImpl { this.lastPublishedSensorData.characteristicsSensorData = sensorData } - publishSensorData(sensorData: PowerSensorData[]): void { + async publishSensorData(sensorData: PowerSensorData[]): Promise { // Remove circular references so we can encode as JSON sensorData = untangleCircularDeps(sensorData) diff --git a/src/sensor/modbus.ts b/src/sensor/modbus.ts index 84f43ea..2c53694 100644 --- a/src/sensor/modbus.ts +++ b/src/sensor/modbus.ts @@ -60,13 +60,13 @@ const readRegisters = async ( switch (register.registerType) { case RegisterType.HOLDING_REGISTER: - return await client.readHoldingRegisters(address, length) + return client.readHoldingRegisters(address, length) case RegisterType.INPUT_REGISTER: - return await client.readInputRegisters(address, length) + return client.readInputRegisters(address, length) case RegisterType.COIL: - return await client.readCoils(address, length) + return client.readCoils(address, length) case RegisterType.DISCRETE_INPUT: - return await client.readDiscreteInputs(address, length) + return client.readDiscreteInputs(address, length) } }