homebridge-frontier-silicon-plugin 1.0.0 → 1.0.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 ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on Keep a Changelog, and this project follows Semantic Versioning.
6
+
7
+ ## [1.0.2] – 2025-12-28
8
+
9
+ ### Changed
10
+ - Improved volume handling using a non-linear volume curve.
11
+ - Low volume levels are now significantly softer, allowing precise control at the bottom of the HomeKit slider.
12
+ - Higher volume levels ramp up faster to maintain full output range.
13
+
14
+ ### Improved
15
+ - Volume control now feels more natural and audio-appropriate.
16
+ - Better alignment between HomeKit slider behaviour and perceived loudness on Frontier Silicon radios.
17
+ - Overall usability of volume control in daily use.
18
+
19
+ ## [1.0.1] – 2025-12-28
20
+
21
+ ### Fixed
22
+ - Fixed FSAPI SET request formatting so power and volume changes are correctly applied to the radio.
23
+ - Resolved issue where HomeKit could read state changes but not write them back to the device.
24
+ - Improved reliability of write operations across Frontier Silicon devices.
25
+
26
+ ## [1.0.0] – 2025-12-28
27
+
28
+ ### Added
29
+ - Initial stable release of the Homebridge Frontier Silicon plugin.
30
+ - Power control via HomeKit using native FSAPI communication.
31
+ - Volume control with safe polling and automatic recovery when the device is unreachable.
32
+ - Configurable polling interval.
33
+ - Direct HTTP communication without external or native dependencies.
34
+
35
+ ### Fixed
36
+ - Eliminated crashes when the radio becomes temporarily unreachable.
37
+ - Removed dependency on legacy wifiradio and request modules.
38
+ - Replaced legacy polling mechanisms with safe async polling.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "homebridge-frontier-silicon-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Homebridge plugin for Frontier Silicon FSAPI devices, power and volume with safe polling",
5
+ "license": "ISC",
6
+ "main": "index.js",
7
+ "keywords": [
8
+ "homebridge-pluginconfig.schema.json",
9
+ "homebridge",
10
+ "frontier",
11
+ "silicon",
12
+ "fsapi",
13
+ "internet-radio"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/boikedamhuis/homebridge-frontier-silicon"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/boikedamhuis/homebridge-frontier-silicon/issues"
21
+ },
22
+ "author": {
23
+ "name": "Boike Damhuis"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0",
27
+ "homebridge": ">=1.6.0"
28
+ }
29
+ }
package/index.js CHANGED
@@ -24,7 +24,9 @@ function FrontierSiliconAccessory(log, config) {
24
24
  this.enableVolume = config.enableVolume !== false;
25
25
 
26
26
  this.lastKnownPower = null;
27
- this.lastKnownVolume = null;
27
+
28
+ // This stores the last known radio volume (0..100, device scale)
29
+ this.lastKnownRadioVolume = null;
28
30
 
29
31
  if (!this.ip) {
30
32
  this.log.warn("No ip configured, accessory will not work.");
@@ -44,11 +46,13 @@ function FrontierSiliconAccessory(log, config) {
44
46
  .on("set", this.handleSetPower.bind(this));
45
47
 
46
48
  if (this.enableVolume) {
49
+ // Volume is exposed as a separate slider using Lightbulb Brightness
47
50
  this.volumeService = new Service.Lightbulb(this.name + " Volume");
51
+
48
52
  this.volumeService
49
53
  .getCharacteristic(Characteristic.On)
50
54
  .on("get", (cb) => cb(null, true))
51
- .on("set", (val, cb) => cb(null));
55
+ .on("set", (_val, cb) => cb(null));
52
56
 
53
57
  this.volumeService
54
58
  .getCharacteristic(Characteristic.Brightness)
@@ -96,21 +100,25 @@ FrontierSiliconAccessory.prototype.handleSetPower = async function (value, callb
96
100
 
97
101
  FrontierSiliconAccessory.prototype.handleGetVolume = async function (callback) {
98
102
  try {
99
- const vol = await this.client.getVolume();
100
- this.lastKnownVolume = vol;
101
- callback(null, vol);
103
+ const radioVol = await this.client.getVolume();
104
+ this.lastKnownRadioVolume = radioVol;
105
+
106
+ const homekitVol = radioToHomekitVolume(radioVol);
107
+ callback(null, homekitVol);
102
108
  } catch (err) {
103
109
  this.log.warn("Volume get failed, returning last known level.", toMsg(err));
104
- callback(null, this.lastKnownVolume ?? 0);
110
+ const fallbackRadio = this.lastKnownRadioVolume ?? 0;
111
+ callback(null, radioToHomekitVolume(fallbackRadio));
105
112
  }
106
113
  };
107
114
 
108
115
  FrontierSiliconAccessory.prototype.handleSetVolume = async function (value, callback) {
109
- const vol = clampInt(Number(value), 0, 100);
116
+ // Non linear mapping so low slider values are much softer
117
+ const radioVol = homekitToRadioVolume(value);
110
118
 
111
119
  try {
112
- await this.client.setVolume(vol);
113
- this.lastKnownVolume = vol;
120
+ await this.client.setVolume(radioVol);
121
+ this.lastKnownRadioVolume = radioVol;
114
122
  callback(null);
115
123
  } catch (err) {
116
124
  this.log.warn("Volume set failed, keeping last known level.", toMsg(err));
@@ -131,24 +139,26 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
131
139
  this.switchService.getCharacteristic(Characteristic.On).updateValue(power);
132
140
  }
133
141
  } catch (err) {
134
- this.log.debug ? this.log.debug("Polling power failed.", toMsg(err)) : this.log("Polling power failed.");
142
+ if (this.log.debug) this.log.debug("Polling power failed.", toMsg(err));
135
143
  }
136
144
 
137
145
  if (this.enableVolume && this.volumeService) {
138
146
  try {
139
- const vol = await this.client.getVolume();
140
- if (this.lastKnownVolume !== vol) {
141
- this.lastKnownVolume = vol;
142
- this.volumeService.getCharacteristic(Characteristic.Brightness).updateValue(vol);
147
+ const radioVol = await this.client.getVolume();
148
+ if (this.lastKnownRadioVolume !== radioVol) {
149
+ this.lastKnownRadioVolume = radioVol;
150
+ const homekitVol = radioToHomekitVolume(radioVol);
151
+ this.volumeService
152
+ .getCharacteristic(Characteristic.Brightness)
153
+ .updateValue(homekitVol);
143
154
  }
144
155
  } catch (err) {
145
- this.log.debug ? this.log.debug("Polling volume failed.", toMsg(err)) : this.log("Polling volume failed.");
156
+ if (this.log.debug) this.log.debug("Polling volume failed.", toMsg(err));
146
157
  }
147
158
  }
148
159
  };
149
160
 
150
161
  tick();
151
-
152
162
  this.pollTimer = setInterval(tick, intervalMs);
153
163
  };
154
164
 
@@ -169,7 +179,8 @@ FsApiClient.prototype.getPower = async function () {
169
179
 
170
180
  FsApiClient.prototype.setPower = async function (on) {
171
181
  const v = on ? 1 : 0;
172
- await this.fetchText("/fsapi/SET/netRemote.sys.power&value=" + v);
182
+ // Important: SET must start query with ?value= so pin can be appended with &
183
+ await this.fetchText("/fsapi/SET/netRemote.sys.power?value=" + v);
173
184
  };
174
185
 
175
186
  FsApiClient.prototype.getVolume = async function () {
@@ -180,12 +191,18 @@ FsApiClient.prototype.getVolume = async function () {
180
191
 
181
192
  FsApiClient.prototype.setVolume = async function (volume) {
182
193
  const v = clampInt(Number(volume), 0, 100);
183
- await this.fetchText("/fsapi/SET/netRemote.sys.audio.volume&value=" + v);
194
+ // Important: SET must start query with ?value= so pin can be appended with &
195
+ await this.fetchText("/fsapi/SET/netRemote.sys.audio.volume?value=" + v);
184
196
  };
185
197
 
186
198
  FsApiClient.prototype.fetchText = async function (pathAndQuery) {
187
199
  const joiner = pathAndQuery.includes("?") ? "&" : "?";
188
- const url = this.baseUrl + pathAndQuery + joiner + "pin=" + encodeURIComponent(this.pin);
200
+ const url =
201
+ this.baseUrl +
202
+ pathAndQuery +
203
+ joiner +
204
+ "pin=" +
205
+ encodeURIComponent(this.pin);
189
206
 
190
207
  const controller = new AbortController();
191
208
  const t = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -242,3 +259,17 @@ function toMsg(err) {
242
259
  if (err instanceof Error) return err.message;
243
260
  return String(err);
244
261
  }
262
+
263
+ // Non linear volume mapping
264
+ // HomeKit slider 0..100 is mapped to device volume 0..100
265
+ // Low slider values become much softer, high end remains reachable
266
+
267
+ function homekitToRadioVolume(homekitValue) {
268
+ const x = clampInt(Number(homekitValue), 0, 100) / 100;
269
+ return clampInt(Math.round(Math.pow(x, 2) * 100), 0, 100);
270
+ }
271
+
272
+ function radioToHomekitVolume(radioValue) {
273
+ const x = clampInt(Number(radioValue), 0, 100) / 100;
274
+ return clampInt(Math.round(Math.sqrt(x) * 100), 0, 100);
275
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-frontier-silicon-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",