Skip to content

Commit

Permalink
feat(bluetooth-classic): added inquiries switch
Browse files Browse the repository at this point in the history
A switch that can enable/disable Bluetooth inquiries will now be
registered for each instance of room-assistant.
  • Loading branch information
mKeRix committed Feb 10, 2020
1 parent 52eaef5 commit 8f4d7f4
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 10 deletions.
6 changes: 6 additions & 0 deletions docs/integrations/bluetooth-classic.md
Expand Up @@ -32,6 +32,12 @@ This integration assumes that you have loaded it and configured it in the same m

Each configured device will create a sensor that has the name of the closest room-assistant instance as its state - or `not_home` if the device could not be found by any of them. The distance in the attributes of those sensors is the inverted signal strength value and does not actually represent any physical distance. It may also show negative numbers.

## Inquiries switch

Each instance running this integration will also create a switch for enabling or disabling Bluetooth inquiries. A disabled switch blocks all Bluetooth requests of the instance, which essentially means that your Bluetooth device won't be discovered by this instance anymore. It does not take the instance out of the rotation however, so the time to detection stays the same.

You could use this to reduce the resources used by room-assistant when you are certain nobody is home. Another example would be disabling the inquiries when you are asleep to save the batteries of your Bluetooth devices at night.

## Settings

| Name | Type | Default | Description |
Expand Down
Expand Up @@ -14,6 +14,7 @@ import {
import { NewRssiEvent } from './new-rssi.event';
import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor';
import KalmanFilter from 'kalmanjs';
import { Switch } from '../../entities/switch';

jest.mock('../room-presence/room-presence-distance.sensor');
jest.mock('kalmanjs', () => {
Expand Down Expand Up @@ -94,6 +95,10 @@ describe('BluetoothClassicService', () => {
});

it('should setup the cluster bindings on bootstrap', () => {
entitiesService.add.mockReturnValue(
new Switch('query-switch', 'Query Switch')
);

service.onApplicationBootstrap();

expect(clusterService.on).toHaveBeenCalledWith(
Expand All @@ -107,6 +112,23 @@ describe('BluetoothClassicService', () => {
expect(clusterService.subscribe).toHaveBeenCalledWith(NEW_RSSI_CHANNEL);
});

it('should create and register an inquiries switch on bootstrap', () => {
const mockSwitch = new Switch('inquiries-switch', 'Inquiries Switch');
entitiesService.add.mockReturnValue(mockSwitch);
const turnOnSpy = jest.spyOn(mockSwitch, 'turnOn');

service.onApplicationBootstrap();

expect(entitiesService.add).toHaveBeenCalledWith(
expect.objectContaining({
id: 'bluetooth-classic-inquiries-switch',
name: 'test-instance Bluetooth Inquiries'
}),
expect.any(Array)
);
expect(turnOnSpy).toHaveBeenCalled();
});

it('should return measured RSSI value from command output', () => {
jest.spyOn(util, 'promisify').mockImplementation(() => {
return jest.fn().mockResolvedValue({ stdout: 'RSSI return value: -4' });
Expand Down Expand Up @@ -172,6 +194,7 @@ Requesting information ...
});

it('should publish the RSSI if found', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(0);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
Expand All @@ -189,6 +212,7 @@ Requesting information ...
});

it('should not publish an RSSI value if none was found', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(undefined);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
Expand All @@ -200,6 +224,16 @@ Requesting information ...
expect(handleRssiMock).not.toHaveBeenCalled();
});

it('should ignore RSSI requests of inquiries are disabled', () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(false);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);

expect(clusterService.publish).not.toHaveBeenCalled();
expect(handleRssiMock).not.toHaveBeenCalled();
});

it('should register a new sensor for a previously unknown device', async () => {
entitiesService.has.mockReturnValue(false);
entitiesService.add.mockImplementation(entity => entity);
Expand Down Expand Up @@ -367,6 +401,7 @@ Requesting information ...
});

it('should filter the RSSI of inquired devices before publishing', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-3);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
Expand Down Expand Up @@ -394,4 +429,24 @@ Requesting information ...

expect(KalmanFilter).toHaveBeenCalledTimes(2);
});

it('should not allow inquiries if the inquiries switch is turned off', () => {
const mockSwitch = new Switch('inquiries-switch', 'Inquiries Switch');
entitiesService.add.mockReturnValue(mockSwitch);

service.onApplicationBootstrap();
mockSwitch.state = false;

expect(service.shouldInquire()).toBeFalsy();
});

it('should allow inquiries if the inquiries switch is turned on', () => {
const mockSwitch = new Switch('inquiries-switch', 'Inquiries Switch');
entitiesService.add.mockReturnValue(mockSwitch);

service.onApplicationBootstrap();
mockSwitch.state = true;

expect(service.shouldInquire()).toBeTruthy();
});
});
63 changes: 53 additions & 10 deletions src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Expand Up @@ -25,6 +25,8 @@ import { KalmanFilterable } from '../../util/filters';
import { makeId } from '../../util/id';
import { Device } from './device';
import { DISTRIBUTED_DEVICE_ID } from '../home-assistant/home-assistant.const';
import { Switch } from '../../entities/switch';
import { SwitchConfig } from '../home-assistant/switch-config';

const INTERVAL = 6 * 1000;

Expand All @@ -33,6 +35,7 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
implements OnModuleInit, OnApplicationBootstrap {
private readonly config: BluetoothClassicConfig;
private rotationOffset = 0;
private inquiriesSwitch: Switch;
private logger: Logger;

constructor(
Expand Down Expand Up @@ -65,6 +68,8 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
* Lifecycle hook, called once the application has started.
*/
onApplicationBootstrap(): void {
this.inquiriesSwitch = this.createInquiriesSwitch();

this.clusterService.on(
REQUEST_RSSI_CHANNEL,
this.handleRssiRequest.bind(this)
Expand All @@ -79,18 +84,20 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
* @param address - Bluetooth MAC address
*/
async handleRssiRequest(address: string): Promise<void> {
let rssi = await this.inquireRssi(address);
if (this.shouldInquire()) {
let rssi = await this.inquireRssi(address);

if (rssi !== undefined) {
rssi = _.round(this.filterRssi(address, rssi), 1);
const event = new NewRssiEvent(
this.configService.get('global').instanceName,
address,
rssi
);
if (rssi !== undefined) {
rssi = _.round(this.filterRssi(address, rssi), 1);
const event = new NewRssiEvent(
this.configService.get('global').instanceName,
address,
rssi
);

this.clusterService.publish(NEW_RSSI_CHANNEL, event);
this.handleNewRssi(event);
this.clusterService.publish(NEW_RSSI_CHANNEL, event);
this.handleNewRssi(event);
}
}
}

Expand Down Expand Up @@ -228,6 +235,42 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
return nodes.filter(node => node.channels?.includes(NEW_RSSI_CHANNEL));
}

/**
* Checks whether this instance should send out Bluetooth Inquiries at the moment or not.
*
* @returns Bluetooth inquiries allowed?
*/
shouldInquire(): boolean {
return this.inquiriesSwitch?.state;
}

/**
* Creates and registers a new switch as a setting for whether Bluetooth queries should be made from this device or not.
*
* @returns Registered query switch
*/
protected createInquiriesSwitch(): Switch {
const instanceName = this.configService.get('global').instanceName;
const customizations: Array<EntityCustomization<any>> = [
{
for: SwitchConfig,
overrides: {
icon: 'mdi:bluetooth-audio'
}
}
];
const inquiriesSwitch = this.entitiesService.add(
new Switch(
'bluetooth-classic-inquiries-switch',
`${instanceName} Bluetooth Inquiries`
),
customizations
) as Switch;
inquiriesSwitch.turnOn();

return inquiriesSwitch;
}

/**
* Creates and registers a new room presence sensor.
*
Expand Down

0 comments on commit 8f4d7f4

Please sign in to comment.