@tplnet/homebridge-denon-heos-audio 1.1.0 → 1.2.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.
- package/CHANGELOG.md +35 -0
- package/README.md +20 -4
- package/config.schema.json +1 -0
- package/dist/denonClient.d.ts +1 -1
- package/dist/denonClientHeosCli.d.ts +12 -1
- package/dist/denonClientHeosCli.js +64 -9
- package/dist/denonProtocol.js +4 -10
- package/dist/platform.d.ts +5 -0
- package/dist/platform.js +34 -13
- package/dist/platformAccessory.d.ts +1 -0
- package/dist/platformAccessory.js +13 -1
- package/dist/protocolCheck.d.ts +20 -0
- package/dist/protocolCheck.js +46 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes of this fork. The fork is based on [hov3rcraft/homebridge-denon-heos-audio](https://github.com/hov3rcraft/homebridge-denon-heos-audio) 1.0.0-beta.1.
|
|
4
4
|
|
|
5
|
+
## 1.2.1
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- When switching on took a few seconds (e.g. while the power on favorite was being started), the tile briefly showed "off" before it switched to "on". The requested state is now reported from the moment of the tap, and intermediate states of the device are ignored until switching has finished.
|
|
10
|
+
- The player list was requested twice per device at startup.
|
|
11
|
+
|
|
12
|
+
## 1.2.0
|
|
13
|
+
|
|
14
|
+
### New
|
|
15
|
+
|
|
16
|
+
- **Control protocol check at startup:** the plugin checks which protocols a device supports. If the configured protocol isn't supported (e.g. *Hybrid* or *AVR Control* on a Denon Home speaker), it uses a supported one and logs a warning with what to change, instead of the device showing "No Response". A device configured with *HEOS CLI* that also supports AVR Control gets a hint that *Hybrid* is possible. Devices that are switched off at startup keep their configuration.
|
|
17
|
+
- The plugin settings explain which devices support which protocol.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The protocol support of a device is checked on both ports in parallel (faster startup if a device is offline).
|
|
22
|
+
|
|
23
|
+
## 1.1.2
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Switching on (HEOS CLI) also starts the HEOS favorite when the speaker reports "paused" but has nothing to play: if the current media is empty, or if the speaker accepts the play command but playback doesn't start within 3 seconds. Seen on a Denon Home Amp after a streaming session.
|
|
28
|
+
|
|
29
|
+
## 1.1.1
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Switching on a speaker that is already playing no longer sends an unnecessary play command.
|
|
34
|
+
- Starting the power on favorite is shown in the normal log (not only in the debug log), e.g. `Nothing to resume, starting HEOS favorite 1.`
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- README: a HEOS favorite is required for switching on without a source; fixed ports and the *Ciao* mDNS advertiser help if devices appear offline in the Homebridge UI after restarts.
|
|
39
|
+
|
|
5
40
|
## 1.1.0
|
|
6
41
|
|
|
7
42
|
### New
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Each device appears in the Home app as a TV accessory, so you can:
|
|
|
10
10
|
- play, pause and skip tracks with the Remote in Control Center
|
|
11
11
|
|
|
12
12
|
[](https://www.npmjs.com/package/@tplnet/homebridge-denon-heos-audio)
|
|
13
|
-
[](https://github.com/tplnet/homebridge-denon-heos-audio/actions/workflows/ci.yml)
|
|
13
|
+
[](https://github.com/tplnet/homebridge-denon-heos-audio/actions/workflows/ci.yml)
|
|
14
14
|
|
|
15
15
|
> This is a fork of [hov3rcraft/homebridge-denon-heos-audio](https://github.com/hov3rcraft/homebridge-denon-heos-audio) with stability fixes
|
|
16
16
|
> (no more Homebridge crashes on unexpected device messages), automatic setup of discovered devices, inputs read from the device,
|
|
@@ -56,9 +56,15 @@ Not sure what your device supports? Turn on [device discovery](#1-find-your-devi
|
|
|
56
56
|
| **Hybrid** | AV receivers and amplifiers with HEOS (e.g. Marantz Cinema series, Denon AVR-X, Denon Home Amp) | Real power on / standby | ✅ | ✅ |
|
|
57
57
|
|
|
58
58
|
**Why does "power" mean "playback" with HEOS CLI?** HEOS speakers have no real standby command. The plugin therefore treats "playing" as on and "stopped/paused" as off.
|
|
59
|
-
Switching on resumes the last source. If there is nothing to resume (e.g. after the speaker was restarted), the first HEOS favorite is started, so switching on also works in scenes and automations. You can choose another favorite or turn this off with `powerOnFavorite`.
|
|
59
|
+
Switching on resumes the last source. If there is nothing to resume (e.g. after the speaker was restarted or a streaming session ended), or playback doesn't start within 3 seconds, the first HEOS favorite is started, so switching on also works in scenes and automations. You can choose another favorite or turn this off with `powerOnFavorite`.
|
|
60
60
|
|
|
61
|
-
**
|
|
61
|
+
> **Requirement:** at least one favorite in the HEOS app (*Music → Favorites*, e.g. a radio station). Without a favorite, a speaker that has nothing to resume can't be switched on from HomeKit.
|
|
62
|
+
|
|
63
|
+
**Hybrid** uses AVR Control for power and HEOS CLI for everything else. Pick it whenever discovery reports both protocols and you want real standby.
|
|
64
|
+
|
|
65
|
+
**Wrong protocol?** At startup the plugin checks which protocols the device supports. If the configured one isn't supported (e.g. *Hybrid* on a Denon Home 150), it uses a supported one instead and logs a warning:
|
|
66
|
+
`"Office Speaker" doesn't support AVR Control (port 23 closed), so "Hybrid" can't be used. Using "HEOS CLI" instead – please change "Control Protocol" in the plugin settings.`
|
|
67
|
+
If a device configured with *HEOS CLI* also supports AVR Control, the log mentions that *Hybrid* is possible. If the device is switched off at startup, the configuration is used unchanged.
|
|
62
68
|
|
|
63
69
|
## Installation
|
|
64
70
|
|
|
@@ -226,7 +232,7 @@ Repeat these steps for every device. You only have to do it once per device. Upd
|
|
|
226
232
|
| `devices[].volumeLimitEnabled` | no | `false` | Enables the volume limit. |
|
|
227
233
|
| `devices[].volumeLimit` | no | `99` | 0–99. With a limit, 100 % in the Home app equals this value on the device. |
|
|
228
234
|
| `devices[].volumeStepSize` | no | `2` | 1–10. Volume change per press of the volume buttons in the Remote. |
|
|
229
|
-
| `devices[].powerOnFavorite` | no | `1` | HEOS CLI only: HEOS favorite that is started when the speaker is switched on but has nothing to resume (1 = first favorite in the HEOS app, 0 = disabled). Also used for automatically added devices. |
|
|
235
|
+
| `devices[].powerOnFavorite` | no | `1` | HEOS CLI only: HEOS favorite that is started when the speaker is switched on but has nothing to resume (1 = first favorite in the HEOS app, 0 = disabled). The favorite must exist in the HEOS app. Also used for automatically added devices. |
|
|
230
236
|
| `devices[].inputs` | no | `[]` | Inputs to show in the Home app, see [Inputs](#inputs). |
|
|
231
237
|
| `devices[].inputs[].inputID` | yes | | Input ID (depends on the protocol, see below). |
|
|
232
238
|
| `devices[].inputs[].name` | yes | | Name shown in the Home app. |
|
|
@@ -297,6 +303,16 @@ Enable *Network Control* (sometimes called *IP Control*) in the receiver's netwo
|
|
|
297
303
|
**The device can't be added in the Home app.**
|
|
298
304
|
See [The device does not appear in step 4?](#adding-a-device-to-apple-home-step-by-step)
|
|
299
305
|
|
|
306
|
+
**Devices appear offline in the Homebridge UI after a restart ("fetch failed"), or commands from the UI don't arrive.**
|
|
307
|
+
Every device runs on its own port. Without a port range, the ports change at every restart and the Homebridge UI may still use the old ones, especially if mDNS doesn't work reliably (e.g. on a Synology NAS: `Failed to create listener for avahi-daemon`). In the Homebridge UI under **Settings → Network**:
|
|
308
|
+
1. set a **port range** for accessories, e.g. `52100`–`52150` (or `"ports": { "start": 52100, "end": 52150 }` in `config.json`), outside of the ports of your bridges,
|
|
309
|
+
2. set the **mDNS Advertiser** to **Ciao**,
|
|
310
|
+
|
|
311
|
+
then restart Homebridge **once** and reload the page.
|
|
312
|
+
|
|
313
|
+
**Homebridge doesn't start: `EADDRINUSE: address already in use`.**
|
|
314
|
+
Usually an old Homebridge process is still running (e.g. after several restarts in a row): `ps aux | grep -i [h]omebridge` shows two main `homebridge` processes. Stop Homebridge, end the old process (`kill <pid>`) and start Homebridge again.
|
|
315
|
+
|
|
300
316
|
**A device is added automatically although I don't want it in HomeKit.**
|
|
301
317
|
Add its serial number to `excludedSerialNumbers` and restart Homebridge. Then remove it from the Home app.
|
|
302
318
|
|
package/config.schema.json
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"controlProtocol": {
|
|
63
63
|
"title": "Control Protocol",
|
|
64
|
+
"description": "HEOS CLI: speakers like the Denon Home series (on/off = play/stop). Hybrid: devices that also support AVR Control, e.g. AV receivers and the Denon Home Amp (real standby). AVR Control: receivers without HEOS. Device discovery lists the supported protocols in the log; an unsupported protocol is replaced automatically at startup (with a warning).",
|
|
64
65
|
"type": "string",
|
|
65
66
|
"oneOf": [
|
|
66
67
|
{
|
package/dist/denonClient.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export interface IDenonClient {
|
|
|
59
59
|
* HEOS favorite that is started when switching on a HEOS CLI device that has nothing to resume (0 = disabled).
|
|
60
60
|
* Not used by protocols with a real power state.
|
|
61
61
|
*/
|
|
62
|
-
setPowerOnFavorite?(preset: number): void;
|
|
62
|
+
setPowerOnFavorite?(preset: number, infoLog?: (message: string) => void): void;
|
|
63
63
|
}
|
|
64
64
|
export declare abstract class DenonClient implements IDenonClient {
|
|
65
65
|
readonly serialNumber: string;
|
|
@@ -198,9 +198,16 @@ export declare class DenonClientHeosCli extends DenonClient {
|
|
|
198
198
|
private player_id;
|
|
199
199
|
/** HEOS favorite (1, 2, …) that is started when switching on without something to resume; 0 = disabled. */
|
|
200
200
|
private powerOnFavorite;
|
|
201
|
+
private static readonly PLAY_VERIFY_CHECKS;
|
|
202
|
+
private playVerifyIntervalMs;
|
|
203
|
+
/** Log for actions the user should see without debug logging (e.g. starting the power on favorite). */
|
|
204
|
+
private infoLog;
|
|
201
205
|
private player_name;
|
|
202
206
|
constructor(serialNumber: string, name: string, host: string, connect_timeout: number, response_timeout: number, debugLogCallback?: (message: string, ...parameters: any[]) => void, powerUpdateCallback?: (power: boolean) => void, muteUpdateCallback?: (mute: boolean) => void, volumeUpdateCallback?: (volume: number) => void, inputUpdateCallback?: (input: string) => void);
|
|
207
|
+
private playerIdLookup;
|
|
208
|
+
/** Looks up the player id; concurrent calls share one request. */
|
|
203
209
|
private findPlayerId;
|
|
210
|
+
private findPlayerIdUncached;
|
|
204
211
|
protected subscribeToChangeEvents(): Promise<void>;
|
|
205
212
|
protected sendCommand(command: ProtocolCommand, commandMode: CommandMode, { value, passPayload }: {
|
|
206
213
|
value?: string;
|
|
@@ -214,13 +221,17 @@ export declare class DenonClientHeosCli extends DenonClient {
|
|
|
214
221
|
setPlayNext(): Promise<void>;
|
|
215
222
|
setPlayPrevious(): Promise<void>;
|
|
216
223
|
getPower(raceStatus?: RaceStatus): Promise<boolean>;
|
|
217
|
-
setPowerOnFavorite(preset: number): void;
|
|
224
|
+
setPowerOnFavorite(preset: number, infoLog?: (message: string) => void): void;
|
|
218
225
|
/**
|
|
219
226
|
* Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
|
|
220
227
|
* since the speaker was started) or the device refuses to play, the configured HEOS favorite is started instead.
|
|
221
228
|
*/
|
|
222
229
|
setPower(power: boolean): Promise<boolean>;
|
|
223
230
|
private powerOn;
|
|
231
|
+
/**
|
|
232
|
+
* True if the player has no current media to resume (e.g. an empty entry after a streaming session ended).
|
|
233
|
+
*/
|
|
234
|
+
private hasNoMedia;
|
|
224
235
|
private playFavorite;
|
|
225
236
|
getMute(raceStatus?: RaceStatus): Promise<boolean>;
|
|
226
237
|
setMute(mute: boolean): Promise<boolean>;
|
|
@@ -248,6 +248,10 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
248
248
|
player_id;
|
|
249
249
|
/** HEOS favorite (1, 2, …) that is started when switching on without something to resume; 0 = disabled. */
|
|
250
250
|
powerOnFavorite = 1;
|
|
251
|
+
static PLAY_VERIFY_CHECKS = 3;
|
|
252
|
+
playVerifyIntervalMs = 1000;
|
|
253
|
+
/** Log for actions the user should see without debug logging (e.g. starting the power on favorite). */
|
|
254
|
+
infoLog = (message) => this.debugLog(message);
|
|
251
255
|
player_name;
|
|
252
256
|
constructor(serialNumber, name, host, connect_timeout, response_timeout, debugLogCallback, powerUpdateCallback, muteUpdateCallback, volumeUpdateCallback, inputUpdateCallback) {
|
|
253
257
|
super(serialNumber, name, {
|
|
@@ -271,7 +275,15 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
271
275
|
}
|
|
272
276
|
});
|
|
273
277
|
}
|
|
274
|
-
|
|
278
|
+
playerIdLookup;
|
|
279
|
+
/** Looks up the player id; concurrent calls share one request. */
|
|
280
|
+
findPlayerId() {
|
|
281
|
+
this.playerIdLookup ??= this.findPlayerIdUncached().finally(() => {
|
|
282
|
+
this.playerIdLookup = undefined;
|
|
283
|
+
});
|
|
284
|
+
return this.playerIdLookup;
|
|
285
|
+
}
|
|
286
|
+
async findPlayerIdUncached() {
|
|
275
287
|
const payload_str = await super.sendCommand(DenonClientHeosCli.PROTOCOL.PLAYERS, CommandMode.GET, { passPayload: true });
|
|
276
288
|
const payload = JSON.parse(payload_str);
|
|
277
289
|
if (!Array.isArray(payload)) {
|
|
@@ -433,8 +445,11 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
433
445
|
}
|
|
434
446
|
return isPlaying[playing];
|
|
435
447
|
}
|
|
436
|
-
setPowerOnFavorite(preset) {
|
|
448
|
+
setPowerOnFavorite(preset, infoLog) {
|
|
437
449
|
this.powerOnFavorite = preset;
|
|
450
|
+
if (infoLog) {
|
|
451
|
+
this.infoLog = infoLog;
|
|
452
|
+
}
|
|
438
453
|
}
|
|
439
454
|
/**
|
|
440
455
|
* Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
|
|
@@ -452,20 +467,60 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
452
467
|
return playing;
|
|
453
468
|
}
|
|
454
469
|
async powerOn() {
|
|
470
|
+
const PLAY = DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES.PLAY.VALUE;
|
|
455
471
|
const rawState = await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_STATE, CommandMode.GET, {});
|
|
456
|
-
if (rawState ===
|
|
457
|
-
this.debugLog(
|
|
458
|
-
return
|
|
472
|
+
if (rawState === PLAY) {
|
|
473
|
+
this.debugLog("Already playing, nothing to do.");
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const favorite = this.powerOnFavorite;
|
|
477
|
+
if (favorite > 0) {
|
|
478
|
+
if (rawState === DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES.UNKNOWN.VALUE) {
|
|
479
|
+
this.infoLog(`Nothing to resume, starting HEOS favorite ${favorite}.`);
|
|
480
|
+
return this.playFavorite(favorite);
|
|
481
|
+
}
|
|
482
|
+
if (await this.hasNoMedia()) {
|
|
483
|
+
this.infoLog(`Nothing to resume (no current media), starting HEOS favorite ${favorite}.`);
|
|
484
|
+
return this.playFavorite(favorite);
|
|
485
|
+
}
|
|
459
486
|
}
|
|
460
487
|
try {
|
|
461
|
-
|
|
488
|
+
await this.setPlaying(Playing.PLAY);
|
|
462
489
|
}
|
|
463
490
|
catch (error) {
|
|
464
|
-
if (!(error instanceof CommandFailedException) ||
|
|
491
|
+
if (!(error instanceof CommandFailedException) || favorite <= 0) {
|
|
465
492
|
throw error;
|
|
466
493
|
}
|
|
467
|
-
this.
|
|
468
|
-
return this.playFavorite(
|
|
494
|
+
this.infoLog(`The speaker refused to resume playback, starting HEOS favorite ${favorite}.`);
|
|
495
|
+
return this.playFavorite(favorite);
|
|
496
|
+
}
|
|
497
|
+
if (favorite <= 0) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
// Some devices accept "play" without having anything to play (they stay paused), so check that playback started.
|
|
501
|
+
for (let check = 0; check < DenonClientHeosCli.PLAY_VERIFY_CHECKS; check++) {
|
|
502
|
+
await new Promise((resolve) => setTimeout(resolve, this.playVerifyIntervalMs));
|
|
503
|
+
if ((await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_STATE, CommandMode.GET, {})) === PLAY) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.infoLog(`Playback did not start, starting HEOS favorite ${favorite}.`);
|
|
508
|
+
return this.playFavorite(favorite);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* True if the player has no current media to resume (e.g. an empty entry after a streaming session ended).
|
|
512
|
+
*/
|
|
513
|
+
async hasNoMedia() {
|
|
514
|
+
try {
|
|
515
|
+
const payload = JSON.parse(await this.sendCommand(DenonClientHeosCli.PROTOCOL.INPUT, CommandMode.GET, { passPayload: true }));
|
|
516
|
+
if (typeof payload?.mid === "string" && payload.mid.startsWith("inputs/")) {
|
|
517
|
+
return false; // a physical input can always be resumed
|
|
518
|
+
}
|
|
519
|
+
return ["song", "station", "artist", "album"].every((field) => !payload?.[field]);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.debugLog("Could not read the current media:", error instanceof Error ? error.message : error);
|
|
523
|
+
return false;
|
|
469
524
|
}
|
|
470
525
|
}
|
|
471
526
|
async playFavorite(preset) {
|
package/dist/denonProtocol.js
CHANGED
|
@@ -30,15 +30,9 @@ function checkProtocolSupportAtPort(host, port, debugLogCallback) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
export async function checkProtocolSupport(host, debugLogCallback) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (supported) {
|
|
38
|
-
supportedProtocols.push(controlMode);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return supportedProtocols;
|
|
33
|
+
const modes = Object.values(ControlMode).filter((value) => typeof value === "number" && value >= 0);
|
|
34
|
+
// check all ports in parallel, so an unreachable device delays the startup only once
|
|
35
|
+
const results = await Promise.all(modes.map((mode) => checkProtocolSupportAtPort(host, mode, debugLogCallback)));
|
|
36
|
+
return modes.filter((_mode, index) => results[index]);
|
|
43
37
|
}
|
|
44
38
|
//# sourceMappingURL=denonProtocol.js.map
|
package/dist/platform.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare class DenonAudioPlatform implements DynamicPlatformPlugin {
|
|
|
30
30
|
* (HomeKit only allows one television accessory per bridge).
|
|
31
31
|
*/
|
|
32
32
|
private publishNewAccessory;
|
|
33
|
+
/**
|
|
34
|
+
* Checks the configured control protocol against the protocols the device supports and falls back to a supported
|
|
35
|
+
* one if necessary (with a warning). If the device can't be reached, the configuration is used unchanged.
|
|
36
|
+
*/
|
|
37
|
+
private withCheckedProtocol;
|
|
33
38
|
/**
|
|
34
39
|
* Adds a discovered device as new accessory, unless it is configured manually, excluded or already set up.
|
|
35
40
|
*/
|
package/dist/platform.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as CustomLogging from "./customLogging.js";
|
|
|
3
3
|
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
|
|
4
4
|
import * as DenonProtocol from "./denonProtocol.js";
|
|
5
5
|
import { buildDiscoveredDeviceConfig } from "./discovery.js";
|
|
6
|
+
import { checkControlProtocol } from "./protocolCheck.js";
|
|
6
7
|
import { HeosDiscovery } from "./heosDiscovery.js";
|
|
7
8
|
const MINUTE = 60 * 1000;
|
|
8
9
|
export class DenonAudioPlatform {
|
|
@@ -77,9 +78,9 @@ export class DenonAudioPlatform {
|
|
|
77
78
|
// the accessory already exists
|
|
78
79
|
this.log.info("Restoring existing accessory from cache:", existingAccessory.displayName);
|
|
79
80
|
// create the accessory handler for the restored accessory
|
|
80
|
-
this.
|
|
81
|
-
?.setupInputs()
|
|
82
|
-
.catch((error) => this.log.error(`Could not set up
|
|
81
|
+
this.withCheckedProtocol(deviceConfig)
|
|
82
|
+
.then((checkedConfig) => this.createAccessoryHandler(existingAccessory, checkedConfig)?.setupInputs())
|
|
83
|
+
.catch((error) => this.log.error(`Could not set up '${deviceConfig.name}':`, error));
|
|
83
84
|
// after it has been restored, the existing accessory should be removed form the uuid list
|
|
84
85
|
existingAccessoriesUuids = existingAccessoriesUuids.filter((uuid) => uuid !== existingAccessory.UUID);
|
|
85
86
|
}
|
|
@@ -116,18 +117,38 @@ export class DenonAudioPlatform {
|
|
|
116
117
|
* Sets up a new accessory and publishes it as external accessory
|
|
117
118
|
* (HomeKit only allows one television accessory per bridge).
|
|
118
119
|
*/
|
|
119
|
-
publishNewAccessory(deviceConfig, accessory) {
|
|
120
|
+
publishNewAccessory(deviceConfig, accessory, supportedModes) {
|
|
120
121
|
// store a copy of the device object in the `accessory.context`
|
|
121
122
|
accessory.context.device = deviceConfig;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
this.withCheckedProtocol(deviceConfig, supportedModes)
|
|
124
|
+
.then(async (checkedConfig) => {
|
|
125
|
+
const handler = this.createAccessoryHandler(accessory, checkedConfig);
|
|
126
|
+
if (!handler) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// the inputs are read from the device first, as the services of a published accessory should not change anymore
|
|
130
|
+
await handler.setupInputs().catch((error) => this.log.error(`Could not set up the inputs of '${deviceConfig.name}':`, error));
|
|
131
|
+
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
|
|
132
|
+
})
|
|
133
|
+
.catch((error) => this.log.error(`Could not set up '${deviceConfig.name}':`, error));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Checks the configured control protocol against the protocols the device supports and falls back to a supported
|
|
137
|
+
* one if necessary (with a warning). If the device can't be reached, the configuration is used unchanged.
|
|
138
|
+
*/
|
|
139
|
+
async withCheckedProtocol(deviceConfig, knownSupportedModes) {
|
|
140
|
+
const supportedModes = knownSupportedModes ?? (await DenonProtocol.checkProtocolSupport(deviceConfig.ip, this.log.debug.bind(this.log)));
|
|
141
|
+
const result = checkControlProtocol(deviceConfig.name, deviceConfig.controlProtocol, supportedModes);
|
|
142
|
+
if (result.warning) {
|
|
143
|
+
this.log.warn(result.warning);
|
|
144
|
+
}
|
|
145
|
+
if (result.hint) {
|
|
146
|
+
this.log.info(result.hint);
|
|
147
|
+
}
|
|
148
|
+
if (supportedModes.length === 0) {
|
|
149
|
+
this.log.debug(`Could not check the protocols of '${deviceConfig.name}' (${deviceConfig.ip} not reachable). Using the configuration.`);
|
|
125
150
|
}
|
|
126
|
-
|
|
127
|
-
handler
|
|
128
|
-
.setupInputs()
|
|
129
|
-
.catch((error) => this.log.error(`Could not set up the inputs of '${deviceConfig.name}':`, error))
|
|
130
|
-
.finally(() => this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]));
|
|
151
|
+
return result.protocol === deviceConfig.controlProtocol ? deviceConfig : { ...deviceConfig, controlProtocol: result.protocol };
|
|
131
152
|
}
|
|
132
153
|
/**
|
|
133
154
|
* Adds a discovered device as new accessory, unless it is configured manually, excluded or already set up.
|
|
@@ -147,7 +168,7 @@ export class DenonAudioPlatform {
|
|
|
147
168
|
}
|
|
148
169
|
this.activeSerialNumbers.add(serialNumber);
|
|
149
170
|
this.log.info(`Automatically adding discovered device '${deviceConfig.name}' (${deviceConfig.controlProtocol}, ${ip}).`);
|
|
150
|
-
this.publishNewAccessory(deviceConfig, new this.api.platformAccessory(deviceConfig.name, this.api.hap.uuid.generate(serialNumber)));
|
|
171
|
+
this.publishNewAccessory(deviceConfig, new this.api.platformAccessory(deviceConfig.name, this.api.hap.uuid.generate(serialNumber)), supportedModes);
|
|
151
172
|
}
|
|
152
173
|
/**
|
|
153
174
|
* Creates the accessory handler. Configuration errors are logged instead of being thrown,
|
|
@@ -38,6 +38,7 @@ export declare class DenonAudioAccessory {
|
|
|
38
38
|
private targetMediaState;
|
|
39
39
|
private volumeChangeOngoing;
|
|
40
40
|
private pendingActive;
|
|
41
|
+
private powerChangeInProgress;
|
|
41
42
|
private playingChangeOngoing;
|
|
42
43
|
constructor(platform: DenonAudioPlatform, accessory: PlatformAccessory, config: DeviceConfig, log: Logger);
|
|
43
44
|
/**
|
|
@@ -44,6 +44,7 @@ export class DenonAudioAccessory {
|
|
|
44
44
|
targetMediaState;
|
|
45
45
|
volumeChangeOngoing = false;
|
|
46
46
|
pendingActive;
|
|
47
|
+
powerChangeInProgress = false;
|
|
47
48
|
playingChangeOngoing = false;
|
|
48
49
|
constructor(platform, accessory, config, log) {
|
|
49
50
|
log.debug("Initializing DenonAudioAccessory...");
|
|
@@ -111,7 +112,7 @@ export class DenonAudioAccessory {
|
|
|
111
112
|
if (!Number.isInteger(powerOnFavorite) || powerOnFavorite < 0) {
|
|
112
113
|
throw new Error("Power on favorite must be 0 (disabled) or the number of a HEOS favorite (1, 2, …)");
|
|
113
114
|
}
|
|
114
|
-
this.denonClient.setPowerOnFavorite?.(powerOnFavorite);
|
|
115
|
+
this.denonClient.setPowerOnFavorite?.(powerOnFavorite, (message) => this.log.info(message));
|
|
115
116
|
this.configuredInputs = config.inputs ?? [];
|
|
116
117
|
this.unknownInput = new ConfiguredInput("unknown", "Unknown Input", false, 0, false);
|
|
117
118
|
this.log.info("Finished initializing accessory.");
|
|
@@ -312,6 +313,10 @@ export class DenonAudioAccessory {
|
|
|
312
313
|
async setActive(newValue) {
|
|
313
314
|
this.log.debug(`setActive for ${this.name} set to ${newValue}`);
|
|
314
315
|
const active = newValue === this.platform.Characteristic.Active.ACTIVE;
|
|
316
|
+
// Report the requested state right away: switching on can take a few seconds (e.g. when the power on favorite is
|
|
317
|
+
// started), and the device reports intermediate states during that time.
|
|
318
|
+
this.pendingActive = { value: active, until: Number.POSITIVE_INFINITY };
|
|
319
|
+
this.powerChangeInProgress = true;
|
|
315
320
|
try {
|
|
316
321
|
await this.denonClient.setPower(active);
|
|
317
322
|
this.pendingActive = { value: active, until: Date.now() + DenonAudioAccessory.POWER_STATE_SETTLE_TIME };
|
|
@@ -321,6 +326,9 @@ export class DenonAudioAccessory {
|
|
|
321
326
|
this.log.error(`An error occured while setting power status for ${this.name}.`, error);
|
|
322
327
|
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
323
328
|
}
|
|
329
|
+
finally {
|
|
330
|
+
this.powerChangeInProgress = false;
|
|
331
|
+
}
|
|
324
332
|
}
|
|
325
333
|
async setActiveIdentifier(newValue) {
|
|
326
334
|
this.log.debug(`setActiveIdentifier for ${this.name} set to ${newValue}`);
|
|
@@ -542,6 +550,10 @@ export class DenonAudioAccessory {
|
|
|
542
550
|
* Handles characteristic updates from the API
|
|
543
551
|
*/
|
|
544
552
|
callbackActive(active) {
|
|
553
|
+
if (this.powerChangeInProgress && this.pendingActive && this.pendingActive.value !== active) {
|
|
554
|
+
this.log.debug(`Ignoring intermediate power state ${active} of ${this.name} while switching.`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
545
557
|
if (this.pendingActive && this.pendingActive.value !== active) {
|
|
546
558
|
// changed on the device (e.g. in the HEOS app) - the device state wins
|
|
547
559
|
this.pendingActive = undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ControlMode } from "./controlMode.js";
|
|
2
|
+
import type { DeviceConfig } from "./config.js";
|
|
3
|
+
type ProtocolName = DeviceConfig["controlProtocol"];
|
|
4
|
+
export interface ProtocolCheckResult {
|
|
5
|
+
/** The protocol to use. */
|
|
6
|
+
protocol: ProtocolName;
|
|
7
|
+
/** Warning for the log if the configured protocol can't be used. */
|
|
8
|
+
warning?: string;
|
|
9
|
+
/** Hint for the log (e.g. a better protocol is available). */
|
|
10
|
+
hint?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Checks the configured control protocol against the protocols the device supports.
|
|
14
|
+
*
|
|
15
|
+
* - The device supports everything the protocol needs: keep it (with a hint if Hybrid would be possible instead of HEOS CLI).
|
|
16
|
+
* - The device doesn't support it, but another one: use the best supported protocol and return a warning.
|
|
17
|
+
* - The device can't be reached (nothing supported): keep the configuration, the device may just be switched off.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkControlProtocol(deviceName: string, configured: ProtocolName, supportedModes: ControlMode[]): ProtocolCheckResult;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ControlMode, CONTROL_MODE_BY_NAME } from "./controlMode.js";
|
|
2
|
+
import { chooseControlProtocol } from "./discovery.js";
|
|
3
|
+
/** Ports (= control modes) a protocol needs. */
|
|
4
|
+
const REQUIRED_MODES = {
|
|
5
|
+
AVRCONTROL: [ControlMode.AVRCONTROL],
|
|
6
|
+
HEOSCLI: [ControlMode.HEOSCLI],
|
|
7
|
+
HYBRID: [ControlMode.AVRCONTROL, ControlMode.HEOSCLI],
|
|
8
|
+
};
|
|
9
|
+
const PROTOCOL_LABEL = {
|
|
10
|
+
AVRCONTROL: "AVR Control",
|
|
11
|
+
HEOSCLI: "HEOS CLI",
|
|
12
|
+
HYBRID: "Hybrid",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Checks the configured control protocol against the protocols the device supports.
|
|
16
|
+
*
|
|
17
|
+
* - The device supports everything the protocol needs: keep it (with a hint if Hybrid would be possible instead of HEOS CLI).
|
|
18
|
+
* - The device doesn't support it, but another one: use the best supported protocol and return a warning.
|
|
19
|
+
* - The device can't be reached (nothing supported): keep the configuration, the device may just be switched off.
|
|
20
|
+
*/
|
|
21
|
+
export function checkControlProtocol(deviceName, configured, supportedModes) {
|
|
22
|
+
if (!(configured in CONTROL_MODE_BY_NAME) || supportedModes.length === 0) {
|
|
23
|
+
return { protocol: configured };
|
|
24
|
+
}
|
|
25
|
+
const missing = REQUIRED_MODES[configured].filter((mode) => !supportedModes.includes(mode));
|
|
26
|
+
if (missing.length === 0) {
|
|
27
|
+
if (configured === "HEOSCLI" && supportedModes.includes(ControlMode.AVRCONTROL)) {
|
|
28
|
+
return {
|
|
29
|
+
protocol: configured,
|
|
30
|
+
hint: `"${deviceName}" also supports AVR Control. "Hybrid" would switch it to real standby instead of only stopping the playback.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { protocol: configured };
|
|
34
|
+
}
|
|
35
|
+
const fallback = chooseControlProtocol(supportedModes);
|
|
36
|
+
if (!fallback) {
|
|
37
|
+
return { protocol: configured };
|
|
38
|
+
}
|
|
39
|
+
const missingText = missing.map((mode) => `${ControlMode[mode] === "AVRCONTROL" ? "AVR Control" : "HEOS CLI"} (port ${mode} closed)`).join(", ");
|
|
40
|
+
return {
|
|
41
|
+
protocol: fallback,
|
|
42
|
+
warning: `"${deviceName}" doesn't support ${missingText}, so "${PROTOCOL_LABEL[configured]}" can't be used. ` +
|
|
43
|
+
`Using "${PROTOCOL_LABEL[fallback]}" instead – please change "Control Protocol" in the plugin settings.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=protocolCheck.js.map
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tplnet/homebridge-denon-heos-audio",
|
|
3
3
|
"displayName": "Homebridge Denon Heos Audio (tplnet)",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.1
|
|
5
|
+
"version": "1.2.1",
|
|
6
6
|
"description": "Control your Denon and Marantz HEOS speakers, amplifiers and AVRs via AVR Control and/or HEOS CLI",
|
|
7
7
|
"author": "tplnet",
|
|
8
8
|
"contributors": [
|