diff --git a/docs/integrations/grid-eye.md b/docs/integrations/grid-eye.md index 71389fb..7fa8cc5 100644 --- a/docs/integrations/grid-eye.md +++ b/docs/integrations/grid-eye.md @@ -77,6 +77,7 @@ When placing your sensor you need to consider a few factors to get reliable resu | `busNumber` | Number | `1` | I2C bus number of your machine that the sensor is connected to. | | `address` | Number | `0x69` | I2C address of the D6T sensor that you want to use. | | `deltaThreshold` | Number | `2` | Minimum temperature difference between average and single temperature pixel in °C for it to be considered as human presence. Increase if you are seeing false positives, decrease if you are seeing false negatives. | +| `maskZeroBasedValues` | Boolean | `false` | Mask values < 1 that are incorrectly reported, replacing with nearest valid value or mean of grid for first pixel. | | `heatmap` | [Heatmap](#heatmap) | | A number of options for configuring the heatmap output. | ### Heatmap diff --git a/src/config/config.service.spec.fail.yml b/src/config/config.service.spec.fail.yml index 251d21f..aec5c89 100644 --- a/src/config/config.service.spec.fail.yml +++ b/src/config/config.service.spec.fail.yml @@ -142,6 +142,7 @@ gridEye: busNumber: 1 address: 0x69 deltaThreshold: 2 + maskZeroBasedValues: 10 # TYPE ERROR: Boolean required heatmap: enabled: true minTemperature: "23" # TYPE ERROR: Number Required diff --git a/src/config/config.service.spec.pass.yml b/src/config/config.service.spec.pass.yml index 5dc8992..4fccf1c 100644 --- a/src/config/config.service.spec.pass.yml +++ b/src/config/config.service.spec.pass.yml @@ -133,6 +133,7 @@ gridEye: busNumber: 1 address: 0x69 deltaThreshold: 2 + maskZeroBasedValues: true heatmap: enabled: true minTemperature: 23 diff --git a/src/config/config.service.spec.ts b/src/config/config.service.spec.ts index 7245463..cf5302f 100644 --- a/src/config/config.service.spec.ts +++ b/src/config/config.service.spec.ts @@ -93,6 +93,7 @@ describe('ConfigService', () => { `bluetoothClassic.timeoutCycles`, `bluetoothClassic.entityOverrides.ebef1234567890-55555-333.id`, `omronD6t.heatmap.enabled`, + `gridEye.maskZeroBasedValues`, `gridEye.heatmap.minTemperature`, `gridEye.heatmap.rotation`, `gpio.binarySensors[1].deviceClass`, diff --git a/src/integrations/grid-eye/grid-eye.config.ts b/src/integrations/grid-eye/grid-eye.config.ts index 3b7b342..0475ff5 100644 --- a/src/integrations/grid-eye/grid-eye.config.ts +++ b/src/integrations/grid-eye/grid-eye.config.ts @@ -8,6 +8,8 @@ export class GridEyeConfig { address = 0x69; @(jf.number().min(0).required()) deltaThreshold = 2; + @(jf.boolean().required()) + maskZeroBasedValues = false; @(jf.object({ objectClass: HeatmapOptions }).required()) heatmap = new HeatmapOptions(); } diff --git a/src/integrations/grid-eye/grid-eye.service.spec.ts b/src/integrations/grid-eye/grid-eye.service.spec.ts index 0a02333..636dad2 100644 --- a/src/integrations/grid-eye/grid-eye.service.spec.ts +++ b/src/integrations/grid-eye/grid-eye.service.spec.ts @@ -4,6 +4,8 @@ const mockI2cBus = { close: jest.fn(), }; +import { ConfigService } from '../../config/config.service'; +import c from 'config'; import { Test, TestingModule } from '@nestjs/testing'; import { GridEyeService } from './grid-eye.service'; import { EntitiesModule } from '../../entities/entities.module'; @@ -12,6 +14,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { EntitiesService } from '../../entities/entities.service'; import { ClusterService } from '../../cluster/cluster.service'; import { Sensor } from '../../entities/sensor'; +import { GridEyeConfig } from './grid-eye.config'; import i2cBus from 'i2c-bus'; import * as math from 'mathjs'; @@ -31,10 +34,28 @@ describe('GridEyeService', () => { add: jest.fn(), }; const clusterService = jest.fn(); + let mockConfig: Partial; + const configService = { + get: jest.fn().mockImplementation((key: string) => { + return key === 'gridEye' ? mockConfig : c.get(key); + }), + }; beforeEach(async () => { jest.clearAllMocks(); - + mockConfig = { + busNumber: 1, + address: 0x69, + deltaThreshold: 2, + maskZeroBasedValues: false, + heatmap: { + enabled: true, + minTemperature: 16, + maxTemperature: 40, + rotation: 0, + drawTemperatures: true, + }, + }; const module: TestingModule = await Test.createTestingModule({ imports: [EntitiesModule, ConfigModule, ScheduleModule.forRoot()], providers: [GridEyeService], @@ -43,6 +64,8 @@ describe('GridEyeService', () => { .useValue(entitiesService) .overrideProvider(ClusterService) .useValue(clusterService) + .overrideProvider(ConfigService) + .useValue(configService) .compile(); service = module.get(GridEyeService); @@ -184,6 +207,34 @@ describe('GridEyeService', () => { expect(temperatures).toStrictEqual(math.ones([8, 8])); }); + it('should override zero based temperatures when configured', async () => { + mockConfig.maskZeroBasedValues = true; + const temperatureSpy = jest + .spyOn(service, 'getPixelTemperature') + .mockResolvedValue(16) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0.3) + .mockResolvedValueOnce(0.5); + + const temperatures = await service.getPixelTemperatures(); + expect(temperatureSpy).toHaveBeenCalledTimes(64); + expect(temperatures.flat().every((x) => x >= 1)).toBe(true); + }); + + it('should not override zero based temperatures when not configured', async () => { + mockConfig.maskZeroBasedValues = false; + const temperatureSpy = jest + .spyOn(service, 'getPixelTemperature') + .mockResolvedValue(16) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0.3) + .mockResolvedValueOnce(0.5); + + const temperatures = await service.getPixelTemperatures(); + expect(temperatureSpy).toHaveBeenCalledTimes(64); + expect(temperatures.flat().every((x) => x >= 1)).toBe(false); + }); + it('should get the temperature of a single pixel', async () => { const registerSpy = jest .spyOn(service, 'getRegister') diff --git a/src/integrations/grid-eye/grid-eye.service.ts b/src/integrations/grid-eye/grid-eye.service.ts index aa7976d..9ebb5ae 100644 --- a/src/integrations/grid-eye/grid-eye.service.ts +++ b/src/integrations/grid-eye/grid-eye.service.ts @@ -24,7 +24,8 @@ const FRAMERATE_REGISTER = 0x02; @Injectable() export class GridEyeService extends ThermopileOccupancyService - implements OnApplicationBootstrap, OnApplicationShutdown { + implements OnApplicationBootstrap, OnApplicationShutdown +{ private readonly config: GridEyeConfig; private readonly logger: Logger; private i2cBus: PromisifiedBus; @@ -105,7 +106,24 @@ export class GridEyeService temperatures.push(await this.getPixelTemperature(i)); } - return math.reshape(temperatures, [8, 8]) as number[][]; + let grid = math.reshape(temperatures, [8, 8]) as number[][]; + if (this.config.maskZeroBasedValues) { + grid = await this.maskZeroBasedValues(grid); + } + return grid; + } + + /** + * Replace 0 based values (0 to 1) with nearest preceding valid value. + * + * @returns 8x8 matrix of temperatures + */ + async maskZeroBasedValues(temperatures: number[][]): Promise { + const correctedMean = math.mean(temperatures.flat().filter(v => v >= 1 || v < 0)) + + return math.matrix(temperatures) + .map(v => v >= 1 || v < 0 ? v : correctedMean) + .toArray() as number[][]; } /**