homebridge-frontier-silicon-plugin 1.2.0 → 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 +45 -42
- 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
|
@@ -29,11 +29,15 @@ function FrontierSiliconAccessory(log, config) {
|
|
|
29
29
|
|
|
30
30
|
this.autoPowerOnOnPreset = config.autoPowerOnOnPreset !== false;
|
|
31
31
|
|
|
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
|
|
32
34
|
this.stations = Array.isArray(config.stations) ? config.stations : [];
|
|
33
35
|
|
|
34
36
|
this.lastKnownPower = null;
|
|
35
37
|
this.lastKnownRadioVolume = null;
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
// 0 based FSAPI preset key that we last selected via HomeKit
|
|
40
|
+
this.lastKnownPresetKey = null;
|
|
37
41
|
|
|
38
42
|
this.isUpdatingStationSwitches = false;
|
|
39
43
|
|
|
@@ -91,8 +95,6 @@ function FrontierSiliconAccessory(log, config) {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
this.stationServices = [];
|
|
94
|
-
this.stationServiceByPreset = new Map();
|
|
95
|
-
|
|
96
98
|
this.buildStationServices();
|
|
97
99
|
|
|
98
100
|
this.startPolling();
|
|
@@ -107,12 +109,16 @@ FrontierSiliconAccessory.prototype.buildStationServices = function () {
|
|
|
107
109
|
if (!s || typeof s !== "object") continue;
|
|
108
110
|
|
|
109
111
|
const stationName = String(s.name ?? "").trim();
|
|
110
|
-
const
|
|
112
|
+
const presetUi = Number(s.preset);
|
|
111
113
|
|
|
112
114
|
if (!stationName) continue;
|
|
113
|
-
if (!Number.isFinite(
|
|
115
|
+
if (!Number.isFinite(presetUi)) continue;
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
// Convert 1 based UI preset number to 0 based FSAPI key
|
|
118
|
+
const presetKey = Math.trunc(presetUi) - 1;
|
|
119
|
+
if (presetKey < 0) continue;
|
|
120
|
+
|
|
121
|
+
const subtype = "preset_" + String(presetKey);
|
|
116
122
|
if (seenSubtypes.has(subtype)) continue;
|
|
117
123
|
seenSubtypes.add(subtype);
|
|
118
124
|
|
|
@@ -120,15 +126,18 @@ FrontierSiliconAccessory.prototype.buildStationServices = function () {
|
|
|
120
126
|
|
|
121
127
|
sw.getCharacteristic(Characteristic.On)
|
|
122
128
|
.on("get", (cb) => {
|
|
123
|
-
|
|
124
|
-
cb(null, isOn);
|
|
129
|
+
cb(null, this.lastKnownPresetKey === presetKey);
|
|
125
130
|
})
|
|
126
131
|
.on("set", (value, cb) => {
|
|
127
|
-
this.handleSetStationPreset(
|
|
132
|
+
this.handleSetStationPreset(presetKey, !!value, cb);
|
|
128
133
|
});
|
|
129
134
|
|
|
130
|
-
this.stationServices.push({
|
|
131
|
-
|
|
135
|
+
this.stationServices.push({
|
|
136
|
+
presetKey,
|
|
137
|
+
presetUi: Math.trunc(presetUi),
|
|
138
|
+
name: stationName,
|
|
139
|
+
service: sw
|
|
140
|
+
});
|
|
132
141
|
}
|
|
133
142
|
};
|
|
134
143
|
|
|
@@ -171,9 +180,7 @@ FrontierSiliconAccessory.prototype.handleGetVolume = async function (callback) {
|
|
|
171
180
|
try {
|
|
172
181
|
const radioVol = await this.client.getVolume();
|
|
173
182
|
this.lastKnownRadioVolume = radioVol;
|
|
174
|
-
|
|
175
|
-
const homekitVol = radioToHomekitVolume(radioVol);
|
|
176
|
-
callback(null, homekitVol);
|
|
183
|
+
callback(null, radioToHomekitVolume(radioVol));
|
|
177
184
|
} catch (err) {
|
|
178
185
|
this.log.warn("Volume get failed, returning last known level.", toMsg(err));
|
|
179
186
|
const fallbackRadio = this.lastKnownRadioVolume ?? 0;
|
|
@@ -194,18 +201,24 @@ FrontierSiliconAccessory.prototype.handleSetVolume = async function (value, call
|
|
|
194
201
|
}
|
|
195
202
|
};
|
|
196
203
|
|
|
197
|
-
FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (
|
|
204
|
+
FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (presetKey, turnOn, callback) {
|
|
198
205
|
if (this.isUpdatingStationSwitches) {
|
|
199
206
|
callback(null);
|
|
200
207
|
return;
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
// Exclusive selector behaviour
|
|
211
|
+
// Turning OFF does not change the radio, we snap back to the current selection
|
|
203
212
|
if (!turnOn) {
|
|
204
213
|
callback(null);
|
|
205
|
-
this.
|
|
214
|
+
this.syncStationSwitchesFromPresetKey(this.lastKnownPresetKey);
|
|
206
215
|
return;
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
// Optimistic UI update so previous station immediately turns off
|
|
219
|
+
this.lastKnownPresetKey = presetKey;
|
|
220
|
+
this.syncStationSwitchesFromPresetKey(presetKey);
|
|
221
|
+
|
|
209
222
|
try {
|
|
210
223
|
if (this.autoPowerOnOnPreset) {
|
|
211
224
|
try {
|
|
@@ -213,29 +226,30 @@ FrontierSiliconAccessory.prototype.handleSetStationPreset = async function (pres
|
|
|
213
226
|
this.lastKnownPower = true;
|
|
214
227
|
this.switchService.getCharacteristic(Characteristic.On).updateValue(true);
|
|
215
228
|
} catch (_e) {
|
|
229
|
+
// ignore
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
|
|
219
|
-
await this.client.
|
|
220
|
-
this.lastKnownPresetIndex = preset;
|
|
233
|
+
await this.client.setPresetKey(presetKey);
|
|
221
234
|
|
|
222
|
-
this.syncStationSwitchesFromPreset(preset);
|
|
223
235
|
callback(null);
|
|
224
236
|
} catch (err) {
|
|
225
237
|
this.log.warn("Preset set failed.", toMsg(err));
|
|
226
238
|
callback(null);
|
|
227
|
-
|
|
239
|
+
|
|
240
|
+
// Keep UI consistent with the last selection we know about
|
|
241
|
+
this.syncStationSwitchesFromPresetKey(this.lastKnownPresetKey);
|
|
228
242
|
}
|
|
229
243
|
};
|
|
230
244
|
|
|
231
|
-
FrontierSiliconAccessory.prototype.
|
|
245
|
+
FrontierSiliconAccessory.prototype.syncStationSwitchesFromPresetKey = function (presetKey) {
|
|
232
246
|
if (!this.stationServices || this.stationServices.length === 0) return;
|
|
233
247
|
|
|
234
248
|
this.isUpdatingStationSwitches = true;
|
|
235
249
|
|
|
236
250
|
try {
|
|
237
251
|
for (const s of this.stationServices) {
|
|
238
|
-
const shouldBeOn =
|
|
252
|
+
const shouldBeOn = presetKey === s.presetKey;
|
|
239
253
|
s.service.getCharacteristic(Characteristic.On).updateValue(shouldBeOn);
|
|
240
254
|
}
|
|
241
255
|
} finally {
|
|
@@ -264,7 +278,6 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
|
|
|
264
278
|
const radioVol = await this.client.getVolume();
|
|
265
279
|
if (this.lastKnownRadioVolume !== radioVol) {
|
|
266
280
|
this.lastKnownRadioVolume = radioVol;
|
|
267
|
-
|
|
268
281
|
const homekitVol = radioToHomekitVolume(radioVol);
|
|
269
282
|
|
|
270
283
|
if (this.speakerService) {
|
|
@@ -280,17 +293,8 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
|
|
|
280
293
|
}
|
|
281
294
|
}
|
|
282
295
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const preset = await this.client.getPresetIndex();
|
|
286
|
-
if (Number.isFinite(preset) && this.lastKnownPresetIndex !== preset) {
|
|
287
|
-
this.lastKnownPresetIndex = preset;
|
|
288
|
-
this.syncStationSwitchesFromPreset(preset);
|
|
289
|
-
}
|
|
290
|
-
} catch (err) {
|
|
291
|
-
if (this.log.debug) this.log.debug("Polling preset failed.", toMsg(err));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
296
|
+
// Preset readback is not implemented for this firmware
|
|
297
|
+
// We keep station switch states based on the last selected presetKey via HomeKit
|
|
294
298
|
};
|
|
295
299
|
|
|
296
300
|
tick();
|
|
@@ -328,15 +332,14 @@ FsApiClient.prototype.setVolume = async function (volume) {
|
|
|
328
332
|
await this.fetchText("/fsapi/SET/netRemote.sys.audio.volume?value=" + v);
|
|
329
333
|
};
|
|
330
334
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
336
|
-
};
|
|
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");
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
await this.fetchText("/fsapi/SET/netRemote.
|
|
340
|
+
await this.fetchText("/fsapi/SET/netRemote.nav.state?value=1");
|
|
341
|
+
await this.fetchText("/fsapi/SET/netRemote.nav.action.selectPreset?value=" + encodeURIComponent(String(p)));
|
|
342
|
+
await this.fetchText("/fsapi/SET/netRemote.nav.state?value=0");
|
|
340
343
|
};
|
|
341
344
|
|
|
342
345
|
FsApiClient.prototype.fetchText = async function (pathAndQuery) {
|
package/package.json
CHANGED