Skip to content

Commit

Permalink
feat(bluetooth-classic): adapts closest instance detection to be instant
Browse files Browse the repository at this point in the history
Instead of waiting for an event with a closer distance the closest
instance is now calculated instantly from the state of the entire
cluster on an update. This leads to snappier updates, especially when
using minRssi.
  • Loading branch information
mKeRix committed Feb 14, 2020
1 parent cc5b8a9 commit 3966ab6
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 33 deletions.
Expand Up @@ -277,17 +277,23 @@ Requesting information ...
expect(clusterService.publish).toHaveBeenCalled();
});

it('should not publish RSSI values that are smaller than the min RSSI', async () => {
it('should mark RSSI values that are smaller than the min RSSI as out of range', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-11);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);
config.minRssi = -10;

await service.handleRssiRequest('77:50:fb:4d:ab:70');
expect(handleRssiMock).not.toHaveBeenCalled();
expect(clusterService.publish).not.toHaveBeenCalled();
const address = '77:50:fb:4d:ab:70';
const expectedEvent = new NewRssiEvent('test-instance', address, -11, true);

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

it('should ignore RSSI requests of inquiries are disabled', () => {
Expand Down Expand Up @@ -325,7 +331,8 @@ Requesting information ...
.instances[0];
expect(sensorInstance.handleNewDistance).toHaveBeenCalledWith(
'test-instance',
10
10,
false
);
expect(sensorInstance.timeout).toBe(24);
});
Expand Down
14 changes: 8 additions & 6 deletions src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Expand Up @@ -87,15 +87,13 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
if (this.shouldInquire()) {
let rssi = await this.inquireRssi(address);

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

this.clusterService.publish(NEW_RSSI_CHANNEL, event);
Expand Down Expand Up @@ -124,7 +122,11 @@ export class BluetoothClassicService extends KalmanFilterable(Object, 1.4, 1)
}

sensor.timeout = this.calculateCurrentTimeout();
sensor.handleNewDistance(event.instanceName, event.rssi * -1);
sensor.handleNewDistance(
event.instanceName,
event.rssi * -1,
event.outOfRange
);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion src/integrations/bluetooth-classic/new-rssi.event.ts
Expand Up @@ -2,10 +2,17 @@ export class NewRssiEvent {
instanceName: string;
address: string;
rssi: number;
outOfRange: boolean;

constructor(instanceName: string, address: string, rssi: number) {
constructor(
instanceName: string,
address: string,
rssi: number,
outOfRange = false
) {
this.instanceName = instanceName;
this.address = address;
this.rssi = rssi;
this.outOfRange = outOfRange;
}
}
Expand Up @@ -42,9 +42,9 @@ describe('RoomPresenceDistanceSensor', () => {
it('should update the state with a further away measurement if timeout has passed', () => {
sensor.timeout = 60;
sensor.handleNewDistance('room1', 1);
sensor.attributes.lastUpdatedAt = new Date(
sensor.distances.get('room1').lastUpdatedAt = new Date(
Date.now() - 61 * 1000
).toISOString();
);

sensor.handleNewDistance('room2', 5);
expect(sensor.state).toBe('room2');
Expand Down Expand Up @@ -84,4 +84,58 @@ describe('RoomPresenceDistanceSensor', () => {
sensor.checkForTimeout();
expect(sensor.state).toBe('room1');
});

it('should switch to a closer instance if the device moves away from the current one', () => {
sensor.handleNewDistance('room1', 5);
sensor.handleNewDistance('room2', 6);
sensor.handleNewDistance('room1', 7);

expect(sensor.state).toBe('room2');
expect(sensor.attributes.distance).toBe(6);
});

it('should switch to the next closest instance in range if current one goes out of range', () => {
sensor.handleNewDistance('room1', 3);
sensor.handleNewDistance('room2', 10);
sensor.handleNewDistance('room1', 4, true);

expect(sensor.state).toBe('room2');
expect(sensor.attributes.distance).toBe(10);
});

it('should ignore old values when switching to a closer instance', () => {
sensor.handleNewDistance('room1', 3);
sensor.handleNewDistance('room2', 4);
sensor.handleNewDistance('room3', 5);
sensor.timeout = 10;
sensor.distances.get('room2').lastUpdatedAt = new Date(
Date.now() - 20 * 1000
);
sensor.handleNewDistance('room1', 7);

expect(sensor.state).toBe('room3');
expect(sensor.attributes.distance).toBe(5);
});

it('should switch to not_home if all instances go out of range', () => {
sensor.handleNewDistance('room1', 3);
sensor.handleNewDistance('room2', 10, true);
sensor.handleNewDistance('room1', 10, true);

expect(sensor.state).toBe(STATE_NOT_HOME);
expect(sensor.attributes.distance).toBeUndefined();
});

it('should switch to not_home if all other distance values are too old', () => {
sensor.handleNewDistance('room1', 3);
sensor.handleNewDistance('room2', 4);
sensor.timeout = 5;
sensor.distances.get('room2').lastUpdatedAt = new Date(
Date.now() - 10 * 1000
);
sensor.handleNewDistance('room1', 5, true);

expect(sensor.state).toBe(STATE_NOT_HOME);
expect(sensor.attributes.distance).toBeUndefined();
});
});
79 changes: 60 additions & 19 deletions src/integrations/room-presence/room-presence-distance.sensor.ts
Expand Up @@ -2,8 +2,20 @@ import { Sensor } from '../../entities/sensor';

export const STATE_NOT_HOME = 'not_home';

class TimedDistance {
distance: number;
outOfRange: boolean;
lastUpdatedAt: Date = new Date();

constructor(distance: number, outOfRange = false) {
this.distance = distance;
this.outOfRange = outOfRange;
}
}

export class RoomPresenceDistanceSensor extends Sensor {
timeout: number;
distances = new Map<string, TimedDistance>();

constructor(id: string, name: string, timeout: number) {
super(id, name, true);
Expand All @@ -15,25 +27,27 @@ export class RoomPresenceDistanceSensor extends Sensor {
*
* @param instanceName - Name of the instance from which the distance was measured
* @param distance - Distance to the matching device
* @param outOfRange - If the distance is considered too far away from the instance
*/
handleNewDistance(instanceName: string, distance: number): void {
const lastDistance = this.attributes.distance as number;
const lastUpdate = Date.parse(this.attributes.lastUpdatedAt as string);
const timeoutLimit = new Date(lastUpdate + this.timeout * 1000);

if (this.state !== instanceName) {
if (
lastDistance === undefined ||
distance < lastDistance ||
(this.timeout > 0 && Date.now() > timeoutLimit.getTime())
) {
this.state = instanceName;
handleNewDistance(
instanceName: string,
distance: number,
outOfRange = false
): void {
this.distances.set(instanceName, new TimedDistance(distance, outOfRange));
const closestInRange = this.getClosestInRange();

if (closestInRange) {
if (this.state !== closestInRange[0]) {
this.state = closestInRange[0];
}
}

if (this.state === instanceName) {
this.attributes.distance = distance;
this.attributes.lastUpdatedAt = new Date().toISOString();
if (this.state === closestInRange[0]) {
this.attributes.distance = closestInRange[1].distance;
this.attributes.lastUpdatedAt = closestInRange[1].lastUpdatedAt.toISOString();
}
} else {
this.setNotHome();
}
}

Expand All @@ -46,10 +60,37 @@ export class RoomPresenceDistanceSensor extends Sensor {
const timeoutLimit = new Date(lastUpdate + this.timeout * 1000);

if (Date.now() > timeoutLimit.getTime()) {
this.state = STATE_NOT_HOME;
this.attributes.distance = undefined;
this.attributes.lastUpdatedAt = new Date().toISOString();
this.setNotHome();
}
}
}

/**
* Determines the closest instance.
*
* @returns Tuple of the instance name and the timed distance to it
*/
protected getClosestInRange(): [string, TimedDistance] {
const distances = Array.from(this.distances.entries())
.filter(value => {
return (
!value[1].outOfRange &&
(this.timeout <= 0 ||
Date.now() < value[1].lastUpdatedAt.getTime() + this.timeout * 1000)
);
})
.sort((a, b) => {
return a[1].distance - b[1].distance;
});
return distances.length > 0 ? distances[0] : undefined;
}

/**
* Marks the sensor as not_home.
*/
protected setNotHome(): void {
this.state = STATE_NOT_HOME;
this.attributes.distance = undefined;
this.attributes.lastUpdatedAt = new Date().toISOString();
}
}

0 comments on commit 3966ab6

Please sign in to comment.