Skip to content

Commit

Permalink
feat(omron-d6t): add heatmap camera entity
Browse files Browse the repository at this point in the history
Closes #199
  • Loading branch information
mKeRix committed May 31, 2020
1 parent bc941ad commit aee7f09
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
@@ -1,7 +1,7 @@
FROM node:12-alpine as build
ARG ROOM_ASSISTANT_VERSION=latest

RUN apk add --no-cache python make g++ libusb-dev eudev-dev avahi-dev
RUN apk add --no-cache python make g++ libusb-dev eudev-dev avahi-dev cairo-dev jpeg-dev pango-dev giflib-dev
RUN npm install -g --unsafe-perm room-assistant@$ROOM_ASSISTANT_VERSION

FROM node:12-alpine
Expand Down
2 changes: 1 addition & 1 deletion dev.Dockerfile
@@ -1,7 +1,7 @@
FROM node:12-alpine as build
WORKDIR /room-assistant

RUN apk add --no-cache python make g++ libusb-dev eudev-dev avahi-dev
RUN apk add --no-cache python make g++ libusb-dev eudev-dev avahi-dev cairo-dev jpeg-dev pango-dev giflib-dev
COPY ./*.tgz /room-assistant/
RUN npm install -g --unsafe-perm *.tgz

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/quickstart-pi-zero-w.md
Expand Up @@ -54,7 +54,7 @@ This page will guide you through setting up a Pi Zero W to run room-assistant.

3. To make the commands we install with npm available the $PATH environment variable needs to be extended as well. Edit the file `~/.profile` (e.g. with `nano ~/.profile`) and add the `PATH="$PATH:/opt/nodejs/bin"` to the end of the file. Save, then run `source ~/.profile`.

4. We need to install some other dependencies as well, do so by running `sudo apt-get update && sudo apt-get install libavahi-compat-libdnssd-dev bluetooth libbluetooth-dev libudev-dev`.
4. We need to install some other dependencies as well, do so by running `sudo apt-get update && sudo apt-get install build-essential libavahi-compat-libdnssd-dev bluetooth libbluetooth-dev libudev-dev libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev`.

5. Now let's get install room-assistant! Run `sudo npm i --global --unsafe-perm room-assistant`. You will see messages like the one shown below during the installation process. Don't worry about them - they're not errors!

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/quickstart-pi.md
Expand Up @@ -57,7 +57,7 @@ This page will guide you through setting up a Raspberry Pi 3 or 4 to run room-as
sudo apt-get install -y nodejs
```

4. We need to install some other dependencies as well, do so by running `sudo apt-get update && sudo apt-get install libavahi-compat-libdnssd-dev bluetooth libbluetooth-dev libudev-dev`.
4. We need to install some other dependencies as well, do so by running `sudo apt-get update && sudo apt-get install build-essential libavahi-compat-libdnssd-dev bluetooth libbluetooth-dev libudev-dev libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev`.

5. Now let's get install room-assistant! Run `sudo npm i --global --unsafe-perm room-assistant`. You will see messages like the one shown below during the installation process. Don't worry about them - they're not errors!

Expand Down
29 changes: 25 additions & 4 deletions docs/integrations/omron-d6t.md
Expand Up @@ -18,6 +18,13 @@ This integration only supports the D6T-44L-06 sensor at the moment. You will nee

### Running with NodeJS

To enable heatmap generation you may be required to install some [additional system packages](https://github.com/Automattic/node-canvas#compiling) for compilation:

```shell
sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
sudo npm i --global --unsafe-perm room-assistant
```

On Raspberry Pi devices the I<sup>2</sup>C interface also needs to be enabled using `sudo raspi-config` and then enabling the I<sup>2</sup>C option under Advanced Options.

### Running with Docker
Expand Down Expand Up @@ -71,11 +78,20 @@ When placing your sensor you need to consider a few factors to get reliable resu

## Settings

| Name | Type | Default | Description |
| ---------------- | ------------------- | ------- | ------------------------------------------------------------ |
| `busNumber` | Number | `1` | I<sup>2</sup>C bus number of your machine that the sensor is connected to. |
| `address` | Number | `0x0a` | I<sup>2</sup>C address of the D6T sensor that you want to use. |
| `deltaThreshold` | Number | `1.5` | Minimum temperature difference between average and single temperature pixel in &deg;C for it to be considered as human presence. Increase if you are seeing false positives, decrease if you are seeing false negatives. |
| `heatmap` | [Heatmap](#heatmap) | | A number of options for configuring the heatmap output. |

### Heatmap

| Name | Type | Default | Description |
| ---------------- | ------ | ------- | ------------------------------------------------------------ |
| `busNumber` | Number | `1` | I<sup>2</sup>C bus number of your machine that the sensor is connected to. |
| `address` | Number | `0x0a` | I<sup>2</sup>C address of the D6T sensor that you want to use. |
| `deltaThreshold` | Number | `1.5` | Minimum temperature difference between average and single temperature pixel in &deg;C for it to be considered as human presence. Increase if you are seeing false positives, decrease if you are seeing false negatives. |
| `minTemperature` | Number | `16` | Temperature that will be considered the lower bound for the color scale in &deg;C. |
| `maxTemperature` | Number | `30` | Temperature that will be considered the upper bound for the color scale in &deg;C. |
| `rotation` | Number | `0` | The amount of degrees that the heatmap output image should be rotated. Only `0`, `90`, `180` or `270` are supported as values. |

::: details Example Config

Expand All @@ -85,6 +101,11 @@ global:
- omronD6t
omronD6t:
deltaThreshold: 2
heatmap:
minTemperature: 16
maxTemperature: 30
rotation: 90
```

:::
:::

41 changes: 41 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -62,7 +62,6 @@
"nest-emitter": "^1.1.0",
"nest-winston": "^1.3.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.5.5",
"slugify": "^1.4.0",
"systeminformation": "^4.26.4",
Expand Down Expand Up @@ -95,6 +94,7 @@
"jest": "^26.0.0",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1",
"rimraf": "^3.0.2",
"supertest": "^4.0.2",
"ts-jest": "^26.0.0",
"ts-loader": "^7.0.5",
Expand All @@ -107,6 +107,7 @@
},
"optionalDependencies": {
"@abandonware/noble": "^1.9.2-5",
"canvas": "^2.6.1",
"i2c-bus": "^5.1.0",
"mdns": "^2.5.1",
"onoff": "^5.0.1"
Expand Down
6 changes: 5 additions & 1 deletion src/integrations/grid-eye/grid-eye.service.ts
Expand Up @@ -60,7 +60,11 @@ export class GridEyeService extends ThermopileOccupancyService
*/
@Interval(1000)
async updateState(): Promise<void> {
const coordinates = await this.getCoordinates(this.config.deltaThreshold);
const temperatures = await this.getPixelTemperatures();
const coordinates = await this.getCoordinates(
temperatures,
this.config.deltaThreshold
);

this.sensor.state = coordinates.length;
this.sensor.attributes.coordinates = coordinates;
Expand Down
3 changes: 3 additions & 0 deletions src/integrations/omron-d6t/omron-d6t.config.ts
@@ -1,5 +1,8 @@
import { HeatmapOptions } from '../thermopile/thermopile-occupancy.config';

export class OmronD6tConfig {
busNumber = 1;
address = 0x0a;
deltaThreshold = 1.5;
heatmap = new HeatmapOptions();
}
35 changes: 33 additions & 2 deletions src/integrations/omron-d6t/omron-d6t.service.spec.ts
Expand Up @@ -23,6 +23,7 @@ jest.mock(
},
{ virtual: true }
);
jest.mock('canvas', () => undefined, { virtual: true });

const VALID_RESPONSE = Buffer.from([
0xe5,
Expand Down Expand Up @@ -110,6 +111,18 @@ describe('OmronD6tService', () => {
);
});

it('should register a new camera on bootstrap if available', async () => {
jest.spyOn(service, 'isHeatmapAvailable').mockReturnValue(true);
await service.onApplicationBootstrap();

expect(entitiesService.add).toHaveBeenCalledWith(
expect.objectContaining({
id: 'd6t_heatmap',
name: 'D6T Heatmap',
})
);
});

it('should close the i2c bus on shutdown', async () => {
await service.onApplicationBootstrap();
await service.onApplicationShutdown();
Expand All @@ -120,6 +133,7 @@ describe('OmronD6tService', () => {
it('should update its state based on the calculated coordinates', async () => {
const mockSensor = new Sensor('d6t', 'Occupancy');
entitiesService.add.mockReturnValue(mockSensor);
jest.spyOn(service, 'getPixelTemperatures').mockResolvedValue([]);
jest.spyOn(service, 'getCoordinates').mockResolvedValue([
[1, 0],
[2, 3],
Expand All @@ -136,9 +150,26 @@ describe('OmronD6tService', () => {
});
});

it('should update the camera entity with the generated heatmap', async () => {
jest.spyOn(service, 'isHeatmapAvailable').mockReturnValue(true);
entitiesService.add.mockImplementation((entity) => entity);
jest.spyOn(service, 'getPixelTemperatures').mockResolvedValue([]);
jest.spyOn(service, 'getCoordinates').mockResolvedValue([
[1, 0],
[2, 3],
]);

const imageData = new Buffer('abc');
jest.spyOn(service, 'generateHeatmap').mockResolvedValue(imageData);

await service.onApplicationBootstrap();
await service.updateState();
expect(entitiesService.add.mock.calls[1][0].state).toBe(imageData);
});

it('should log PEC check errors to debug', async () => {
jest
.spyOn(service, 'getCoordinates')
.spyOn(service, 'getPixelTemperatures')
.mockRejectedValue(new I2CError('PEC check failed'));

await service.onApplicationBootstrap();
Expand All @@ -152,7 +183,7 @@ describe('OmronD6tService', () => {

it('should log other errors in the error level', async () => {
jest
.spyOn(service, 'getCoordinates')
.spyOn(service, 'getPixelTemperatures')
.mockRejectedValue(new Error('bus unavailable'));

await service.onApplicationBootstrap();
Expand Down
36 changes: 34 additions & 2 deletions src/integrations/omron-d6t/omron-d6t.service.ts
Expand Up @@ -15,6 +15,7 @@ import { Entity } from '../../entities/entity';
import { I2CError } from './i2c.error';
import { SensorConfig } from '../home-assistant/sensor-config';
import { ThermopileOccupancyService } from '../thermopile/thermopile-occupancy.service';
import { Camera } from '../../entities/camera';

const TEMPERATURE_COMMAND = 0x4c;

Expand All @@ -24,6 +25,7 @@ export class OmronD6tService extends ThermopileOccupancyService
private readonly config: OmronD6tConfig;
private i2cBus: PromisifiedBus;
private sensor: Entity;
private camera: Camera;
private readonly logger: Logger;

constructor(
Expand All @@ -42,6 +44,14 @@ export class OmronD6tService extends ThermopileOccupancyService
this.logger.log(`Opening i2c bus ${this.config.busNumber}`);
this.i2cBus = await i2cBus.openPromisified(this.config.busNumber);
this.sensor = this.createSensor();

if (this.isHeatmapAvailable()) {
this.camera = this.createHeatmapCamera();
} else {
this.logger.warn(
'Heatmap is unavailable due to the canvas dependency not being installed'
);
}
}

/**
Expand All @@ -53,15 +63,26 @@ export class OmronD6tService extends ThermopileOccupancyService
}

/**
* Updates the state of the sensor that this integration manages.
* Updates the state of the entities that this integration manages.
*/
@Interval(250)
async updateState(): Promise<void> {
try {
const coordinates = await this.getCoordinates(this.config.deltaThreshold);
const temperatures = await this.getPixelTemperatures();
const coordinates = await this.getCoordinates(
temperatures,
this.config.deltaThreshold
);

this.sensor.state = coordinates.length;
this.sensor.attributes.coordinates = coordinates;

if (this.camera != undefined) {
this.camera.state = await this.generateHeatmap(
temperatures,
this.config.heatmap
);
}
} catch (e) {
if (e instanceof I2CError) {
this.logger.debug(`Error during I2C communication: ${e.message}`);
Expand Down Expand Up @@ -164,4 +185,15 @@ export class OmronD6tService extends ThermopileOccupancyService
customizations
) as Sensor;
}

/**
* Creates and registers a new heatmap camera entity.
*
* @returns Registered camera
*/
protected createHeatmapCamera(): Camera {
return this.entitiesService.add(
new Camera('d6t_heatmap', 'D6T Heatmap')
) as Camera;
}
}
7 changes: 7 additions & 0 deletions src/integrations/thermopile/thermopile-occupancy.config.ts
@@ -0,0 +1,7 @@
import { RotationOption } from './thermopile-occupancy.service';

export class HeatmapOptions {
minTemperature = 16;
maxTemperature = 30;
rotation: RotationOption = 0;
}

0 comments on commit aee7f09

Please sign in to comment.