@@ -0,0 +1,205 @@
Loading
1 +
import { Injectable, Logger } from '@nestjs/common';
2 +
import noble, { Peripheral } from '@abandonware/noble';
3 +
import util from 'util';
4 +
import { exec } from 'child_process';
5 +
import { BluetoothHealthIndicator } from './bluetooth.health';
6 +
import { BluetoothClassicConfig } from '../bluetooth-classic/bluetooth-classic.config';
7 +
import { ConfigService } from '../../config/config.service';
8 +
import { Device } from '../bluetooth-classic/device';
9 +
10 +
type BluetoothAdapterState = 'inquiry' | 'scan' | 'inactive';
11 +
12 +
const execPromise = util.promisify(exec);
13 +
const rssiRegex = new RegExp(/-?[0-9]+/);
14 +
15 +
@Injectable()
16 +
export class BluetoothService {
17 +
  private readonly logger: Logger = new Logger(BluetoothService.name);
18 +
  private readonly classicConfig: BluetoothClassicConfig;
19 +
  private readonly adapterStates = new Map<number, BluetoothAdapterState>();
20 +
  private lowEnergyAdapterId: number;
21 +
22 +
  constructor(
23 +
    private readonly configService: ConfigService,
24 +
    private readonly healthIndicator: BluetoothHealthIndicator
25 +
  ) {
26 +
    this.classicConfig = this.configService.get('bluetoothClassic');
27 +
  }
28 +
29 +
  /**
30 +
   * Registers a callback function that will be invoked when a
31 +
   * Bluetooth Low Energy peripheral advertisement was received.
32 +
   *
33 +
   * @param callback - Callback function that receives a peripheral
34 +
   */
35 +
  onLowEnergyDiscovery(callback: (peripheral: Peripheral) => void): void {
36 +
    if (this.lowEnergyAdapterId == undefined) {
37 +
      this.setupNoble();
38 +
    }
39 +
40 +
    noble.on('discover', callback);
41 +
  }
42 +
43 +
  /**
44 +
   * Queries for the RSSI of a Bluetooth device using the hcitool shell command.
45 +
   *
46 +
   * @param adapterId - HCI Adapter ID to use for queries
47 +
   * @param address - Bluetooth MAC address
48 +
   * @returns RSSI value
49 +
   */
50 +
  async inquireClassicRssi(
51 +
    adapterId: number,
52 +
    address: string
53 +
  ): Promise<number> {
54 +
    this.lockAdapter(adapterId);
55 +
56 +
    this.logger.debug(`Querying for RSSI of ${address} using hcitool`);
57 +
    try {
58 +
      const output = await execPromise(
59 +
        `hcitool -i hci${adapterId} cc "${address}" && hcitool -i hci${adapterId} rssi "${address}"`,
60 +
        {
61 +
          timeout: this.classicConfig.scanTimeLimit * 1000,
62 +
          killSignal: 'SIGKILL',
63 +
        }
64 +
      );
65 +
      const matches = output.stdout.match(rssiRegex);
66 +
67 +
      this.healthIndicator.reportSuccess();
68 +
69 +
      return matches?.length > 0 ? parseInt(matches[0], 10) : undefined;
70 +
    } catch (e) {
71 +
      if (e.signal === 'SIGKILL') {
72 +
        this.logger.debug(
73 +
          `Query of ${address} reached scan time limit, resetting hci${this.classicConfig.hciDeviceId}`
74 +
        );
75 +
        await this.resetHciDevice(adapterId);
76 +
77 +
        // when not reachable a scan runs for 6s, so lower time limits might not be an error
78 +
        if (this.classicConfig.scanTimeLimit >= 6) {
79 +
          this.healthIndicator.reportError();
80 +
        }
81 +
      } else if (
82 +
        e.message?.includes('Input/output') ||
83 +
        e.message?.includes('I/O')
84 +
      ) {
85 +
        this.logger.debug(e.message);
86 +
      } else {
87 +
        this.logger.error(e.message);
88 +
        this.healthIndicator.reportError();
89 +
      }
90 +
91 +
      return undefined;
92 +
    } finally {
93 +
      this.unlockAdapter(adapterId);
94 +
    }
95 +
  }
96 +
97 +
  /**
98 +
   * Inquires device information of a Bluetooth peripheral.
99 +
   *
100 +
   * @param adapterId - HCI Adapter ID to use for queries
101 +
   * @param address - Bluetooth MAC address
102 +
   * @returns Device information
103 +
   */
104 +
  async inquireClassicDeviceInfo(
105 +
    adapterId: number,
106 +
    address: string
107 +
  ): Promise<Device> {
108 +
    this.lockAdapter(adapterId);
109 +
110 +
    try {
111 +
      const output = await execPromise(
112 +
        `hcitool -i hci${adapterId} info "${address}"`
113 +
      );
114 +
115 +
      const nameMatches = /Device Name: (.+)/g.exec(output.stdout);
116 +
      const manufacturerMatches = /OUI Company: (.+) \(.+\)/g.exec(
117 +
        output.stdout
118 +
      );
119 +
120 +
      return {
121 +
        address,
122 +
        name: nameMatches ? nameMatches[1] : address,
123 +
        manufacturer: manufacturerMatches ? manufacturerMatches[1] : undefined,
124 +
      };
125 +
    } catch (e) {
126 +
      this.logger.error(e.message, e.stack);
127 +
      return {
128 +
        address,
129 +
        name: address,
130 +
      };
131 +
    } finally {
132 +
      this.unlockAdapter(adapterId);
133 +
    }
134 +
  }
135 +
136 +
  /**
137 +
   * Reset the hci (Bluetooth) device used for inquiries.
138 +
   */
139 +
  protected async resetHciDevice(adapterId: number): Promise<void> {
140 +
    try {
141 +
      await execPromise(`hciconfig hci${adapterId} reset`);
142 +
    } catch (e) {
143 +
      this.logger.error(e.message);
144 +
    }
145 +
  }
146 +
147 +
  /**
148 +
   * Locks an adapter for an active inquiry.
149 +
   *
150 +
   * @param adapterId - HCI Device ID of the adapter to lock
151 +
   */
152 +
  protected lockAdapter(adapterId: number): void {
153 +
    if (this.adapterStates.get(adapterId) == 'scan') {
154 +
      noble.stopScanning();
155 +
    }
156 +
157 +
    this.adapterStates.set(adapterId, 'inquiry');
158 +
  }
159 +
160 +
  /**
161 +
   * Unlocks an adapter and returns it to scan or inactive state.
162 +
   *
163 +
   * @param adapterId - HCI Device ID of the adapter to unlock
164 +
   */
165 +
  protected unlockAdapter(adapterId: number): void {
166 +
    this.adapterStates.set(adapterId, 'inactive');
167 +
168 +
    if (adapterId == this.lowEnergyAdapterId) {
169 +
      this.handleAdapterStateChange(noble.state);
170 +
    }
171 +
  }
172 +
173 +
  /**
174 +
   * Sets up Noble hooks.
175 +
   */
176 +
  private setupNoble(): void {
177 +
    this.lowEnergyAdapterId = parseInt(process.env.NOBLE_HCI_DEVICE_ID) || 0;
178 +
179 +
    noble.on('stateChange', this.handleAdapterStateChange.bind(this));
180 +
    noble.on('warning', (message) => {
181 +
      if (message == 'unknown peripheral undefined RSSI update!') {
182 +
        return;
183 +
      }
184 +
185 +
      this.logger.warn(message);
186 +
    });
187 +
  }
188 +
189 +
  /**
190 +
   * Handles state adapter changes as reported by Noble.
191 +
   *
192 +
   * @param state - State of the HCI adapter
193 +
   */
194 +
  private handleAdapterStateChange(state: string): void {
195 +
    if (this.adapterStates.get(this.lowEnergyAdapterId) != 'inquiry') {
196 +
      if (state === 'poweredOn') {
197 +
        noble.startScanning([], true);
198 +
        this.adapterStates.set(this.lowEnergyAdapterId, 'scan');
199 +
      } else {
200 +
        noble.stopScanning();
201 +
        this.adapterStates.set(this.lowEnergyAdapterId, 'inactive');
202 +
      }
203 +
    }
204 +
  }
205 +
}

@@ -7,7 +7,7 @@
Loading
7 7
import { HealthIndicatorService } from '../../status/health-indicator.service';
8 8
9 9
@Injectable()
10 -
export class BluetoothClassicHealthIndicator extends HealthIndicator {
10 +
export class BluetoothHealthIndicator extends HealthIndicator {
11 11
  private errorsOccurred = 0;
12 12
13 13
  constructor(@Optional() healthIndicatorService?: HealthIndicatorService) {

@@ -3,16 +3,15 @@
Loading
3 3
import { ConfigModule } from '../../config/config.module';
4 4
import { EntitiesModule } from '../../entities/entities.module';
5 5
import { ClusterModule } from '../../cluster/cluster.module';
6 -
import { BluetoothClassicHealthIndicator } from './bluetooth-classic.health';
7 -
import { StatusModule } from '../../status/status.module';
6 +
import { BluetoothModule } from '../bluetooth/bluetooth.module';
8 7
9 8
@Module({})
10 9
export default class BluetoothClassicModule {
11 10
  static forRoot(): DynamicModule {
12 11
    return {
13 12
      module: BluetoothClassicModule,
14 -
      imports: [ConfigModule, EntitiesModule, ClusterModule, StatusModule],
15 -
      providers: [BluetoothClassicService, BluetoothClassicHealthIndicator],
13 +
      imports: [BluetoothModule, ConfigModule, EntitiesModule, ClusterModule],
14 +
      providers: [BluetoothClassicService],
16 15
    };
17 16
  }
18 17
}

@@ -6,4 +6,5 @@
Loading
6 6
  scanTimeLimit = 2;
7 7
  timeoutCycles = 2;
8 8
  preserveState = false;
9 +
  inquireFromStart = true;
9 10
}

@@ -29,7 +29,7 @@
Loading
29 29
import { SwitchConfig } from '../home-assistant/switch-config';
30 30
import { DeviceTracker } from '../../entities/device-tracker';
31 31
import { RoomPresenceDeviceTrackerProxyHandler } from '../room-presence/room-presence-device-tracker.proxy';
32 -
import { BluetoothClassicHealthIndicator } from './bluetooth-classic.health';
32 +
import { BluetoothService } from '../bluetooth/bluetooth.service';
33 33
34 34
const execPromise = util.promisify(exec);
35 35
@@ -44,11 +44,11 @@
Loading
44 44
  private logger: Logger;
45 45
46 46
  constructor(
47 +
    private readonly bluetoothService: BluetoothService,
47 48
    private readonly configService: ConfigService,
48 49
    private readonly entitiesService: EntitiesService,
49 50
    private readonly clusterService: ClusterService,
50 -
    private readonly schedulerRegistry: SchedulerRegistry,
51 -
    private readonly healthIndicator: BluetoothClassicHealthIndicator
51 +
    private readonly schedulerRegistry: SchedulerRegistry
52 52
  ) {
53 53
    super();
54 54
    this.config = this.configService.get('bluetoothClassic');
@@ -101,7 +101,10 @@
Loading
101 101
    }
102 102
103 103
    if (this.shouldInquire()) {
104 -
      let rssi = await this.inquireRssi(address);
104 +
      let rssi = await this.bluetoothService.inquireClassicRssi(
105 +
        this.config.hciDeviceId,
106 +
        address
107 +
      );
105 108
106 109
      if (rssi !== undefined) {
107 110
        rssi = _.round(this.filterRssi(address, rssi), 1);
@@ -110,7 +113,10 @@
Loading
110 113
        if (this.deviceMap.has(address)) {
111 114
          device = this.deviceMap.get(address);
112 115
        } else {
113 -
          device = await this.inquireDeviceInfo(address);
116 +
          device = await this.bluetoothService.inquireClassicDeviceInfo(
117 +
            this.config.hciDeviceId,
118 +
            address
119 +
          );
114 120
          this.deviceMap.set(address, device);
115 121
        }
116 122
@@ -171,7 +177,7 @@
Loading
171 177
  distributeInquiries(): void {
172 178
    if (this.clusterService.isMajorityLeader()) {
173 179
      const nodes = this.getParticipatingNodes();
174 -
      const addresses = this.config.addresses;
180 +
      const addresses = [...this.config.addresses];
175 181
      if (this.rotationOffset >= Math.max(nodes.length, addresses.length)) {
176 182
        this.rotationOffset = 0;
177 183
      }
@@ -184,7 +190,7 @@
Loading
184 190
      nodeSubset.forEach((node, index) => {
185 191
        if (addressSubset[index] == null) {
186 192
          this.logger.error(
187 -
            `Trying to request inquiry without MAC! Current index: ${index}. Addresses in this round: ${addressSubset}. Nodes in this round: ${nodeSubset}.`
193 +
            `Trying to request inquiry without MAC! Current index: ${this.rotationOffset}. Addresses in this round: ${addressSubset}. Addresses overall: ${addresses}.`
188 194
          );
189 195
        }
190 196
@@ -204,54 +210,6 @@
Loading
204 210
    }
205 211
  }
206 212
207 -
  /**
208 -
   * Queries for the RSSI of a Bluetooth device using the hcitool shell command.
209 -
   *
210 -
   * @param address - Bluetooth MAC address
211 -
   * @returns RSSI value
212 -
   */
213 -
  async inquireRssi(address: string): Promise<number> {
214 -
    const regex = new RegExp(/-?[0-9]+/);
215 -
216 -
    this.logger.debug(`Querying for RSSI of ${address} using hcitool`);
217 -
    try {
218 -
      const output = await execPromise(
219 -
        `hcitool -i hci${this.config.hciDeviceId} cc "${address}" && hcitool -i hci${this.config.hciDeviceId} rssi "${address}"`,
220 -
        {
221 -
          timeout: this.config.scanTimeLimit * 1000,
222 -
          killSignal: 'SIGKILL',
223 -
        }
224 -
      );
225 -
      const matches = output.stdout.match(regex);
226 -
227 -
      this.healthIndicator.reportSuccess();
228 -
229 -
      return matches?.length > 0 ? parseInt(matches[0], 10) : undefined;
230 -
    } catch (e) {
231 -
      if (e.signal === 'SIGKILL') {
232 -
        this.logger.debug(
233 -
          `Query of ${address} reached scan time limit, resetting hci${this.config.hciDeviceId}`
234 -
        );
235 -
        this.resetHciDevice();
236 -
237 -
        // when not reachable a scan runs for 6s, so lower time limits might not be an error
238 -
        if (this.config.scanTimeLimit >= 6) {
239 -
          this.healthIndicator.reportError();
240 -
        }
241 -
      } else if (
242 -
        e.message?.includes('Input/output') ||
243 -
        e.message?.includes('I/O')
244 -
      ) {
245 -
        this.logger.debug(e.message);
246 -
      } else {
247 -
        this.logger.error(e.message);
248 -
        this.healthIndicator.reportError();
249 -
      }
250 -
251 -
      return undefined;
252 -
    }
253 -
  }
254 -
255 213
  /**
256 214
   * Applies the Kalman filter based on the historic values with the same address.
257 215
   *
@@ -263,37 +221,6 @@
Loading
263 221
    return this.kalmanFilter(rssi, address);
264 222
  }
265 223
266 -
  /**
267 -
   * Inquires device information of a Bluetooth peripheral.
268 -
   *
269 -
   * @param address - Bluetooth MAC address
270 -
   * @returns Device information
271 -
   */
272 -
  async inquireDeviceInfo(address: string): Promise<Device> {
273 -
    try {
274 -
      const output = await execPromise(
275 -
        `hcitool -i hci${this.config.hciDeviceId} info "${address}"`
276 -
      );
277 -
278 -
      const nameMatches = /Device Name: (.+)/g.exec(output.stdout);
279 -
      const manufacturerMatches = /OUI Company: (.+) \(.+\)/g.exec(
280 -
        output.stdout
281 -
      );
282 -
283 -
      return {
284 -
        address,
285 -
        name: nameMatches ? nameMatches[1] : address,
286 -
        manufacturer: manufacturerMatches ? manufacturerMatches[1] : undefined,
287 -
      };
288 -
    } catch (e) {
289 -
      this.logger.error(e.message, e.stack);
290 -
      return {
291 -
        address,
292 -
        name: address,
293 -
      };
294 -
    }
295 -
  }
296 -
297 224
  /**
298 225
   * Updates the underlying room presence sensor state.
299 226
   * Called regularly.
@@ -351,17 +278,6 @@
Loading
351 278
    return this.inquiriesSwitch?.state;
352 279
  }
353 280
354 -
  /**
355 -
   * Reset the hci (Bluetooth) device used for inquiries.
356 -
   */
357 -
  protected async resetHciDevice(): Promise<void> {
358 -
    try {
359 -
      await execPromise(`hciconfig hci${this.config.hciDeviceId} reset`);
360 -
    } catch (e) {
361 -
      this.logger.error(e.message);
362 -
    }
363 -
  }
364 -
365 281
  /**
366 282
   * Creates and registers a new switch as a setting for whether Bluetooth queries should be made from this device or not.
367 283
   *
@@ -384,7 +300,12 @@
Loading
384 300
      ),
385 301
      customizations
386 302
    ) as Switch;
387 -
    inquiriesSwitch.turnOn();
303 +
304 +
    if (this.config.inquireFromStart) {
305 +
      inquiriesSwitch.turnOn();
306 +
    } else {
307 +
      inquiriesSwitch.turnOff();
308 +
    }
388 309
389 310
    return inquiriesSwitch;
390 311
  }
@@ -484,9 +405,7 @@
Loading
484 405
    const [small, large] = a1.length > a2.length ? [a2, a1] : [a1, a2];
485 406
    const largeSubset = large.slice(offset, offset + small.length);
486 407
    if (offset + small.length > large.length) {
487 -
      largeSubset.push(
488 -
        ...large.slice(0, small.length - this.rotationOffset + 1)
489 -
      );
408 +
      largeSubset.push(...large.slice(0, small.length - largeSubset.length));
490 409
    }
491 410
492 411
    return a1.length > a2.length ? [largeSubset, a2] : [a1, largeSubset];

@@ -0,0 +1,12 @@
Loading
1 +
import { Module } from '@nestjs/common';
2 +
import { BluetoothService } from './bluetooth.service';
3 +
import { BluetoothHealthIndicator } from './bluetooth.health';
4 +
import { ConfigModule } from '../../config/config.module';
5 +
import { StatusModule } from '../../status/status.module';
6 +
7 +
@Module({
8 +
  imports: [ConfigModule, StatusModule],
9 +
  providers: [BluetoothService, BluetoothHealthIndicator],
10 +
  exports: [BluetoothService],
11 +
})
12 +
export class BluetoothModule {}

@@ -5,6 +5,7 @@
Loading
5 5
import { ConfigModule } from '../../config/config.module';
6 6
import { ClusterModule } from '../../cluster/cluster.module';
7 7
import { ScheduleModule } from '@nestjs/schedule';
8 +
import { BluetoothModule } from '../bluetooth/bluetooth.module';
8 9
9 10
@Module({})
10 11
export default class BluetoothLowEnergyModule {
@@ -12,6 +13,7 @@
Loading
12 13
    return {
13 14
      module: BluetoothLowEnergyModule,
14 15
      imports: [
16 +
        BluetoothModule,
15 17
        EntitiesModule,
16 18
        ConfigModule,
17 19
        ClusterModule,

@@ -4,7 +4,7 @@
Loading
4 4
  OnApplicationBootstrap,
5 5
  OnModuleInit,
6 6
} from '@nestjs/common';
7 -
import noble, { Peripheral, Advertisement } from '@abandonware/noble';
7 +
import { Peripheral, Advertisement } from '@abandonware/noble';
8 8
import { EntitiesService } from '../../entities/entities.service';
9 9
import { ConfigService } from '../../config/config.service';
10 10
import { XiaomiMiSensorOptions } from './xiaomi-mi.config';
@@ -13,6 +13,7 @@
Loading
13 13
import { Sensor } from '../../entities/sensor';
14 14
import { EntityCustomization } from '../../entities/entity-customization.interface';
15 15
import { SensorConfig } from '../home-assistant/sensor-config';
16 +
import { BluetoothService } from '../bluetooth/bluetooth.service';
16 17
17 18
@Injectable()
18 19
export class XiaomiMiService implements OnModuleInit, OnApplicationBootstrap {
@@ -20,6 +21,7 @@
Loading
20 21
  private readonly logger: Logger;
21 22
22 23
  constructor(
24 +
    private readonly bluetoothService: BluetoothService,
23 25
    private readonly entitiesService: EntitiesService,
24 26
    private readonly configService: ConfigService
25 27
  ) {
@@ -46,16 +48,7 @@
Loading
46 48
   * Lifecycle hook, called once the application has started.
47 49
   */
48 50
  onApplicationBootstrap(): void {
49 -
    noble.on('stateChange', XiaomiMiService.handleStateChange);
50 -
    noble.on('discover', this.handleDiscovery.bind(this));
51 -
    noble.on('warning', this.onWarning.bind(this));
52 -
  }
53 -
54 -
  /**
55 -
   * Log warnings from noble.
56 -
   */
57 -
  private onWarning(message: string): void {
58 -
    this.logger.warn('Warning: ', message);
51 +
    this.bluetoothService.onLowEnergyDiscovery(this.handleDiscovery.bind(this));
59 52
  }
60 53
61 54
  /**
@@ -233,17 +226,4 @@
Loading
233 226
  ): ServiceData {
234 227
    return new Parser(buffer, bindKey).parse();
235 228
  }
236 -
237 -
  /**
238 -
   * Stops or starts BLE scans based on the adapter state.
239 -
   *
240 -
   * @param state - Noble adapter state string
241 -
   */
242 -
  private static handleStateChange(state: string): void {
243 -
    if (state === 'poweredOn') {
244 -
      noble.startScanning([], true);
245 -
    } else {
246 -
      noble.stopScanning();
247 -
    }
248 -
  }
249 229
}

@@ -3,13 +3,14 @@
Loading
3 3
import { XiaomiMiService } from './xiaomi-mi.service';
4 4
import { EntitiesModule } from '../../entities/entities.module';
5 5
import { ConfigModule } from '../../config/config.module';
6 +
import { BluetoothModule } from '../bluetooth/bluetooth.module';
6 7
7 8
@Module({})
8 9
export default class XiaomiMiModule {
9 10
  static forRoot(): DynamicModule {
10 11
    return {
11 12
      module: XiaomiMiModule,
12 -
      imports: [EntitiesModule, ConfigModule],
13 +
      imports: [BluetoothModule, EntitiesModule, ConfigModule],
13 14
      providers: [XiaomiMiService],
14 15
    };
15 16
  }

@@ -4,7 +4,7 @@
Loading
4 4
  OnApplicationBootstrap,
5 5
  OnModuleInit,
6 6
} from '@nestjs/common';
7 -
import noble, { Peripheral } from '@abandonware/noble';
7 +
import { Peripheral } from '@abandonware/noble';
8 8
import { EntitiesService } from '../../entities/entities.service';
9 9
import { IBeacon } from './i-beacon';
10 10
import { Tag } from './tag';
@@ -23,11 +23,13 @@
Loading
23 23
import { DeviceTracker } from '../../entities/device-tracker';
24 24
import { RoomPresenceDeviceTrackerProxyHandler } from '../room-presence/room-presence-device-tracker.proxy';
25 25
import { BluetoothLowEnergyPresenceSensor } from './bluetooth-low-energy-presence.sensor';
26 +
import { BluetoothService } from '../bluetooth/bluetooth.service';
26 27
27 28
export const NEW_DISTANCE_CHANNEL = 'bluetooth-low-energy.new-distance';
28 29
29 30
@Injectable() // parameters determined experimentally
30 -
export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
31 +
export class BluetoothLowEnergyService
32 +
  extends KalmanFilterable(Object, 0.8, 15)
31 33
  implements OnModuleInit, OnApplicationBootstrap {
32 34
  private readonly config: BluetoothLowEnergyConfig;
33 35
  private readonly logger: Logger;
@@ -37,6 +39,7 @@
Loading
37 39
  } = {};
38 40
39 41
  constructor(
42 +
    private readonly bluetoothService: BluetoothService,
40 43
    private readonly entitiesService: EntitiesService,
41 44
    private readonly configService: ConfigService,
42 45
    private readonly clusterService: ClusterService,
@@ -62,12 +65,7 @@
Loading
62 65
   * Lifecycle hook, called once the application has started.
63 66
   */
64 67
  onApplicationBootstrap(): void {
65 -
    noble.on('stateChange', BluetoothLowEnergyService.handleStateChange);
66 -
    noble.on('discover', this.handleDiscovery.bind(this));
67 -
    noble.on('warning', (message) => {
68 -
      this.logger.warn(message);
69 -
    });
70 -
68 +
    this.bluetoothService.onLowEnergyDiscovery(this.handleDiscovery.bind(this));
71 69
    this.clusterService.on(
72 70
      NEW_DISTANCE_CHANNEL,
73 71
      this.handleNewDistance.bind(this)
@@ -339,17 +337,4 @@
Loading
339 337
340 338
    return tag;
341 339
  }
342 -
343 -
  /**
344 -
   * Stops or starts BLE scans based on the adapter state.
345 -
   *
346 -
   * @param state - Noble adapter state string
347 -
   */
348 -
  private static handleStateChange(state: string): void {
349 -
    if (state === 'poweredOn') {
350 -
      noble.startScanning([], true);
351 -
    } else {
352 -
      noble.stopScanning();
353 -
    }
354 -
  }
355 340
}
356 341
imilarity index 78%
357 342
ename from src/integrations/bluetooth-classic/bluetooth-classic.health.spec.ts
358 343
ename to src/integrations/bluetooth/bluetooth.health.spec.ts
Files Coverage
src 91.63%
Project Totals (86 files) 91.63%
Node.js 12.x
Build #327818001 -
unittests
1
coverage:
2
  status:
3
    project:
4
      default:
5
        threshold: 1
6
    patch:
7
      default:
8
        threshold: 5
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading