@tplnet/homebridge-denon-heos-audio 1.0.1 → 1.1.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 +10 -0
- package/README.md +8 -3
- package/config.schema.json +9 -0
- package/dist/config.d.ts +1 -0
- package/dist/denonClient.d.ts +5 -0
- package/dist/denonClientHeosCli.d.ts +17 -0
- package/dist/denonClientHeosCli.js +47 -4
- package/dist/platformAccessory.d.ts +3 -0
- package/dist/platformAccessory.js +22 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.1.0
|
|
6
|
+
|
|
7
|
+
### New
|
|
8
|
+
|
|
9
|
+
- **Switching on always plays something (HEOS CLI):** if a speaker has nothing to resume (no active source, e.g. after a restart) or refuses to resume, the first HEOS favorite is started. This makes switching on via scenes and automations reliable. Configurable per device with `powerOnFavorite` (number of the favorite, 0 = disabled).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- After switching on or off, the tile could briefly jump back to the old state, because devices report the old state for a moment. The new state is now kept for a few seconds, unless the device reports a different change (e.g. from the HEOS app).
|
|
14
|
+
|
|
5
15
|
## 1.0.1
|
|
6
16
|
|
|
7
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ 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
60
|
|
|
60
61
|
**Hybrid** uses AVR Control for power and HEOS CLI for everything else. Pick it whenever discovery reports both protocols.
|
|
61
62
|
|
|
@@ -126,7 +127,7 @@ Enable **Add Discovered Devices Automatically** in the plugin settings (or set `
|
|
|
126
127
|
- the **name** from the HEOS app (e.g. *Patio Music*),
|
|
127
128
|
- the **control protocol** `HYBRID` if the device supports both protocols, otherwise `HEOSCLI`,
|
|
128
129
|
- the inputs reported by the device,
|
|
129
|
-
- default settings (volume step 2, no volume limit).
|
|
130
|
+
- default settings (volume step 2, no volume limit, first HEOS favorite when switching on without a source).
|
|
130
131
|
|
|
131
132
|
The log confirms every device: `Automatically adding discovered device 'Patio Music' (HYBRID, 192.168.1.149).`
|
|
132
133
|
|
|
@@ -225,6 +226,7 @@ Repeat these steps for every device. You only have to do it once per device. Upd
|
|
|
225
226
|
| `devices[].volumeLimitEnabled` | no | `false` | Enables the volume limit. |
|
|
226
227
|
| `devices[].volumeLimit` | no | `99` | 0–99. With a limit, 100 % in the Home app equals this value on the device. |
|
|
227
228
|
| `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. |
|
|
228
230
|
| `devices[].inputs` | no | `[]` | Inputs to show in the Home app, see [Inputs](#inputs). |
|
|
229
231
|
| `devices[].inputs[].inputID` | yes | | Input ID (depends on the protocol, see below). |
|
|
230
232
|
| `devices[].inputs[].name` | yes | | Name shown in the Home app. |
|
|
@@ -268,7 +270,7 @@ Streaming sources (Spotify, TuneIn, AirPlay, …) are recognised and shown as th
|
|
|
268
270
|
|
|
269
271
|
| Action | What happens |
|
|
270
272
|
| --- | --- |
|
|
271
|
-
| Power button | Power on / standby (AVR Control, Hybrid) or play / stop (HEOS CLI) |
|
|
273
|
+
| Power button | Power on / standby (AVR Control, Hybrid) or play / stop (HEOS CLI). With HEOS CLI, switching on resumes the last source; if there is none, the first HEOS favorite is started (see `powerOnFavorite`). |
|
|
272
274
|
| Input picker | Switches the input |
|
|
273
275
|
| Control Center → Remote → volume buttons on your iPhone | Volume up / down by `volumeStepSize` |
|
|
274
276
|
| Remote → centre button / play-pause | Play / pause (HEOS CLI, Hybrid) |
|
|
@@ -286,6 +288,9 @@ The serial number doesn't match. Use the value from device discovery exactly as
|
|
|
286
288
|
**Discovery finds nothing.**
|
|
287
289
|
SSDP doesn't cross VLANs or guest networks. Docker installations need `network_mode: host`.
|
|
288
290
|
|
|
291
|
+
**A speaker doesn't switch on (HEOS CLI).**
|
|
292
|
+
Switching on starts playback. If the speaker has nothing to resume, the plugin starts the HEOS favorite set in `powerOnFavorite` (default: the first one). Make sure at least one favorite exists in the HEOS app (*Music → Favorites*). The debug log shows `starting HEOS favorite …` when this happens.
|
|
293
|
+
|
|
289
294
|
**The Marantz receiver doesn't react to AVR Control commands.**
|
|
290
295
|
Enable *Network Control* (sometimes called *IP Control*) in the receiver's network settings so it stays reachable in standby.
|
|
291
296
|
|
|
@@ -304,7 +309,7 @@ Every change is checked against the current Homebridge release and for known vul
|
|
|
304
309
|
| Checked | Versions |
|
|
305
310
|
| --- | --- |
|
|
306
311
|
| Homebridge | 1.11.4 and 2.5.0 (HAP-NodeJS 0.14.3 / 2.3.0) |
|
|
307
|
-
| Node.js | 22, 24 (Homebridge
|
|
312
|
+
| Node.js | 22, 24 and 26 (Homebridge 1.x: up to 24) |
|
|
308
313
|
| Known vulnerabilities | `npm audit`: 0 findings (runtime and development dependencies) |
|
|
309
314
|
|
|
310
315
|
*Last checked: November 2026.* The GitHub workflow repeats these checks on every push and **weekly**, so new Homebridge releases and newly published CVEs are noticed even without code changes.
|
package/config.schema.json
CHANGED
|
@@ -102,6 +102,15 @@
|
|
|
102
102
|
"maximum": 10,
|
|
103
103
|
"default": 2
|
|
104
104
|
},
|
|
105
|
+
"powerOnFavorite": {
|
|
106
|
+
"title": "Power On Favorite",
|
|
107
|
+
"description": "HEOS CLI only: if a speaker has nothing to resume when it is switched on, this HEOS favorite is started (1 = first favorite in the HEOS app). 0 = disabled.",
|
|
108
|
+
"type": "integer",
|
|
109
|
+
"required": false,
|
|
110
|
+
"minimum": 0,
|
|
111
|
+
"maximum": 50,
|
|
112
|
+
"default": 1
|
|
113
|
+
},
|
|
105
114
|
"inputs": {
|
|
106
115
|
"title": "Inputs",
|
|
107
116
|
"description": "The list of inputs to your device. If you configure inputs here, those inputs will become selectable in the Home app.",
|
package/dist/config.d.ts
CHANGED
package/dist/denonClient.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ export interface IDenonClient {
|
|
|
55
55
|
* Inputs that are only used to show the current source (e.g. streaming services), if the inputs could be read from the device.
|
|
56
56
|
*/
|
|
57
57
|
readonly displayOnlyInputs: DefaultInput[];
|
|
58
|
+
/**
|
|
59
|
+
* HEOS favorite that is started when switching on a HEOS CLI device that has nothing to resume (0 = disabled).
|
|
60
|
+
* Not used by protocols with a real power state.
|
|
61
|
+
*/
|
|
62
|
+
setPowerOnFavorite?(preset: number): void;
|
|
58
63
|
}
|
|
59
64
|
export declare abstract class DenonClient implements IDenonClient {
|
|
60
65
|
readonly serialNumber: string;
|
|
@@ -63,6 +63,14 @@ export declare class DenonClientHeosCli extends DenonClient {
|
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
65
|
};
|
|
66
|
+
PLAY_PRESET: {
|
|
67
|
+
SET: {
|
|
68
|
+
COMMAND: string;
|
|
69
|
+
PARAMS: string;
|
|
70
|
+
EXP_RES: RegExp;
|
|
71
|
+
TIMEOUT: number;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
66
74
|
PLAY_NEXT: {
|
|
67
75
|
SET: {
|
|
68
76
|
COMMAND: string;
|
|
@@ -188,6 +196,8 @@ export declare class DenonClientHeosCli extends DenonClient {
|
|
|
188
196
|
static readonly DEFAULT_INPUT_SOURCES: DefaultInput[];
|
|
189
197
|
readonly displayOnlyInputs: DefaultInput[];
|
|
190
198
|
private player_id;
|
|
199
|
+
/** HEOS favorite (1, 2, …) that is started when switching on without something to resume; 0 = disabled. */
|
|
200
|
+
private powerOnFavorite;
|
|
191
201
|
private player_name;
|
|
192
202
|
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);
|
|
193
203
|
private findPlayerId;
|
|
@@ -204,7 +214,14 @@ export declare class DenonClientHeosCli extends DenonClient {
|
|
|
204
214
|
setPlayNext(): Promise<void>;
|
|
205
215
|
setPlayPrevious(): Promise<void>;
|
|
206
216
|
getPower(raceStatus?: RaceStatus): Promise<boolean>;
|
|
217
|
+
setPowerOnFavorite(preset: number): void;
|
|
218
|
+
/**
|
|
219
|
+
* Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
|
|
220
|
+
* since the speaker was started) or the device refuses to play, the configured HEOS favorite is started instead.
|
|
221
|
+
*/
|
|
207
222
|
setPower(power: boolean): Promise<boolean>;
|
|
223
|
+
private powerOn;
|
|
224
|
+
private playFavorite;
|
|
208
225
|
getMute(raceStatus?: RaceStatus): Promise<boolean>;
|
|
209
226
|
setMute(mute: boolean): Promise<boolean>;
|
|
210
227
|
getVolume(raceStatus?: RaceStatus): Promise<number>;
|
|
@@ -66,6 +66,15 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
|
+
PLAY_PRESET: {
|
|
70
|
+
// plays a HEOS favorite (preset position starting at 1)
|
|
71
|
+
SET: {
|
|
72
|
+
COMMAND: "browse/play_preset",
|
|
73
|
+
PARAMS: "?pid=[PID]&preset=[VALUE]",
|
|
74
|
+
EXP_RES: /preset=(\d+)/,
|
|
75
|
+
TIMEOUT: 6000,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
69
78
|
PLAY_NEXT: {
|
|
70
79
|
SET: {
|
|
71
80
|
COMMAND: "player/play_next",
|
|
@@ -237,6 +246,8 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
237
246
|
static DEFAULT_INPUT_SOURCES = [...DenonClientHeosCli.DISPLAY_ONLY_INPUT_SOURCES, ...DenonClientHeosCli.INPUT_SOURCES];
|
|
238
247
|
displayOnlyInputs = DenonClientHeosCli.DISPLAY_ONLY_INPUT_SOURCES;
|
|
239
248
|
player_id;
|
|
249
|
+
/** HEOS favorite (1, 2, …) that is started when switching on without something to resume; 0 = disabled. */
|
|
250
|
+
powerOnFavorite = 1;
|
|
240
251
|
player_name;
|
|
241
252
|
constructor(serialNumber, name, host, connect_timeout, response_timeout, debugLogCallback, powerUpdateCallback, muteUpdateCallback, volumeUpdateCallback, inputUpdateCallback) {
|
|
242
253
|
super(serialNumber, name, {
|
|
@@ -422,12 +433,44 @@ export class DenonClientHeosCli extends DenonClient {
|
|
|
422
433
|
}
|
|
423
434
|
return isPlaying[playing];
|
|
424
435
|
}
|
|
436
|
+
setPowerOnFavorite(preset) {
|
|
437
|
+
this.powerOnFavorite = preset;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Switching on means starting playback. If there is nothing to resume (play state "unknown", e.g. no source
|
|
441
|
+
* since the speaker was started) or the device refuses to play, the configured HEOS favorite is started instead.
|
|
442
|
+
*/
|
|
425
443
|
async setPower(power) {
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
this.
|
|
444
|
+
let playing;
|
|
445
|
+
if (!power) {
|
|
446
|
+
playing = isPlaying[await this.setPlaying(Playing.STOP)];
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
playing = await this.powerOn();
|
|
429
450
|
}
|
|
430
|
-
|
|
451
|
+
this.powerUpdateCallback?.(playing);
|
|
452
|
+
return playing;
|
|
453
|
+
}
|
|
454
|
+
async powerOn() {
|
|
455
|
+
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);
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
return isPlaying[await this.setPlaying(Playing.PLAY)];
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (!(error instanceof CommandFailedException) || this.powerOnFavorite <= 0) {
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
this.debugLog(`The device refused to resume playback, starting HEOS favorite ${this.powerOnFavorite}.`);
|
|
468
|
+
return this.playFavorite(this.powerOnFavorite);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async playFavorite(preset) {
|
|
472
|
+
await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_PRESET, CommandMode.SET, { value: preset.toString() });
|
|
473
|
+
return true;
|
|
431
474
|
}
|
|
432
475
|
async getMute(raceStatus) {
|
|
433
476
|
const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.MUTE, CommandMode.GET, {});
|
|
@@ -11,6 +11,8 @@ export declare class DenonAudioAccessory {
|
|
|
11
11
|
private static readonly API_CONNECT_TIMEOUT;
|
|
12
12
|
private static readonly API_RESPONSE_TIMEOUT;
|
|
13
13
|
private static readonly DEFAULT_VOLUME_STEP_SIZE;
|
|
14
|
+
private static readonly DEFAULT_POWER_ON_FAVORITE;
|
|
15
|
+
private static readonly POWER_STATE_SETTLE_TIME;
|
|
14
16
|
private static readonly INPUT_QUERY_TIMEOUT;
|
|
15
17
|
private static readonly DEFAULT_VOLUME_LIMIT;
|
|
16
18
|
private readonly platform;
|
|
@@ -35,6 +37,7 @@ export declare class DenonAudioAccessory {
|
|
|
35
37
|
private lastSetVolume;
|
|
36
38
|
private targetMediaState;
|
|
37
39
|
private volumeChangeOngoing;
|
|
40
|
+
private pendingActive;
|
|
38
41
|
private playingChangeOngoing;
|
|
39
42
|
constructor(platform: DenonAudioPlatform, accessory: PlatformAccessory, config: DeviceConfig, log: Logger);
|
|
40
43
|
/**
|
|
@@ -15,6 +15,9 @@ export class DenonAudioAccessory {
|
|
|
15
15
|
static API_CONNECT_TIMEOUT = 5 * 1000;
|
|
16
16
|
static API_RESPONSE_TIMEOUT = 1 * 1000;
|
|
17
17
|
static DEFAULT_VOLUME_STEP_SIZE = 2;
|
|
18
|
+
static DEFAULT_POWER_ON_FAVORITE = 1;
|
|
19
|
+
// after switching, devices report the old state for a moment; HomeKit gets the new state during this time
|
|
20
|
+
static POWER_STATE_SETTLE_TIME = 5 * 1000;
|
|
18
21
|
// includes the time to connect to the device and to look up the player id
|
|
19
22
|
static INPUT_QUERY_TIMEOUT = 15 * 1000;
|
|
20
23
|
static DEFAULT_VOLUME_LIMIT = 99;
|
|
@@ -40,6 +43,7 @@ export class DenonAudioAccessory {
|
|
|
40
43
|
lastSetVolume;
|
|
41
44
|
targetMediaState;
|
|
42
45
|
volumeChangeOngoing = false;
|
|
46
|
+
pendingActive;
|
|
43
47
|
playingChangeOngoing = false;
|
|
44
48
|
constructor(platform, accessory, config, log) {
|
|
45
49
|
log.debug("Initializing DenonAudioAccessory...");
|
|
@@ -103,6 +107,11 @@ export class DenonAudioAccessory {
|
|
|
103
107
|
this.speakerService.setCharacteristic(this.platform.Characteristic.VolumeControlType, this.platform.Characteristic.VolumeControlType.ABSOLUTE);
|
|
104
108
|
// choose appropriate client
|
|
105
109
|
this.denonClient = new DenonProtocol.CLIENT_MAP[this.controlMode](this.serialNumber, this.name, this.ip, DenonAudioAccessory.API_CONNECT_TIMEOUT, DenonAudioAccessory.API_RESPONSE_TIMEOUT, this.log.debug.bind(this.log), this.callbackActive.bind(this), this.callbackMute.bind(this), this.callbackVolume.bind(this), this.callbackInput.bind(this));
|
|
110
|
+
const powerOnFavorite = config.powerOnFavorite ?? DenonAudioAccessory.DEFAULT_POWER_ON_FAVORITE;
|
|
111
|
+
if (!Number.isInteger(powerOnFavorite) || powerOnFavorite < 0) {
|
|
112
|
+
throw new Error("Power on favorite must be 0 (disabled) or the number of a HEOS favorite (1, 2, …)");
|
|
113
|
+
}
|
|
114
|
+
this.denonClient.setPowerOnFavorite?.(powerOnFavorite);
|
|
106
115
|
this.configuredInputs = config.inputs ?? [];
|
|
107
116
|
this.unknownInput = new ConfiguredInput("unknown", "Unknown Input", false, 0, false);
|
|
108
117
|
this.log.info("Finished initializing accessory.");
|
|
@@ -206,6 +215,11 @@ export class DenonAudioAccessory {
|
|
|
206
215
|
* These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on.
|
|
207
216
|
*/
|
|
208
217
|
async getActive() {
|
|
218
|
+
if (this.pendingActive && Date.now() < this.pendingActive.until) {
|
|
219
|
+
this.log.debug(`getActive for ${this.name}. [recently set to ${this.pendingActive.value}]`);
|
|
220
|
+
return this.pendingActive.value;
|
|
221
|
+
}
|
|
222
|
+
this.pendingActive = undefined;
|
|
209
223
|
return this.queryDevice("getActive", "power status", (raceStatus) => this.denonClient.getPower(raceStatus));
|
|
210
224
|
}
|
|
211
225
|
async getActiveIdentifier() {
|
|
@@ -297,10 +311,13 @@ export class DenonAudioAccessory {
|
|
|
297
311
|
}
|
|
298
312
|
async setActive(newValue) {
|
|
299
313
|
this.log.debug(`setActive for ${this.name} set to ${newValue}`);
|
|
314
|
+
const active = newValue === this.platform.Characteristic.Active.ACTIVE;
|
|
300
315
|
try {
|
|
301
|
-
await this.denonClient.setPower(
|
|
316
|
+
await this.denonClient.setPower(active);
|
|
317
|
+
this.pendingActive = { value: active, until: Date.now() + DenonAudioAccessory.POWER_STATE_SETTLE_TIME };
|
|
302
318
|
}
|
|
303
319
|
catch (error) {
|
|
320
|
+
this.pendingActive = undefined;
|
|
304
321
|
this.log.error(`An error occured while setting power status for ${this.name}.`, error);
|
|
305
322
|
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
306
323
|
}
|
|
@@ -525,6 +542,10 @@ export class DenonAudioAccessory {
|
|
|
525
542
|
* Handles characteristic updates from the API
|
|
526
543
|
*/
|
|
527
544
|
callbackActive(active) {
|
|
545
|
+
if (this.pendingActive && this.pendingActive.value !== active) {
|
|
546
|
+
// changed on the device (e.g. in the HEOS app) - the device state wins
|
|
547
|
+
this.pendingActive = undefined;
|
|
548
|
+
}
|
|
528
549
|
this.tvService.updateCharacteristic(this.platform.Characteristic.Active, active);
|
|
529
550
|
}
|
|
530
551
|
callbackMute(mute) {
|
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.0
|
|
5
|
+
"version": "1.1.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": [
|