From ee4fbc1ac4635ac0bcd78c8b0a74e60e7a61abc1 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 12 Jun 2024 12:38:15 +0300 Subject: [PATCH 1/3] npm audit fix, updates braces and fill-range --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 484596a..f9102ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2245,12 +2245,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3099,9 +3099,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" From 139d540028147c2ad8e594695d248b1738fa3b61 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 12 Jun 2024 12:39:07 +0300 Subject: [PATCH 2/3] Remove unused parameter --- src/sensor/dummy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sensor/dummy.ts b/src/sensor/dummy.ts index b66fab7..0093ec3 100644 --- a/src/sensor/dummy.ts +++ b/src/sensor/dummy.ts @@ -12,8 +12,6 @@ import { Characteristics } from '../characteristics' export const getSensorData: PowerSensorPollFunction = async ( timestamp: number, circuit: Circuit, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - existingSensorData?: PowerSensorData[], ): Promise => { return emptySensorData(timestamp, circuit) } From cb15181be9b1d47993482003a572a6f70452f83c Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 12 Jun 2024 12:41:08 +0300 Subject: [PATCH 3/3] Introduce generic "filters" concept, add high-pass filter My heatpump often uses < 2W when on but not doing anything (because conditions don't require it to actually run). I want to ignore this to get cleaner graphs. --- README.md | 1 + examples/config.sample.full.yml | 8 +++-- src/eachwatt.ts | 9 +++--- src/filter/filter.ts | 25 ++++++++++++++++ src/sensor.ts | 3 +- tests/filter/filter.test.ts | 52 +++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/filter/filter.ts create mode 100644 tests/filter/filter.test.ts diff --git a/README.md b/README.md index 42db408..1be06a4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ to any number of different targets, such as databases or MQTT. * Supports arbitrary _**grouping of devices**_ * A group can be for example "Heating" or "Lights". This allows users to get a better overview of their energy consumption when many circuits and devices are involved. +* Can apply various _**filters**_ to the power sensor data (clamping, high-pass etc.) * Can _**measure unmetered power**_ too * You can have a current-transformer type sensor measuring a circuit, then a smart plug measuring some specific device on that circuit, then an unmetered type sensor which calculates the difference between the two, yielding the diff --git a/examples/config.sample.full.yml b/examples/config.sample.full.yml index e913571..cc5f8a0 100644 --- a/examples/config.sample.full.yml +++ b/examples/config.sample.full.yml @@ -31,7 +31,8 @@ circuits: - Vägg vardagsrum - Vägg kök - Vägg arbetsrum - clamp: positive # Don't allow negative values + filters: + clamp: positive # Don't allow negative values # A IotaWatt main circuit, phase A/L1 - name: Main L1 type: main @@ -78,6 +79,8 @@ circuits: iotawatt: address: 10.112.4.4 name: Vagg_Kok + filters: + highPass: 2 # Ignore loads less than 2 watts (show as 0) group: Matlagning # Yet another IotaWatt circuit. This one has a different "parent" than the others. - name: Vägg arbetsrum @@ -145,7 +148,8 @@ circuits: unit: 100 register: 866 type: int16 - clamp: positive + filters: + clamp: positive # # Characteristics. Characteristics mean voltage and frequency, and potentially other non-power related readings. diff --git a/src/eachwatt.ts b/src/eachwatt.ts index 6428b68..29e6089 100644 --- a/src/eachwatt.ts +++ b/src/eachwatt.ts @@ -11,6 +11,7 @@ import { pollCharacteristicsSensors } from './characteristics' import { createLogger } from './logger' import { setRequestTimeout as setHttpRequestTimeout } from './http/client' import { setRequestTimeout as setModbusRequestTimeout } from './modbus/client' +import { applyFilters } from './filter/filter' // Set up a signal handler, so we can exit on Ctrl + C when run from Docker process.on('SIGINT', () => { @@ -54,14 +55,14 @@ const mainPollerFunc = async (config: Config) => { const characteristicsSensorData = await pollCharacteristicsSensors(now, config.characteristics) // Post-process power sensor data - for (const data of powerSensorData) { + for (let data of powerSensorData) { if (data.power !== undefined) { // Round all numbers to one decimal point data.power = Number(data.power.toFixed(1)) - // Optionally clamp values - if (data.circuit.sensor.clamp === 'positive') { - data.power = Math.max(0, data.power) + // Apply optional data filters + if (data.circuit.sensor.filters) { + data = applyFilters(data.circuit.sensor.filters, data) } } } diff --git a/src/filter/filter.ts b/src/filter/filter.ts new file mode 100644 index 0000000..1f1cb64 --- /dev/null +++ b/src/filter/filter.ts @@ -0,0 +1,25 @@ +import { PowerSensorData } from '../sensor' + +export type PowerSensorFilters = { + clamp?: 'positive' + highPass?: number +} + +export const applyFilters = (filters: PowerSensorFilters, data: PowerSensorData): PowerSensorData => { + if (data.power === undefined) { + return data + } + + // Clamp + if (filters?.clamp === 'positive') { + data.power = Math.max(0, data.power) + } + + // High-pass + const highPass = filters?.highPass + if (highPass !== undefined && data.power < highPass) { + data.power = 0 + } + + return data +} diff --git a/src/sensor.ts b/src/sensor.ts index 084533c..d2d73b3 100644 --- a/src/sensor.ts +++ b/src/sensor.ts @@ -1,5 +1,6 @@ import { Circuit } from './circuit' import { Characteristics } from './characteristics' +import { PowerSensorFilters } from './filter/filter' export enum SensorType { Iotawatt = 'iotawatt', @@ -36,7 +37,7 @@ export type CharacteristicsSensorPollFunction = ( export interface PowerSensor { type: SensorType pollFunc: PowerSensorPollFunction - clamp?: 'positive' + filters?: PowerSensorFilters } export interface CharacteristicsSensor { diff --git a/tests/filter/filter.test.ts b/tests/filter/filter.test.ts new file mode 100644 index 0000000..2db1735 --- /dev/null +++ b/tests/filter/filter.test.ts @@ -0,0 +1,52 @@ +import { emptySensorData, PowerSensorData, SensorType } from '../../src/sensor' +import { applyFilters, PowerSensorFilters } from '../../src/filter/filter' +import { Circuit } from '../../src/circuit' +import { getSensorData as getDummySensorData } from '../../src/sensor/dummy' +import exp = require('node:constants') + +test('clamping works', () => { + const filters: PowerSensorFilters = {} + let data: PowerSensorData = dummySensorData() + + data.power = 10 + data = applyFilters(filters, data) + expect(data.power).toEqual(10) + + data.power = -10 + data = applyFilters(filters, data) + expect(data.power).toEqual(-10) + + filters.clamp = 'positive' + data = applyFilters(filters, data) + expect(data.power).toEqual(0) +}) + +test('high-pas works', () => { + const filters: PowerSensorFilters = {} + let data: PowerSensorData = dummySensorData() + + data.power = 100 + data = applyFilters(filters, data) + expect(data.power).toEqual(100) + + data.power = 1.5 + filters.highPass = 2 + data = applyFilters(filters, data) + expect(data.power).toEqual(0) + data.power = 3 + data = applyFilters(filters, data) + expect(data.power).toEqual(3) +}) + +const dummySensorData = (): PowerSensorData => { + const circuit: Circuit = { + name: 'dummy', + children: [], + sensor: { + type: SensorType.Dummy, + pollFunc: getDummySensorData, + }, + } + + return emptySensorData(0, circuit) +}