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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/index.js +39 -43
  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
@@ -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
- // Stations: [{ name: "Radio 2", preset: 0 }, ...]
35
- // Important: preset numbers refer to the radio preset slots.
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
- // We cannot reliably read current preset index on all firmwares.
45
- // We track what we last selected via HomeKit and sync switches accordingly.
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 preset = Number(s.preset);
112
+ const presetUi = Number(s.preset);
122
113
 
123
114
  if (!stationName) continue;
124
- if (!Number.isFinite(preset)) continue;
115
+ if (!Number.isFinite(presetUi)) continue;
125
116
 
126
- const p = Math.trunc(preset);
127
- const subtype = "preset_" + String(p);
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
- const isOn = this.lastKnownPresetIndex === p;
137
- cb(null, isOn);
129
+ cb(null, this.lastKnownPresetKey === presetKey);
138
130
  })
139
131
  .on("set", (value, cb) => {
140
- this.handleSetStationPreset(p, !!value, cb);
132
+ this.handleSetStationPreset(presetKey, !!value, cb);
141
133
  });
142
134
 
143
- this.stationServices.push({ preset: p, name: stationName, service: sw });
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 (preset, turnOn, callback) {
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.syncStationSwitchesFromPreset(this.lastKnownPresetIndex);
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.setPresetIndex(preset);
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
- this.syncStationSwitchesFromPreset(this.lastKnownPresetIndex);
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.syncStationSwitchesFromPreset = function (presetIndex) {
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 = presetIndex === s.preset;
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 polling is intentionally not implemented for this firmware,
298
- // because netRemote.sys.preset.index does not exist.
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 via navigation state
339
- FsApiClient.prototype.setPresetIndex = async function (preset) {
340
- const p = Math.trunc(Number(preset));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-frontier-silicon-plugin",
3
- "version": "1.2.1",
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",