@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 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
  [![npm](https://img.shields.io/npm/v/@tplnet/homebridge-denon-heos-audio)](https://www.npmjs.com/package/@tplnet/homebridge-denon-heos-audio)
13
- [![CI](https://github.com/tplnet/homebridge-denon-heos-audio/actions/workflows/ci.yml/badge.svg)](https://github.com/tplnet/homebridge-denon-heos-audio/actions/workflows/ci.yml)
13
+ [![CI](https://github.com/tplnet/homebridge-denon-heos-audio/actions/workflows/ci.yml/badge.svg?branch=main)](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
- **Hybrid** uses AVR Control for power and HEOS CLI for everything else. Pick it whenever discovery reports both protocols.
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
 
@@ -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
  {
@@ -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
- async findPlayerId() {
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 === DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES.UNKNOWN.VALUE && this.powerOnFavorite > 0) {
457
- this.debugLog(`Nothing to resume (play state "${rawState}"), starting HEOS favorite ${this.powerOnFavorite}.`);
458
- return this.playFavorite(this.powerOnFavorite);
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
- return isPlaying[await this.setPlaying(Playing.PLAY)];
488
+ await this.setPlaying(Playing.PLAY);
462
489
  }
463
490
  catch (error) {
464
- if (!(error instanceof CommandFailedException) || this.powerOnFavorite <= 0) {
491
+ if (!(error instanceof CommandFailedException) || favorite <= 0) {
465
492
  throw error;
466
493
  }
467
- this.debugLog(`The device refused to resume playback, starting HEOS favorite ${this.powerOnFavorite}.`);
468
- return this.playFavorite(this.powerOnFavorite);
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) {
@@ -30,15 +30,9 @@ function checkProtocolSupportAtPort(host, port, debugLogCallback) {
30
30
  });
31
31
  }
32
32
  export async function checkProtocolSupport(host, debugLogCallback) {
33
- const supportedProtocols = [];
34
- for (const controlMode of Object.values(ControlMode).filter((value) => typeof value === "number")) {
35
- if (controlMode >= 0) {
36
- const supported = await checkProtocolSupportAtPort(host, controlMode, debugLogCallback);
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
@@ -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.createAccessoryHandler(existingAccessory, deviceConfig)
81
- ?.setupInputs()
82
- .catch((error) => this.log.error(`Could not set up the inputs of '${deviceConfig.name}':`, error));
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
- const handler = this.createAccessoryHandler(accessory, deviceConfig);
123
- if (!handler) {
124
- return;
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
- // the inputs are read from the device first, as the services of a published accessory should not change anymore
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.0",
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": [