Skip to content

Commit

Permalink
Merge pull request #60 from Jalle19/cutoff
Browse files Browse the repository at this point in the history
Introduce generic filters concept, add high-pass filter
  • Loading branch information
Jalle19 authored Jun 12, 2024
2 parents 8226c4b + cb15181 commit 06e89be
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 16 deletions.
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)
}

0 comments on commit 06e89be

Please sign in to comment.