homebridge-frontier-silicon-plugin 1.0.1 → 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 +38 -0
- package/index.js +48 -17
- package/package.json +1 -1
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.
|
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
|
-
|
|
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", (
|
|
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
|
|
100
|
-
this.
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
113
|
-
this.
|
|
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
|
|
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
|
|
140
|
-
if (this.
|
|
141
|
-
this.
|
|
142
|
-
|
|
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
|
|
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,6 +179,7 @@ FsApiClient.prototype.getPower = async function () {
|
|
|
169
179
|
|
|
170
180
|
FsApiClient.prototype.setPower = async function (on) {
|
|
171
181
|
const v = on ? 1 : 0;
|
|
182
|
+
// Important: SET must start query with ?value= so pin can be appended with &
|
|
172
183
|
await this.fetchText("/fsapi/SET/netRemote.sys.power?value=" + v);
|
|
173
184
|
};
|
|
174
185
|
|
|
@@ -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);
|
|
194
|
+
// Important: SET must start query with ?value= so pin can be appended with &
|
|
183
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 =
|
|
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