homebridge-frontier-silicon-plugin 1.0.3 → 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
@@ -6,15 +6,17 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
6
6
 
7
7
  ## [1.1.0] – 2025-12-28
8
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
+
9
13
  ### Changed
10
- - Replaced the Lightbulb Brightness based volume control with a native HomeKit **Speaker** service.
11
- - Volume is now exposed using the official `Volume` characteristic.
12
- - Power and volume are logically separated, matching HomeKit audio device expectations.
14
+ 1. Volume can now be controlled through either the Speaker service, the Apple Home slider service, or both.
13
15
 
14
16
  ### Improved
15
- - More natural HomeKit UI for audio devices.
16
- - Better compatibility with HomeKit audio controls and third-party Home apps.
17
- - Non-linear volume mapping retained for precise low-volume control.
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.
18
20
 
19
21
  ## [1.0.2] – 2025-12-28
20
22
 
@@ -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
@@ -22,12 +22,17 @@ function FrontierSiliconAccessory(log, config) {
22
22
  this.pin = String(config.pin ?? "1234");
23
23
  this.pollIntervalSeconds = Number(config.pollIntervalSeconds ?? 5);
24
24
 
25
- // Keep volume enabled by default. This now uses a Speaker service.
26
25
  this.enableVolume = config.enableVolume !== false;
27
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
+
28
33
  this.lastKnownPower = null;
29
34
 
30
- // Store last known RADIO volume in device scale 0..100
35
+ // Store last known RADIO volume on device scale 0..100
31
36
  this.lastKnownRadioVolume = null;
32
37
 
33
38
  if (!this.ip) {
@@ -45,31 +50,45 @@ function FrontierSiliconAccessory(log, config) {
45
50
  .setCharacteristic(Characteristic.Model, "FSAPI Radio")
46
51
  .setCharacteristic(Characteristic.SerialNumber, this.ip || "unknown");
47
52
 
48
- // Power as Switch (kept as-is for maximum Home app compatibility)
53
+ // Power as Switch
49
54
  this.switchService = new Service.Switch(this.name);
50
55
  this.switchService
51
56
  .getCharacteristic(Characteristic.On)
52
57
  .on("get", this.handleGetPower.bind(this))
53
58
  .on("set", this.handleSetPower.bind(this));
54
59
 
55
- // Speaker service for volume (and optional mute)
56
60
  if (this.enableVolume) {
57
- this.speakerService = new Service.Speaker(this.name + " Speaker");
58
-
59
- // Volume characteristic uses HomeKit scale 0..100
60
- this.speakerService
61
- .getCharacteristic(Characteristic.Volume)
62
- .on("get", this.handleGetVolume.bind(this))
63
- .on("set", this.handleSetVolume.bind(this));
64
-
65
- // Optional mute support placeholder:
66
- // If you want real mute later, we can map this to an FSAPI endpoint if available.
67
- // For now, we expose mute but keep it non-functional (always false) to avoid confusion.
68
- if (Characteristic.Mute) {
61
+ if (this.exposeSpeakerService) {
62
+ this.speakerService = new Service.Speaker(this.name + " Speaker");
63
+
69
64
  this.speakerService
70
- .getCharacteristic(Characteristic.Mute)
71
- .on("get", (cb) => cb(null, false))
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
+ }
78
+
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))
72
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));
73
92
  }
74
93
  }
75
94
 
@@ -78,7 +97,10 @@ function FrontierSiliconAccessory(log, config) {
78
97
 
79
98
  FrontierSiliconAccessory.prototype.getServices = function () {
80
99
  const services = [this.informationService, this.switchService];
100
+
81
101
  if (this.enableVolume && this.speakerService) services.push(this.speakerService);
102
+ if (this.enableVolume && this.volumeSliderService) services.push(this.volumeSliderService);
103
+
82
104
  return services;
83
105
  };
84
106
 
@@ -121,7 +143,6 @@ FrontierSiliconAccessory.prototype.handleGetVolume = async function (callback) {
121
143
  };
122
144
 
123
145
  FrontierSiliconAccessory.prototype.handleSetVolume = async function (value, callback) {
124
- // Non linear mapping so low slider values are much softer
125
146
  const radioVol = homekitToRadioVolume(value);
126
147
 
127
148
  try {
@@ -150,15 +171,26 @@ FrontierSiliconAccessory.prototype.startPolling = function () {
150
171
  if (this.log.debug) this.log.debug("Polling power failed.", toMsg(err));
151
172
  }
152
173
 
153
- if (this.enableVolume && this.speakerService) {
174
+ if (this.enableVolume) {
154
175
  try {
155
176
  const radioVol = await this.client.getVolume();
177
+
156
178
  if (this.lastKnownRadioVolume !== radioVol) {
157
179
  this.lastKnownRadioVolume = radioVol;
180
+
158
181
  const homekitVol = radioToHomekitVolume(radioVol);
159
- this.speakerService
160
- .getCharacteristic(Characteristic.Volume)
161
- .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
+ }
162
194
  }
163
195
  } catch (err) {
164
196
  if (this.log.debug) this.log.debug("Polling volume failed.", toMsg(err));
@@ -203,12 +235,7 @@ FsApiClient.prototype.setVolume = async function (volume) {
203
235
 
204
236
  FsApiClient.prototype.fetchText = async function (pathAndQuery) {
205
237
  const joiner = pathAndQuery.includes("?") ? "&" : "?";
206
- const url =
207
- this.baseUrl +
208
- pathAndQuery +
209
- joiner +
210
- "pin=" +
211
- encodeURIComponent(this.pin);
238
+ const url = this.baseUrl + pathAndQuery + joiner + "pin=" + encodeURIComponent(this.pin);
212
239
 
213
240
  const controller = new AbortController();
214
241
  const t = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -266,10 +293,6 @@ function toMsg(err) {
266
293
  return String(err);
267
294
  }
268
295
 
269
- // Non linear volume mapping
270
- // HomeKit slider 0..100 is mapped to device volume 0..100
271
- // Low slider values become much softer, high end remains reachable
272
-
273
296
  function homekitToRadioVolume(homekitValue) {
274
297
  const x = clampInt(Number(homekitValue), 0, 100) / 100;
275
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.3",
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",