Skip to content

Commit

Permalink
feat(bluetooth-low-energy): allow the update frequency to be throttled
Browse files Browse the repository at this point in the history
The option makes a return from room-assistant v1.x and allows
quick-updating tags to be throttled on the software side. Useful to
reduce network and MQTT load.

Closes #125
  • Loading branch information
mKeRix committed Apr 13, 2020
1 parent 567327d commit 0143309
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 17 deletions.
29 changes: 15 additions & 14 deletions docs/integrations/bluetooth-low-energy.md
Expand Up @@ -43,20 +43,21 @@ If you are unsure what ID your device has you can start room-assistant with the

## Settings

| Name | Type | Default | Description |
| ---------------- | ------------------------------- | -------- | ------------------------------------------------------------ |
| `whitelist` | Array | | A list of [BLE tag IDs](#determining-the-ids) that should be tracked. |
| `whitelistRegex` | Boolean | `false` | Whether the whitelist should be evaluated as a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) or not. |
| `blacklist` | Array | | A list of [BLE tag IDs](#determining-the-ids) that should not be tracked. If an ID matches both whitelist and blacklist it will not be tracked. |
| `blacklistRegex` | Boolean | `false` | Whether the blacklist should be evaluated as a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) or not. |
| `processIBeacon` | Boolean | `true` | Whether additional data from iBeacon devices should be taken into account or not. Affects tag IDs and distance estimation. |
| `onlyIBeacon` | Boolean | `false` | Whether only iBeacons should be considered when scanning for devices ot not. |
| `timeout` | Number | `5` | The time after which a recorded distance is considered outdated. This value should be higher than the advertisement frequency of your peripheral. |
| `maxDistance` | Number | | Limits the distance at which a received BLE advertisement is still reported if configured. Value is in meters. |
| `majorMask` | Number | `0xffff` | Filter out bits of the major ID to make dynamic tag IDs with encoded information consistent for filtering. |
| `minorMask` | Number | `0xffff` | Filter out bits of the minor ID to make dynamic tag IDs with encoded information consistent for filtering. |
| `tagOverrides` | [Tag Overrides](#tag-overrides) | | Allows you to override some properties of the tracked devices. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |
| Name | Type | Default | Description |
| ----------------- | ------------------------------- | -------- | ------------------------------------------------------------ |
| `whitelist` | Array | | A list of [BLE tag IDs](#determining-the-ids) that should be tracked. |
| `whitelistRegex` | Boolean | `false` | Whether the whitelist should be evaluated as a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) or not. |
| `blacklist` | Array | | A list of [BLE tag IDs](#determining-the-ids) that should not be tracked. If an ID matches both whitelist and blacklist it will not be tracked. |
| `blacklistRegex` | Boolean | `false` | Whether the blacklist should be evaluated as a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) or not. |
| `processIBeacon` | Boolean | `true` | Whether additional data from iBeacon devices should be taken into account or not. Affects tag IDs and distance estimation. |
| `onlyIBeacon` | Boolean | `false` | Whether only iBeacons should be considered when scanning for devices ot not. |
| `timeout` | Number | `5` | The time after which a recorded distance is considered outdated. This value should be higher than the advertisement frequency of your peripheral. |
| `updateFrequency` | Number | `0` | Minimum amount of seconds that should be waited between distance updates for each tag. The default value disables the throttling. |
| `maxDistance` | Number | | Limits the distance at which a received BLE advertisement is still reported if configured. Value is in meters. |
| `majorMask` | Number | `0xffff` | Filter out bits of the major ID to make dynamic tag IDs with encoded information consistent for filtering. |
| `minorMask` | Number | `0xffff` | Filter out bits of the minor ID to make dynamic tag IDs with encoded information consistent for filtering. |
| `tagOverrides` | [Tag Overrides](#tag-overrides) | | Allows you to override some properties of the tracked devices. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |

### Tag Overrides

Expand Down
Expand Up @@ -11,6 +11,7 @@ export class BluetoothLowEnergyConfig {
tagOverrides: { [key: string]: TagOverride } = {};

timeout = 5;
updateFrequency = 0;
maxDistance?: number;
}

Expand Down
Expand Up @@ -36,6 +36,8 @@ import { NewDistanceEvent } from './new-distance.event';
import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor';
import KalmanFilter from 'kalmanjs';

jest.useFakeTimers();

describe('BluetoothLowEnergyService', () => {
let service: BluetoothLowEnergyService;
const clusterService = {
Expand Down Expand Up @@ -536,6 +538,94 @@ describe('BluetoothLowEnergyService', () => {
);
});

it('should throttle distance reporting if updateFrequency is configured', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);
mockConfig.updateFrequency = 10;
const now = new Date();

service.handleDiscovery({
id: '12:ab:cd:12:cd',
rssi: -89,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);
service.handleDiscovery({
id: 'ab:ab:cd:cd:cd',
rssi: -90,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);
service.handleDiscovery({
id: '12:ab:cd:12:cd',
rssi: -91,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);

expect(handleDistanceSpy).toHaveBeenCalledTimes(2);
expect(handleDistanceSpy).toHaveBeenCalledWith(
expect.objectContaining({
distance: 21.5
})
);

jest
.spyOn(Date, 'now')
.mockReturnValue(now.setSeconds(now.getSeconds() + 11));
service.handleDiscovery({
id: '12:ab:cd:12:cd',
rssi: -100,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);
expect(handleDistanceSpy).toHaveBeenCalledTimes(3);
expect(handleDistanceSpy).toHaveBeenCalledWith(
expect.objectContaining({
distance: 52.7
})
);
});

it('should allow immediate updates if no updateFrequency was configured', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

service.handleDiscovery({
id: '12:ab:cd:12:cd',
rssi: -89,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);
service.handleDiscovery({
id: 'ab:ab:cd:cd:cd',
rssi: -90,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);
service.handleDiscovery({
id: '12:ab:cd:12:cd',
rssi: -91,
advertisement: {
localName: 'Test BLE Device'
}
} as Peripheral);

expect(handleDistanceSpy).toHaveBeenCalledTimes(3);
});

it('should reuse existing Kalman filters for the same id', () => {
const sensor = new Sensor('testid', 'Test');
entitiesService.has.mockReturnValue(true);
Expand Down Expand Up @@ -579,7 +669,6 @@ describe('BluetoothLowEnergyService', () => {
});

it('should add new room presence sensor if no matching ones exist yet', () => {
jest.useFakeTimers();
const sensor = new RoomPresenceDistanceSensor('test', 'Test', 0);
entitiesService.has.mockReturnValue(false);
entitiesService.add.mockReturnValue(sensor);
Expand Down
Expand Up @@ -19,6 +19,7 @@ import { SchedulerRegistry } from '@nestjs/schedule';
import { KalmanFilterable } from '../../util/filters';
import { makeId } from '../../util/id';
import { DISTRIBUTED_DEVICE_ID } from '../home-assistant/home-assistant.const';
import * as _ from 'lodash';

export const NEW_DISTANCE_CHANNEL = 'bluetooth-low-energy.new-distance';

Expand All @@ -28,6 +29,9 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
private readonly config: BluetoothLowEnergyConfig;
private readonly logger: Logger;
private readonly seenIds = new Set<string>();
private tagUpdaters: {
[tagId: string]: (event: NewDistanceEvent) => void;
} = {};

constructor(
private readonly entitiesService: EntitiesService,
Expand Down Expand Up @@ -99,8 +103,15 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
tag.distance,
tag.distance > this.config.maxDistance
);
this.handleNewDistance(event);
this.clusterService.publish(NEW_DISTANCE_CHANNEL, event);

if (!this.tagUpdaters.hasOwnProperty(tag.id)) {
this.tagUpdaters[tag.id] = _.throttle((event: NewDistanceEvent) => {
this.handleNewDistance(event);
this.clusterService.publish(NEW_DISTANCE_CHANNEL, event);
}, this.config.updateFrequency * 1000);
}

this.tagUpdaters[tag.id](event);
}
}

Expand Down

0 comments on commit 0143309

Please sign in to comment.