Skip to content

Commit

Permalink
feat(bluetooth-low-energy): add blacklist option
Browse files Browse the repository at this point in the history
Promised this to @monster1025 in #99! Blacklist trumps whitelist.
  • Loading branch information
mKeRix committed Mar 22, 2020
1 parent 6c37878 commit 0f6eac4
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 7 deletions.
6 changes: 4 additions & 2 deletions docs/integrations/bluetooth-low-energy.md
Expand Up @@ -37,7 +37,7 @@ This integration requires you to run room-assistant in the `host` network.

## Determining the IDs

In order to not clutter your home automation software with the many BLE devices broadcasting their status nearby, room-assistant requires you to set up a whitelist before it will pass on any information. For regular BLE devices this is the lowercase MAC address without `:`, for example `7750fb4dab70` for a peripheral with the MAC address `77:50:FB:4D:AB:70`. When using iBeacons the ID will be in the format of `uuid-major-minor`, for example `2f234454cf6d4a0fadf2f4911ba9ffa6-1-2`.
In order to not clutter your home automation software with the many BLE devices broadcasting their status nearby, room-assistant requires you to set up a whitelist or blacklist before it will pass on any information. For regular BLE devices this is the lowercase MAC address without `:`, for example `7750fb4dab70` for a peripheral with the MAC address `77:50:FB:4D:AB:70`. When using iBeacons the ID will be in the format of `uuid-major-minor`, for example `2f234454cf6d4a0fadf2f4911ba9ffa6-1-2`.

If you are unsure what ID your device has you can start room-assistant with the BLE integration enabled, but no whitelist. Devices that are seen for the first time after starting will be logged with their ID to the console.

Expand All @@ -47,14 +47,16 @@ If you are unsure what ID your device has you can start room-assistant with the
| ---------------- | ------------------------------- | -------- | ------------------------------------------------------------ |
| `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`. |
| `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 @@ -2,6 +2,8 @@ export class BluetoothLowEnergyConfig {
hciDeviceId = 0;
whitelist: string[] = [];
whitelistRegex = false;
blacklist: string[] = [];
blacklistRegex = false;
processIBeacon = true;
onlyIBeacon = false;
majorMask = 0xffff;
Expand Down
Expand Up @@ -164,6 +164,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

mockConfig.onlyIBeacon = true;
Expand Down Expand Up @@ -196,6 +197,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

mockConfig.processIBeacon = false;
Expand Down Expand Up @@ -226,6 +228,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

service.handleDiscovery({
Expand Down Expand Up @@ -264,11 +267,60 @@ describe('BluetoothLowEnergyService', () => {
expect(clusterService.publish).not.toHaveBeenCalled();
});

it('should not publish anything if the whitelist is empty', () => {
it('should not publish anything if the whitelist and blacklist are empty', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
mockConfig.whitelist = [];
mockConfig.blacklist = [];

service.handleDiscovery({
id: '89:47:65',
rssi: -82,
advertisement: {}
} as Peripheral);
expect(handleDistanceSpy).not.toHaveBeenCalled();
expect(clusterService.publish).not.toHaveBeenCalled();
});

it('should not publish anything if the device is on the blacklist', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
mockConfig.whitelist = ['89:47:65', 'abcd'];
mockConfig.blacklist = ['89:47:65'];

service.handleDiscovery({
id: '89:47:65',
rssi: -82,
advertisement: {}
} as Peripheral);
expect(handleDistanceSpy).not.toHaveBeenCalled();
expect(clusterService.publish).not.toHaveBeenCalled();
});

it('should publish all devices not on blacklist if there is no whitelist', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
mockConfig.whitelist = [];
mockConfig.blacklist = ['89:47:65'];

service.handleDiscovery({
id: 'abcd',
rssi: -82,
advertisement: {}
} as Peripheral);
expect(handleDistanceSpy).toHaveBeenCalled();
expect(clusterService.publish).toHaveBeenCalled();
});

it('should not publish devices on blacklist if there is no whitelist', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
mockConfig.whitelist = [];
mockConfig.blacklist = ['89:47:65'];

service.handleDiscovery({
id: '89:47:65',
Expand All @@ -283,6 +335,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);
mockConfig.tagOverrides = {
abcd: {
Expand Down Expand Up @@ -331,6 +384,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);
mockConfig.tagOverrides = {
abcd: {
Expand Down Expand Up @@ -384,11 +438,30 @@ describe('BluetoothLowEnergyService', () => {
expect(service.isOnWhitelist('test123')).toBeFalsy();
});

it('should match ids to a normal blacklist', () => {
mockConfig.blacklist = ['vip-id', 'vip2-id'];

expect(service.isOnBlacklist('vip-id')).toBeTruthy();
expect(service.isOnBlacklist('random-id')).toBeFalsy();
});

it('should match ids to a regex blacklist', () => {
mockConfig.blacklist = ['vip-[a-z]+', '^[1-9]+$'];
mockConfig.blacklistRegex = true;

expect(service.isOnBlacklist('vip-def')).toBeTruthy();
expect(service.isOnBlacklist('asvip-abcd')).toBeTruthy();
expect(service.isOnBlacklist('123')).toBeTruthy();
expect(service.isOnBlacklist('test')).toBeFalsy();
expect(service.isOnBlacklist('test123')).toBeFalsy();
});

it('should filter the measured RSSI of the peripherals', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
const filterSpy = jest.spyOn(service, 'filterRssi').mockReturnValue(-50);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

service.handleDiscovery({
Expand All @@ -411,6 +484,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);
mockConfig.maxDistance = 5;

Expand Down Expand Up @@ -438,6 +512,7 @@ describe('BluetoothLowEnergyService', () => {
const handleDistanceSpy = jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);
mockConfig.maxDistance = 5;

Expand Down Expand Up @@ -468,6 +543,7 @@ describe('BluetoothLowEnergyService', () => {
jest
.spyOn(service, 'handleNewDistance')
.mockImplementation(() => undefined);
jest.spyOn(service, 'isWhitelistEnabled').mockReturnValue(true);
jest.spyOn(service, 'isOnWhitelist').mockReturnValue(true);

service.handleDiscovery({
Expand Down
Expand Up @@ -44,9 +44,9 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
* Lifecycle hook, called once the host module has been initialized.
*/
onModuleInit(): void {
if (!this.isWhitelistEnabled()) {
if (!this.isWhitelistEnabled() && !this.isBlacklistEnabled()) {
this.logger.warn(
'The whitelist is empty, no sensors will be created! Please add some of the discovered IDs below to your configuration.'
'The whitelist and blacklist are empty, no sensors will be created! Please add some of the discovered IDs below to your configuration.'
);
}
}
Expand Down Expand Up @@ -83,7 +83,11 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
this.seenIds.add(tag.id);
}

if (this.isOnWhitelist(tag.id)) {
if (
(this.isOnWhitelist(tag.id) ||
(!this.isWhitelistEnabled() && this.isBlacklistEnabled())) &&
!this.isOnBlacklist(tag.id)
) {
tag = this.applyOverrides(tag);
tag.rssi = this.filterRssi(tag.id, tag.rssi);

Expand Down Expand Up @@ -150,9 +154,17 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
return this.config.whitelist?.length > 0;
}

/**
* Determines whether a blacklist has been configured or not.
*
* @returns Blacklist status
*/
isBlacklistEnabled(): boolean {
return this.config.blacklist?.length > 0;
}

/**
* Checks if an id is on the whitelist of this component.
* Always returns true if the whitelist is empty.
*
* @param id - Device id
* @return Whether the id is on the whitelist or not
Expand All @@ -168,6 +180,23 @@ export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
: whitelist.includes(id);
}

/**
* Checks if an id is on the blacklist of this component.
*
* @param id - Device id
* @return Whether the id is on the blacklist or not
*/
isOnBlacklist(id: string): boolean {
const blacklist = this.config.blacklist;
if (blacklist === undefined || blacklist.length === 0) {
return false;
}

return this.config.blacklistRegex
? blacklist.some(regex => id.match(regex))
: blacklist.includes(id);
}

/**
* Applies the Kalman filter based on the historic values with the same tag id.
*
Expand Down

0 comments on commit 0f6eac4

Please sign in to comment.