-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bluetooth-low-energy.service.ts
253 lines (230 loc) · 7.66 KB
/
bluetooth-low-energy.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import {
Injectable,
Logger,
OnApplicationBootstrap,
OnModuleInit
} from '@nestjs/common';
import noble, { Peripheral } from '@abandonware/noble';
import { EntitiesService } from '../../entities/entities.service';
import { IBeacon } from './i-beacon';
import { Tag } from './tag';
import { ConfigService } from '../../config/config.service';
import { BluetoothLowEnergyConfig } from './bluetooth-low-energy.config';
import { ClusterService } from '../../cluster/cluster.service';
import { NewDistanceEvent } from './new-distance.event';
import { EntityCustomization } from '../../entities/entity-customization.interface';
import { SensorConfig } from '../home-assistant/sensor-config';
import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor';
import { SchedulerRegistry } from '@nestjs/schedule';
import { KalmanFilterable } from '../../util/filters';
import { makeId } from '../../util/id';
export const NEW_DISTANCE_CHANNEL = 'bluetooth-low-energy.new-distance';
@Injectable() // parameters determined experimentally
export class BluetoothLowEnergyService extends KalmanFilterable(Object, 0.8, 15)
implements OnModuleInit, OnApplicationBootstrap {
private readonly config: BluetoothLowEnergyConfig;
private readonly logger: Logger;
constructor(
private readonly entitiesService: EntitiesService,
private readonly configService: ConfigService,
private readonly clusterService: ClusterService,
private readonly schedulerRegistry: SchedulerRegistry
) {
super();
this.config = this.configService.get('bluetoothLowEnergy');
this.logger = new Logger(BluetoothLowEnergyService.name);
}
/**
* Lifecycle hook, called once the host module has been initialized.
*/
onModuleInit(): void {
noble.on('stateChange', BluetoothLowEnergyService.handleStateChange);
noble.on('discover', this.handleDiscovery.bind(this));
if (!this.isWhitelistEnabled()) {
this.logger.warn('The whitelist is empty, no sensors will be created!');
}
}
/**
* Lifecycle hook, called once the application has started.
*/
onApplicationBootstrap(): void {
this.clusterService.on(
NEW_DISTANCE_CHANNEL,
this.handleNewDistance.bind(this)
);
this.clusterService.subscribe(NEW_DISTANCE_CHANNEL);
}
/**
* Filters found BLE peripherals and publishes new distance data to sensors, depending on configuration.
*
* @param peripheral - BLE peripheral
*/
handleDiscovery(peripheral: Peripheral): void {
let tag = this.createTag(peripheral);
if (this.config.onlyIBeacon && !(tag instanceof IBeacon)) {
return;
}
if (this.isOnWhitelist(tag.id)) {
tag = this.applyOverrides(tag);
tag.rssi = this.filterRssi(tag.id, tag.rssi);
const globalSettings = this.configService.get('global');
const event = new NewDistanceEvent(
globalSettings.instanceName,
tag.id,
tag.name,
tag.distance
);
this.handleNewDistance(event);
this.clusterService.publish(NEW_DISTANCE_CHANNEL, event);
} else {
this.logger.debug(
`Ignoring tag with id ${tag.id} and signal strength ${tag.rssi}`
);
}
}
/**
* Passes newly found distance information to aggregated room presence sensors.
*
* @param event - Event with new distance data
*/
handleNewDistance(event: NewDistanceEvent): void {
const sensorId = makeId(`ble ${event.tagId}`);
let sensor: RoomPresenceDistanceSensor;
if (this.entitiesService.has(sensorId)) {
sensor = this.entitiesService.get(sensorId) as RoomPresenceDistanceSensor;
} else {
sensor = this.createRoomPresenceSensor(sensorId, event.tagName);
}
sensor.handleNewDistance(event.instanceName, event.distance);
}
/**
* Determines if the manufacturer data of a BLE peripheral belongs to an iBeacon or not.
*
* @param manufacturerData - Buffer of BLE peripheral manufacturer data
* @returns Whether the data belongs to an iBeacon or not
*/
isIBeacon(manufacturerData: Buffer): boolean {
return (
manufacturerData &&
25 <= manufacturerData.length && // expected data length
0x004c === manufacturerData.readUInt16LE(0) && // apple company identifier
0x02 === manufacturerData.readUInt8(2) && // ibeacon type
0x15 === manufacturerData.readUInt8(3)
); // expected ibeacon data length
}
/**
* Determines whether a whitelist has been configured or not.
*
* @returns Whitelist status
*/
isWhitelistEnabled(): boolean {
return this.config.whitelist?.length > 0;
}
/**
* Checks if an id is on the whitelist of this component.
* Always returns true if the whitelist is empty.
*
* @param id - Device id
* @return Whether the id is on the whitelist or not
*/
isOnWhitelist(id: string): boolean {
const whitelist = this.config.whitelist;
if (whitelist === undefined || whitelist.length === 0) {
return false;
}
return this.config.whitelistRegex
? whitelist.some(regex => id.match(regex))
: whitelist.includes(id);
}
/**
* Applies the Kalman filter based on the historic values with the same tag id.
*
* @param tagId - Tag id that matches the measured device
* @param rssi - Measured signal strength
* @returns Smoothed signal strength value
*/
filterRssi(tagId: string, rssi: number): number {
return this.kalmanFilter(rssi, tagId);
}
/**
* Creates and registers a new room presence sensor.
*
* @param sensorId - Id that the sensor should receive
* @param deviceName - Name of the BLE peripheral
* @returns Registered room presence sensor
*/
protected createRoomPresenceSensor(
sensorId: string,
deviceName: string
): RoomPresenceDistanceSensor {
const sensorName = `${deviceName} Room Presence`;
const customizations: Array<EntityCustomization<any>> = [
{
for: SensorConfig,
overrides: {
icon: 'mdi:bluetooth'
}
}
];
const sensor = this.entitiesService.add(
new RoomPresenceDistanceSensor(sensorId, sensorName, this.config.timeout),
customizations
) as RoomPresenceDistanceSensor;
const interval = setInterval(
sensor.checkForTimeout.bind(sensor),
this.config.timeout * 1000
);
this.schedulerRegistry.addInterval(`${sensorId}_timeout_check`, interval);
return sensor;
}
/**
* Creates a tag based on a given BLE peripheral.
*
* @param peripheral - Noble BLE peripheral
* @returns Tag or IBeacon
*/
protected createTag(peripheral: Peripheral): Tag {
if (
this.config.processIBeacon &&
this.isIBeacon(peripheral.advertisement.manufacturerData)
) {
return new IBeacon(
peripheral,
this.config.majorMask,
this.config.minorMask
);
} else {
return new Tag(peripheral);
}
}
/**
* Checks if overrides have been configured for a tag and then applies them.
*
* @param tag - Tag that should be overridden
* @returns Same tag with potentially overridden data
*/
protected applyOverrides(tag: Tag): Tag {
if (this.config.tagOverrides.hasOwnProperty(tag.id)) {
const overrides = this.config.tagOverrides[tag.id];
if (overrides.name !== undefined) {
tag.name = overrides.name;
}
if (overrides.measuredPower !== undefined) {
tag.measuredPower = overrides.measuredPower;
}
}
return tag;
}
/**
* Stops or starts BLE scans based on the adapter state.
*
* @param state - Noble adapter state string
*/
private static handleStateChange(state: string): void {
if (state === 'poweredOn') {
noble.startScanning([], true);
} else {
noble.stopScanning();
}
}
}