homebridge-frontier-silicon-plugin 1.2.1 → 1.2.2
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 +11 -0
- package/index.js +39 -43
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
|
|
6
6
|
|
|
7
|
+
## [1.2.1] – 2025-12-28
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Corrected preset numbering by mapping Homebridge presets (1-based) to Frontier Silicon FSAPI keys (0-based).
|
|
11
|
+
- Ensured station preset switches behave exclusively (activating one station automatically deactivates the others).
|
|
12
|
+
- Fixed incorrect preset selection where presets were offset by one position on the radio.
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- More reliable DAB+ / preset switching on Frontier Silicon radios using navigation-based FSAPI calls.
|
|
16
|
+
- Better HomeKit UI consistency when switching between stations.
|
|
17
|
+
|
|
7
18
|
## [1.2.0] – 2025-12-28
|
|
8
19
|
|
|
9
20
|
### Added
|
package/index.js
CHANGED
|
@@ -24,26 +24,20 @@ function FrontierSiliconAccessory(log, config) {
|
|
|
24
24
|
|
|
25
25
|
this.enableVolume = config.enableVolume !== false;
|
|
26
26
|
|
|
27
|
-
// Keep both by default
|
|
28
27
|
this.exposeSpeakerService = config.exposeSpeakerService !== false;
|
|
29
28
|
this.exposeVolumeSlider = config.exposeVolumeSlider !== false;
|
|
30
29
|
|
|
31
|
-
// When selecting a station, power on first
|
|
32
30
|
this.autoPowerOnOnPreset = config.autoPowerOnOnPreset !== false;
|
|
33
31
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// If the radio says "empty preset", store that station to that preset slot on the radio first.
|
|
32
|
+
// stations: [{ name: "Radio 2", preset: 2 }, ...]
|
|
33
|
+
// preset is 1 based like the radio UI, internally we convert to 0 based presetKey for FSAPI
|
|
37
34
|
this.stations = Array.isArray(config.stations) ? config.stations : [];
|
|
38
35
|
|
|
39
36
|
this.lastKnownPower = null;
|
|
40
|
-
|
|
41
|
-
// Store last known RADIO volume on device scale 0..100
|
|
42
37
|
this.lastKnownRadioVolume = null;
|
|
43
38
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
this.lastKnownPresetIndex = null;
|
|
39
|
+
// 0 based FSAPI preset key that we last selected via HomeKit
|
|
40
|
+
this.lastKnownPresetKey = null;
|
|
47
41
|
|
|
48
42
|
this.isUpdatingStationSwitches = false;
|
|
49
43
|
|
|
@@ -62,14 +56,12 @@ function FrontierSiliconAccessory(log, config) {
|
|
|
62
56
|
.setCharacteristic(Characteristic.Model, "FSAPI Radio")
|
|
63
57
|
.setCharacteristic(Characteristic.SerialNumber, this.ip || "unknown");
|
|
64
58
|
|
|
65
|
-
// Power switch
|
|
66
59
|
this.switchService = new Service.Switch(this.name);
|
|
67
60
|
this.switchService
|
|
68
61
|
.getCharacteristic(Characteristic.On)
|
|
69
62
|
.on("get", this.handleGetPower.bind(this))
|
|
70
63
|
.on("set", this.handleSetPower.bind(this));
|
|
71
64
|
|
|
72
|
-
// Volume services
|
|
73
65
|
if (this.enableVolume) {
|
|
74
66
|
if (this.exposeSpeakerService) {
|
|
75
67
|
this.speakerService = new Service.Speaker(this.name + " Speaker");
|
|
@@ -102,7 +94,6 @@ function FrontierSiliconAccessory(log, config) {
|
|
|
102
94
|
}
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
// Station switches
|
|
106
97
|
this.stationServices = [];
|
|
107
98
|
this.buildStationServices();
|
|
108
99
|
|
|
@@ -118,14 +109,16 @@ FrontierSiliconAccessory.prototype.buildStationServices = function () {
|
|
|
118
109
|
if (!s || typeof s !== "object") continue;
|
|
119
110
|
|
|
120
111
|
const stationName = String(s.name ?? "").trim();
|
|
121
|
-
const
|
|
112
|
+
const presetUi = Number(s.preset);
|
|
122
113
|
|
|
123
114
|
if (!stationName) continue;
|
|
124
|
-
if (!Number.isFinite(
|
|
115
|
+
if (!Number.isFinite(presetUi)) continue;
|
|
125
116
|
|
|
126
|
-
|
|
127
|
-
const
|
|
117
|
+
// Convert 1 based UI preset number to 0 based FSAPI key
|
|
118
|
+
const presetKey = Math.trunc(presetUi) - 1;
|
|
119
|
+
if (presetKey < 0) continue;
|
|
128
120
|
|
|
121
|
+
const subtype = "preset_" + String(presetKey);
|
|
129
122
|
if (seenSubtypes.has(subtype)) continue;
|
|
130
123
|
seenSubtypes.add(subtype);
|
|
131
124
|
|
|
@@ -133,14 +126,18 @@ FrontierSiliconAccessory.prototype.buildStationServices = function () {
|
|
|
133
126
|
|
|
134
127
|
sw.getCharacteristic(Characteristic.On)
|
|
135
128
|
.on("get", (cb) => {
|
|
136
|
-
|
|
137
|
-
cb(null, isOn);
|
|
129
|
+
cb(null, this.lastKnownPresetKey === presetKey);
|
|
138
130
|
})
|
|
139
131
|
.on("set", (value, cb) => {
|
|
140
|
-
this.handleSetStationPreset(
|
|
132
|
+
this.handleSetStationPreset(presetKey, !!value, cb);
|
|
141
133
|
});
|
|
142
134
|
|
|
143
|
-
this.stationServices.push({
|
|
135
|
+
this.stationServices.push({
|
|
136
|
+
presetKey,
|
|
137
|
+
presetUi: Math.trunc(presetUi),
|
|
138
|
+
name: stationName,
|
|
139
|
+
service: sw
|
|
140
|
+
});
|
|
144
141
|
}
|
|
145
142
|
};
|
|
146
143
|
|
|
@@ -183,9 +180,7 @@ FrontierSiliconAccessory.prototype.handleGetVolume = async function (callback) {
|
|
|
183
180
|
try {
|
|
184
181
|
const radioVol = await this.client.getVolume();
|
|
185
182
|
this.lastKnownRadioVolume = radioVol;
|
|
186
|
-
|
|
187
|
-
const homekitVol = radioToHomekitVolume(radioVol);
|
|
188
|
-
callback(null, homekitVol);
|
|
183
|
+
callback(null, radioToHomekitVolume(radioVol));
|
|
189
184
|
} catch (err) {
|
|
190
185
|
this.log.warn("Volume get failed, returning last known level.", toMsg(err));
|
|
191
186
|
const fallbackRadio = this.lastKnownRadioVolume ?? 0;
|
|
@@ -206,18 +201,24 @@ FrontierSiliconAccessory.prototype.handleSetVolume = async function (value, call
|
|
|
206
201
|
}
|
|
207
202
|
};
|
|
208
203
|
|
|
209
|
-
FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (
|
|
204
|
+
FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (presetKey, turnOn, callback) {
|
|
210
205
|
if (this.isUpdatingStationSwitches) {
|
|
211
206
|
callback(null);
|
|
212
207
|
return;
|
|
213
208
|
}
|
|
214
209
|
|
|
210
|
+
// Exclusive selector behaviour
|
|
211
|
+
// Turning OFF does not change the radio, we snap back to the current selection
|
|
215
212
|
if (!turnOn) {
|
|
216
213
|
callback(null);
|
|
217
|
-
this.
|
|
214
|
+
this.syncStationSwitchesFromPresetKey(this.lastKnownPresetKey);
|
|
218
215
|
return;
|
|
219
216
|
}
|
|
220
217
|
|
|
218
|
+
// Optimistic UI update so previous station immediately turns off
|
|
219
|
+
this.lastKnownPresetKey = presetKey;
|
|
220
|
+
this.syncStationSwitchesFromPresetKey(presetKey);
|
|
221
|
+
|
|
221
222
|
try {
|
|
222
223
|
if (this.autoPowerOnOnPreset) {
|
|
223
224
|
try {
|
|
@@ -229,27 +230,26 @@ FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (pres
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
await this.client.
|
|
233
|
-
|
|
234
|
-
this.lastKnownPresetIndex = preset;
|
|
235
|
-
this.syncStationSwitchesFromPreset(preset);
|
|
233
|
+
await this.client.setPresetKey(presetKey);
|
|
236
234
|
|
|
237
235
|
callback(null);
|
|
238
236
|
} catch (err) {
|
|
239
237
|
this.log.warn("Preset set failed.", toMsg(err));
|
|
240
238
|
callback(null);
|
|
241
|
-
|
|
239
|
+
|
|
240
|
+
// Keep UI consistent with the last selection we know about
|
|
241
|
+
this.syncStationSwitchesFromPresetKey(this.lastKnownPresetKey);
|
|
242
242
|
}
|
|
243
243
|
};
|
|
244
244
|
|
|
245
|
-
FrontierSiliconAccessory.prototype.
|
|
245
|
+
FrontierSiliconAccessory.prototype.syncStationSwitchesFromPresetKey = function (presetKey) {
|
|
246
246
|
if (!this.stationServices || this.stationServices.length === 0) return;
|
|
247
247
|
|
|
248
248
|
this.isUpdatingStationSwitches = true;
|
|
249
249
|
|
|
250
250
|
try {
|
|
251
251
|
for (const s of this.stationServices) {
|
|
252
|
-
const shouldBeOn =
|
|
252
|
+
const shouldBeOn = presetKey === s.presetKey;
|
|
253
253
|
s.service.getCharacteristic(Characteristic.On).updateValue(shouldBeOn);
|
|
254
254
|
}
|
|
255
255
|
} finally {
|
|
@@ -278,7 +278,6 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
|
|
|
278
278
|
const radioVol = await this.client.getVolume();
|
|
279
279
|
if (this.lastKnownRadioVolume !== radioVol) {
|
|
280
280
|
this.lastKnownRadioVolume = radioVol;
|
|
281
|
-
|
|
282
281
|
const homekitVol = radioToHomekitVolume(radioVol);
|
|
283
282
|
|
|
284
283
|
if (this.speakerService) {
|
|
@@ -294,10 +293,8 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
|
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
|
|
297
|
-
// Preset
|
|
298
|
-
//
|
|
299
|
-
// If you want full sync from the radio front panel later,
|
|
300
|
-
// we can add alternative detection based on play.info nodes.
|
|
296
|
+
// Preset readback is not implemented for this firmware
|
|
297
|
+
// We keep station switch states based on the last selected presetKey via HomeKit
|
|
301
298
|
};
|
|
302
299
|
|
|
303
300
|
tick();
|
|
@@ -335,10 +332,10 @@ FsApiClient.prototype.setVolume = async function (volume) {
|
|
|
335
332
|
await this.fetchText("/fsapi/SET/netRemote.sys.audio.volume?value=" + v);
|
|
336
333
|
};
|
|
337
334
|
|
|
338
|
-
// DIR3010 style preset selection
|
|
339
|
-
FsApiClient.prototype.
|
|
340
|
-
const p = Math.trunc(Number(
|
|
341
|
-
if (!Number.isFinite(p)) throw new Error("Invalid preset");
|
|
335
|
+
// DIR3010 style preset selection using nav.state and selectPreset
|
|
336
|
+
FsApiClient.prototype.setPresetKey = async function (presetKey) {
|
|
337
|
+
const p = Math.trunc(Number(presetKey));
|
|
338
|
+
if (!Number.isFinite(p)) throw new Error("Invalid preset key");
|
|
342
339
|
|
|
343
340
|
await this.fetchText("/fsapi/SET/netRemote.nav.state?value=1");
|
|
344
341
|
await this.fetchText("/fsapi/SET/netRemote.nav.action.selectPreset?value=" + encodeURIComponent(String(p)));
|
|
@@ -405,7 +402,6 @@ function toMsg(err) {
|
|
|
405
402
|
return String(err);
|
|
406
403
|
}
|
|
407
404
|
|
|
408
|
-
// Non linear volume mapping
|
|
409
405
|
function homekitToRadioVolume(homekitValue) {
|
|
410
406
|
const x = clampInt(Number(homekitValue), 0, 100) / 100;
|
|
411
407
|
return clampInt(Math.round(Math.pow(x, 2) * 100), 0, 100);
|
package/package.json
CHANGED