Skip to content

Commit

Permalink
Merge branch 'master' into iphone-ble
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/integrations/bluetooth/bluetooth.service.ts
  • Loading branch information
mKeRix committed Nov 1, 2020
2 parents 7bbce59 + 4cc2fca commit 4d6fbb7
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 57 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
env:
CI: true
- name: Upload code coverage
uses: codecov/codecov-action@v1.0.13
uses: codecov/codecov-action@v1.0.14
if: matrix.node-version == '12.x'
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down Expand Up @@ -104,9 +104,16 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Login to DockerHub Registry
- name: Login to DockerHub
if: steps.semantic.outputs.new_release_published == 'true'
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
if: steps.semantic.outputs.new_release_published == 'true'
uses: docker/setup-buildx-action@v1
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "room-assistant",
"version": "2.11.0-beta.1",
"version": "2.11.3",
"description": "Presence tracking and more for automation on the room-level",
"author": "Heiko Rothe <me@heikorothe.com> (https://heikorothe.com)",
"license": "MIT",
Expand Down Expand Up @@ -66,7 +66,7 @@
"rxjs": "^6.6.3",
"slugify": "^1.4.5",
"swagger-ui-express": "^4.1.4",
"systeminformation": "^4.27.10",
"systeminformation": "^4.27.11",
"update-notifier": "^5.0.0",
"winston": "^3.3.3"
},
Expand Down Expand Up @@ -108,7 +108,7 @@
"vuepress-plugin-sitemap": "^2.3.1"
},
"optionalDependencies": {
"@abandonware/noble": "^1.9.2-5",
"@abandonware/noble": "1.9.2-9",
"canvas": "^2.6.1",
"i2c-bus": "^5.1.0",
"mdns": "^2.5.1",
Expand Down
7 changes: 4 additions & 3 deletions src/cluster/cluster.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ try {
}

@Injectable()
export class ClusterService extends Democracy
export class ClusterService
extends Democracy
implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown {
private readonly configService: ConfigService;
private readonly config: ClusterConfig;
Expand Down Expand Up @@ -119,7 +120,7 @@ export class ClusterService extends Democracy
*/
quorumReached(): boolean {
const activeNodes = Object.values(this.nodes()).filter(
(node) => node.state !== 'removed'
(node) => node?.state !== 'removed'
);
return !this.config.quorum || activeNodes.length >= this.config.quorum;
}
Expand Down Expand Up @@ -162,7 +163,7 @@ export class ClusterService extends Democracy

if (!data.chunk && data.state === 'leader') {
const leaders = Object.entries(this._nodes).filter(
(node) => node[1].state === 'leader'
(node) => node[1]?.state === 'leader'
);

if (leaders.length > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,23 @@ describe('BluetoothClassicService', () => {
expect(handleRssiMock).not.toHaveBeenCalled();
});

it('should not publish an RSSI value if an error occured', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
bluetoothService.inquireClassicRssi.mockRejectedValue(
new Error('expected for this test')
);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);

await expect(
service.handleRssiRequest('77:50:fb:4d:ab:70')
).resolves.not.toThrow();

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

it('should publish RSSI values that are bigger than the min RSSI', async () => {
jest.spyOn(service, 'shouldInquire').mockReturnValue(true);
bluetoothService.inquireClassicRssi.mockResolvedValue(-9);
Expand Down
15 changes: 11 additions & 4 deletions src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,17 @@ export class BluetoothClassicService
}

if (this.shouldInquire()) {
let rssi = await this.bluetoothService.inquireClassicRssi(
this.config.hciDeviceId,
address
);
let rssi;
try {
rssi = await this.bluetoothService.inquireClassicRssi(
this.config.hciDeviceId,
address
);
} catch (e) {
this.logger.error(
`Failed to retrieve RSSI for ${address}: ${e.message}`
);
}

if (rssi !== undefined) {
rssi = _.round(this.filterRssi(address, rssi), 1);
Expand Down
69 changes: 37 additions & 32 deletions src/integrations/bluetooth/bluetooth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Peripheral } from '@abandonware/noble';

const mockExec = jest.fn();
const mockNoble = {
state: 'poweredOn',
on: jest.fn(),
startScanning: jest.fn(),
startScanningAsync: jest.fn(),
stopScanning: jest.fn(),
};
jest.mock(
Expand All @@ -19,6 +17,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { BluetoothService } from './bluetooth.service';
import { ConfigModule } from '../../config/config.module';
import { BluetoothHealthIndicator } from './bluetooth.health';
import { Peripheral } from '@abandonware/noble';

jest.mock('util', () => ({
...jest.requireActual('util'),
Expand Down Expand Up @@ -48,35 +47,43 @@ describe('BluetoothService', () => {
});

describe('Bluetooth Classic', () => {
it('should return measured RSSI value from command output', () => {
it('should return measured RSSI value from command output', async () => {
mockExec.mockResolvedValue({ stdout: 'RSSI return value: -4' });

const address = '77:50:fb:4d:ab:70';

expect(service.inquireClassicRssi(1, address)).resolves.toBe(-4);
expect(await service.inquireClassicRssi(1, address)).toBe(-4);
expect(mockExec).toHaveBeenCalledWith(
`hcitool -i hci1 cc \"${address}\" && hcitool -i hci1 rssi \"${address}\"`,
expect.anything()
);
});

it('should return undefined if no RSSI could be determined', () => {
it('should return undefined if no RSSI could be determined', async () => {
mockExec.mockResolvedValue({
stdout: "Can't create connection: Input/output error",
stderr: 'Not connected.',
});

expect(
service.inquireClassicRssi(0, '08:05:90:ed:3b:60')
).resolves.toBeUndefined();
await service.inquireClassicRssi(0, '08:05:90:ed:3b:60')
).toBeUndefined();
});

it('should return undefined if the command failed', () => {
it('should return undefined if the command failed', async () => {
mockExec.mockRejectedValue({ message: 'Command failed' });

expect(
service.inquireClassicRssi(0, '08:05:90:ed:3b:60')
).resolves.toBeUndefined();
await service.inquireClassicRssi(0, '08:05:90:ed:3b:60')
).toBeUndefined();
});

it('should throw an exception if an inquiry is requested for a locked adapter', async () => {
service.lockAdapter(1);

await expect(
service.inquireClassicRssi(1, '77:50:fb:4d:ab:71')
).rejects.toThrow();
});

it('should reset the HCI device if the query took too long', async () => {
Expand All @@ -87,18 +94,18 @@ describe('BluetoothService', () => {
expect(mockExec).toHaveBeenCalledWith('hciconfig hci1 reset');
});

it('should stop scanning on an adapter while performing an inquiry', () => {
it('should stop scanning on an adapter while performing an inquiry', async () => {
service.onLowEnergyDiscovery(() => undefined);
const stateChangeHandler = mockNoble.on.mock.calls[0][1];
stateChangeHandler('poweredOn');
await stateChangeHandler('poweredOn');

expect(mockNoble.startScanning).toHaveBeenCalledTimes(1);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1);

let execResolve;
const execPromise = new Promise((r) => (execResolve = r));
mockExec.mockReturnValue(execPromise);
const inquirePromise = service.inquireClassicRssi(0, 'x').then(() => {
expect(mockNoble.startScanning).toHaveBeenCalledTimes(2);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(2);
});

expect(mockNoble.stopScanning).toHaveBeenCalledTimes(1);
Expand All @@ -116,23 +123,23 @@ describe('BluetoothService', () => {
mockExec.mockRejectedValue({ stderr: 'error' });
await service.inquireClassicRssi(0, 'x');

expect(mockNoble.startScanning).toHaveBeenCalledTimes(2);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(2);
});

it('should stop scanning on an adapter while getting Classic device info', () => {
it('should stop scanning on an adapter while getting Classic device info', async () => {
service.onLowEnergyDiscovery(() => undefined);
const stateChangeHandler = mockNoble.on.mock.calls[0][1];
stateChangeHandler('poweredOn');
await stateChangeHandler('poweredOn');

expect(mockNoble.startScanning).toHaveBeenCalledTimes(1);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1);

let execResolve;
const execPromise = new Promise((r) => (execResolve = r));
mockExec.mockReturnValue(execPromise);
const inquirePromise = service
.inquireClassicDeviceInfo(0, 'x')
.then(() => {
expect(mockNoble.startScanning).toHaveBeenCalledTimes(2);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(2);
});

expect(mockNoble.stopScanning).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -249,7 +256,7 @@ Requesting information ...

stateChangeHandler('poweredOn');

expect(mockNoble.startScanning).toHaveBeenCalledTimes(1);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1);
});

it('should not enable scanning when the adapter is performing a Classic inquiry', () => {
Expand All @@ -260,12 +267,12 @@ Requesting information ...
const execPromise = new Promise((r) => (execResolve = r));
mockExec.mockReturnValue(execPromise);
const inquirePromise = service.inquireClassicRssi(0, 'x').then(() => {
expect(mockNoble.startScanning).toHaveBeenCalledTimes(1);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1);
});

stateChangeHandler('poweredOn');

expect(mockNoble.startScanning).not.toHaveBeenCalled();
expect(mockNoble.startScanningAsync).not.toHaveBeenCalled();

execResolve({ stdout: '-1' });

Expand All @@ -281,7 +288,7 @@ Requesting information ...
mockExec.mockReturnValue(execPromise);
await service.inquireClassicRssi(1, 'x');

expect(mockNoble.startScanning).toHaveBeenCalledTimes(1);
expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1);
expect(mockNoble.stopScanning).not.toHaveBeenCalled();
});

Expand All @@ -307,11 +314,9 @@ Requesting information ...

service.connectLowEnergyDevice((peripheral as unknown) as Peripheral);

await expect(async () => {
await service.connectLowEnergyDevice(
(peripheral as unknown) as Peripheral
);
}).rejects.toThrow();
await expect(
service.connectLowEnergyDevice((peripheral as unknown) as Peripheral)
).rejects.toThrow();

connectResolve();
});
Expand Down Expand Up @@ -341,7 +346,7 @@ Requesting information ...
const peripheral = {
connectable: true,
connectAsync: jest.fn().mockRejectedValue(new Error('expected')),
disconnectAsync: jest.fn(),
disconnect: jest.fn(),
removeAllListeners: jest.fn(),
once: jest.fn(),
};
Expand All @@ -352,7 +357,7 @@ Requesting information ...
);
}).rejects.toThrow();

expect(peripheral.disconnectAsync).toHaveBeenCalled();
expect(peripheral.disconnect).toHaveBeenCalled();
expect(peripheral.removeAllListeners).toHaveBeenCalled();
});

Expand All @@ -367,7 +372,7 @@ Requesting information ...
.mockReturnValue(
new Promise((resolve) => setTimeout(resolve, 11 * 1000))
),
disconnectAsync: jest.fn(),
disconnect: jest.fn(),
removeAllListeners: jest.fn(),
once: jest.fn(),
};
Expand Down
14 changes: 7 additions & 7 deletions src/integrations/bluetooth/bluetooth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class BluetoothService {
`Failed to connect to ${peripheral.address}: ${e.message}`,
e.trace
);
peripheral.disconnectAsync();
peripheral.disconnect();
peripheral.removeAllListeners();
this.unlockAdapter(this.lowEnergyAdapterId);
throw e;
Expand Down Expand Up @@ -212,7 +212,7 @@ export class BluetoothService {
*
* @param adapterId - HCI Device ID of the adapter to lock
*/
protected lockAdapter(adapterId: number): void {
lockAdapter(adapterId: number): void {
switch (this.adapterStates.get(adapterId)) {
case 'inquiry':
throw new Error(
Expand All @@ -230,11 +230,11 @@ export class BluetoothService {
*
* @param adapterId - HCI Device ID of the adapter to unlock
*/
protected unlockAdapter(adapterId: number): void {
async unlockAdapter(adapterId: number): Promise<void> {
this.adapterStates.set(adapterId, 'inactive');

if (adapterId == this.lowEnergyAdapterId) {
this.handleAdapterStateChange(noble.state);
await this.handleAdapterStateChange(noble.state);
}
}

Expand All @@ -259,13 +259,13 @@ export class BluetoothService {
*
* @param state - State of the HCI adapter
*/
private handleAdapterStateChange(state: string): void {
private async handleAdapterStateChange(state: string): Promise<void> {
if (this.adapterStates.get(this.lowEnergyAdapterId) != 'inquiry') {
if (state === 'poweredOn') {
noble.startScanning([], true);
await noble.startScanningAsync([], true);
this.adapterStates.set(this.lowEnergyAdapterId, 'scan');
} else {
noble.stopScanning();
await noble.stopScanning();
this.adapterStates.set(this.lowEnergyAdapterId, 'inactive');
}
}
Expand Down
Loading

0 comments on commit 4d6fbb7

Please sign in to comment.