homebridge-frontier-silicon-plugin 1.0.2 → 1.1.0

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 CHANGED
@@ -4,6 +4,20 @@ 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.1.0] – 2025-12-28
8
+
9
+ ### Added
10
+ 1. Added an optional native HomeKit Speaker service for volume control using the Volume characteristic.
11
+ 2. Added configuration switches to expose the Speaker service and the Apple Home slider independently.
12
+
13
+ ### Changed
14
+ 1. Volume can now be controlled through either the Speaker service, the Apple Home slider service, or both.
15
+
16
+ ### Improved
17
+ 1. Better compatibility with third party HomeKit apps via the Speaker service.
18
+ 2. Apple Home usability retained via the Brightness based slider.
19
+ 3. Non linear volume mapping remains for precise low volume control.
20
+
7
21
  ## [1.0.2] – 2025-12-28
8
22
 
9
23
  ### Changed
@@ -1,29 +1,71 @@
1
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"
2
+ "pluginAlias": "frontier-silicon",
3
+ "pluginType": "accessory",
4
+ "singular": false,
5
+ "headerDisplay": "Frontier Silicon FSAPI radios",
6
+ "footerDisplay": "Tip: most devices use PIN 1234. If the device is offline, Homebridge will keep the last known state and recover automatically.",
7
+ "schema": {
8
+ "type": "object",
9
+ "required": ["name", "ip"],
10
+ "properties": {
11
+ "name": {
12
+ "title": "Name",
13
+ "type": "string",
14
+ "default": "Living Room Radio",
15
+ "description": "The name shown in HomeKit."
16
+ },
17
+ "ip": {
18
+ "title": "IP Address",
19
+ "type": "string",
20
+ "default": "192.168.1.50",
21
+ "description": "The IP address of the radio on your network.",
22
+ "pattern": "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$"
23
+ },
24
+ "pin": {
25
+ "title": "FSAPI PIN",
26
+ "type": "string",
27
+ "default": "1234",
28
+ "description": "FSAPI PIN for the radio. Default is usually 1234."
29
+ },
30
+ "pollIntervalSeconds": {
31
+ "title": "Poll interval (seconds)",
32
+ "type": "integer",
33
+ "default": 5,
34
+ "minimum": 2,
35
+ "maximum": 120,
36
+ "description": "How often to poll the device for updates."
37
+ },
38
+ "enableVolume": {
39
+ "title": "Enable volume control",
40
+ "type": "boolean",
41
+ "default": true,
42
+ "description": "Enable volume support."
43
+ },
44
+ "exposeSpeakerService": {
45
+ "title": "Expose Speaker service",
46
+ "type": "boolean",
47
+ "default": true,
48
+ "description": "Adds a native HomeKit Speaker service using the Volume characteristic. Some apps show this, Apple Home often does not."
49
+ },
50
+ "exposeVolumeSlider": {
51
+ "title": "Expose Apple Home volume slider",
52
+ "type": "boolean",
53
+ "default": true,
54
+ "description": "Adds a Lightbulb Brightness slider for volume control that is visible in the Apple Home app."
55
+ }
56
+ }
21
57
  },
22
- "author": {
23
- "name": "Boike Damhuis"
24
- },
25
- "engines": {
26
- "node": ">=18.0.0",
27
- "homebridge": ">=1.6.0"
58
+ "form": [
59
+ "name",
60
+ "ip",
61
+ "pin",
62
+ "pollIntervalSeconds",
63
+ "enableVolume",
64
+ "exposeSpeakerService",
65
+ "exposeVolumeSlider"
66
+ ],
67
+ "display": {
68
+ "name": "Frontier Silicon Radio",
69
+ "description": "Control power and volume via FSAPI."
28
70
  }
29
71
  }
package/index.js CHANGED
@@ -21,11 +21,18 @@ function FrontierSiliconAccessory(log, config) {
21
21
  this.ip = config.ip;
22
22
  this.pin = String(config.pin ?? "1234");
23
23
  this.pollIntervalSeconds = Number(config.pollIntervalSeconds ?? 5);
24
+
24
25
  this.enableVolume = config.enableVolume !== false;
25
26
 
27
+ // In 1.1.0 we expose both by default
28
+ // Speaker service is for correct semantics
29
+ // Lightbulb slider is for Apple Home app usability
30
+ this.exposeSpeakerService = config.exposeSpeakerService !== false;
31
+ this.exposeVolumeSlider = config.exposeVolumeSlider !== false;
32
+
26
33
  this.lastKnownPower = null;
27
34
 
28
- // This stores the last known radio volume (0..100, device scale)
35
+ // Store last known RADIO volume on device scale 0..100
29
36
  this.lastKnownRadioVolume = null;
30
37
 
31
38
  if (!this.ip) {
@@ -38,39 +45,62 @@ function FrontierSiliconAccessory(log, config) {
38
45
  log: this.log
39
46
  });
40
47
 
41
- this.switchService = new Service.Switch(this.name);
48
+ this.informationService = new Service.AccessoryInformation()
49
+ .setCharacteristic(Characteristic.Manufacturer, "Frontier Silicon")
50
+ .setCharacteristic(Characteristic.Model, "FSAPI Radio")
51
+ .setCharacteristic(Characteristic.SerialNumber, this.ip || "unknown");
42
52
 
53
+ // Power as Switch
54
+ this.switchService = new Service.Switch(this.name);
43
55
  this.switchService
44
56
  .getCharacteristic(Characteristic.On)
45
57
  .on("get", this.handleGetPower.bind(this))
46
58
  .on("set", this.handleSetPower.bind(this));
47
59
 
48
60
  if (this.enableVolume) {
49
- // Volume is exposed as a separate slider using Lightbulb Brightness
50
- this.volumeService = new Service.Lightbulb(this.name + " Volume");
51
-
52
- this.volumeService
53
- .getCharacteristic(Characteristic.On)
54
- .on("get", (cb) => cb(null, true))
55
- .on("set", (_val, cb) => cb(null));
56
-
57
- this.volumeService
58
- .getCharacteristic(Characteristic.Brightness)
59
- .on("get", this.handleGetVolume.bind(this))
60
- .on("set", this.handleSetVolume.bind(this));
61
- }
61
+ if (this.exposeSpeakerService) {
62
+ this.speakerService = new Service.Speaker(this.name + " Speaker");
63
+
64
+ this.speakerService
65
+ .getCharacteristic(Characteristic.Volume)
66
+ .on("get", this.handleGetVolume.bind(this))
67
+ .on("set", this.handleSetVolume.bind(this));
68
+
69
+ // Optional mute placeholder, kept non functional by design
70
+ // Some radios support mute through FSAPI, but that endpoint differs per model
71
+ if (Characteristic.Mute) {
72
+ this.speakerService
73
+ .getCharacteristic(Characteristic.Mute)
74
+ .on("get", (cb) => cb(null, false))
75
+ .on("set", (_val, cb) => cb(null));
76
+ }
77
+ }
62
78
 
63
- this.informationService = new Service.AccessoryInformation()
64
- .setCharacteristic(Characteristic.Manufacturer, "Frontier Silicon")
65
- .setCharacteristic(Characteristic.Model, "FSAPI Radio")
66
- .setCharacteristic(Characteristic.SerialNumber, this.ip || "unknown");
79
+ if (this.exposeVolumeSlider) {
80
+ // Slider that works in Apple Home app
81
+ this.volumeSliderService = new Service.Lightbulb(this.name + " Volume");
82
+
83
+ this.volumeSliderService
84
+ .getCharacteristic(Characteristic.On)
85
+ .on("get", (cb) => cb(null, true))
86
+ .on("set", (_val, cb) => cb(null));
87
+
88
+ this.volumeSliderService
89
+ .getCharacteristic(Characteristic.Brightness)
90
+ .on("get", this.handleGetVolume.bind(this))
91
+ .on("set", this.handleSetVolume.bind(this));
92
+ }
93
+ }
67
94
 
68
95
  this.startPolling();
69
96
  }
70
97
 
71
98
  FrontierSiliconAccessory.prototype.getServices = function () {
72
99
  const services = [this.informationService, this.switchService];
73
- if (this.enableVolume && this.volumeService) services.push(this.volumeService);
100
+
101
+ if (this.enableVolume && this.speakerService) services.push(this.speakerService);
102
+ if (this.enableVolume && this.volumeSliderService) services.push(this.volumeSliderService);
103
+
74
104
  return services;
75
105
  };
76
106
 
@@ -113,7 +143,6 @@ FrontierSiliconAccessory.prototype.handleGetVolume = async function (callback) {
113
143
  };
114
144
 
115
145
  FrontierSiliconAccessory.prototype.handleSetVolume = async function (value, callback) {
116
- // Non linear mapping so low slider values are much softer
117
146
  const radioVol = homekitToRadioVolume(value);
118
147
 
119
148
  try {
@@ -142,15 +171,26 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
142
171
  if (this.log.debug) this.log.debug("Polling power failed.", toMsg(err));
143
172
  }
144
173
 
145
- if (this.enableVolume && this.volumeService) {
174
+ if (this.enableVolume) {
146
175
  try {
147
176
  const radioVol = await this.client.getVolume();
177
+
148
178
  if (this.lastKnownRadioVolume !== radioVol) {
149
179
  this.lastKnownRadioVolume = radioVol;
180
+
150
181
  const homekitVol = radioToHomekitVolume(radioVol);
151
- this.volumeService
152
- .getCharacteristic(Characteristic.Brightness)
153
- .updateValue(homekitVol);
182
+
183
+ if (this.speakerService) {
184
+ this.speakerService
185
+ .getCharacteristic(Characteristic.Volume)
186
+ .updateValue(homekitVol);
187
+ }
188
+
189
+ if (this.volumeSliderService) {
190
+ this.volumeSliderService
191
+ .getCharacteristic(Characteristic.Brightness)
192
+ .updateValue(homekitVol);
193
+ }
154
194
  }
155
195
  } catch (err) {
156
196
  if (this.log.debug) this.log.debug("Polling volume failed.", toMsg(err));
@@ -179,7 +219,6 @@ FsApiClient.prototype.getPower = async function () {
179
219
 
180
220
  FsApiClient.prototype.setPower = async function (on) {
181
221
  const v = on ? 1 : 0;
182
- // Important: SET must start query with ?value= so pin can be appended with &
183
222
  await this.fetchText("/fsapi/SET/netRemote.sys.power?value=" + v);
184
223
  };
185
224
 
@@ -191,18 +230,12 @@ FsApiClient.prototype.getVolume = async function () {
191
230
 
192
231
  FsApiClient.prototype.setVolume = async function (volume) {
193
232
  const v = clampInt(Number(volume), 0, 100);
194
- // Important: SET must start query with ?value= so pin can be appended with &
195
233
  await this.fetchText("/fsapi/SET/netRemote.sys.audio.volume?value=" + v);
196
234
  };
197
235
 
198
236
  FsApiClient.prototype.fetchText = async function (pathAndQuery) {
199
237
  const joiner = pathAndQuery.includes("?") ? "&" : "?";
200
- const url =
201
- this.baseUrl +
202
- pathAndQuery +
203
- joiner +
204
- "pin=" +
205
- encodeURIComponent(this.pin);
238
+ const url = this.baseUrl + pathAndQuery + joiner + "pin=" + encodeURIComponent(this.pin);
206
239
 
207
240
  const controller = new AbortController();
208
241
  const t = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -260,10 +293,6 @@ function toMsg(err) {
260
293
  return String(err);
261
294
  }
262
295
 
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
296
  function homekitToRadioVolume(homekitValue) {
268
297
  const x = clampInt(Number(homekitValue), 0, 100) / 100;
269
298
  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.0.2",
3
+ "version": "1.1.0",
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",