Skip to content

Commit

Permalink
feat(bluetooth-classic): add option for preserving sensor state
Browse files Browse the repository at this point in the history
Closes #156
  • Loading branch information
mKeRix committed Aug 16, 2020
1 parent 85064e6 commit 7cd2afe
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/integrations/bluetooth-classic.md
Expand Up @@ -55,6 +55,8 @@ Each instance running this integration will also create a switch for enabling or

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.

You can also use this feature to only trigger scans when you are certain that some change has happened, e.g. whenever a motion sensor was triggered. In that case you would need to enable the `preserveState` setting to still get accurate results after turning the switch off again. Note that room-assistant internally sends out pseudo updates in that case, so the timestamps will update in the API even though no scans are happening.

## Troubleshooting

### Random incorrect not_home states
Expand Down Expand Up @@ -85,6 +87,7 @@ If you don't have anything else running on the Pis this shouldn't be much of an
| `interval` | Number | `6` | The interval at which the Bluetooth devices are queried in seconds. |
| `scanTimeLimit` | Number | `2` | The maximum time allowed for completing a device query in seconds. This should be set lower than the interval. |
| `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`. |
| `preserveState` | Boolean | `false` | Whether the last recorded distance should be preserved when the inquiries switch is turned off or not. |

### Minimum RSSI

Expand Down
Expand Up @@ -5,4 +5,5 @@ export class BluetoothClassicConfig {
interval = 6;
scanTimeLimit = 2;
timeoutCycles = 2;
preserveState = false;
}
Expand Up @@ -68,6 +68,7 @@ describe('BluetoothClassicService', () => {
hciDeviceId: 0,
interval: 6,
timeoutCycles: 2,
preserveState: false,
};
const configService = {
get: jest.fn().mockImplementation((key: string) => {
Expand Down Expand Up @@ -484,6 +485,67 @@ Requesting information ...
expect(sensorInstance.timeout).toBe(24);
});

it('should trigger regular sensor state updates if the inquiries switch is on', () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
config.preserveState = false;
const sensor = new RoomPresenceDistanceSensor('test', 'Test', 5);
const device = { address: 'test', name: 'Test' };

service.updateSensorState(sensor, device);

expect(sensor.updateState).toHaveBeenCalled();
});

it('should trigger regular sensor state updates if preserve state is not enabled', () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(false);
config.preserveState = false;
const sensor = new RoomPresenceDistanceSensor('test', 'Test', 5);
const device = { address: 'test', name: 'Test' };

service.updateSensorState(sensor, device);

expect(sensor.updateState).toHaveBeenCalled();
});

it('should send pseudo state updates if the inquiries switch is off and preserve state is enabled', () => {
const updateSpy = jest.spyOn(service, 'handleNewRssi').mockResolvedValue();
jest.spyOn(service, 'shouldInquire').mockReturnValue(false);
config.preserveState = true;

const sensor = new RoomPresenceDistanceSensor('bt-test', 'Test', 5);
sensor.distances = {};
sensor.distances['test-instance'] = {
distance: 10,
outOfRange: false,
lastUpdatedAt: new Date(),
};
const device = { address: 'test', name: 'Test' };

service.updateSensorState(sensor, device);

const expectedEvent = new NewRssiEvent('test-instance', device, -10, false);
expect(updateSpy).toHaveBeenCalledWith(expectedEvent);
expect(clusterService.publish).toHaveBeenCalledWith(
NEW_RSSI_CHANNEL,
expectedEvent
);
});

it('should send no pseudo state update if no distance was recorded previously', () => {
const updateSpy = jest.spyOn(service, 'handleNewRssi').mockResolvedValue();
jest.spyOn(service, 'shouldInquire').mockReturnValue(false);
config.preserveState = true;

const sensor = new RoomPresenceDistanceSensor('bt-test', 'Test', 5);
sensor.distances = {};
const device = { address: 'test', name: 'Test' };

service.updateSensorState(sensor, device);

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

it('should not distribute inquiries if not the leader', () => {
clusterService.isMajorityLeader.mockReturnValue(false);
const inquireSpy = jest.spyOn(service, 'inquireRssi');
Expand Down
32 changes: 31 additions & 1 deletion src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Expand Up @@ -283,6 +283,36 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
}
}

/**
* Updates the underlying room presence sensor state.
* Called regularly.
* May emit pseudo update events to keep state correct.
*
* @param sensor - Room presence sensor that should be update
* @param device - Data of device corresponding to the sensor
*/
updateSensorState(sensor: RoomPresenceDistanceSensor, device: Device): void {
if (!this.shouldInquire() && this.config.preserveState) {
const instanceName = this.configService.get('global').instanceName;
const previousReading = sensor.distances[instanceName];

if (previousReading) {
// emit pseudo update to keep local state alive
const event = new NewRssiEvent(
instanceName,
device,
previousReading.distance * -1, // "distance" needs to be converted back to the RSSI value
previousReading.outOfRange
);

this.clusterService.publish(NEW_RSSI_CHANNEL, event);
this.handleNewRssi(event);
}
} else {
sensor.updateState();
}
}

/**
* Filters the nodes in the cluster to those who have this integration loaded.
*
Expand Down Expand Up @@ -390,7 +420,7 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
) as RoomPresenceDistanceSensor;

const interval = setInterval(
sensor.updateState.bind(sensor),
() => this.updateSensorState(sensor, device),
this.config.interval * 1000
);
this.schedulerRegistry.addInterval(`${sensorId}_timeout_check`, interval);
Expand Down

0 comments on commit 7cd2afe

Please sign in to comment.