forked from homebridge/HAP-NodeJS
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Service.js
282 lines (239 loc) · 10.4 KB
/
Service.js
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
'use strict';
var inherits = require('util').inherits;
var clone = require('./util/clone').clone;
var EventEmitter = require('events').EventEmitter;
var Characteristic = require('./Characteristic').Characteristic;
module.exports = {
Service: Service
};
/**
* Service represents a set of grouped values necessary to provide a logical function. For instance, a
* "Door Lock Mechanism" service might contain two values, one for the "desired lock state" and one for the
* "current lock state". A particular Service is distinguished from others by its "type", which is a UUID.
* HomeKit provides a set of known Service UUIDs defined in HomeKitTypes.js along with a corresponding
* concrete subclass that you can instantiate directly to setup the necessary values. These natively-supported
* Services are expected to contain a particular set of Characteristics.
*
* Unlike Characteristics, where you cannot have two Characteristics with the same UUID in the same Service,
* you can actually have multiple Services with the same UUID in a single Accessory. For instance, imagine
* a Garage Door Opener with both a "security light" and a "backlight" for the display. Each light could be
* a "Lightbulb" Service with the same UUID. To account for this situation, we define an extra "subtype"
* property on Service, that can be a string or other string-convertible object that uniquely identifies the
* Service among its peers in an Accessory. For instance, you might have `service1.subtype = 'security_light'`
* for one and `service2.subtype = 'backlight'` for the other.
*
* You can also define custom Services by providing your own UUID for the type that you generate yourself.
* Custom Services can contain an arbitrary set of Characteristics, but Siri will likely not be able to
* work with these.
*
* @event 'characteristic-change' => function({characteristic, oldValue, newValue, context}) { }
* Emitted after a change in the value of one of our Characteristics has occurred.
*/
function Service(displayName, UUID, subtype) {
if (!UUID) throw new Error("Services must be created with a valid UUID.");
this.displayName = displayName;
this.UUID = UUID;
this.subtype = subtype;
this.iid = null; // assigned later by our containing Accessory
this.characteristics = [];
this.optionalCharacteristics = [];
this.isHiddenService = false;
this.isPrimaryService = false;
this.linkedServices = [];
// every service has an optional Characteristic.Name property - we'll set it to our displayName
// if one was given
// if you don't provide a display name, some HomeKit apps may choose to hide the device.
if (displayName) {
// create the characteristic if necessary
var nameCharacteristic =
this.getCharacteristic(Characteristic.Name) ||
this.addCharacteristic(Characteristic.Name);
nameCharacteristic.setValue(displayName);
}
}
inherits(Service, EventEmitter);
Service.prototype.addCharacteristic = function (characteristic) {
// characteristic might be a constructor like `Characteristic.Brightness` instead of an instance
// of Characteristic. Coerce if necessary.
if (typeof characteristic === 'function') {
characteristic = new (Function.prototype.bind.apply(characteristic, arguments));
}
// check for UUID conflict
for (var index in this.characteristics) {
var existing = this.characteristics[index];
if (existing.UUID === characteristic.UUID) {
if (characteristic.UUID === '00000052-0000-1000-8000-0026BB765291') {
//This is a special workaround for the Firmware Revision characteristic.
return existing;
}
throw new Error("Cannot add a Characteristic with the same UUID as another Characteristic in this Service: " + existing.UUID);
}
}
// listen for changes in characteristics and bubble them up
characteristic.on('change', function (change) {
// make a new object with the relevant characteristic added, and bubble it up
this.emit('characteristic-change', clone(change, { characteristic: characteristic }));
}.bind(this));
this.characteristics.push(characteristic);
this.emit('service-configurationChange', clone({ service: this }));
return characteristic;
}
//Defines this service as hidden
Service.prototype.setHiddenService = function (isHidden) {
this.isHiddenService = isHidden;
this.emit('service-configurationChange', clone({ service: this }));
}
//Allows setting other services that link to this one.
Service.prototype.addLinkedService = function (newLinkedService) {
//TODO: Add a check if the service is on the same accessory.
if (!this.linkedServices.includes(newLinkedService))
this.linkedServices.push(newLinkedService);
this.emit('service-configurationChange', clone({ service: this }));
}
Service.prototype.removeLinkedService = function (oldLinkedService) {
//TODO: Add a check if the service is on the same accessory.
if (this.linkedServices.includes(oldLinkedService))
this.linkedServices.splice(this.linkedServices.indexOf(oldLinkedService), 1);
this.emit('service-configurationChange', clone({ service: this }));
}
Service.prototype.removeCharacteristic = function (characteristic) {
var targetCharacteristicIndex;
for (var index in this.characteristics) {
var existingCharacteristic = this.characteristics[index];
if (existingCharacteristic === characteristic) {
targetCharacteristicIndex = index;
break;
}
}
if (targetCharacteristicIndex) {
this.characteristics.splice(targetCharacteristicIndex, 1);
characteristic.removeAllListeners();
this.emit('service-configurationChange', clone({ service: this }));
}
}
Service.prototype.getCharacteristic = function (name) {
// returns a characteristic object from the service
// If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic,
// but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it.
var index, characteristic;
for (index in this.characteristics) {
characteristic = this.characteristics[index];
if (typeof name === 'string' && characteristic.displayName === name) {
return characteristic;
}
else if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) {
return characteristic;
}
}
if (typeof name === 'function') {
for (index in this.optionalCharacteristics) {
characteristic = this.optionalCharacteristics[index];
if ((characteristic instanceof name) || (name.UUID === characteristic.UUID)) {
return this.addCharacteristic(name);
}
}
//Not found in optional Characteristics. Adding anyway, but warning about it if it isn't the Name.
if (name !== Characteristic.Name) {
console.warn("HAP Warning: Characteristic %s not in required or optional characteristics for service %s. Adding anyway.", name.UUID, this.UUID);
return this.addCharacteristic(name);
}
}
};
Service.prototype.testCharacteristic = function (name) {
// checks for the existence of a characteristic object in the service
var index, characteristic;
for (index in this.characteristics) {
characteristic = this.characteristics[index];
if (typeof name === 'string' && characteristic.displayName === name) {
return true;
}
else if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) {
return true;
}
}
return false;
}
Service.prototype.setCharacteristic = function (name, value) {
this.getCharacteristic(name).setValue(value);
return this; // for chaining
}
// A function to only updating the remote value, but not firiring the 'set' event.
Service.prototype.updateCharacteristic = function (name, value) {
this.getCharacteristic(name).updateValue(value);
return this;
}
Service.prototype.addOptionalCharacteristic = function (characteristic) {
// characteristic might be a constructor like `Characteristic.Brightness` instead of an instance
// of Characteristic. Coerce if necessary.
if (typeof characteristic === 'function')
characteristic = new characteristic();
this.optionalCharacteristics.push(characteristic);
}
Service.prototype.getCharacteristicByIID = function (iid) {
for (var index in this.characteristics) {
var characteristic = this.characteristics[index];
if (characteristic.iid === iid)
return characteristic;
}
}
Service.prototype._assignIDs = function (identifierCache, accessoryName, baseIID) {
if (baseIID === undefined) {
baseIID = 0;
}
// the Accessory Information service must have a (reserved by IdentifierCache) ID of 1
if (this.UUID === '0000003E-0000-1000-8000-0026BB765291') {
this.iid = 1;
}
else {
// assign our own ID based on our UUID
this.iid = baseIID + identifierCache.getIID(accessoryName, this.UUID, this.subtype);
}
// assign IIDs to our Characteristics
for (var index in this.characteristics) {
var characteristic = this.characteristics[index];
characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype);
}
}
/**
* Returns a JSON representation of this Accessory suitable for delivering to HAP clients.
*/
Service.prototype.toHAP = function (opt) {
var characteristicsHAP = [];
for (var index in this.characteristics) {
var characteristic = this.characteristics[index];
characteristicsHAP.push(characteristic.toHAP(opt));
}
var hap = {
iid: this.iid,
type: this.UUID,
characteristics: characteristicsHAP
};
if (this.isPrimaryService !== undefined) {
hap['primary'] = this.isPrimaryService;
}
if (this.isHiddenService !== undefined) {
hap['hidden'] = this.isHiddenService;
}
if (this.linkedServices.length > 0) {
hap['linked'] = [];
for (var index in this.linkedServices) {
var otherService = this.linkedServices[index];
hap['linked'].push(otherService.iid);
}
}
return hap;
}
Service.prototype._setupCharacteristic = function (characteristic) {
// listen for changes in characteristics and bubble them up
characteristic.on('change', function (change) {
// make a new object with the relevant characteristic added, and bubble it up
this.emit('characteristic-change', clone(change, { characteristic: characteristic }));
}.bind(this));
}
Service.prototype._sideloadCharacteristics = function (targetCharacteristics) {
for (var index in targetCharacteristics) {
var target = targetCharacteristics[index];
this._setupCharacteristic(target);
}
this.characteristics = targetCharacteristics.slice();
}