homebridge-pax-calima 2.1.0 → 2.3.1
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/index.js +219 -160
- package/package.json +4 -3
package/index.js
CHANGED
|
@@ -1,224 +1,283 @@
|
|
|
1
1
|
const noble = require('@abandonware/noble');
|
|
2
2
|
|
|
3
|
-
let Service, Characteristic,
|
|
3
|
+
let Service, Characteristic, Categories, API, PlatformAccessory;
|
|
4
|
+
|
|
5
|
+
const UUID_AUTH = '4cad343a-209a-40b7-b911-4d9b3df569b2';
|
|
6
|
+
const UUID_STATUS = '528b80e8-c47a-4c0a-bdf1-916a7748f412'; // read: RPM + temp
|
|
7
|
+
const UUID_SENSOR = 'a22eae12-dba8-49f3-9c69-1721dcff1d96'; // read: humidity + lux
|
|
8
|
+
const UUID_CONTROL = '1488a757-35bc-4ec8-9a6b-9ecf1502778e'; // read/write: speed config
|
|
9
|
+
|
|
10
|
+
const MAC_PREFIX = '58:2b:db';
|
|
11
|
+
const POLL_MS = 10000;
|
|
12
|
+
const SCAN_TIMEOUT_MS = 15000;
|
|
4
13
|
|
|
5
14
|
module.exports = (homebridge) => {
|
|
6
|
-
Service
|
|
7
|
-
Characteristic
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
API = homebridge;
|
|
15
|
+
Service = homebridge.hap.Service;
|
|
16
|
+
Characteristic = homebridge.hap.Characteristic;
|
|
17
|
+
Categories = homebridge.hap.Categories;
|
|
18
|
+
API = homebridge;
|
|
11
19
|
PlatformAccessory = homebridge.platformAccessory;
|
|
12
|
-
|
|
13
20
|
homebridge.registerPlatform('homebridge-pax-calima', 'PaxCalima', PaxCalimaPlatform, true);
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
class PaxCalimaPlatform {
|
|
17
24
|
constructor(log, config, api) {
|
|
18
|
-
this.log
|
|
19
|
-
this.config
|
|
20
|
-
this.api
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
25
|
+
this.log = log;
|
|
26
|
+
this.config = config || {};
|
|
27
|
+
this.api = api;
|
|
28
|
+
this.registeredMACs = new Set();
|
|
29
|
+
this.discoveryStarted = false;
|
|
30
|
+
this.cachedAccessories = [];
|
|
31
|
+
|
|
32
|
+
this.log.info('PaxCalima platform initialised');
|
|
33
|
+
|
|
34
|
+
this.api.on('didFinishLaunching', () => {
|
|
35
|
+
this.log.info('Homebridge finished launching - starting BLE');
|
|
36
|
+
noble.on('stateChange', (state) => {
|
|
37
|
+
this.log.info('BLE state: ' + state);
|
|
38
|
+
if (state === 'poweredOn') this.startDiscovery();
|
|
39
|
+
});
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
this.log.info('BLE current state: ' + noble.state);
|
|
42
|
+
if (noble.state === 'poweredOn') this.startDiscovery();
|
|
43
|
+
}, 3000);
|
|
33
44
|
});
|
|
45
|
+
}
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
configureAccessory(accessory) {
|
|
48
|
+
const mac = accessory.context && accessory.context.mac;
|
|
49
|
+
this.log.info('Restoring cached accessory: ' + accessory.displayName);
|
|
50
|
+
if (mac) {
|
|
51
|
+
this.registeredMACs.add(mac);
|
|
52
|
+
this.cachedAccessories.push({ accessory, mac });
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
startDiscovery() {
|
|
42
|
-
this.
|
|
57
|
+
if (this.discoveryStarted) return;
|
|
58
|
+
this.discoveryStarted = true;
|
|
59
|
+
|
|
60
|
+
for (const { accessory, mac } of this.cachedAccessories) {
|
|
61
|
+
const pin = this.pinForMac(mac);
|
|
62
|
+
this.log.info('Starting BLE handler for cached fan: ' + mac);
|
|
63
|
+
new PaxCalimaAccessory(this.log, accessory, mac, pin);
|
|
64
|
+
}
|
|
65
|
+
this.cachedAccessories = [];
|
|
66
|
+
|
|
67
|
+
this.log.info('Starting BLE discovery scan...');
|
|
43
68
|
noble.startScanning([], false);
|
|
44
69
|
|
|
45
70
|
noble.on('discover', (peripheral) => {
|
|
46
71
|
const mac = peripheral.address.toLowerCase();
|
|
47
|
-
if (mac.startsWith(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
if (!mac.startsWith(MAC_PREFIX)) return;
|
|
73
|
+
if (this.registeredMACs.has(mac)) return;
|
|
74
|
+
this.registeredMACs.add(mac);
|
|
75
|
+
this.log.info('Discovered new fan: ' + mac);
|
|
76
|
+
const name = this.nameForMac(mac);
|
|
77
|
+
const pin = this.pinForMac(mac);
|
|
78
|
+
const uuid = API.hap.uuid.generate('pax-svara-' + mac);
|
|
79
|
+
const hbAcc = new PlatformAccessory(name, uuid, Categories.FAN);
|
|
80
|
+
hbAcc.context.mac = mac;
|
|
81
|
+
new PaxCalimaAccessory(this.log, hbAcc, mac, pin);
|
|
82
|
+
this.api.registerPlatformAccessories('homebridge-pax-calima', 'PaxCalima', [hbAcc]);
|
|
53
83
|
});
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
86
|
+
fanConfig(mac) { return (this.config.fans || []).find(f => f.mac && f.mac.toLowerCase() === mac) || {}; }
|
|
87
|
+
pinForMac(mac) { return this.fanConfig(mac).pin || '012345'; }
|
|
88
|
+
nameForMac(mac) { return this.fanConfig(mac).name || ('Vent-Axia Svara ' + mac.slice(-5).toUpperCase()); }
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
class PaxCalimaAccessory {
|
|
62
|
-
constructor(log, mac,
|
|
63
|
-
this.log
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.pin
|
|
67
|
-
|
|
68
|
-
const uuid = API.hap.uuid.generate(`pax-svara-${this.mac}`);
|
|
69
|
-
this.accessory = new PlatformAccessory(this.name, uuid, Categories.FAN);
|
|
92
|
+
constructor(log, accessory, mac, pin) {
|
|
93
|
+
this.log = log;
|
|
94
|
+
this.accessory = accessory;
|
|
95
|
+
this.mac = mac.toLowerCase();
|
|
96
|
+
this.pin = pin || '012345';
|
|
70
97
|
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
98
|
+
this.currentRPM = 0;
|
|
99
|
+
this.humidity = 0;
|
|
100
|
+
this.temperature = 20;
|
|
101
|
+
this.lightLevel = 0.0001;
|
|
102
|
+
this.silentHours = false;
|
|
73
103
|
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.temperature = 0;
|
|
77
|
-
this.lightLevel = 0;
|
|
78
|
-
this.currentMode = 0;
|
|
104
|
+
this.pollTimer = null;
|
|
105
|
+
this.polling = false;
|
|
79
106
|
|
|
80
107
|
this.setupServices();
|
|
81
|
-
this.
|
|
108
|
+
this.schedulePoll(0);
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
setupServices() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.fanService.
|
|
112
|
+
const n = this.accessory.displayName;
|
|
113
|
+
|
|
114
|
+
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
115
|
+
|| this.accessory.addService(new Service.Fanv2(n, 'fan'));
|
|
116
|
+
this.fanService.getCharacteristic(Characteristic.Active)
|
|
117
|
+
.onGet(() => 1);
|
|
88
118
|
this.fanService.getCharacteristic(Characteristic.RotationSpeed)
|
|
89
119
|
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
90
|
-
.onGet(() =>
|
|
91
|
-
.onSet((
|
|
92
|
-
|
|
93
|
-
// Modes
|
|
94
|
-
this.multiMode = new Service.Switch(`${this.name} - Multi Mode`);
|
|
95
|
-
this.multiMode.getCharacteristic(Characteristic.On)
|
|
96
|
-
.onGet(() => this.currentMode === 0)
|
|
97
|
-
.onSet((on) => this.setMode(0, on));
|
|
98
|
-
|
|
99
|
-
this.heatMode = new Service.Switch(`${this.name} - Heat Distribution`);
|
|
100
|
-
this.heatMode.getCharacteristic(Characteristic.On)
|
|
101
|
-
.onGet(() => this.currentMode === 4)
|
|
102
|
-
.onSet((on) => this.setMode(4, on));
|
|
103
|
-
|
|
104
|
-
// Boost Button
|
|
105
|
-
this.boostButton = new Service.StatelessProgrammableSwitch(`${this.name} - Boost`);
|
|
106
|
-
this.boostButton.getCharacteristic(Characteristic.ProgrammableSwitchEvent)
|
|
107
|
-
.onSet(() => this.triggerBoost());
|
|
108
|
-
|
|
109
|
-
// Silent Hours
|
|
110
|
-
this.silentService = new Service.Switch(`${this.name} - Silent Hours`);
|
|
111
|
-
this.silentService.getCharacteristic(Characteristic.On).onSet((on) => this.setSilentHours(on));
|
|
120
|
+
.onGet(() => this.rpmToPercent(this.currentRPM))
|
|
121
|
+
.onSet((v) => this.writeSpeed(v));
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.accessory.
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.accessory.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
this.boostButton = this.accessory.getService(Service.StatelessProgrammableSwitch)
|
|
124
|
+
|| this.accessory.addService(new Service.StatelessProgrammableSwitch(n + ' Boost', 'boost'));
|
|
125
|
+
this.boostButton.getCharacteristic(Characteristic.ProgrammableSwitchEvent)
|
|
126
|
+
.setProps({ validValues: [0] });
|
|
127
|
+
|
|
128
|
+
this.silentService = this.accessory.getServiceByUUIDAndSubType(Service.Switch, 'silent-hours')
|
|
129
|
+
|| this.accessory.addService(new Service.Switch(n + ' Silent Hours', 'silent-hours'));
|
|
130
|
+
this.silentService.getCharacteristic(Characteristic.On)
|
|
131
|
+
.onGet(() => this.silentHours)
|
|
132
|
+
.onSet((on) => this.setSilentHours(on));
|
|
133
|
+
|
|
134
|
+
this.humidityService = this.accessory.getService(Service.HumiditySensor)
|
|
135
|
+
|| this.accessory.addService(new Service.HumiditySensor(n + ' Humidity', 'humidity'));
|
|
136
|
+
this.humidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity)
|
|
137
|
+
.onGet(() => this.humidity);
|
|
138
|
+
|
|
139
|
+
this.tempService = this.accessory.getService(Service.TemperatureSensor)
|
|
140
|
+
|| this.accessory.addService(new Service.TemperatureSensor(n + ' Temperature', 'temperature'));
|
|
141
|
+
this.tempService.getCharacteristic(Characteristic.CurrentTemperature)
|
|
142
|
+
.onGet(() => this.temperature);
|
|
143
|
+
|
|
144
|
+
this.lightService = this.accessory.getService(Service.LightSensor)
|
|
145
|
+
|| this.accessory.addService(new Service.LightSensor(n + ' Light', 'light'));
|
|
146
|
+
this.lightService.getCharacteristic(Characteristic.CurrentAmbientLightLevel)
|
|
147
|
+
.onGet(() => Math.max(0.0001, this.lightLevel));
|
|
148
|
+
|
|
149
|
+
this.log.info('Services set up for: ' + n);
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.log.warn(`BLE state is ${noble.state} — waiting...`);
|
|
140
|
-
setTimeout(() => this.connectAndPoll(), 8000);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
152
|
+
schedulePoll(delayMs) {
|
|
153
|
+
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
154
|
+
this.pollTimer = setTimeout(() => this.poll(), delayMs);
|
|
155
|
+
}
|
|
143
156
|
|
|
157
|
+
async poll() {
|
|
158
|
+
if (this.polling) { this.schedulePoll(POLL_MS); return; }
|
|
159
|
+
this.polling = true;
|
|
160
|
+
let peripheral = null;
|
|
144
161
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
noble.startScanning([], false);
|
|
148
|
-
noble.on('discover', (periph) => {
|
|
149
|
-
if (periph.address.toLowerCase() === this.mac) {
|
|
150
|
-
clearTimeout(timeout);
|
|
151
|
-
noble.stopScanning();
|
|
152
|
-
resolve(periph);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
});
|
|
162
|
+
peripheral = await this.findPeripheral();
|
|
163
|
+
await peripheral.connectAsync();
|
|
156
164
|
|
|
157
|
-
await this.peripheral
|
|
158
|
-
this.connected = true;
|
|
159
|
-
this.log.info(`✅ Connected to ${this.mac}`);
|
|
160
|
-
|
|
161
|
-
// PIN Authentication
|
|
162
|
-
const authChar = await this.getCharacteristic('4cad343a-209a-40b7-b911-4d9b3df569b2');
|
|
165
|
+
const authChar = await this.getCharacteristic(peripheral, UUID_AUTH);
|
|
163
166
|
const pinBuf = Buffer.alloc(4);
|
|
164
|
-
pinBuf.writeUInt32LE(parseInt(this.pin), 0);
|
|
167
|
+
pinBuf.writeUInt32LE(parseInt(this.pin, 10), 0);
|
|
165
168
|
await authChar.writeAsync(pinBuf, true);
|
|
166
|
-
this.log.info('✅ PIN authentication successful');
|
|
167
169
|
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
170
|
+
const statusChar = await this.getCharacteristic(peripheral, UUID_STATUS);
|
|
171
|
+
const statusData = await statusChar.readAsync();
|
|
172
|
+
this.currentRPM = Math.max(0, statusData.readUInt16LE(6));
|
|
173
|
+
this.temperature = Math.round((statusData.readUInt16LE(2) / 6) * 10) / 10;
|
|
174
|
+
|
|
175
|
+
const sensorChar = await this.getCharacteristic(peripheral, UUID_SENSOR);
|
|
176
|
+
const sensorData = await sensorChar.readAsync();
|
|
177
|
+
this.humidity = Math.max(0, Math.min(100, sensorData.readUInt8(0) * 2));
|
|
178
|
+
this.lightLevel = Math.max(0.0001, sensorData.readUInt16BE(2));
|
|
179
|
+
|
|
180
|
+
// Also log the raw control characteristic on each poll for diagnostics
|
|
181
|
+
try {
|
|
182
|
+
const ctrlChar = await this.getCharacteristic(peripheral, UUID_CONTROL);
|
|
183
|
+
const ctrlData = await ctrlChar.readAsync();
|
|
184
|
+
this.log.info('CTRL raw: ' + Array.from(ctrlData).join(',') +
|
|
185
|
+
' | UInt16s: ' + ctrlData.readUInt16LE(0) + ',' +
|
|
186
|
+
ctrlData.readUInt16LE(2) + ',' + ctrlData.readUInt16LE(4));
|
|
187
|
+
} catch (_) {}
|
|
188
|
+
|
|
189
|
+
const speed = this.rpmToPercent(this.currentRPM);
|
|
190
|
+
this.fanService.getCharacteristic(Characteristic.RotationSpeed).updateValue(speed);
|
|
191
|
+
this.humidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity).updateValue(this.humidity);
|
|
192
|
+
this.tempService.getCharacteristic(Characteristic.CurrentTemperature).updateValue(this.temperature);
|
|
193
|
+
this.lightService.getCharacteristic(Characteristic.CurrentAmbientLightLevel).updateValue(this.lightLevel);
|
|
194
|
+
|
|
195
|
+
this.log.info('Status: RPM=' + this.currentRPM + ' Speed=' + speed +
|
|
196
|
+
'% Hum=' + this.humidity + '% Temp=' + this.temperature +
|
|
197
|
+
'C Light=' + this.lightLevel + 'lx');
|
|
198
|
+
|
|
199
|
+
} catch (err) {
|
|
200
|
+
this.log.warn('Poll error: ' + err.message);
|
|
201
|
+
} finally {
|
|
202
|
+
if (peripheral) { try { await peripheral.disconnectAsync(); } catch (_) {} }
|
|
203
|
+
this.polling = false;
|
|
204
|
+
this.schedulePoll(POLL_MS);
|
|
172
205
|
}
|
|
173
206
|
}
|
|
174
207
|
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
findPeripheral() {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const timeout = setTimeout(() => {
|
|
211
|
+
noble.removeListener('discover', onDiscover);
|
|
212
|
+
reject(new Error('Scan timeout for ' + this.mac));
|
|
213
|
+
}, SCAN_TIMEOUT_MS);
|
|
214
|
+
const onDiscover = (periph) => {
|
|
215
|
+
if (periph.address.toLowerCase() === this.mac) {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
noble.removeListener('discover', onDiscover);
|
|
218
|
+
resolve(periph);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
noble.on('discover', onDiscover);
|
|
222
|
+
noble.startScanning([], false);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getCharacteristic(peripheral, uuid) {
|
|
227
|
+
const stripped = uuid.replace(/-/g, '');
|
|
228
|
+
const services = await peripheral.discoverServicesAsync();
|
|
177
229
|
for (const service of services) {
|
|
178
|
-
const chars = await service.discoverCharacteristicsAsync(
|
|
179
|
-
|
|
230
|
+
const chars = await service.discoverCharacteristicsAsync();
|
|
231
|
+
for (const char of chars) {
|
|
232
|
+
if (char.uuid.replace(/-/g, '') === stripped) return char;
|
|
233
|
+
}
|
|
180
234
|
}
|
|
181
|
-
throw new Error(
|
|
235
|
+
throw new Error('Characteristic ' + uuid + ' not found');
|
|
182
236
|
}
|
|
183
237
|
|
|
184
|
-
async
|
|
185
|
-
|
|
238
|
+
async writeSpeed(percent) {
|
|
239
|
+
// Pause the poll cycle to avoid BLE conflicts during a write
|
|
240
|
+
if (this.pollTimer) { clearTimeout(this.pollTimer); this.pollTimer = null; }
|
|
186
241
|
|
|
242
|
+
let peripheral = null;
|
|
187
243
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
244
|
+
peripheral = await this.findPeripheral();
|
|
245
|
+
await peripheral.connectAsync();
|
|
190
246
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
247
|
+
const authChar = await this.getCharacteristic(peripheral, UUID_AUTH);
|
|
248
|
+
const pinBuf = Buffer.alloc(4);
|
|
249
|
+
pinBuf.writeUInt32LE(parseInt(this.pin, 10), 0);
|
|
250
|
+
await authChar.writeAsync(pinBuf, true);
|
|
195
251
|
|
|
196
|
-
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
this.
|
|
252
|
+
// Read current control config to preserve trickle/boost values
|
|
253
|
+
const ctrlChar = await this.getCharacteristic(peripheral, UUID_CONTROL);
|
|
254
|
+
const current = await ctrlChar.readAsync();
|
|
255
|
+
this.log.info('CTRL before write: ' + Array.from(current).join(','));
|
|
200
256
|
|
|
201
|
-
|
|
257
|
+
// Control characteristic layout (6 bytes, all UInt16LE):
|
|
258
|
+
// [0-1] trickle RPM, [2-3] normal RPM, [4-5] boost RPM
|
|
259
|
+
// We write the target RPM into the normal speed slot [2-3]
|
|
260
|
+
const targetRPM = Math.round((percent / 100) * 2400);
|
|
261
|
+
const buf = Buffer.from(current); // preserve existing trickle + boost values
|
|
262
|
+
buf.writeUInt16LE(targetRPM, 2); // set normal speed
|
|
263
|
+
await ctrlChar.writeAsync(buf, true);
|
|
264
|
+
|
|
265
|
+
this.log.info('Speed set to ' + percent + '% (' + targetRPM + ' RPM) — CTRL written: ' + Array.from(buf).join(','));
|
|
202
266
|
} catch (e) {
|
|
203
|
-
this.log.
|
|
267
|
+
this.log.error('writeSpeed failed: ' + e.message);
|
|
268
|
+
} finally {
|
|
269
|
+
if (peripheral) { try { await peripheral.disconnectAsync(); } catch (_) {} }
|
|
270
|
+
// Resume polling after write
|
|
271
|
+
this.schedulePoll(2000);
|
|
204
272
|
}
|
|
205
|
-
|
|
206
|
-
setTimeout(() => this.pollStatus(), 8000);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async setMode(modeValue, on) {
|
|
210
|
-
this.log.info(`Mode set to ${on ? (modeValue === 0 ? 'Multi' : 'Heat Distribution') : 'Default'}`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async triggerBoost() {
|
|
214
|
-
this.log.info('Boost button pressed — high speed activated');
|
|
215
273
|
}
|
|
216
274
|
|
|
217
275
|
async setSilentHours(on) {
|
|
218
|
-
this.
|
|
276
|
+
this.silentHours = on;
|
|
277
|
+
this.log.info('Silent Hours ' + (on ? 'enabled' : 'disabled'));
|
|
219
278
|
}
|
|
220
279
|
|
|
221
|
-
|
|
222
|
-
return
|
|
280
|
+
rpmToPercent(rpm) {
|
|
281
|
+
return Math.min(100, Math.max(0, Math.round(rpm / 25)));
|
|
223
282
|
}
|
|
224
283
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-pax-calima",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "Homebridge plugin for Pax Calima and Vent-Axia Svara Bluetooth bathroom fans",
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "Homebridge platform plugin for Pax Calima and Vent-Axia Svara Bluetooth LE bathroom extractor fans",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"homebridge-plugin",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"bathroom"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"homebridge": "^1.6.0 || ^2.0.0"
|
|
19
|
+
"homebridge": "^1.6.0 || ^2.0.0",
|
|
20
|
+
"node": ">=18.0.0"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@abandonware/noble": "1.9.2-26"
|