homebridge-blueair-plugin 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/.eslintignore +3 -0
- package/.gitattributes +3 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +176 -0
- package/README.md +99 -0
- package/branding/Blueair.png +0 -0
- package/branding/Homebridge_x_Blueair.svg +82 -0
- package/dist/accessory.d.ts +111 -0
- package/dist/accessory.d.ts.map +1 -0
- package/dist/accessory.js +821 -0
- package/dist/accessory.js.map +1 -0
- package/dist/api/BlueAirAwsApi.d.ts +79 -0
- package/dist/api/BlueAirAwsApi.d.ts.map +1 -0
- package/dist/api/BlueAirAwsApi.js +216 -0
- package/dist/api/BlueAirAwsApi.js.map +1 -0
- package/dist/api/Consts.d.ts +36 -0
- package/dist/api/Consts.d.ts.map +1 -0
- package/dist/api/Consts.js +52 -0
- package/dist/api/Consts.js.map +1 -0
- package/dist/api/GigyaApi.d.ts +19 -0
- package/dist/api/GigyaApi.d.ts.map +1 -0
- package/dist/api/GigyaApi.js +79 -0
- package/dist/api/GigyaApi.js.map +1 -0
- package/dist/constants.d.ts +40 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +146 -0
- package/dist/constants.js.map +1 -0
- package/dist/device.d.ts +49 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +136 -0
- package/dist/device.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +35 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +277 -0
- package/dist/platform.js.map +1 -0
- package/dist/utils.d.ts +96 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +98 -0
- package/dist/utils.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BlueAirAccessory = exports.BlueAirDevice = void 0;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
// Re-export BlueAirDevice for backward compatibility
|
|
7
|
+
var device_1 = require("./device");
|
|
8
|
+
Object.defineProperty(exports, "BlueAirDevice", { enumerable: true, get: function () { return device_1.BlueAirDevice; } });
|
|
9
|
+
class BlueAirAccessory {
|
|
10
|
+
constructor(platform, accessory, device, configDev, deviceType = "air-purifier") {
|
|
11
|
+
this.platform = platform;
|
|
12
|
+
this.accessory = accessory;
|
|
13
|
+
this.device = device;
|
|
14
|
+
this.configDev = configDev;
|
|
15
|
+
this.optionalServices = new Map();
|
|
16
|
+
this.humidityAutoControlEnabled = false;
|
|
17
|
+
this.lastManualOverride = 0;
|
|
18
|
+
this.lastAutoAdjust = 0;
|
|
19
|
+
this.loggedNoWritableTargetHumidity = false;
|
|
20
|
+
// Dynamic capabilities detected from device state keys
|
|
21
|
+
this.capabilities = {
|
|
22
|
+
hasBrightness: false,
|
|
23
|
+
hasNightLight: false,
|
|
24
|
+
hasNightMode: false,
|
|
25
|
+
hasAutoMode: false,
|
|
26
|
+
hasHumidity: false,
|
|
27
|
+
hasHumidityTarget: false,
|
|
28
|
+
hasWaterLevel: false,
|
|
29
|
+
hasTemperature: false,
|
|
30
|
+
hasAirQuality: false,
|
|
31
|
+
hasGermShield: false,
|
|
32
|
+
hasFilterUsage: false,
|
|
33
|
+
hasChildLock: false,
|
|
34
|
+
hasFanSpeed: false,
|
|
35
|
+
};
|
|
36
|
+
this.deviceType = deviceType;
|
|
37
|
+
// Initialize last brightness from device state
|
|
38
|
+
this.lastBrightness = this.device.state.brightness || 0;
|
|
39
|
+
// Initialize debouncers
|
|
40
|
+
this.fanSpeedDebouncer = new utils_1.Debouncer(constants_1.DEBOUNCE_DELAY_MS, this.executeFanSpeedChange.bind(this));
|
|
41
|
+
this.nlBrightnessDebouncer = new utils_1.Debouncer(constants_1.DEBOUNCE_DELAY_MS, this.executeNlBrightnessChange.bind(this));
|
|
42
|
+
this.humidityDebouncer = new utils_1.Debouncer(constants_1.DEBOUNCE_DELAY_MS, this.executeHumidityChange.bind(this));
|
|
43
|
+
// Detect capabilities from available state keys
|
|
44
|
+
this.initCapabilities();
|
|
45
|
+
this.setupAccessoryInformation();
|
|
46
|
+
this.setupMainService();
|
|
47
|
+
this.setupOptionalServices();
|
|
48
|
+
this.setupEventListeners();
|
|
49
|
+
}
|
|
50
|
+
initCapabilities() {
|
|
51
|
+
this.capabilities = (0, utils_1.detectCapabilities)(this.device.state, this.device.sensorData);
|
|
52
|
+
this.platform.log.info(`[${this.device.name}] Detected capabilities: ${(0, utils_1.formatCapabilities)(this.capabilities)}`);
|
|
53
|
+
}
|
|
54
|
+
setupAccessoryInformation() {
|
|
55
|
+
const typeLabel = this.deviceType === "humidifier" ? "Humidifier" : "Purifier";
|
|
56
|
+
this.accessory
|
|
57
|
+
.getService(this.platform.Service.AccessoryInformation)
|
|
58
|
+
.setCharacteristic(this.platform.Characteristic.Manufacturer, "BlueAir")
|
|
59
|
+
.setCharacteristic(this.platform.Characteristic.Model, this.configDev.model || `BlueAir ${typeLabel}`)
|
|
60
|
+
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.configDev.serialNumber || "BlueAir Device");
|
|
61
|
+
if (this.configDev.room) {
|
|
62
|
+
this.accessory.displayName = `${this.configDev.name} (${this.configDev.room})`;
|
|
63
|
+
}
|
|
64
|
+
this.platform.log.info(`[${this.configDev.name}] Initializing ${typeLabel} accessory (Model: ${this.configDev.model || "Unknown"})`);
|
|
65
|
+
}
|
|
66
|
+
setupMainService() {
|
|
67
|
+
if (this.deviceType === "humidifier") {
|
|
68
|
+
this.setupHumidifierService();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.setupAirPurifierService();
|
|
72
|
+
}
|
|
73
|
+
// Common setup for all device types
|
|
74
|
+
this.setupCommonServiceCharacteristics();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Setup characteristics common to both Air Purifier and Humidifier services
|
|
78
|
+
*/
|
|
79
|
+
setupCommonServiceCharacteristics() {
|
|
80
|
+
const C = this.platform.Characteristic;
|
|
81
|
+
// Active state
|
|
82
|
+
this.service
|
|
83
|
+
.getCharacteristic(C.Active)
|
|
84
|
+
.onGet(this.getActive.bind(this))
|
|
85
|
+
.onSet(this.setActive.bind(this));
|
|
86
|
+
// Lock physical controls (child lock)
|
|
87
|
+
this.service
|
|
88
|
+
.getCharacteristic(C.LockPhysicalControls)
|
|
89
|
+
.onGet(this.getLockPhysicalControls.bind(this))
|
|
90
|
+
.onSet(this.setLockPhysicalControls.bind(this));
|
|
91
|
+
// Rotation speed (fan speed) - discrete steps matching device capability
|
|
92
|
+
this.service
|
|
93
|
+
.getCharacteristic(C.RotationSpeed)
|
|
94
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: constants_1.FAN_SPEED_HOMEKIT_STEP })
|
|
95
|
+
.onGet(this.getRotationSpeed.bind(this))
|
|
96
|
+
.onSet(this.setRotationSpeed.bind(this));
|
|
97
|
+
// Filter maintenance service
|
|
98
|
+
this.filterMaintenanceService =
|
|
99
|
+
this.accessory.getService(this.platform.Service.FilterMaintenance) ||
|
|
100
|
+
this.accessory.addService(this.platform.Service.FilterMaintenance);
|
|
101
|
+
this.filterMaintenanceService
|
|
102
|
+
.getCharacteristic(C.FilterChangeIndication)
|
|
103
|
+
.onGet(this.getFilterChangeIndication.bind(this));
|
|
104
|
+
this.filterMaintenanceService
|
|
105
|
+
.getCharacteristic(C.FilterLifeLevel)
|
|
106
|
+
.onGet(this.getFilterLifeLevel.bind(this));
|
|
107
|
+
// Link for grouping in Home app
|
|
108
|
+
this.service.addLinkedService(this.filterMaintenanceService);
|
|
109
|
+
}
|
|
110
|
+
setupAirPurifierService() {
|
|
111
|
+
this.service =
|
|
112
|
+
this.accessory.getService(this.platform.Service.AirPurifier) ||
|
|
113
|
+
this.accessory.addService(this.platform.Service.AirPurifier);
|
|
114
|
+
this.service.setCharacteristic(this.platform.Characteristic.Name, this.configDev.name);
|
|
115
|
+
this.service
|
|
116
|
+
.getCharacteristic(this.platform.Characteristic.CurrentAirPurifierState)
|
|
117
|
+
.onGet(this.getCurrentAirPurifierState.bind(this));
|
|
118
|
+
this.service
|
|
119
|
+
.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState)
|
|
120
|
+
.onGet(this.getTargetAirPurifierState.bind(this))
|
|
121
|
+
.onSet(this.setTargetAirPurifierState.bind(this));
|
|
122
|
+
// Add humidity to main control screen if available
|
|
123
|
+
if (this.capabilities.hasHumidity) {
|
|
124
|
+
this.service
|
|
125
|
+
.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
|
|
126
|
+
.onGet(this.getCurrentRelativeHumidity.bind(this));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
setupHumidifierService() {
|
|
130
|
+
this.service =
|
|
131
|
+
this.accessory.getService(this.platform.Service.HumidifierDehumidifier) ||
|
|
132
|
+
this.accessory.addService(this.platform.Service.HumidifierDehumidifier);
|
|
133
|
+
this.service.setCharacteristic(this.platform.Characteristic.Name, this.configDev.name);
|
|
134
|
+
this.service
|
|
135
|
+
.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
|
|
136
|
+
.onGet(this.getCurrentRelativeHumidity.bind(this));
|
|
137
|
+
// RelativeHumidityHumidifierThreshold for target humidity
|
|
138
|
+
this.service
|
|
139
|
+
.getCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold)
|
|
140
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
141
|
+
.onGet(this.getTargetRelativeHumidity.bind(this))
|
|
142
|
+
.onSet(this.setTargetRelativeHumidity.bind(this));
|
|
143
|
+
this.service
|
|
144
|
+
.getCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState)
|
|
145
|
+
.onGet(this.getCurrentHumidifierState.bind(this));
|
|
146
|
+
// Lock to HUMIDIFIER mode only
|
|
147
|
+
this.service
|
|
148
|
+
.getCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState)
|
|
149
|
+
.setProps({
|
|
150
|
+
validValues: [
|
|
151
|
+
this.platform.Characteristic.TargetHumidifierDehumidifierState
|
|
152
|
+
.HUMIDIFIER,
|
|
153
|
+
],
|
|
154
|
+
})
|
|
155
|
+
.onGet(this.getTargetHumidifierState.bind(this))
|
|
156
|
+
.onSet(this.setTargetHumidifierState.bind(this));
|
|
157
|
+
// Water level
|
|
158
|
+
this.service
|
|
159
|
+
.getCharacteristic(this.platform.Characteristic.WaterLevel)
|
|
160
|
+
.onGet(this.getWaterLevel.bind(this));
|
|
161
|
+
// Optional separate Fan tile for Home app visibility
|
|
162
|
+
if (this.configDev.showFanTile !== false) {
|
|
163
|
+
this.setupFanService();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Setup separate Fan service for humidifiers (optional)
|
|
168
|
+
*/
|
|
169
|
+
setupFanService() {
|
|
170
|
+
const C = this.platform.Characteristic;
|
|
171
|
+
this.fanService =
|
|
172
|
+
this.accessory.getService(this.platform.Service.Fanv2) ||
|
|
173
|
+
this.accessory.addService(this.platform.Service.Fanv2, "Fan", "Fan");
|
|
174
|
+
this.fanService.setCharacteristic(C.Name, this.configDev.name + " Fan");
|
|
175
|
+
this.fanService
|
|
176
|
+
.getCharacteristic(C.Active)
|
|
177
|
+
.onGet(this.getActive.bind(this))
|
|
178
|
+
.onSet(this.setActive.bind(this));
|
|
179
|
+
this.fanService
|
|
180
|
+
.getCharacteristic(C.RotationSpeed)
|
|
181
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: constants_1.FAN_SPEED_HOMEKIT_STEP })
|
|
182
|
+
.onGet(this.getRotationSpeed.bind(this))
|
|
183
|
+
.onSet(this.setRotationSpeed.bind(this));
|
|
184
|
+
// Link for better UI grouping
|
|
185
|
+
this.service.addLinkedService(this.fanService);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Helper to set up or remove an optional service based on config.
|
|
189
|
+
* Returns the service if enabled, undefined otherwise.
|
|
190
|
+
*
|
|
191
|
+
* Note: By default, optional services are NOT linked to the primary service.
|
|
192
|
+
* This means they appear as separate tiles in the Home app, which provides
|
|
193
|
+
* a cleaner UX than stacked controls in the main control popup.
|
|
194
|
+
* The primary service (purifier/humidifier) handles the main device controls.
|
|
195
|
+
*/
|
|
196
|
+
setupOptionalService(serviceConstructor, subtype, displayName, enabled, configure, linkToMain = false) {
|
|
197
|
+
const existingService = this.accessory.getServiceById(serviceConstructor, subtype);
|
|
198
|
+
if (enabled) {
|
|
199
|
+
const service = existingService !== null && existingService !== void 0 ? existingService : this.accessory.addService(serviceConstructor, `${this.device.name} ${displayName}`, subtype);
|
|
200
|
+
service.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} ${displayName}`);
|
|
201
|
+
configure(service);
|
|
202
|
+
this.optionalServices.set(subtype, service);
|
|
203
|
+
// Only link if explicitly requested
|
|
204
|
+
if (linkToMain) {
|
|
205
|
+
this.service.addLinkedService(service);
|
|
206
|
+
}
|
|
207
|
+
return service;
|
|
208
|
+
}
|
|
209
|
+
else if (existingService) {
|
|
210
|
+
// Remove any existing link before removing service
|
|
211
|
+
try {
|
|
212
|
+
this.service.removeLinkedService(existingService);
|
|
213
|
+
}
|
|
214
|
+
catch (_a) {
|
|
215
|
+
// Ignore if not linked
|
|
216
|
+
}
|
|
217
|
+
this.accessory.removeService(existingService);
|
|
218
|
+
this.optionalServices.delete(subtype);
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
setupOptionalServices() {
|
|
223
|
+
const C = this.platform.Characteristic;
|
|
224
|
+
const S = this.platform.Service;
|
|
225
|
+
// Remove legacy "Led" service from cache (renamed to DisplayBrightness)
|
|
226
|
+
this.setupOptionalService(S.Lightbulb, "Led", "Led", false, // Always remove - replaced by DisplayBrightness
|
|
227
|
+
() => { });
|
|
228
|
+
// Display brightness - only if device has brightness capability
|
|
229
|
+
this.setupOptionalService(S.Lightbulb, "DisplayBrightness", "Display Brightness", this.capabilities.hasBrightness && this.configDev.led !== false, (svc) => {
|
|
230
|
+
svc.setCharacteristic(C.ConfiguredName, `${this.device.name} Display`);
|
|
231
|
+
svc
|
|
232
|
+
.getCharacteristic(C.On)
|
|
233
|
+
.onGet(this.getDisplayOn.bind(this))
|
|
234
|
+
.onSet(this.setDisplayOn.bind(this));
|
|
235
|
+
svc
|
|
236
|
+
.getCharacteristic(C.Brightness)
|
|
237
|
+
.onGet(this.getDisplayBrightness.bind(this))
|
|
238
|
+
.onSet(this.setDisplayBrightness.bind(this));
|
|
239
|
+
});
|
|
240
|
+
// Night light brightness - only if device has nlbrightness capability
|
|
241
|
+
this.setupOptionalService(S.Lightbulb, "NightLight", "Night Light", this.capabilities.hasNightLight && this.configDev.nightLight !== false, (svc) => {
|
|
242
|
+
svc.setCharacteristic(C.ConfiguredName, `${this.device.name} Night Light`);
|
|
243
|
+
svc
|
|
244
|
+
.getCharacteristic(C.On)
|
|
245
|
+
.onGet(this.getNightLightOn.bind(this))
|
|
246
|
+
.onSet(this.setNightLightOn.bind(this));
|
|
247
|
+
svc
|
|
248
|
+
.getCharacteristic(C.Brightness)
|
|
249
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: constants_1.NL_HOMEKIT_STEP })
|
|
250
|
+
.onGet(this.getNightLightBrightness.bind(this))
|
|
251
|
+
.onSet(this.setNightLightBrightness.bind(this));
|
|
252
|
+
});
|
|
253
|
+
// Temperature sensor - only if device has temperature data
|
|
254
|
+
this.setupOptionalService(S.TemperatureSensor, "Temperature", "Temperature", this.capabilities.hasTemperature &&
|
|
255
|
+
this.configDev.temperatureSensor !== false, (svc) => {
|
|
256
|
+
svc
|
|
257
|
+
.getCharacteristic(C.CurrentTemperature)
|
|
258
|
+
.onGet(this.getCurrentTemperature.bind(this));
|
|
259
|
+
});
|
|
260
|
+
// Night mode switch removed - night mode is triggered automatically when fan speed is low
|
|
261
|
+
this.setupOptionalService(S.Switch, "NightMode", "Night Mode", false, // Always remove
|
|
262
|
+
() => { });
|
|
263
|
+
// Humidity sensor - only if device has humidity data
|
|
264
|
+
this.setupOptionalService(S.HumiditySensor, "Humidity", "Humidity", this.capabilities.hasHumidity && this.configDev.humiditySensor !== false, (svc) => {
|
|
265
|
+
svc
|
|
266
|
+
.getCharacteristic(C.CurrentRelativeHumidity)
|
|
267
|
+
.onGet(this.getCurrentRelativeHumidity.bind(this));
|
|
268
|
+
});
|
|
269
|
+
// Air quality sensor - only if device has air quality data
|
|
270
|
+
this.setupOptionalService(S.AirQualitySensor, "AirQuality", "Air Quality", this.capabilities.hasAirQuality &&
|
|
271
|
+
this.configDev.airQualitySensor !== false, (svc) => {
|
|
272
|
+
svc
|
|
273
|
+
.getCharacteristic(C.AirQuality)
|
|
274
|
+
.onGet(this.getAirQuality.bind(this));
|
|
275
|
+
svc
|
|
276
|
+
.getCharacteristic(C.PM2_5Density)
|
|
277
|
+
.onGet(this.getPM2_5Density.bind(this));
|
|
278
|
+
svc
|
|
279
|
+
.getCharacteristic(C.PM10Density)
|
|
280
|
+
.onGet(this.getPM10Density.bind(this));
|
|
281
|
+
svc
|
|
282
|
+
.getCharacteristic(C.VOCDensity)
|
|
283
|
+
.onGet(this.getVOCDensity.bind(this));
|
|
284
|
+
});
|
|
285
|
+
// Germ shield switch - only if device has germshield capability
|
|
286
|
+
this.setupOptionalService(S.Switch, "GermShield", "Germ Shield", this.capabilities.hasGermShield && this.configDev.germShield !== false, (svc) => {
|
|
287
|
+
svc.setCharacteristic(C.ConfiguredName, `${this.device.name} Germ Shield`);
|
|
288
|
+
svc
|
|
289
|
+
.getCharacteristic(C.On)
|
|
290
|
+
.onGet(this.getGermShield.bind(this))
|
|
291
|
+
.onSet(this.setGermShield.bind(this));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
setupEventListeners() {
|
|
295
|
+
this.device.on("stateUpdated", this.updateCharacteristics.bind(this));
|
|
296
|
+
}
|
|
297
|
+
/** Helper to get an optional service by subtype */
|
|
298
|
+
getOptionalService(subtype) {
|
|
299
|
+
return this.optionalServices.get(subtype);
|
|
300
|
+
}
|
|
301
|
+
/** Helper to update a characteristic on an optional service if it exists */
|
|
302
|
+
updateOptionalCharacteristic(subtype, characteristic, value) {
|
|
303
|
+
var _a;
|
|
304
|
+
(_a = this.getOptionalService(subtype)) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(characteristic, value);
|
|
305
|
+
}
|
|
306
|
+
updateCharacteristics(changedStates) {
|
|
307
|
+
var _a, _b;
|
|
308
|
+
const C = this.platform.Characteristic;
|
|
309
|
+
for (const key of Object.keys(changedStates)) {
|
|
310
|
+
this.platform.log.debug(`[${this.device.name}] ${key} changed`);
|
|
311
|
+
switch (key) {
|
|
312
|
+
case "standby":
|
|
313
|
+
this.handleStandbyChange();
|
|
314
|
+
break;
|
|
315
|
+
case "automode":
|
|
316
|
+
this.handleAutomodeChange();
|
|
317
|
+
break;
|
|
318
|
+
case "childlock":
|
|
319
|
+
this.service.updateCharacteristic(C.LockPhysicalControls, this.getLockPhysicalControls());
|
|
320
|
+
break;
|
|
321
|
+
case "fanspeed":
|
|
322
|
+
this.handleFanspeedChange();
|
|
323
|
+
break;
|
|
324
|
+
case "filterusage":
|
|
325
|
+
(_a = this.filterMaintenanceService) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(C.FilterChangeIndication, this.getFilterChangeIndication());
|
|
326
|
+
(_b = this.filterMaintenanceService) === null || _b === void 0 ? void 0 : _b.updateCharacteristic(C.FilterLifeLevel, this.getFilterLifeLevel());
|
|
327
|
+
break;
|
|
328
|
+
case "temperature":
|
|
329
|
+
this.updateOptionalCharacteristic("Temperature", C.CurrentTemperature, this.getCurrentTemperature());
|
|
330
|
+
break;
|
|
331
|
+
case "humidity":
|
|
332
|
+
// Update humidity on main service
|
|
333
|
+
this.service.updateCharacteristic(C.CurrentRelativeHumidity, this.getCurrentRelativeHumidity());
|
|
334
|
+
// Also update optional humidity sensor tile
|
|
335
|
+
this.updateOptionalCharacteristic("Humidity", C.CurrentRelativeHumidity, this.getCurrentRelativeHumidity());
|
|
336
|
+
break;
|
|
337
|
+
case "brightness":
|
|
338
|
+
this.updateOptionalCharacteristic("DisplayBrightness", C.On, this.getDisplayOn());
|
|
339
|
+
this.updateOptionalCharacteristic("DisplayBrightness", C.Brightness, this.getDisplayBrightness());
|
|
340
|
+
break;
|
|
341
|
+
case "nlbrightness":
|
|
342
|
+
this.updateOptionalCharacteristic("NightLight", C.On, this.getNightLightOn());
|
|
343
|
+
this.updateOptionalCharacteristic("NightLight", C.Brightness, this.getNightLightBrightness());
|
|
344
|
+
break;
|
|
345
|
+
case "pm25":
|
|
346
|
+
case "pm10":
|
|
347
|
+
case "voc":
|
|
348
|
+
this.handleAirQualityChange(key);
|
|
349
|
+
break;
|
|
350
|
+
case "germshield":
|
|
351
|
+
this.updateOptionalCharacteristic("GermShield", C.On, this.getGermShield());
|
|
352
|
+
break;
|
|
353
|
+
case "nightmode":
|
|
354
|
+
// Update the dropdown to reflect night mode state for humidifiers
|
|
355
|
+
if (this.deviceType === "humidifier") {
|
|
356
|
+
this.service.updateCharacteristic(C.TargetHumidifierDehumidifierState, this.getTargetHumidifierState());
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
case "wlevel":
|
|
360
|
+
// Update water level for humidifiers
|
|
361
|
+
if (this.deviceType === "humidifier") {
|
|
362
|
+
this.service.updateCharacteristic(C.WaterLevel, this.getWaterLevel());
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
case "autorh":
|
|
366
|
+
// Update target humidity for humidifiers
|
|
367
|
+
if (this.deviceType === "humidifier") {
|
|
368
|
+
this.service.updateCharacteristic(C.RelativeHumidityHumidifierThreshold, this.getTargetRelativeHumidity());
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
handleStandbyChange() {
|
|
375
|
+
var _a;
|
|
376
|
+
const C = this.platform.Characteristic;
|
|
377
|
+
this.service.updateCharacteristic(C.Active, this.getActive());
|
|
378
|
+
if (this.deviceType === "air-purifier") {
|
|
379
|
+
this.service.updateCharacteristic(C.CurrentAirPurifierState, this.getCurrentAirPurifierState());
|
|
380
|
+
this.service.updateCharacteristic(C.TargetAirPurifierState, this.getTargetAirPurifierState());
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
this.service.updateCharacteristic(C.CurrentHumidifierDehumidifierState, this.getCurrentHumidifierState());
|
|
384
|
+
this.service.updateCharacteristic(C.TargetHumidifierDehumidifierState, this.getTargetHumidifierState());
|
|
385
|
+
this.service.updateCharacteristic(C.CurrentRelativeHumidity, this.getCurrentRelativeHumidity());
|
|
386
|
+
this.service.updateCharacteristic(C.RelativeHumidityHumidifierThreshold, this.getTargetRelativeHumidity());
|
|
387
|
+
(_a = this.fanService) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(C.RotationSpeed, this.getRotationSpeed());
|
|
388
|
+
}
|
|
389
|
+
this.service.updateCharacteristic(C.RotationSpeed, this.getRotationSpeed());
|
|
390
|
+
this.updateOptionalCharacteristic("DisplayBrightness", C.On, this.getDisplayOn());
|
|
391
|
+
this.updateOptionalCharacteristic("GermShield", C.On, this.getGermShield());
|
|
392
|
+
this.updateOptionalCharacteristic("NightMode", C.On, this.getNightMode());
|
|
393
|
+
this.maybeAutoAdjustFanSpeed();
|
|
394
|
+
}
|
|
395
|
+
handleAutomodeChange() {
|
|
396
|
+
const C = this.platform.Characteristic;
|
|
397
|
+
if (this.deviceType === "air-purifier") {
|
|
398
|
+
this.service.updateCharacteristic(C.TargetAirPurifierState, this.getTargetAirPurifierState());
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.service.updateCharacteristic(C.TargetHumidifierDehumidifierState, this.getTargetHumidifierState());
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
handleFanspeedChange() {
|
|
405
|
+
var _a;
|
|
406
|
+
const C = this.platform.Characteristic;
|
|
407
|
+
this.service.updateCharacteristic(C.RotationSpeed, this.getRotationSpeed());
|
|
408
|
+
(_a = this.fanService) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(C.RotationSpeed, this.getRotationSpeed());
|
|
409
|
+
if (this.deviceType === "air-purifier") {
|
|
410
|
+
this.service.updateCharacteristic(C.CurrentAirPurifierState, this.getCurrentAirPurifierState());
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
this.service.updateCharacteristic(C.CurrentHumidifierDehumidifierState, this.getCurrentHumidifierState());
|
|
414
|
+
// Update target state dropdown (Auto/Manual/Sleep) based on fanspeed
|
|
415
|
+
this.service.updateCharacteristic(C.TargetHumidifierDehumidifierState, this.getTargetHumidifierState());
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
handleAirQualityChange(sensor) {
|
|
419
|
+
const C = this.platform.Characteristic;
|
|
420
|
+
const aqService = this.getOptionalService("AirQuality");
|
|
421
|
+
if (!aqService) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
switch (sensor) {
|
|
425
|
+
case "pm25":
|
|
426
|
+
aqService.updateCharacteristic(C.PM2_5Density, this.getPM2_5Density());
|
|
427
|
+
break;
|
|
428
|
+
case "pm10":
|
|
429
|
+
aqService.updateCharacteristic(C.PM10Density, this.getPM10Density());
|
|
430
|
+
break;
|
|
431
|
+
case "voc":
|
|
432
|
+
aqService.updateCharacteristic(C.VOCDensity, this.getVOCDensity());
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
aqService.updateCharacteristic(C.AirQuality, this.getAirQuality());
|
|
436
|
+
}
|
|
437
|
+
// Common getters/setters
|
|
438
|
+
getActive() {
|
|
439
|
+
return this.device.state.standby === false
|
|
440
|
+
? this.platform.Characteristic.Active.ACTIVE
|
|
441
|
+
: this.platform.Characteristic.Active.INACTIVE;
|
|
442
|
+
}
|
|
443
|
+
async setActive(value) {
|
|
444
|
+
const isActive = value === this.platform.Characteristic.Active.ACTIVE;
|
|
445
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setActive: ${isActive ? "ON" : "OFF"}`);
|
|
446
|
+
await this.device.setState("standby", !isActive);
|
|
447
|
+
}
|
|
448
|
+
getCurrentTemperature() {
|
|
449
|
+
return this.device.sensorData.temperature || 0;
|
|
450
|
+
}
|
|
451
|
+
// Display brightness (main LED display)
|
|
452
|
+
getDisplayOn() {
|
|
453
|
+
return (this.device.state.brightness !== undefined &&
|
|
454
|
+
this.device.state.brightness > 0 &&
|
|
455
|
+
this.device.state.nightmode !== true);
|
|
456
|
+
}
|
|
457
|
+
async setDisplayOn(value) {
|
|
458
|
+
const turnOn = value;
|
|
459
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setDisplayOn: ${turnOn ? "ON" : "OFF"}`);
|
|
460
|
+
// Save current brightness before turning off, restore when turning on
|
|
461
|
+
if (!turnOn) {
|
|
462
|
+
this.lastBrightness = this.device.state.brightness || 0;
|
|
463
|
+
}
|
|
464
|
+
const brightness = turnOn ? this.lastBrightness || 100 : 0;
|
|
465
|
+
await this.device.setState("brightness", brightness);
|
|
466
|
+
}
|
|
467
|
+
getDisplayBrightness() {
|
|
468
|
+
return this.device.state.brightness || 0;
|
|
469
|
+
}
|
|
470
|
+
async setDisplayBrightness(value) {
|
|
471
|
+
const brightness = value;
|
|
472
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setDisplayBrightness: ${brightness}%`);
|
|
473
|
+
// Update lastBrightness when user sets a non-zero value
|
|
474
|
+
if (brightness > 0) {
|
|
475
|
+
this.lastBrightness = brightness;
|
|
476
|
+
}
|
|
477
|
+
await this.device.setState("brightness", brightness);
|
|
478
|
+
}
|
|
479
|
+
// Night light uses imported NL_LEVELS and helper functions from constants.ts
|
|
480
|
+
getNightLightOn() {
|
|
481
|
+
return (this.device.state.nlbrightness !== undefined &&
|
|
482
|
+
this.device.state.nlbrightness > 0 &&
|
|
483
|
+
this.device.state.nlbrightness !== 0);
|
|
484
|
+
}
|
|
485
|
+
async setNightLightOn(value) {
|
|
486
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setNightLightOn: ${value ? "ON" : "OFF"}`);
|
|
487
|
+
const deviceBrightness = value ? constants_1.NL_LEVELS.NORMAL : constants_1.NL_LEVELS.OFF;
|
|
488
|
+
await this.device.setState("nlbrightness", deviceBrightness);
|
|
489
|
+
}
|
|
490
|
+
getNightLightBrightness() {
|
|
491
|
+
return (0, constants_1.nlDeviceToHomeKit)(this.device.state.nlbrightness || 0);
|
|
492
|
+
}
|
|
493
|
+
async setNightLightBrightness(value) {
|
|
494
|
+
const hkValue = value;
|
|
495
|
+
const deviceValue = (0, constants_1.nlHomeKitToDevice)(hkValue);
|
|
496
|
+
this.nlBrightnessDebouncer.call(deviceValue);
|
|
497
|
+
}
|
|
498
|
+
/** Debounced execution for night light brightness */
|
|
499
|
+
async executeNlBrightnessChange(deviceValue) {
|
|
500
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setNightLightBrightness: ${(0, constants_1.nlDeviceToName)(deviceValue)} (${deviceValue})`);
|
|
501
|
+
await this.device.setState("nlbrightness", deviceValue);
|
|
502
|
+
}
|
|
503
|
+
getNightMode() {
|
|
504
|
+
return this.device.state.nightmode === true;
|
|
505
|
+
}
|
|
506
|
+
async setNightMode(value) {
|
|
507
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setNightMode: ${value ? "ON" : "OFF"}`);
|
|
508
|
+
await this.device.setState("nightmode", value);
|
|
509
|
+
}
|
|
510
|
+
// Air Purifier specific
|
|
511
|
+
getCurrentAirPurifierState() {
|
|
512
|
+
if (this.device.state.standby === false) {
|
|
513
|
+
return this.device.state.automode && this.device.state.fanspeed === 0
|
|
514
|
+
? this.platform.Characteristic.CurrentAirPurifierState.IDLE
|
|
515
|
+
: this.platform.Characteristic.CurrentAirPurifierState.PURIFYING_AIR;
|
|
516
|
+
}
|
|
517
|
+
return this.platform.Characteristic.CurrentAirPurifierState.INACTIVE;
|
|
518
|
+
}
|
|
519
|
+
getTargetAirPurifierState() {
|
|
520
|
+
return this.device.state.automode
|
|
521
|
+
? this.platform.Characteristic.TargetAirPurifierState.AUTO
|
|
522
|
+
: this.platform.Characteristic.TargetAirPurifierState.MANUAL;
|
|
523
|
+
}
|
|
524
|
+
async setTargetAirPurifierState(value) {
|
|
525
|
+
const isAuto = value === this.platform.Characteristic.TargetAirPurifierState.AUTO;
|
|
526
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setTargetAirPurifierState: ${isAuto ? "AUTO" : "MANUAL"}`);
|
|
527
|
+
await this.device.setState("automode", isAuto);
|
|
528
|
+
}
|
|
529
|
+
getLockPhysicalControls() {
|
|
530
|
+
return this.device.state.childlock
|
|
531
|
+
? this.platform.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
|
|
532
|
+
: this.platform.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED;
|
|
533
|
+
}
|
|
534
|
+
async setLockPhysicalControls(value) {
|
|
535
|
+
const isLocked = value ===
|
|
536
|
+
this.platform.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED;
|
|
537
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setLockPhysicalControls: ${isLocked ? "LOCKED" : "UNLOCKED"}`);
|
|
538
|
+
await this.device.setState("childlock", isLocked);
|
|
539
|
+
}
|
|
540
|
+
// Fan speed uses discrete HomeKit values (0, 25, 50, 75, 100) to prevent slider jumping
|
|
541
|
+
// 0% = Off, 25% = Sleep/Night, 50% = Low, 75% = Medium, 100% = High
|
|
542
|
+
getRotationSpeed() {
|
|
543
|
+
var _a;
|
|
544
|
+
const isStandby = this.device.state.standby !== false;
|
|
545
|
+
const isNightMode = this.device.state.nightmode === true;
|
|
546
|
+
const deviceSpeed = (_a = this.device.state.fanspeed) !== null && _a !== void 0 ? _a : 0;
|
|
547
|
+
return (0, constants_1.fanSpeedDeviceToHomeKit)(deviceSpeed, isNightMode, isStandby);
|
|
548
|
+
}
|
|
549
|
+
async setRotationSpeed(value) {
|
|
550
|
+
const hkSpeed = value;
|
|
551
|
+
const fanState = (0, constants_1.fanSpeedHomeKitToDevice)(hkSpeed);
|
|
552
|
+
this.fanSpeedDebouncer.call({ ...fanState, hkSpeed });
|
|
553
|
+
}
|
|
554
|
+
/** Debounced execution for fan speed changes */
|
|
555
|
+
async executeFanSpeedChange(params) {
|
|
556
|
+
const { speed, isNightMode, isStandby, hkSpeed } = params;
|
|
557
|
+
try {
|
|
558
|
+
if (isStandby) {
|
|
559
|
+
// 0% = Turn device off
|
|
560
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setFanSpeed: ${hkSpeed}% → Standby`);
|
|
561
|
+
await this.device.setState("standby", true);
|
|
562
|
+
}
|
|
563
|
+
else if (isNightMode && this.capabilities.hasNightMode) {
|
|
564
|
+
// 25% = Sleep/Night mode
|
|
565
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setFanSpeed: ${hkSpeed}% → Night Mode`);
|
|
566
|
+
this.humidityAutoControlEnabled = false;
|
|
567
|
+
await this.device.setState("standby", false);
|
|
568
|
+
if (this.capabilities.hasAutoMode) {
|
|
569
|
+
await this.device.setState("automode", false);
|
|
570
|
+
}
|
|
571
|
+
await this.device.setState("nightmode", true);
|
|
572
|
+
// Set target humidity to current humidity when leaving auto mode
|
|
573
|
+
await this.syncTargetHumidityToCurrent();
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
// 50%, 75%, 100% = Manual fan speeds
|
|
577
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setFanSpeed: ${hkSpeed}% → speed ${speed}`);
|
|
578
|
+
this.humidityAutoControlEnabled = false;
|
|
579
|
+
this.lastManualOverride = Date.now();
|
|
580
|
+
await this.device.setState("standby", false);
|
|
581
|
+
if (this.capabilities.hasNightMode) {
|
|
582
|
+
await this.device.setState("nightmode", false);
|
|
583
|
+
}
|
|
584
|
+
if (this.capabilities.hasAutoMode) {
|
|
585
|
+
await this.device.setState("automode", false);
|
|
586
|
+
}
|
|
587
|
+
await this.device.setState("fanspeed", speed);
|
|
588
|
+
// Set target humidity to current humidity when leaving auto mode
|
|
589
|
+
await this.syncTargetHumidityToCurrent();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
this.platform.log.error(`[${this.device.name}] Failed to set fan speed: ${error}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
getFilterChangeIndication() {
|
|
597
|
+
return this.device.state.filterusage !== undefined &&
|
|
598
|
+
this.device.state.filterusage >= this.configDev.filterChangeLevel
|
|
599
|
+
? this.platform.Characteristic.FilterChangeIndication.CHANGE_FILTER
|
|
600
|
+
: this.platform.Characteristic.FilterChangeIndication.FILTER_OK;
|
|
601
|
+
}
|
|
602
|
+
getFilterLifeLevel() {
|
|
603
|
+
return 100 - (this.device.state.filterusage || 0);
|
|
604
|
+
}
|
|
605
|
+
getPM2_5Density() {
|
|
606
|
+
return this.device.sensorData.pm2_5 || 0;
|
|
607
|
+
}
|
|
608
|
+
getPM10Density() {
|
|
609
|
+
return this.device.sensorData.pm10 || 0;
|
|
610
|
+
}
|
|
611
|
+
getVOCDensity() {
|
|
612
|
+
return this.device.sensorData.voc || 0;
|
|
613
|
+
}
|
|
614
|
+
getAirQuality() {
|
|
615
|
+
const aqi = this.device.sensorData.aqi;
|
|
616
|
+
if (aqi === undefined) {
|
|
617
|
+
return this.platform.Characteristic.AirQuality.UNKNOWN;
|
|
618
|
+
}
|
|
619
|
+
const C = this.platform.Characteristic.AirQuality;
|
|
620
|
+
if (aqi <= constants_1.AQI_THRESHOLDS.EXCELLENT) {
|
|
621
|
+
return C.EXCELLENT;
|
|
622
|
+
}
|
|
623
|
+
if (aqi <= constants_1.AQI_THRESHOLDS.GOOD) {
|
|
624
|
+
return C.GOOD;
|
|
625
|
+
}
|
|
626
|
+
if (aqi <= constants_1.AQI_THRESHOLDS.FAIR) {
|
|
627
|
+
return C.FAIR;
|
|
628
|
+
}
|
|
629
|
+
if (aqi <= constants_1.AQI_THRESHOLDS.INFERIOR) {
|
|
630
|
+
return C.INFERIOR;
|
|
631
|
+
}
|
|
632
|
+
return C.POOR;
|
|
633
|
+
}
|
|
634
|
+
getGermShield() {
|
|
635
|
+
return this.device.state.germshield === true;
|
|
636
|
+
}
|
|
637
|
+
async setGermShield(value) {
|
|
638
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setGermShield: ${value ? "ON" : "OFF"}`);
|
|
639
|
+
await this.device.setState("germshield", value);
|
|
640
|
+
}
|
|
641
|
+
// Humidifier specific
|
|
642
|
+
findHumidityTargetAttribute() {
|
|
643
|
+
// First, check for a manual override from config
|
|
644
|
+
if (this.configDev.targetHumidityAttribute) {
|
|
645
|
+
const key = this.configDev.targetHumidityAttribute;
|
|
646
|
+
const v = this.device.state[key];
|
|
647
|
+
if (typeof v === "number") {
|
|
648
|
+
return key;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const keys = Object.keys(this.device.state || {});
|
|
652
|
+
const patterns = [
|
|
653
|
+
/^autorh$/i, // BlueAir humidifier uses "autorh"
|
|
654
|
+
/target\s*hum/i,
|
|
655
|
+
/hum\s*target/i,
|
|
656
|
+
/humidity\s*target/i,
|
|
657
|
+
/target\s*humidity/i,
|
|
658
|
+
/hum(i)?d(ity)?\s*set(point)?/i,
|
|
659
|
+
/set\s*hum/i,
|
|
660
|
+
];
|
|
661
|
+
for (const k of keys) {
|
|
662
|
+
for (const p of patterns) {
|
|
663
|
+
if (p.test(k)) {
|
|
664
|
+
const v = this.device.state[k];
|
|
665
|
+
if (typeof v === "number") {
|
|
666
|
+
return k;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
getCurrentRelativeHumidity() {
|
|
674
|
+
const humidity = Math.max(0, Math.min(100, this.device.sensorData.humidity || 0));
|
|
675
|
+
return humidity;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Sync target humidity to current humidity when switching to manual mode.
|
|
679
|
+
* This provides a sensible starting point for manual control.
|
|
680
|
+
*/
|
|
681
|
+
async syncTargetHumidityToCurrent() {
|
|
682
|
+
// Only applies to humidifiers
|
|
683
|
+
if (this.deviceType !== "humidifier") {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const currentHumidity = this.device.sensorData.humidity;
|
|
687
|
+
if (currentHumidity === undefined) {
|
|
688
|
+
this.platform.log.debug(`[${this.device.name}] Cannot sync humidity: no current humidity data`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const clampedHumidity = Math.max(constants_1.HUMIDITY_MIN, Math.min(constants_1.HUMIDITY_MAX, Math.round(currentHumidity)));
|
|
692
|
+
const attr = this.findHumidityTargetAttribute();
|
|
693
|
+
if (attr) {
|
|
694
|
+
this.platform.log.info(`[${this.device.name}] Syncing target humidity to current: ${clampedHumidity}%`);
|
|
695
|
+
await this.device.setState(attr, clampedHumidity);
|
|
696
|
+
// Update HomeKit to reflect the new target immediately
|
|
697
|
+
this.service.updateCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold, clampedHumidity);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// No device attribute, update local config and HomeKit
|
|
701
|
+
this.platform.log.debug(`[${this.device.name}] No humidity target attribute, updating locally to ${clampedHumidity}%`);
|
|
702
|
+
this.configDev.targetHumidity = clampedHumidity;
|
|
703
|
+
this.service.updateCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold, clampedHumidity);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
getTargetRelativeHumidity() {
|
|
707
|
+
var _a, _b;
|
|
708
|
+
const attr = this.findHumidityTargetAttribute();
|
|
709
|
+
if (attr && typeof this.device.state[attr] === "number") {
|
|
710
|
+
const v = this.device.state[attr];
|
|
711
|
+
return Math.max(constants_1.HUMIDITY_MIN, Math.min(constants_1.HUMIDITY_MAX, v));
|
|
712
|
+
}
|
|
713
|
+
const fallback = (_b = (_a = this.configDev.targetHumidity) !== null && _a !== void 0 ? _a : this.configDev.defaultTargetHumidity) !== null && _b !== void 0 ? _b : 60;
|
|
714
|
+
return Math.max(constants_1.HUMIDITY_MIN, Math.min(constants_1.HUMIDITY_MAX, fallback));
|
|
715
|
+
}
|
|
716
|
+
async setTargetRelativeHumidity(value) {
|
|
717
|
+
const desired = Math.max(constants_1.HUMIDITY_MIN, Math.min(constants_1.HUMIDITY_MAX, value));
|
|
718
|
+
this.humidityDebouncer.call(desired);
|
|
719
|
+
}
|
|
720
|
+
/** Debounced execution for target humidity changes */
|
|
721
|
+
async executeHumidityChange(valueToSet) {
|
|
722
|
+
this.platform.log.info(`[${this.device.name}] HomeKit → setTargetHumidity: ${valueToSet}% - enabling auto mode`);
|
|
723
|
+
const attr = this.findHumidityTargetAttribute();
|
|
724
|
+
if (attr) {
|
|
725
|
+
await this.device.setState(attr, valueToSet);
|
|
726
|
+
await this.device.setState("automode", true);
|
|
727
|
+
this.humidityAutoControlEnabled = true;
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// Fallback: store locally if device does not expose a writable target
|
|
731
|
+
if (!this.loggedNoWritableTargetHumidity) {
|
|
732
|
+
this.platform.log.info(`[${this.device.name}] Device did not expose a writable target humidity attribute; storing locally.`);
|
|
733
|
+
this.loggedNoWritableTargetHumidity = true;
|
|
734
|
+
}
|
|
735
|
+
this.configDev.targetHumidity = valueToSet;
|
|
736
|
+
await this.device.setState("automode", true);
|
|
737
|
+
this.humidityAutoControlEnabled = true;
|
|
738
|
+
}
|
|
739
|
+
maybeAutoAdjustFanSpeed() {
|
|
740
|
+
var _a, _b;
|
|
741
|
+
if (this.deviceType !== "humidifier") {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (!this.humidityAutoControlEnabled) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this.device.state.standby) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (this.device.state.nightmode) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (this.device.state.automode === false) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
if (now - this.lastAutoAdjust < 60000) {
|
|
758
|
+
return;
|
|
759
|
+
} // adjust at most once per minute
|
|
760
|
+
if (now - this.lastManualOverride < 60000) {
|
|
761
|
+
return;
|
|
762
|
+
} // respect recent manual change
|
|
763
|
+
const current = ((_a = this.device.sensorData.humidity) !== null && _a !== void 0 ? _a : 0);
|
|
764
|
+
const target = this.getTargetRelativeHumidity();
|
|
765
|
+
if (current === 0) {
|
|
766
|
+
return;
|
|
767
|
+
} // no data
|
|
768
|
+
const diff = target - current; // positive -> need more humidity
|
|
769
|
+
const speed = ((_b = this.device.state.fanspeed) !== null && _b !== void 0 ? _b : 0);
|
|
770
|
+
// Find current speed index in valid levels
|
|
771
|
+
const speedIndex = constants_1.FAN_SPEED_LEVELS.findIndex((lvl, i, arr) => speed <= lvl || i === arr.length - 1);
|
|
772
|
+
let newSpeedIndex = speedIndex;
|
|
773
|
+
// Use hysteresis thresholds to avoid oscillation
|
|
774
|
+
// Step up/down one level at a time based on humidity difference
|
|
775
|
+
if (diff > 2) {
|
|
776
|
+
// Need more humidity - increase fan speed one level
|
|
777
|
+
newSpeedIndex = Math.min(constants_1.FAN_SPEED_LEVELS.length - 1, speedIndex + 1);
|
|
778
|
+
}
|
|
779
|
+
else if (diff < -4) {
|
|
780
|
+
// Too humid - decrease fan speed one level
|
|
781
|
+
newSpeedIndex = Math.max(0, speedIndex - 1);
|
|
782
|
+
}
|
|
783
|
+
const newSpeed = constants_1.FAN_SPEED_LEVELS[newSpeedIndex];
|
|
784
|
+
if (newSpeed !== speed) {
|
|
785
|
+
this.platform.log.debug(`[${this.device.name}] Auto-adjust fan: humidity ${current}% target ${target}% -> speed ${speed} -> ${newSpeed}`);
|
|
786
|
+
this.lastAutoAdjust = now;
|
|
787
|
+
void this.device.setState("fanspeed", newSpeed);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
getCurrentHumidifierState() {
|
|
791
|
+
if (this.device.state.standby === false) {
|
|
792
|
+
return this.device.state.automode && this.device.state.fanspeed === 0
|
|
793
|
+
? this.platform.Characteristic.CurrentHumidifierDehumidifierState.IDLE
|
|
794
|
+
: this.platform.Characteristic.CurrentHumidifierDehumidifierState
|
|
795
|
+
.HUMIDIFYING;
|
|
796
|
+
}
|
|
797
|
+
return this.platform.Characteristic.CurrentHumidifierDehumidifierState
|
|
798
|
+
.INACTIVE;
|
|
799
|
+
}
|
|
800
|
+
getTargetHumidifierState() {
|
|
801
|
+
// Always return HUMIDIFIER - dropdown is locked to single value
|
|
802
|
+
// Night mode is controlled via fan speed slider (low speed = night mode)
|
|
803
|
+
return this.platform.Characteristic.TargetHumidifierDehumidifierState
|
|
804
|
+
.HUMIDIFIER;
|
|
805
|
+
}
|
|
806
|
+
async setTargetHumidifierState(_value) {
|
|
807
|
+
// No-op - mode is locked to HUMIDIFIER
|
|
808
|
+
// Auto/manual/night mode is controlled automatically via humidity and fan speed sliders
|
|
809
|
+
}
|
|
810
|
+
getWaterLevel() {
|
|
811
|
+
// Use wlevel from device state if available
|
|
812
|
+
const wlevel = this.device.state.wlevel;
|
|
813
|
+
if (typeof wlevel === "number") {
|
|
814
|
+
return Math.max(0, Math.min(100, wlevel));
|
|
815
|
+
}
|
|
816
|
+
// Fallback if not available
|
|
817
|
+
return 100;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
exports.BlueAirAccessory = BlueAirAccessory;
|
|
821
|
+
//# sourceMappingURL=accessory.js.map
|