homebridge-frontier-silicon-plugin 1.2.1 → 1.2.3

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 (4) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +73 -30
  3. package/index.js +39 -43
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ 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.2] – 2025-12-28
8
+
9
+ ### Fixed
10
+ - Fixed station preset switches not turning off previously selected stations.
11
+ - Ensured station switches behave as an exclusive selector (radio-style behavior).
12
+ - Improved internal state handling to keep HomeKit UI in sync when switching stations.
13
+
14
+ ### Improved
15
+ - More robust preset switching logic for Frontier Silicon radios using navigation-based FSAPI.
16
+
17
+
18
+ ## [1.2.1] – 2025-12-28
19
+
20
+ ### Fixed
21
+ - Corrected preset numbering by mapping Homebridge presets (1-based) to Frontier Silicon FSAPI keys (0-based).
22
+ - Fixed incorrect preset selection where presets were offset by one position on the radio.
23
+
24
+ ### Improved
25
+ - Reliable DAB+ / preset switching using the required navigation-based FSAPI calls.
26
+
7
27
  ## [1.2.0] – 2025-12-28
8
28
 
9
29
  ### Added
package/README.md CHANGED
@@ -30,49 +30,92 @@ This plugin is designed as a modern replacement for older Frontier Silicon Homeb
30
30
 
31
31
  Most internet radios from brands such as Roberts, Ruark, Revo, Hama and similar use Frontier Silicon firmware and are compatible.
32
32
 
33
- ## Installation
33
+ ## Tested and Compatible Radios
34
+
35
+ This plugin is designed for radios based on the **Frontier Silicon FSAPI (NetRemote API)**.
36
+ It has been tested or is known to work on a wide range of internet and DAB+ radios using this platform.
37
+
38
+ ### Known compatible brands and models (non-exhaustive)
39
+
40
+ **Hama**
41
+ - IR100
42
+ - IR110
43
+ - DIR3100 / DIR3110 series
44
+
45
+ **Medion**
46
+ - MD87180
47
+ - MD86988
48
+ - MD86955
49
+ - MD87528
50
+
51
+ **Roberts**
52
+ - Stream 83i
53
+ - Stream 93i
54
+
55
+ **Ruark**
56
+ - R2
57
+ - R5
34
58
 
35
- Install the plugin globally using npm
59
+ **Revo**
60
+ - SuperConnect
36
61
 
37
- npm install -g homebridge-frontier-silicon-plugin
62
+ **Auna**
63
+ - Connect 150
64
+ - Connect CD
65
+ - KR200
38
66
 
39
- After installation, restart Homebridge.
67
+ **TechniSat**
68
+ - DIGITRADIO 350 IR
69
+ - DIGITRADIO 850
70
+ - VIOLA series
40
71
 
41
- ## Configuration
72
+ **Silvercrest (Lidl)**
73
+ - SMRS18A1
74
+ - SMRS30A1
75
+ - SMRS35A1
76
+ - SIRD series
42
77
 
43
- Add the accessory to your Homebridge configuration.
78
+ **Dual / Teufel**
79
+ - Dual IR 3a
80
+ - Teufel 3sixty
44
81
 
45
- ### Example configuration
82
+ ### Important note
46
83
 
47
- {
48
- "accessory": "frontier-silicon",
49
- "name": "Living Room Radio",
50
- "ip": "192.168.1.50",
51
- "pin": "1234",
52
- "pollIntervalSeconds": 5,
53
- "enableVolume": true
54
- }
84
+ This list is **not complete**.
85
+ Many internet and DAB+ radios use the same Frontier Silicon platform under different brand names.
55
86
 
56
- ## Configuration options
87
+ If your radio responds to the following URL:
88
+
89
+ http://<radio-ip>/fsapi/GET/netRemote.sys.info.friendlyName
90
+
91
+ then it is very likely compatible with this plugin.
92
+
93
+ If you successfully use this plugin with a radio that is not listed above, please consider opening an issue or pull request to help extend this list.
94
+
95
+
96
+ ## Installation
57
97
 
58
- name
59
- Displayed name in HomeKit
98
+ ### Recommended: Install via Homebridge UI
60
99
 
61
- ip
62
- IP address of the radio
100
+ This plugin can be installed and configured entirely through the **Homebridge Config UI X**.
101
+ No manual JSON editing is required.
63
102
 
64
- pin
65
- FSAPI PIN code
66
- Default is 1234 on most devices
103
+ 1. Open the Homebridge UI in your browser
104
+ 2. Go to **Plugins**
105
+ 3. Search for **homebridge-frontier-silicon-plugin**
106
+ 4. Click **Install**
107
+ 5. After installation, go to **Settings → Accessories**
108
+ 6. Add a new accessory and select **Frontier Silicon Radio**
109
+ 7. Fill in the required fields:
110
+ - Name
111
+ - IP address of the radio
112
+ - FSAPI PIN (default is usually `1234`)
113
+ 8. (Optional) Configure volume options and station presets via the UI
114
+ 9. Save the configuration and **restart Homebridge**
67
115
 
68
- pollIntervalSeconds
69
- Polling interval in seconds
70
- Minimum value is 2
71
- Default value is 5
116
+ After the restart, the radio and its controls will appear in the Apple Home app.
72
117
 
73
- enableVolume
74
- Enable volume control
75
- Default is true
118
+ ---
76
119
 
77
120
  ## HomeKit behaviour
78
121
 
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.3",
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",