homebridge-sony-audio-extended 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,867 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SonyDevice = void 0;
7
+ const api_1 = require("./api");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const events_1 = require("events");
10
+ /* eslint-disable max-len */
11
+ const url_1 = require("url");
12
+ const ws_1 = __importDefault(require("ws"));
13
+ /**
14
+ * Categories of devices supported by this module
15
+ */
16
+ const COMPATIBLE_DEVICE_CATEGORIES = [
17
+ 'homeTheaterSystem',
18
+ 'personalAudio', // !not tested
19
+ ];
20
+ /**
21
+ * Model names that are explicitly supported even if category doesn't match
22
+ */
23
+ const COMPATIBLE_DEVICE_MODELS = ['STR-AN1000'];
24
+ /**
25
+ * Devices terminals which hasn't in getCurrentExternalTerminalsStatus api
26
+ * from [here](https://developer.sony.com/develop/audio-control-api/api-references/device-uri)
27
+ */
28
+ const DEVICE_TERMINALS = [
29
+ {
30
+ scheme: 'dlna',
31
+ readonly: true,
32
+ terminal: {
33
+ connection: 'connected',
34
+ title: 'DLNA Music',
35
+ uri: 'dlna:music',
36
+ meta: "meta:pc" /* PC */,
37
+ },
38
+ },
39
+ {
40
+ scheme: 'storage',
41
+ readonly: true,
42
+ terminal: {
43
+ connection: 'connected',
44
+ title: 'USB Storage',
45
+ uri: 'storage:usb1',
46
+ meta: "meta:usbdac" /* USBDAC */,
47
+ },
48
+ },
49
+ {
50
+ scheme: 'radio',
51
+ readonly: true,
52
+ terminal: {
53
+ connection: 'connected',
54
+ title: 'FM Radio',
55
+ uri: 'radio:fm',
56
+ meta: "meta:tuner" /* TUNER */,
57
+ },
58
+ },
59
+ {
60
+ scheme: 'netService',
61
+ readonly: false,
62
+ terminal: {
63
+ connection: 'connected',
64
+ title: 'Audio Network',
65
+ uri: 'netService:audio',
66
+ meta: "meta:source" /* SOURCE */,
67
+ },
68
+ },
69
+ {
70
+ scheme: 'multiroom',
71
+ readonly: false,
72
+ terminal: {
73
+ connection: 'connected',
74
+ title: 'Multiroom Audio',
75
+ uri: 'multiroom:audio',
76
+ meta: "meta:source" /* SOURCE */,
77
+ },
78
+ },
79
+ {
80
+ scheme: 'cast',
81
+ readonly: false,
82
+ terminal: {
83
+ connection: 'connected',
84
+ title: 'Cast Audio',
85
+ uri: 'cast:audio',
86
+ meta: "meta:source" /* SOURCE */,
87
+ },
88
+ },
89
+ {
90
+ scheme: 'extInput',
91
+ readonly: false,
92
+ terminal: {
93
+ connection: 'connected',
94
+ title: 'AirPlay',
95
+ uri: 'extInput:airPlay',
96
+ meta: "meta:btaudio" /* BTAUDIO */,
97
+ },
98
+ },
99
+ ];
100
+ const RE_EXT_OUTPUT = new RegExp('extOutput:*');
101
+ const SUBSCRIBE_NOTIFICATIONS = [
102
+ {
103
+ service: 'system',
104
+ notifications: ["notifyPowerStatus" /* POWER */],
105
+ },
106
+ {
107
+ service: 'audio',
108
+ notifications: ["notifyVolumeInformation" /* VOLUME */],
109
+ },
110
+ {
111
+ service: 'avContent',
112
+ notifications: ["notifyExternalTerminalStatus" /* TERMINAL */, "notifyPlayingContentInfo" /* CONTENT */],
113
+ },
114
+ ];
115
+ const RECONNECT_TIMEOUT = 5000;
116
+ const REQUEST_TIMEOUT = 5000;
117
+ const WEBSOCKET_REQUEST_TIMEOUT = 3000;
118
+ const WEBSOCKET_HEARTBEAT_INTERVAL = 60 * 1000;
119
+ class SonyDevice extends events_1.EventEmitter {
120
+ constructor(baseUrl, upnpUrl, udn, apisInfo, log) {
121
+ super();
122
+ /** Flag for emitting of the RESORE event, rised when connection has been lost and after restored */
123
+ this.emitRestoreEvent = false;
124
+ this.systemInfo = {
125
+ area: '',
126
+ bdAddr: '',
127
+ bleID: '',
128
+ cid: '',
129
+ deviceID: '',
130
+ duid: '',
131
+ esn: '',
132
+ generation: '',
133
+ helpUrl: '',
134
+ iconUrl: '',
135
+ initialPowerOnTime: '',
136
+ language: '',
137
+ lastPowerOnTime: '',
138
+ macAddr: '',
139
+ model: '',
140
+ name: '',
141
+ product: '',
142
+ region: '',
143
+ serial: '',
144
+ ssid: '',
145
+ version: '',
146
+ wirelessMacAddr: '',
147
+ };
148
+ this._externalTerminals = null;
149
+ this._volumeInformation = null;
150
+ this.manufacturer = 'Sony Corporation';
151
+ this.readyState = SonyDevice.CREATING;
152
+ this.baseUrl = baseUrl;
153
+ this.upnpUrl = upnpUrl;
154
+ this.UDN = udn;
155
+ this.apisInfo = apisInfo;
156
+ this.log = log;
157
+ // this.systemInfo = systemInfo;
158
+ this.axiosInstance = axios_1.default.create({
159
+ baseURL: this.baseUrl.href,
160
+ headers: { 'content-type': 'application/json' },
161
+ timeout: REQUEST_TIMEOUT,
162
+ });
163
+ this.axiosInstance.interceptors.response.use(SonyDevice.responseInterceptor(this.log));
164
+ this.axiosInstance.interceptors.request.use(SonyDevice.requestInterceptorLogger(this.log));
165
+ if (this.upnpUrl) {
166
+ this.axiosInstanceSoap = axios_1.default.create({
167
+ baseURL: this.upnpUrl.href,
168
+ headers: {
169
+ SOAPACTION: '"urn:schemas-sony-com:service:IRCC:1#X_SendIRCC"',
170
+ 'Content-Type': 'text/xml; charset="utf-8"',
171
+ },
172
+ timeout: REQUEST_TIMEOUT,
173
+ });
174
+ this.axiosInstanceSoap.interceptors.response.use(SonyDevice.responseInterceptor(this.log));
175
+ this.axiosInstanceSoap.interceptors.request.use(SonyDevice.requestInterceptorLogger(this.log));
176
+ }
177
+ this.wsClients = new Map();
178
+ this.readyState = SonyDevice.READY;
179
+ }
180
+ /**
181
+ * Return device id
182
+ */
183
+ getDeviceID() {
184
+ return this.systemInfo.serial !== ''
185
+ ? this.systemInfo.serial
186
+ : this.systemInfo.macAddr !== ''
187
+ ? this.systemInfo.macAddr
188
+ : this.systemInfo.wirelessMacAddr;
189
+ }
190
+ /**
191
+ * Checks the request for API version compliance
192
+ */
193
+ validateRequest(service, request) {
194
+ var _a;
195
+ const version = request.version;
196
+ const method = request.method;
197
+ const reqApis = (_a = this.apisInfo) === null || _a === void 0 ? void 0 : _a.find((a) => a.service === service);
198
+ const reqApi = reqApis === null || reqApis === void 0 ? void 0 : reqApis.apis.find((m) => m.name === method);
199
+ const validVersions = reqApi === null || reqApi === void 0 ? void 0 : reqApi.versions.map((x) => x.version);
200
+ if (validVersions) {
201
+ return validVersions.includes(version);
202
+ }
203
+ else {
204
+ return false;
205
+ }
206
+ }
207
+ async getExternalTerminals() {
208
+ if (!this._externalTerminals) {
209
+ // get the terminals info from device
210
+ const resTerminals = await this.axiosInstance.post('/avContent', JSON.stringify(api_1.ApiRequestCurrentExternalTerminalsStatus));
211
+ const terminals = resTerminals.data;
212
+ this._externalTerminals = terminals.result[0];
213
+ // add other terminals
214
+ const schemes = await this.getSchemes();
215
+ DEVICE_TERMINALS.forEach((t) => {
216
+ var _a, _b;
217
+ if (t.readonly) {
218
+ if (schemes.includes(t.scheme)) {
219
+ // device support that input terminal
220
+ (_a = this._externalTerminals) === null || _a === void 0 ? void 0 : _a.push(t.terminal);
221
+ }
222
+ }
223
+ else {
224
+ // add all readonly terminals
225
+ (_b = this._externalTerminals) === null || _b === void 0 ? void 0 : _b.push(t.terminal);
226
+ }
227
+ });
228
+ }
229
+ return this._externalTerminals;
230
+ }
231
+ /**
232
+ * Terminal is read-only and cannot be select by the user?
233
+ */
234
+ isReadonlyTerminal(terminal) {
235
+ const readonlyTerminalsUri = DEVICE_TERMINALS.find((t) => !t.readonly && t.terminal.uri === terminal.uri);
236
+ return !!readonlyTerminalsUri;
237
+ }
238
+ async getVolumeInformation() {
239
+ if (!this._volumeInformation) {
240
+ // get the volume info from device
241
+ const resVolumes = await this.axiosInstance.post('/audio', JSON.stringify(api_1.ApiRequestVolumeInformation));
242
+ const volumes = resVolumes.data;
243
+ this._volumeInformation = volumes.result[0];
244
+ }
245
+ return this._volumeInformation;
246
+ }
247
+ /**
248
+ * Returns the active input (if exist) for current active zone
249
+ */
250
+ async getActiveInput() {
251
+ const service = 'avContent';
252
+ const req = {
253
+ id: 37,
254
+ method: 'getPlayingContentInfo',
255
+ params: [{}],
256
+ version: '1.2',
257
+ };
258
+ const zone = await this.getActiveZone();
259
+ if (zone) {
260
+ req.params[0].output = zone.uri;
261
+ }
262
+ const res = await this.axiosInstance.post('/' + service, JSON.stringify(req));
263
+ const playingInfo = res.data;
264
+ if (playingInfo.result[0].length === 1) {
265
+ // info with only one zone
266
+ return this.getTerminalBySource(playingInfo.result[0][0].source || playingInfo.result[0][0].uri);
267
+ }
268
+ return null; // no active zone
269
+ }
270
+ /**
271
+ * Return external terminals which are inputs
272
+ */
273
+ async getInputs() {
274
+ const exTerminals = await this.getExternalTerminals();
275
+ const inputs = exTerminals === null || exTerminals === void 0 ? void 0 : exTerminals.filter((t) => !RE_EXT_OUTPUT.test(t.uri));
276
+ return inputs ? inputs : [];
277
+ }
278
+ /**
279
+ * Return external terminals which are zone, aka outputs
280
+ */
281
+ async getZones() {
282
+ const exTerminals = await this.getExternalTerminals();
283
+ const inputs = exTerminals === null || exTerminals === void 0 ? void 0 : exTerminals.filter((t) => RE_EXT_OUTPUT.test(t.uri));
284
+ return inputs ? inputs : null;
285
+ }
286
+ /**
287
+ * Return active zone.
288
+ * If no active zone, return `null`.
289
+ * It's mean that only one zone exist, i.e. no output terminals
290
+ */
291
+ async getActiveZone() {
292
+ const zones = await this.getZones();
293
+ if (zones === null) {
294
+ return null;
295
+ }
296
+ else {
297
+ const activeZone = zones.find((zone) => zone.active === 'active');
298
+ return activeZone ? activeZone : null;
299
+ }
300
+ }
301
+ /**
302
+ * Return the list of schemes that device can handle.
303
+ */
304
+ async getSchemes() {
305
+ const resSchemes = await this.axiosInstance.post('/avContent', JSON.stringify(api_1.ApiRequestGetSchemeList));
306
+ const schemes = resSchemes.data;
307
+ return schemes.result[0].map((s) => s.scheme);
308
+ }
309
+ /**
310
+ * Check the API response for returned error
311
+ * Decsription of errors [here](https://developer.sony.com/develop/audio-control-api/api-references/error-codes).
312
+ * @param response
313
+ */
314
+ static responseInterceptor(log) {
315
+ return (response) => {
316
+ log.debug(`Response from device:\n${JSON.stringify(response.data)}`);
317
+ if (typeof response.data === 'object' && response.data !== null) {
318
+ if ('error' in response.data) {
319
+ // TODO: add a device ip address for identification of the device
320
+ const errMsg = `Device API got an error: ${JSON.stringify(response.data)}`;
321
+ return Promise.reject(new api_1.GenericApiError(errMsg));
322
+ }
323
+ else {
324
+ return response;
325
+ }
326
+ }
327
+ else {
328
+ return response;
329
+ }
330
+ };
331
+ }
332
+ /**
333
+ * Logging requests for debug
334
+ * @param request
335
+ */
336
+ static requestInterceptorLogger(log) {
337
+ return (request) => {
338
+ log.debug(`Request to device\n${request.baseURL}:\n${JSON.stringify(request.data)}`);
339
+ return request;
340
+ };
341
+ }
342
+ /**
343
+ * Create and initialize the new device.
344
+ * Get info about supported api and system
345
+ */
346
+ static async createDevice(baseUrl, upnpUrl, udn, log) {
347
+ const axiosInstance = axios_1.default.create({
348
+ baseURL: baseUrl.href,
349
+ headers: { 'content-type': 'application/json' },
350
+ timeout: 0, // when device is turning on, some time it has long answer time.
351
+ });
352
+ axiosInstance.interceptors.response.use(SonyDevice.responseInterceptor(log));
353
+ axiosInstance.interceptors.request.use(SonyDevice.requestInterceptorLogger(log));
354
+ // Checks the device against a compatible category of the device
355
+ const resInterfaceInfo = await axiosInstance.post('/system', JSON.stringify(api_1.ApiRequestGetInterfaceInformation));
356
+ const interfaceInfo = resInterfaceInfo.data;
357
+ const productCategory = interfaceInfo.result[0].productCategory;
358
+ const modelName = interfaceInfo.result[0].modelName;
359
+ // Log device information for debugging
360
+ log.debug(`Device info - Model: "${modelName}", Category: "${productCategory}"`);
361
+ // Check if device is compatible by category or by model name
362
+ const isCompatibleCategory = COMPATIBLE_DEVICE_CATEGORIES.includes(productCategory);
363
+ const isCompatibleModel = COMPATIBLE_DEVICE_MODELS.some((model) => modelName && modelName.toUpperCase().includes(model.toUpperCase()));
364
+ if (!isCompatibleCategory && !isCompatibleModel) {
365
+ // device has an incompatible category and is not in the compatible models list
366
+ throw new api_1.IncompatibleDeviceCategoryError(`Device at ${baseUrl.href} has an incompatible category "${productCategory}" and model "${modelName}" is not in the supported models list`);
367
+ }
368
+ const resApiInfo = await axiosInstance.post('/guide', JSON.stringify(api_1.ApiRequestSupportedApiInfo));
369
+ const apisInfo = resApiInfo.data.result[0];
370
+ const device = new SonyDevice(baseUrl, upnpUrl, udn, apisInfo, log);
371
+ // Gets general system information for the device.
372
+ // check the request for API version compliance
373
+ const service = 'system';
374
+ if (!device.validateRequest(service, api_1.ApiRequestSystemInformation)) {
375
+ throw new api_1.UnsupportedVersionApiError(`The specified api version is not supported by the device ${baseUrl.hostname}`);
376
+ }
377
+ const resSystemInfo = await axiosInstance.post('/' + service, JSON.stringify(api_1.ApiRequestSystemInformation));
378
+ const systemInfo = resSystemInfo.data.result[0];
379
+ Object.assign(device.systemInfo, systemInfo);
380
+ device.subscribe();
381
+ return device;
382
+ }
383
+ /**
384
+ * Initialize notifications for given events
385
+ */
386
+ subscribe() {
387
+ SUBSCRIBE_NOTIFICATIONS.forEach((subscriber) => this.createWebSocket(subscriber.service));
388
+ }
389
+ /**
390
+ * Disable all notifications subscriptions and close websocket connections
391
+ */
392
+ unsubscribe() {
393
+ this.readyState = SonyDevice.CLOSING;
394
+ this.wsClients.forEach((ws, service) => {
395
+ this.log.debug(`Device ${this.systemInfo.name} unsubscribing from ${service} service`);
396
+ // unsubscribe from all notifications
397
+ const disables = this.getAvailibleNotifications(service);
398
+ if (ws.readyState === ws_1.default.OPEN) {
399
+ ws.send(JSON.stringify(this.switchNotifications(100, disables, [])));
400
+ }
401
+ });
402
+ }
403
+ /**
404
+ * Create a new socket for the given service.
405
+ * If socket already exist, only request current notifications subscriptions
406
+ * @param service
407
+ * @returns
408
+ */
409
+ createWebSocket(service) {
410
+ if (this.wsClients.has(service)) {
411
+ // if socket already created, request current notifications subscriptions
412
+ const ws = this.wsClients.get(service);
413
+ if (ws['subscriptionCommand']) {
414
+ ws.send(ws['subscriptionCommand']);
415
+ }
416
+ return;
417
+ }
418
+ const url = new url_1.URL(this.baseUrl.href);
419
+ url.protocol = 'ws';
420
+ url.pathname = url.pathname + '/' + service;
421
+ const ws = new ws_1.default(url);
422
+ function heartbeat(device) {
423
+ device.log.debug(`Device ${device.systemInfo.name} heartbeat init`);
424
+ ws['isAlive'] = true;
425
+ ws['heartbeat'] = setInterval(() => {
426
+ if (ws['isAlive'] === false) {
427
+ ws.terminate();
428
+ return;
429
+ }
430
+ device.log.debug(`Device ${device.systemInfo.name} heartbeat`);
431
+ if (ws['subscriptionCommand']) {
432
+ ws.send(ws['subscriptionCommand']);
433
+ }
434
+ ws['heartbeatTimeout'] = setTimeout(() => {
435
+ device.log.debug(`Device ${device.systemInfo.name} heartbeat timeout`);
436
+ ws['isAlive'] = false;
437
+ ws.terminate();
438
+ }, WEBSOCKET_REQUEST_TIMEOUT);
439
+ }, WEBSOCKET_HEARTBEAT_INTERVAL);
440
+ }
441
+ ws.on('open', () => {
442
+ this.log.debug(`Device ${this.systemInfo.name} opened a socked ${url.href}`);
443
+ heartbeat(this);
444
+ // To get current notification settings, send an empty 'switchNotifications'
445
+ // message with an ID of '1'
446
+ ws.send(JSON.stringify(this.switchNotifications(1, [], [])));
447
+ });
448
+ ws.on('message', (data) => {
449
+ const response = JSON.parse(data);
450
+ if ('id' in response) {
451
+ if (response.id === 1) {
452
+ // enable notification
453
+ this.log.debug(`Device ${this.systemInfo.name} received initial message ${data}`);
454
+ const enabled = []; // response.result[0].enabled;
455
+ const disabled = []; // response.result[0].disabled;
456
+ const shouldEnabled = SUBSCRIBE_NOTIFICATIONS.filter((s) => s.service === service)[0].notifications;
457
+ // if shouldEnabled equal to returned enabled, this mean what nothing to do. All ok
458
+ if (shouldEnabled.length === enabled.length) {
459
+ return;
460
+ }
461
+ // else we need to resubscribe
462
+ const all_notifications = [...response.result[0].enabled].concat([
463
+ ...response.result[0].disabled,
464
+ ]);
465
+ for (let i = 0; i < all_notifications.length; i++) {
466
+ const item = all_notifications[i];
467
+ if (shouldEnabled.includes(item.name)) {
468
+ enabled.push(item);
469
+ }
470
+ else {
471
+ disabled.push(item);
472
+ }
473
+ }
474
+ if (shouldEnabled.length !== enabled.length) {
475
+ // something wrong... or not. For example HT-ZF9 hasn't a notifyExternalTerminalStatus. See #1
476
+ this.log.debug(`Device ${this.systemInfo.name} does not have the required notifier. Should be ${JSON.stringify(shouldEnabled)}, but found ${JSON.stringify(enabled)}`);
477
+ }
478
+ this.log.debug(`Device ${this.systemInfo.name} sent subscribe message ${JSON.stringify(this.switchNotifications(2, disabled, enabled))}`);
479
+ ws['subscriptionCommand'] = JSON.stringify(this.switchNotifications(2, disabled, enabled));
480
+ ws.send(ws['subscriptionCommand']);
481
+ }
482
+ else if (response.id === 100) {
483
+ // unsubscribe from notifications
484
+ clearInterval(ws['heartbeat']);
485
+ clearTimeout(ws['heartbeatTimeout']);
486
+ ws.terminate();
487
+ }
488
+ else {
489
+ this.log.debug(`Device ${this.systemInfo.name} received subscription status ${data}`);
490
+ if (this.emitRestoreEvent) {
491
+ this.emitRestoreEvent = false;
492
+ this.emit("restore" /* RESTORE */);
493
+ }
494
+ }
495
+ }
496
+ else {
497
+ // here handle received notification
498
+ this.log.debug(`Device ${this.systemInfo.name} received notification ${data}`);
499
+ this.handleNotificationMessage(response);
500
+ }
501
+ clearTimeout(ws['heartbeatTimeout']);
502
+ });
503
+ ws.on('close', () => {
504
+ this.log.debug(`Device ${this.systemInfo.name} socket closed`);
505
+ this.wsClients.delete(service);
506
+ // If the connection was closed illegally, recreate it.
507
+ if (this.readyState !== SonyDevice.CLOSING) {
508
+ setTimeout(() => {
509
+ this.emitRestoreEvent = true;
510
+ this.createWebSocket(service);
511
+ }, RECONNECT_TIMEOUT);
512
+ }
513
+ });
514
+ ws.on('error', (err) => {
515
+ this.log.debug(`ERROR: Device ${this.systemInfo.name} has a comunication error: ${err.message}`);
516
+ if (this.wsClients.has(service)) {
517
+ this.wsClients.get(service).terminate();
518
+ }
519
+ this.wsClients.delete(service);
520
+ });
521
+ this.wsClients.set(service, ws);
522
+ }
523
+ /**
524
+ * A switchNotifications Request
525
+ * taken [here](https://developer.sony.com/develop/audio-control-api/get-started/websocket-example#tutorial-step-3)
526
+ * @param id
527
+ * @param disable
528
+ * @param enable
529
+ */
530
+ switchNotifications(id, disable, enable) {
531
+ return {
532
+ method: 'switchNotifications',
533
+ id: id,
534
+ params: [
535
+ {
536
+ disabled: disable,
537
+ enabled: enable,
538
+ },
539
+ ],
540
+ version: '1.0',
541
+ };
542
+ }
543
+ /**
544
+ * Returns all availible notifications of the device
545
+ * @param service
546
+ * @returns
547
+ */
548
+ getAvailibleNotifications(service) {
549
+ const serviceApiInfo = this.apisInfo.find((v) => v.service === service);
550
+ if (!serviceApiInfo) {
551
+ return [];
552
+ }
553
+ const notifications = [];
554
+ for (let i = 0; i < serviceApiInfo.notifications.length; i++) {
555
+ const notification = serviceApiInfo.notifications[i];
556
+ for (let j = 0; j < notification.versions.length; j++) {
557
+ const version = notification.versions[j];
558
+ const verNotification = {
559
+ name: notification.name,
560
+ version: version.version,
561
+ };
562
+ notifications.push(verNotification);
563
+ }
564
+ }
565
+ return notifications;
566
+ }
567
+ /**
568
+ * Parse the notification message recieved from device
569
+ * @param message
570
+ */
571
+ handleNotificationMessage(message) {
572
+ switch (message.method) {
573
+ case "notifyPowerStatus" /* POWER */: {
574
+ const msg = message;
575
+ const power = msg.params[0].status === 'active';
576
+ this.emit("power" /* POWER */, power);
577
+ break;
578
+ }
579
+ case "notifyVolumeInformation" /* VOLUME */: {
580
+ const msg = message;
581
+ const volumeInfo = msg.params[0];
582
+ // update _volumeInformation
583
+ if (this._volumeInformation) {
584
+ const volumeIdx = this._volumeInformation.findIndex((v) => v.output === volumeInfo.output);
585
+ if (volumeIdx !== -1) {
586
+ Object.assign(this._volumeInformation[volumeIdx], volumeInfo);
587
+ }
588
+ }
589
+ const mute = volumeInfo.mute === 'on'
590
+ ? true
591
+ : volumeInfo.mute === 'off'
592
+ ? false
593
+ : null;
594
+ const volume = volumeInfo.volume;
595
+ if (volume !== -1) {
596
+ this.emit("volume" /* VOLUME */, volume);
597
+ }
598
+ if (mute !== null) {
599
+ this.emit("mute" /* MUTE */, mute);
600
+ }
601
+ break;
602
+ }
603
+ case "notifyPlayingContentInfo" /* CONTENT */: {
604
+ // receive like {"method":"notifyPlayingContentInfo","params":[{"contentKind":"input","output":"extOutput:zone?zone=1","source":"extInput:video?port=1","uri":"extInput:video?port=1"}],"version":"1.0"}
605
+ const msg = message;
606
+ const source = msg.params[0].source || msg.params[0].uri; // maybe overcheck
607
+ this.emit("source" /* SOURCE */, source);
608
+ break;
609
+ }
610
+ case "notifyExternalTerminalStatus" /* TERMINAL */: {
611
+ // receive like {"method":"notifyExternalTerminalStatus","params":[{"active":"active","connection":"connected","label":"","uri":"extOutput:zone?zone=1"}],"version":"1.0"}
612
+ const msg = message;
613
+ // update _externalTerminals
614
+ msg.params.forEach((updateTerminal) => {
615
+ if (this._externalTerminals) {
616
+ const terminalIdx = this._externalTerminals.findIndex((t) => t.uri === updateTerminal.uri);
617
+ if (terminalIdx !== -1) {
618
+ Object.assign(this._externalTerminals[terminalIdx], updateTerminal);
619
+ }
620
+ else {
621
+ this._externalTerminals.push(updateTerminal);
622
+ }
623
+ }
624
+ });
625
+ // if the device is turning off from an external source, a notivication about power doesn't sends.
626
+ // so, force the power status check
627
+ this.getPowerState().then((active) => {
628
+ this.emit("power" /* POWER */, active);
629
+ });
630
+ break;
631
+ }
632
+ default: {
633
+ this.log.error(`Found not implemented notification from device: ${JSON.stringify(message)}`);
634
+ break;
635
+ }
636
+ }
637
+ }
638
+ /**
639
+ * Find a terminal by source name
640
+ * @param source the source name received from notifyPlayingContentInfo event
641
+ */
642
+ getTerminalBySource(source) {
643
+ if (this._externalTerminals === null) {
644
+ return null;
645
+ }
646
+ const terminals = this._externalTerminals.filter((terminal) => terminal.uri === source);
647
+ if (terminals.length !== 0) {
648
+ return terminals[0];
649
+ }
650
+ else {
651
+ return null;
652
+ }
653
+ }
654
+ /**
655
+ * Get current power state.
656
+ * * `true` if power is on
657
+ */
658
+ async getPowerState() {
659
+ const service = 'system';
660
+ const resPowerInfo = await this.axiosInstance.post('/' + service, JSON.stringify(api_1.ApiRequestGetPowerStatus));
661
+ const powerInfo = resPowerInfo.data;
662
+ return (powerInfo.result[0].status === 'activating' ||
663
+ powerInfo.result[0].status === 'active');
664
+ }
665
+ /**
666
+ * Get current volume state with device volume settings
667
+ * Volume state returns only for active zone. If no active zone then returns null
668
+ */
669
+ async getVolumeState() {
670
+ const service = 'audio';
671
+ const resVolumeInfo = await this.axiosInstance.post('/' + service, JSON.stringify(api_1.ApiRequestVolumeInformation));
672
+ const volumeInfo = resVolumeInfo.data;
673
+ const activeZone = await this.getActiveZone();
674
+ if (activeZone) {
675
+ const volumeActiveZone = volumeInfo.result[0].find((vi) => vi.output === activeZone.uri);
676
+ if (volumeActiveZone) {
677
+ return volumeActiveZone;
678
+ }
679
+ }
680
+ return null; // no active zone
681
+ }
682
+ /**
683
+ * Change the audio volume level for the active output zone
684
+ * @param volumeSelector the same as Characteristic.VolumeSelector in homebridge
685
+ * * `0` - increment
686
+ * * `1` - decrement
687
+ */
688
+ async setVolume(volumeSelector) {
689
+ const service = 'audio';
690
+ const zone = await this.getActiveZone();
691
+ const reqSetVolume = {
692
+ id: 98,
693
+ method: 'setAudioVolume',
694
+ params: [
695
+ {
696
+ output: zone ? zone.uri : '',
697
+ volume: volumeSelector === 0 ? '+1' : '-1',
698
+ },
699
+ ],
700
+ version: '1.1',
701
+ };
702
+ await this.axiosInstance.post('/' + service, JSON.stringify(reqSetVolume));
703
+ return volumeSelector;
704
+ }
705
+ /**
706
+ * Sets the power status of the device.
707
+ * @param power
708
+ * * `true` - set device in the power-on state
709
+ * * `false` - set device in the power-off state
710
+ */
711
+ async setPower(power) {
712
+ const service = 'system';
713
+ const reqSetPower = {
714
+ id: 55,
715
+ method: 'setPowerStatus',
716
+ params: [
717
+ {
718
+ status: power ? 'active' : 'off',
719
+ },
720
+ ],
721
+ version: '1.1',
722
+ };
723
+ await this.axiosInstance.post('/' + service, JSON.stringify(reqSetPower));
724
+ return power;
725
+ }
726
+ /**
727
+ * Sets the audio mute status.
728
+ * @param mute
729
+ * * `true` - muted
730
+ * * `false` - not muted
731
+ */
732
+ async setMute(mute) {
733
+ const service = 'audio';
734
+ const zone = await this.getActiveZone();
735
+ const reqSetMute = {
736
+ id: 601,
737
+ method: 'setAudioMute',
738
+ params: [
739
+ {
740
+ mute: mute ? 'on' : 'off',
741
+ },
742
+ ],
743
+ version: '1.1',
744
+ };
745
+ if (zone) {
746
+ reqSetMute.params[0].output = zone.uri;
747
+ }
748
+ await this.axiosInstance.post('/' + service, JSON.stringify(reqSetMute));
749
+ return mute;
750
+ }
751
+ /**
752
+ * Sets the input source
753
+ * @param terminal
754
+ */
755
+ async setSource(terminal) {
756
+ const service = 'avContent';
757
+ const zone = await this.getActiveZone();
758
+ const reqSetPlayContent = {
759
+ id: 47,
760
+ method: 'setPlayContent',
761
+ params: [
762
+ {
763
+ uri: terminal.uri,
764
+ },
765
+ ],
766
+ version: '1.2',
767
+ };
768
+ if (zone) {
769
+ reqSetPlayContent.params[0].output = zone.uri;
770
+ }
771
+ await this.axiosInstance.post('/' + service, JSON.stringify(reqSetPlayContent));
772
+ return terminal;
773
+ }
774
+ /**
775
+ * Toggles between the play and pause states for the current content.
776
+ */
777
+ async setPause() {
778
+ const service = 'avContent';
779
+ const zone = await this.getActiveZone();
780
+ const reqPausePlayingContent = {
781
+ id: 31,
782
+ method: 'pausePlayingContent',
783
+ params: [{}],
784
+ version: '1.1',
785
+ };
786
+ if (zone) {
787
+ reqPausePlayingContent.params[0].output = zone.uri;
788
+ }
789
+ await this.axiosInstance.post('/' + service, JSON.stringify(reqPausePlayingContent));
790
+ return;
791
+ }
792
+ /**
793
+ * Sends command codes of IR remote commander to device via IP
794
+ * Some info [here](https://pro-bravia.sony.net/develop/integrate/ircc-ip/overview/index.html)
795
+ * @param irCode
796
+ */
797
+ async sendIRCC(irCode) {
798
+ if (this.axiosInstanceSoap) {
799
+ const data = `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:X_SendIRCC xmlns:u="urn:schemas-sony-com:service:IRCC:1"><IRCCCode>${irCode}</IRCCCode></u:X_SendIRCC></s:Body></s:Envelope>`;
800
+ await this.axiosInstanceSoap.post('', data);
801
+ }
802
+ }
803
+ /**
804
+ * Press Arrow Up to select the menu items
805
+ * @returns
806
+ */
807
+ async setUp() {
808
+ const irccCode = 'AAAAAgAAALAAAAB4AQ==';
809
+ return this.sendIRCC(irccCode);
810
+ }
811
+ /**
812
+ * Press Arrow Down to select the menu items
813
+ * @returns
814
+ */
815
+ async setDown() {
816
+ const irccCode = 'AAAAAgAAALAAAAB5AQ==';
817
+ return this.sendIRCC(irccCode);
818
+ }
819
+ /**
820
+ * Press Arrow Right to select the menu items
821
+ * @returns
822
+ */
823
+ async setRigth() {
824
+ const irccCode = 'AAAAAgAAALAAAAB7AQ==';
825
+ return this.sendIRCC(irccCode);
826
+ }
827
+ /**
828
+ * Press Arrow Left to select the menu items
829
+ * @returns
830
+ */
831
+ async setLeft() {
832
+ const irccCode = 'AAAAAgAAALAAAAB6AQ==';
833
+ return this.sendIRCC(irccCode);
834
+ }
835
+ /**
836
+ * Press Select to enter the selection
837
+ * @returns
838
+ */
839
+ async setSelect() {
840
+ const irccCode = 'AAAAAgAAADAAAAAMAQ==';
841
+ return this.sendIRCC(irccCode);
842
+ }
843
+ /**
844
+ * Press Back for returns to the previous menu or exits a menu
845
+ * @returns
846
+ */
847
+ async setBack() {
848
+ const irccCode = 'AAAAAwAAARAAAAB9AQ==';
849
+ return this.sendIRCC(irccCode);
850
+ }
851
+ /**
852
+ * Press Information for view some info
853
+ * @returns
854
+ */
855
+ async setInformation() {
856
+ const irccCode = 'AAAAAgAAADAAAABTAQ==';
857
+ return this.sendIRCC(irccCode);
858
+ }
859
+ }
860
+ exports.SonyDevice = SonyDevice;
861
+ /** The device is creating. */
862
+ SonyDevice.CREATING = 0;
863
+ /** The device is ready to communicate. */
864
+ SonyDevice.READY = 1;
865
+ /** The device is in the process of closing. */
866
+ SonyDevice.CLOSING = 2;
867
+ //# sourceMappingURL=sonyDevice.js.map