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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/index.js +45 -42
  3. 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
- this.lastKnownPresetIndex = null;
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 preset = Number(s.preset);
112
+ const presetUi = Number(s.preset);
111
113
 
112
114
  if (!stationName) continue;
113
- if (!Number.isFinite(preset)) continue;
115
+ if (!Number.isFinite(presetUi)) continue;
114
116
 
115
- const subtype = "preset_" + String(Math.trunc(preset));
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
- const isOn = this.lastKnownPresetIndex === Math.trunc(preset);
124
- cb(null, isOn);
129
+ cb(null, this.lastKnownPresetKey === presetKey);
125
130
  })
126
131
  .on("set", (value, cb) => {
127
- this.handleSetStationPreset(Math.trunc(preset), !!value, cb);
132
+ this.handleSetStationPreset(presetKey, !!value, cb);
128
133
  });
129
134
 
130
- this.stationServices.push({ preset: Math.trunc(preset), name: stationName, service: sw });
131
- this.stationServiceByPreset.set(Math.trunc(preset), sw);
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 (preset, turnOn, callback) {
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.syncStationSwitchesFromPreset(this.lastKnownPresetIndex);
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.setPresetIndex(preset);
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
- this.syncStationSwitchesFromPreset(this.lastKnownPresetIndex);
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.syncStationSwitchesFromPreset = function (presetIndex) {
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 = presetIndex === s.preset;
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
- if (this.stationServices && this.stationServices.length > 0) {
284
- try {
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
- FsApiClient.prototype.getPresetIndex = async function () {
332
- const text = await this.fetchText("/fsapi/GET/netRemote.sys.preset.index");
333
- const value = parseFsapiValue(text);
334
- const n = Number(value);
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
- FsApiClient.prototype.setPresetIndex = async function (preset) {
339
- await this.fetchText("/fsapi/SET/netRemote.sys.preset.index?value=" + encodeURIComponent(String(preset)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-frontier-silicon-plugin",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Homebridge plugin for Frontier Silicon FSAPI devices, power and volume with safe polling",
5
5
  "license": "ISC",
6
6
  "main": "index.js",