From 01433099b332d35f0ae60476f793eb8d0ee152d6 Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Mon, 13 Apr 2020 15:41:23 +0200 Subject: [PATCH] feat(bluetooth-low-energy): allow the update frequency to be throttled 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 --- docs/integrations/bluetooth-low-energy.md | 29 +++--- .../bluetooth-low-energy.config.ts | 1 + .../bluetooth-low-energy.service.spec.ts | 91 ++++++++++++++++++- .../bluetooth-low-energy.service.ts | 15 ++- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/docs/integrations/bluetooth-low-energy.md b/docs/integrations/bluetooth-low-energy.md index e448065..93c931c 100644 --- a/docs/integrations/bluetooth-low-energy.md +++ b/docs/integrations/bluetooth-low-energy.md @@ -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 diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.config.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.config.ts index 5f4630a..c306901 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.config.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.config.ts @@ -11,6 +11,7 @@ export class BluetoothLowEnergyConfig { tagOverrides: { [key: string]: TagOverride } = {}; timeout = 5; + updateFrequency = 0; maxDistance?: number; } diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts index be96129..7adda5a 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts @@ -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 = { @@ -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); @@ -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); diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts index 0da4e22..f5a2a46 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts @@ -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'; @@ -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(); + private tagUpdaters: { + [tagId: string]: (event: NewDistanceEvent) => void; + } = {}; constructor( private readonly entitiesService: EntitiesService, @@ -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); } }