@tplnet/homebridge-denon-heos-audio 1.1.0 → 1.2.0

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,34 @@
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.0
6
+
7
+ ### New
8
+
9
+ - **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.
10
+ - The plugin settings explain which devices support which protocol.
11
+
12
+ ### Changed
13
+
14
+ - The protocol support of a device is checked on both ports in parallel (faster startup if a device is offline).
15
+
16
+ ## 1.1.2
17
+
18
+ ### Fixed
19
+
20
+ - 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.
21
+
22
+ ## 1.1.1
23
+
24
+ ### Changed
25
+
26
+ - Switching on a speaker that is already playing no longer sends an unnecessary play command.
27
+ - 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.`
28
+
29
+ ### Documentation
30
+
31
+ - 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.
32
+
5
33
  ## 1.1.0
6
34
 
7
35
  ### 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,6 +198,10 @@ 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);
203
207
  private findPlayerId;
@@ -214,13 +218,17 @@ export declare class DenonClientHeosCli extends DenonClient {
214
218
  setPlayNext(): Promise<void>;
215
219
  setPlayPrevious(): Promise<void>;
216
220
  getPower(raceStatus?: RaceStatus): Promise<boolean>;
217
- setPowerOnFavorite(preset: number): void;
221
+ setPowerOnFavorite(preset: number, infoLog?: (message: string) => void): void;
218
222
  /**
219
223
  * Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
220
224
  * since the speaker was started) or the device refuses to play, the configured HEOS favorite is started instead.
221
225
  */
222
226
  setPower(power: boolean): Promise<boolean>;
223
227
  private powerOn;
228
+ /**
229
+ * True if the player has no current media to resume (e.g. an empty entry after a streaming session ended).
230
+ */
231
+ private hasNoMedia;
224
232
  private playFavorite;
225
233
  getMute(raceStatus?: RaceStatus): Promise<boolean>;
226
234
  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, {
@@ -433,8 +437,11 @@ export class DenonClientHeosCli extends DenonClient {
433
437
  }
434
438
  return isPlaying[playing];
435
439
  }
436
- setPowerOnFavorite(preset) {
440
+ setPowerOnFavorite(preset, infoLog) {
437
441
  this.powerOnFavorite = preset;
442
+ if (infoLog) {
443
+ this.infoLog = infoLog;
444
+ }
438
445
  }
439
446
  /**
440
447
  * Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
@@ -452,20 +459,60 @@ export class DenonClientHeosCli extends DenonClient {
452
459
  return playing;
453
460
  }
454
461
  async powerOn() {
462
+ const PLAY = DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES.PLAY.VALUE;
455
463
  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);
464
+ if (rawState === PLAY) {
465
+ this.debugLog("Already playing, nothing to do.");
466
+ return true;
467
+ }
468
+ const favorite = this.powerOnFavorite;
469
+ if (favorite > 0) {
470
+ if (rawState === DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES.UNKNOWN.VALUE) {
471
+ this.infoLog(`Nothing to resume, starting HEOS favorite ${favorite}.`);
472
+ return this.playFavorite(favorite);
473
+ }
474
+ if (await this.hasNoMedia()) {
475
+ this.infoLog(`Nothing to resume (no current media), starting HEOS favorite ${favorite}.`);
476
+ return this.playFavorite(favorite);
477
+ }
459
478
  }
460
479
  try {
461
- return isPlaying[await this.setPlaying(Playing.PLAY)];
480
+ await this.setPlaying(Playing.PLAY);
462
481
  }
463
482
  catch (error) {
464
- if (!(error instanceof CommandFailedException) || this.powerOnFavorite <= 0) {
483
+ if (!(error instanceof CommandFailedException) || favorite <= 0) {
465
484
  throw error;
466
485
  }
467
- this.debugLog(`The device refused to resume playback, starting HEOS favorite ${this.powerOnFavorite}.`);
468
- return this.playFavorite(this.powerOnFavorite);
486
+ this.infoLog(`The speaker refused to resume playback, starting HEOS favorite ${favorite}.`);
487
+ return this.playFavorite(favorite);
488
+ }
489
+ if (favorite <= 0) {
490
+ return true;
491
+ }
492
+ // Some devices accept "play" without having anything to play (they stay paused), so check that playback started.
493
+ for (let check = 0; check < DenonClientHeosCli.PLAY_VERIFY_CHECKS; check++) {
494
+ await new Promise((resolve) => setTimeout(resolve, this.playVerifyIntervalMs));
495
+ if ((await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_STATE, CommandMode.GET, {})) === PLAY) {
496
+ return true;
497
+ }
498
+ }
499
+ this.infoLog(`Playback did not start, starting HEOS favorite ${favorite}.`);
500
+ return this.playFavorite(favorite);
501
+ }
502
+ /**
503
+ * True if the player has no current media to resume (e.g. an empty entry after a streaming session ended).
504
+ */
505
+ async hasNoMedia() {
506
+ try {
507
+ const payload = JSON.parse(await this.sendCommand(DenonClientHeosCli.PROTOCOL.INPUT, CommandMode.GET, { passPayload: true }));
508
+ if (typeof payload?.mid === "string" && payload.mid.startsWith("inputs/")) {
509
+ return false; // a physical input can always be resumed
510
+ }
511
+ return ["song", "station", "artist", "album"].every((field) => !payload?.[field]);
512
+ }
513
+ catch (error) {
514
+ this.debugLog("Could not read the current media:", error instanceof Error ? error.message : error);
515
+ return false;
469
516
  }
470
517
  }
471
518
  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,
@@ -111,7 +111,7 @@ export class DenonAudioAccessory {
111
111
  if (!Number.isInteger(powerOnFavorite) || powerOnFavorite < 0) {
112
112
  throw new Error("Power on favorite must be 0 (disabled) or the number of a HEOS favorite (1, 2, …)");
113
113
  }
114
- this.denonClient.setPowerOnFavorite?.(powerOnFavorite);
114
+ this.denonClient.setPowerOnFavorite?.(powerOnFavorite, (message) => this.log.info(message));
115
115
  this.configuredInputs = config.inputs ?? [];
116
116
  this.unknownInput = new ConfiguredInput("unknown", "Unknown Input", false, 0, false);
117
117
  this.log.info("Finished initializing accessory.");
@@ -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.0",
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": [