Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce generic filters concept, add high-pass filter #60

Merged
merged 3 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions examples/config.sample.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions src/eachwatt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/filter/filter.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion src/sensor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Circuit } from './circuit'
import { Characteristics } from './characteristics'
import { PowerSensorFilters } from './filter/filter'

export enum SensorType {
Iotawatt = 'iotawatt',
Expand Down Expand Up @@ -36,7 +37,7 @@ export type CharacteristicsSensorPollFunction = (
export interface PowerSensor {
type: SensorType
pollFunc: PowerSensorPollFunction
clamp?: 'positive'
filters?: PowerSensorFilters
}

export interface CharacteristicsSensor {
Expand Down
2 changes: 0 additions & 2 deletions src/sensor/dummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PowerSensorData> => {
return emptySensorData(timestamp, circuit)
}
Expand Down
52 changes: 52 additions & 0 deletions tests/filter/filter.test.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Loading