Skip to content

Commit

Permalink
feat(bluetooth-classic): allow device-specific minRssi values
Browse files Browse the repository at this point in the history
Different devices end up showing different RSSI levels at the same
distance, which is why the minRssi value should also be configurable on
a device-level. Backward compatibility is kept by supporting the old
"simple" format and the new "advanced" format for the config option.

Closes #168
  • Loading branch information
mKeRix committed Apr 13, 2020
1 parent 932d603 commit cf3ddc5
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 11 deletions.
43 changes: 34 additions & 9 deletions docs/integrations/bluetooth-classic.md
Expand Up @@ -67,15 +67,20 @@ Bluetooth uses the 2.4 GHz band, which may also be used by some of your other Wi

## Settings

| Name | Type | Default | Description |
| --------------- | ------ | ------- | ------------------------------------------------------------ |
| `addresses` | Array | | List of Bluetooth MAC addresses that should be tracked. You can usually find them in the device settings. |
| `minRssi` | Number | | Limits the RSSI at which a device is still reported if configured. Remember, the RSSI is the inverse of the sensor attribute distance, so for a cutoff at 10 you would configure -10. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |
| `interval` | Number | `6` | The interval at which the Bluetooth devices are queried in seconds. |
| `timeoutCycles` | Number | `2` | The number of completed query cycles after which collected measurements are considered obsolete. The timeout in seconds is calculated as `max(addresses, clusterDevices) * interval * timeoutCycles`. |

::: details Example Config
| Name | Type | Default | Description |
| --------------- | -------------------------------------------- | ------- | ------------------------------------------------------------ |
| `addresses` | Array | | List of Bluetooth MAC addresses that should be tracked. You can usually find them in the device settings. |
| `minRssi` | Number _or_ [detailed config](#minimum-rssi) | | Limits the RSSI at which a device is still reported if configured. Remember, the RSSI is the inverse of the sensor attribute distance, so for a cutoff at 10 you would configure -10. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |
| `interval` | Number | `6` | The interval at which the Bluetooth devices are queried in seconds. |
| `timeoutCycles` | Number | `2` | The number of completed query cycles after which collected measurements are considered obsolete. The timeout in seconds is calculated as `max(addresses, clusterDevices) * interval * timeoutCycles`. |

### Minimum RSSI

`minRssi` can either be specified as a number that is applied to all devices like shown in the simple example config, or as a map of addresses and `minRssi` values as shown in the advanced example config. The latter allows you to configure different cutoffs for the devices you are using, as they may end up showing different RSSI levels even when placed at the same distance. The `minRssi.default` setting will be applied to all addresses that have not been configured specifically. If it is not set, all devices that don't have a specific value will always be considered to be in range.

::: details Simple Example Config

```yaml
global:
integrations:
Expand All @@ -87,3 +92,23 @@ bluetoothClassic:
- '77:50:fb:4d:ab:70'
```
:::

::: details Advanced Example Config

```yaml
global:
integrations:
- bluetoothClassic
bluetoothClassic:
hciDeviceId: 1
interval: 20
timeoutCycles: 2.5
minRssi:
'08:05:90:ed:3b:60': -10
default: -20
addresses:
- '08:05:90:ed:3b:60'
- '77:50:fb:4d:ab:70'
```

:::
@@ -1,6 +1,6 @@
export class BluetoothClassicConfig {
addresses: string[] = [];
minRssi?: number;
minRssi?: number | { [address: string]: number };
hciDeviceId = 0;
interval = 6;
timeoutCycles = 2;
Expand Down
Expand Up @@ -294,6 +294,80 @@ Requesting information ...
);
});

it('should handle minRssi per device', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-11);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);
config.minRssi = {
'77:50:fb:4d:ab:70': -10,
default: -20
};

const address = '77:50:fb:4d:ab:70';
const device = new Device(address, 'Test Device');
jest.spyOn(service, 'inquireDeviceInfo').mockResolvedValue(device);

const expectedEvent = new NewRssiEvent('test-instance', device, -11, true);

await service.handleRssiRequest(address);
expect(handleRssiMock).toHaveBeenCalledWith(expectedEvent);
expect(clusterService.publish).toHaveBeenCalledWith(
NEW_RSSI_CHANNEL,
expectedEvent
);
});

it('should pick the default minRssi if no device-specific one is configured', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-11);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);
config.minRssi = {
'77:50:fb:4d:ab:70': -10,
default: -20
};

const address = '50:50:50:50:50:50';
const device = new Device(address, 'Test Device');
jest.spyOn(service, 'inquireDeviceInfo').mockResolvedValue(device);

const expectedEvent = new NewRssiEvent('test-instance', device, -11, false);

await service.handleRssiRequest(address);
expect(handleRssiMock).toHaveBeenCalledWith(expectedEvent);
expect(clusterService.publish).toHaveBeenCalledWith(
NEW_RSSI_CHANNEL,
expectedEvent
);
});

it('should consider everything in range when no default minRssi is configured', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-25);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);
config.minRssi = {
'77:50:fb:4d:ab:70': -10
};

const address = '50:50:50:50:50:50';
const device = new Device(address, 'Test Device');
jest.spyOn(service, 'inquireDeviceInfo').mockResolvedValue(device);

const expectedEvent = new NewRssiEvent('test-instance', device, -25, false);

await service.handleRssiRequest(address);
expect(handleRssiMock).toHaveBeenCalledWith(expectedEvent);
expect(clusterService.publish).toHaveBeenCalledWith(
NEW_RSSI_CHANNEL,
expectedEvent
);
});

it('should gather the device info for previously unkown addresses', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(0);
Expand Down
12 changes: 11 additions & 1 deletion src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Expand Up @@ -109,11 +109,21 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
this.deviceMap.set(address, device);
}

let minRssi: number;
if (_.isObject(this.config.minRssi)) {
minRssi = _.defaultTo(
this.config.minRssi[address],
this.config.minRssi.default
);
} else if (_.isNumber(this.config.minRssi)) {
minRssi = this.config.minRssi;
}

const event = new NewRssiEvent(
this.configService.get('global').instanceName,
device,
rssi,
rssi < this.config.minRssi
rssi < minRssi
);

this.clusterService.publish(NEW_RSSI_CHANNEL, event);
Expand Down

0 comments on commit cf3ddc5

Please sign in to comment.