homebridge-slwf-01pro 0.2.0 → 0.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/CHANGELOG.md +69 -0
- package/index.js +9 -0
- package/lib/DeviceAccessory.js +49 -12
- package/lib/esphome.js +55 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ This package is a maintained fork of [`homebridge-esphome-ac`](https://github.co
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.3.1] — 2026-05-06
|
|
11
|
+
|
|
12
|
+
Hotfix for "Out of compliance" error when adding the bridge to Apple Home.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Apple Home flagged the child bridge as "Out of compliance" during pairing.** Root cause: `DeviceAccessory` was constructed immediately on the ESPHome client's `'initialized'` event, but ESPHome state messages may not have arrived yet at that moment. Required HomeKit characteristics like `CurrentTemperature`, `CoolingThresholdTemperature`, and `HeatingThresholdTemperature` got `.updateValue(undefined)` calls. When Apple Home read the accessory tree during pairing, those characteristics returned `null` instead of valid floats — Apple's HAP validator rejects this as malformed metadata and refuses to pair.
|
|
17
|
+
|
|
18
|
+
Three layers of defense applied:
|
|
19
|
+
1. **Wait for first state event** — `lib/esphome.js` now defers `DeviceAccessory` construction until the climate entity has emitted at least one `'state'` event (5 s timeout fallback). For most devices this adds < 100 ms to startup; for slow-responding devices the warning logs and we proceed with safe defaults.
|
|
20
|
+
2. **Safe defaults at construction** — `addClimateService()` substitutes the visual-min/max-temperature midpoint for `CurrentTemperature` and `visualMinTemperature` for the threshold characteristics if state is still missing (defense in depth).
|
|
21
|
+
3. **`safeUpdate(characteristic, value)` helper** — wraps every characteristic update; skips the call entirely when value is `null` / `undefined` / `NaN` / `Infinity`.
|
|
22
|
+
|
|
23
|
+
- **`FirmwareRevision` validation.** HAP requires the format `\d+(\.\d+){0,2}` (e.g. `2024.7.3`). ESPHome's `esphomeVersion` sometimes carries suffixes like `-dev` or `-beta`, which would have produced an invalid characteristic value. New `sanitizeFirmwareRevision(raw)` helper strips anything past the SemVer prefix; if the prefix doesn't match, the characteristic is omitted (Apple Home falls back to the plugin version).
|
|
24
|
+
|
|
25
|
+
- **Primary service marked explicitly.** `addClimateService()` now calls `setPrimaryService(true)` on the `Service.HeaterCooler`. With companion DRY/FAN_ONLY/Beeper/Display Switch services on the same accessory, the absence of an explicit primary previously left the choice up to HAP-NodeJS — Apple Home iOS 16+ is stricter about this and could pick a Switch as primary, leading to a confusing accessory tile in Home.
|
|
26
|
+
|
|
27
|
+
### Internal
|
|
28
|
+
|
|
29
|
+
- New constants: `FIRST_STATE_TIMEOUT_MS = 5000` in `lib/esphome.js`.
|
|
30
|
+
- New helpers: `isPresent(value)`, `safeUpdate(characteristic, value)`, `sanitizeFirmwareRevision(raw)` in `lib/DeviceAccessory.js`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## [0.3.0] — 2026-05-06
|
|
35
|
+
|
|
36
|
+
Final piece of the "fully independent plugin" story: HomeKit UUIDs are now namespaced so this plugin can run **alongside** upstream `homebridge-esphome-ac` (or any other plugin that hashes from the same ESPHome `unique_id`) on the same Homebridge bridge without UUID collisions.
|
|
37
|
+
|
|
38
|
+
### ⚠️ Breaking — accessories will re-pair
|
|
39
|
+
|
|
40
|
+
The UUID input is now `homebridge-slwf-01pro:<deviceId>` instead of bare `<deviceId>`. The same device gets a new UUID under this plugin → Apple Home treats it as a new accessory. **You'll lose room assignments, scenes, and automations** wired to the old accessory tiles. They need to be re-set up in Apple Home after the upgrade.
|
|
41
|
+
|
|
42
|
+
The plugin **automatically evicts** old-schema cached accessories on startup (no manual cleanup required) and registers fresh ones with the new UUIDs.
|
|
43
|
+
|
|
44
|
+
### Why
|
|
45
|
+
|
|
46
|
+
Before 0.3.0, both upstream and this fork derived HomeKit UUIDs from `entity.config.uniqueId` (or its fallback). For a user with both plugins running in the **same** Homebridge bridge (not child-bridged) and both auto-discovering the same physical AC:
|
|
47
|
+
|
|
48
|
+
- Upstream registers UUID `abc-123` for "Air Conditioner"
|
|
49
|
+
- Our fork tries to register UUID `abc-123` too
|
|
50
|
+
- HomeKit rejects the second one (UUID collision within a bridge)
|
|
51
|
+
|
|
52
|
+
Child bridges (which the user is using) insulate against this — each child bridge is its own HomeKit instance — so the bug is theoretical for child-bridge users. But for users who run plugins on the main Homebridge bridge, the fix is mandatory. And it costs nothing to apply universally.
|
|
53
|
+
|
|
54
|
+
After 0.3.0, the two plugins produce **deterministically different UUIDs** for the same device. Both can be installed on the same Homebridge with full auto-discovery enabled and no interaction.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`accessory.context.schemaVersion`** — stamped on every accessory created or restored. Currently `2` (1 was the unstamped pre-0.3.0 schema). Future schema breakages bump this number.
|
|
59
|
+
- **`evictStaleSchemaAccessories(platform)`** in `lib/esphome.js` — runs first thing in `init()`. Walks `platform.staleAccessories` (populated by `configureAccessory` for entries with the wrong schema version) and unregisters them in one batch via `api.unregisterPlatformAccessories`. Clean cache transition with no manual user action.
|
|
60
|
+
- **`UUID_NAMESPACE = 'homebridge-slwf-01pro'`** constant in `lib/DeviceAccessory.js`. Prefixed onto the device id before hashing.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **HomeKit UUID input format**: `homebridge-slwf-01pro:<deviceId>` (was bare `<deviceId>`). For devices with `unique_id` set in YAML, the old UUID was the same as upstream's; now ours is distinct. For devices without `unique_id` (the SLWF-01Pro/Midea default), the old UUID was already MAC-based via `deriveDeviceId` (added in 0.1.2); the prefix makes it distinct from upstream regardless.
|
|
65
|
+
- **`configureAccessory` in `index.js`** now diverts schema-mismatched cached entries to `this.staleAccessories` and emits a warning. The init flow evicts them before doing anything else.
|
|
66
|
+
|
|
67
|
+
### Migration
|
|
68
|
+
|
|
69
|
+
For anyone on `homebridge-slwf-01pro@0.2.x` or earlier:
|
|
70
|
+
1. `sudo npm install -g homebridge-slwf-01pro@latest`
|
|
71
|
+
2. Restart Homebridge.
|
|
72
|
+
3. The log shows `Evicting N cached accessor… from an older plugin schema. They will be re-registered fresh with stable UUIDs.` Old accessories disappear from Apple Home; new ones appear with the same names but no automations attached.
|
|
73
|
+
4. Re-add the new accessories to your Apple Home rooms / scenes / automations.
|
|
74
|
+
|
|
75
|
+
For anyone migrating from upstream: same as 0.2.0 (config.json edit `"ESPHomeAC"` → `"SLWFOnePro"`), plus the same UUID-driven re-pairing.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
10
79
|
## [0.2.0] — 2026-05-06
|
|
11
80
|
|
|
12
81
|
Clean separation from upstream `homebridge-esphome-ac`. Breaking config change (one-line edit) but eliminates all namespace collision risk.
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const ESPHome = require('./lib/esphome');
|
|
|
2
2
|
|
|
3
3
|
const PLUGIN_NAME = 'homebridge-slwf-01pro';
|
|
4
4
|
const PLATFORM_NAME = 'SLWFOnePro';
|
|
5
|
+
const ACCESSORY_SCHEMA_VERSION = 2;
|
|
5
6
|
|
|
6
7
|
class ESPHomeAC {
|
|
7
8
|
constructor(log, config, api) {
|
|
@@ -9,9 +10,11 @@ class ESPHomeAC {
|
|
|
9
10
|
this.log = log;
|
|
10
11
|
|
|
11
12
|
this.accessories = [];
|
|
13
|
+
this.staleAccessories = [];
|
|
12
14
|
this.esphomeDevices = {};
|
|
13
15
|
this.PLUGIN_NAME = PLUGIN_NAME;
|
|
14
16
|
this.PLATFORM_NAME = PLATFORM_NAME;
|
|
17
|
+
this.ACCESSORY_SCHEMA_VERSION = ACCESSORY_SCHEMA_VERSION;
|
|
15
18
|
this.name = config.name || PLATFORM_NAME;
|
|
16
19
|
this.devices = config.devices || [];
|
|
17
20
|
this.debug = config.debug || false;
|
|
@@ -40,6 +43,12 @@ class ESPHomeAC {
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
configureAccessory(accessory) {
|
|
46
|
+
const cachedVersion = accessory.context && accessory.context.schemaVersion;
|
|
47
|
+
if (cachedVersion !== ACCESSORY_SCHEMA_VERSION) {
|
|
48
|
+
this.log.warn(`Cached accessory "${accessory.displayName}" is from an older plugin schema (v${cachedVersion || 1}); will be re-registered with the current schema (v${ACCESSORY_SCHEMA_VERSION}).`);
|
|
49
|
+
this.staleAccessories.push(accessory);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
43
52
|
this.log.easyDebug(`Found cached accessory: ${accessory.displayName} (${accessory.context.deviceId || 'no id'})`);
|
|
44
53
|
this.accessories.push(accessory);
|
|
45
54
|
}
|
package/lib/DeviceAccessory.js
CHANGED
|
@@ -26,6 +26,9 @@ let Characteristic;
|
|
|
26
26
|
const SET_DEBOUNCE_MS = 600;
|
|
27
27
|
const PRIMARY_MODES = [ESP_MODE.COOL, ESP_MODE.HEAT, ESP_MODE.AUTO, ESP_MODE.HEAT_COOL];
|
|
28
28
|
|
|
29
|
+
const UUID_NAMESPACE = 'homebridge-slwf-01pro';
|
|
30
|
+
const ACCESSORY_SCHEMA_VERSION = 2;
|
|
31
|
+
|
|
29
32
|
function readSensorValue(entity) {
|
|
30
33
|
if (!entity || !entity.state) return null;
|
|
31
34
|
if (entity.state.missingState) return null;
|
|
@@ -40,6 +43,24 @@ function clampRange(value, min, max) {
|
|
|
40
43
|
return value;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function isPresent(value) {
|
|
47
|
+
if (value == null) return false;
|
|
48
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeUpdate(characteristic, value) {
|
|
53
|
+
if (!characteristic) return;
|
|
54
|
+
if (!isPresent(value)) return;
|
|
55
|
+
characteristic.updateValue(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sanitizeFirmwareRevision(raw) {
|
|
59
|
+
if (typeof raw !== 'string' || raw.length === 0) return undefined;
|
|
60
|
+
const match = raw.match(/^\d+(?:\.\d+){0,2}/);
|
|
61
|
+
return match ? match[0] : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
const SUPPLEMENTARY_SWITCH_KEYS = ['dryMode', 'fanOnlyMode', 'beeperSwitch', 'displaySwitch'];
|
|
44
65
|
const OPTIONAL_SENSOR_KEYS = ['humiditySensor', 'outdoorTempSensor', 'powerSensor'];
|
|
45
66
|
|
|
@@ -74,7 +95,7 @@ class DeviceAccessory {
|
|
|
74
95
|
this.serial = this.deviceInfo.macAddress || this.id;
|
|
75
96
|
this.manufacturer = this.deviceInfo.manufacturer || 'ESPHome';
|
|
76
97
|
this.model = this.deviceInfo.model || climate.name || 'ESPHome AC';
|
|
77
|
-
this.firmwareRevision = this.deviceInfo.esphomeVersion
|
|
98
|
+
this.firmwareRevision = sanitizeFirmwareRevision(this.deviceInfo.esphomeVersion);
|
|
78
99
|
|
|
79
100
|
this.state = climate.state || {};
|
|
80
101
|
this.connected = true;
|
|
@@ -84,12 +105,13 @@ class DeviceAccessory {
|
|
|
84
105
|
|
|
85
106
|
this.swingModeValue = pickDefaultSwingValue(this.config.supportedSwingModesList);
|
|
86
107
|
|
|
87
|
-
this.UUID = this.api.hap.uuid.generate(this.id);
|
|
108
|
+
this.UUID = this.api.hap.uuid.generate(`${UUID_NAMESPACE}:${this.id}`);
|
|
88
109
|
this.accessory = platform.accessories.find(acc => acc.UUID === this.UUID);
|
|
89
110
|
|
|
90
111
|
if (!this.accessory) {
|
|
91
112
|
this.log(`Creating new ESPHome AC accessory: "${this.name}"`);
|
|
92
113
|
this.accessory = new this.api.platformAccessory(this.name, this.UUID);
|
|
114
|
+
this.accessory.context.schemaVersion = ACCESSORY_SCHEMA_VERSION;
|
|
93
115
|
this.accessory.context.deviceId = this.id;
|
|
94
116
|
this.accessory.context.host = this.host;
|
|
95
117
|
this.accessory.context.lastTargetState = chooseInitialTargetMode(this.state.mode);
|
|
@@ -97,6 +119,7 @@ class DeviceAccessory {
|
|
|
97
119
|
this.api.registerPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, [this.accessory]);
|
|
98
120
|
} else {
|
|
99
121
|
this.log(`ESPHome device "${this.name}" reconnected.`);
|
|
122
|
+
this.accessory.context.schemaVersion = ACCESSORY_SCHEMA_VERSION;
|
|
100
123
|
this.accessory.context.host = this.host;
|
|
101
124
|
if (PRIMARY_MODES.includes(this.state.mode)) {
|
|
102
125
|
this.accessory.context.lastTargetState = this.state.mode;
|
|
@@ -155,6 +178,12 @@ class DeviceAccessory {
|
|
|
155
178
|
this.HeaterCoolerService = this.accessory.getService(Service.HeaterCooler)
|
|
156
179
|
|| this.accessory.addService(Service.HeaterCooler, this.name);
|
|
157
180
|
|
|
181
|
+
// Mark HeaterCooler as the accessory's primary service so Apple Home picks it
|
|
182
|
+
// for the main tile when companion Switch services (DRY/FAN_ONLY/Beeper/Display) are present.
|
|
183
|
+
if (typeof this.HeaterCoolerService.setPrimaryService === 'function') {
|
|
184
|
+
this.HeaterCoolerService.setPrimaryService(true);
|
|
185
|
+
}
|
|
186
|
+
|
|
158
187
|
this.HeaterCoolerService.getCharacteristic(Characteristic.Active)
|
|
159
188
|
.onSet(stateManager.set.Active.bind(this))
|
|
160
189
|
.updateValue(this.isModeActive(this.state.mode) ? 1 : 0);
|
|
@@ -173,27 +202,36 @@ class DeviceAccessory {
|
|
|
173
202
|
.onSet(stateManager.set.TargetHeaterCoolerState.bind(this))
|
|
174
203
|
.updateValue(espModeToHkTargetState(this.accessory.context.lastTargetState));
|
|
175
204
|
|
|
205
|
+
// CurrentTemperature MUST be a valid number for HAP (Apple Home flags accessories with
|
|
206
|
+
// undefined CurrentTemperature as "Out of compliance" during pairing). Default to a
|
|
207
|
+
// safe midpoint if state hasn't arrived yet.
|
|
208
|
+
const safeCurrent = isPresent(this.state.currentTemperature)
|
|
209
|
+
? this.state.currentTemperature
|
|
210
|
+
: (this.config.visualMinTemperature + this.config.visualMaxTemperature) / 2;
|
|
176
211
|
this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentTemperature)
|
|
177
212
|
.setProps({ minValue: -100, maxValue: 100, minStep: 0.1 })
|
|
178
|
-
.updateValue(
|
|
213
|
+
.updateValue(safeCurrent);
|
|
179
214
|
|
|
180
215
|
const tempProps = {
|
|
181
216
|
minValue: this.config.visualMinTemperature,
|
|
182
217
|
maxValue: this.config.visualMaxTemperature,
|
|
183
218
|
minStep: this.config.visualTargetTemperatureStep,
|
|
184
219
|
};
|
|
220
|
+
const safeTarget = isPresent(this.state.targetTemperature)
|
|
221
|
+
? this.clampTargetTemperature(this.state.targetTemperature)
|
|
222
|
+
: this.config.visualMinTemperature;
|
|
185
223
|
|
|
186
224
|
if (this.supportsCooling()) {
|
|
187
225
|
this.HeaterCoolerService.getCharacteristic(Characteristic.CoolingThresholdTemperature)
|
|
188
226
|
.setProps(tempProps)
|
|
189
227
|
.onSet(stateManager.set.CoolingThresholdTemperature.bind(this))
|
|
190
|
-
.updateValue(
|
|
228
|
+
.updateValue(safeTarget);
|
|
191
229
|
}
|
|
192
230
|
if (this.supportsHeating()) {
|
|
193
231
|
this.HeaterCoolerService.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
194
232
|
.setProps(tempProps)
|
|
195
233
|
.onSet(stateManager.set.HeatingThresholdTemperature.bind(this))
|
|
196
|
-
.updateValue(
|
|
234
|
+
.updateValue(safeTarget);
|
|
197
235
|
}
|
|
198
236
|
|
|
199
237
|
if (this.swingModeValue) {
|
|
@@ -202,9 +240,10 @@ class DeviceAccessory {
|
|
|
202
240
|
.updateValue(this.state.swingMode ? 1 : 0);
|
|
203
241
|
}
|
|
204
242
|
if (this.supportsFanControl()) {
|
|
243
|
+
const safeSpeed = fanModeToSpeed(this.state.fanMode, this.config.supportedFanModesList);
|
|
205
244
|
this.HeaterCoolerService.getCharacteristic(Characteristic.RotationSpeed)
|
|
206
245
|
.onSet(stateManager.set.RotationSpeed.bind(this))
|
|
207
|
-
.updateValue(
|
|
246
|
+
.updateValue(isPresent(safeSpeed) ? safeSpeed : 0);
|
|
208
247
|
}
|
|
209
248
|
|
|
210
249
|
this.HeaterCoolerService.getCharacteristic(Characteristic.StatusActive).updateValue(true);
|
|
@@ -430,8 +469,7 @@ class DeviceAccessory {
|
|
|
430
469
|
this.state = state;
|
|
431
470
|
this.syncModeSwitches(this.state.mode);
|
|
432
471
|
|
|
433
|
-
this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentTemperature)
|
|
434
|
-
.updateValue(this.state.currentTemperature);
|
|
472
|
+
safeUpdate(this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentTemperature), this.state.currentTemperature);
|
|
435
473
|
|
|
436
474
|
if (this.state.mode === ESP_MODE.OFF) {
|
|
437
475
|
this.HeaterCoolerService.getCharacteristic(Characteristic.Active).updateValue(0);
|
|
@@ -445,18 +483,17 @@ class DeviceAccessory {
|
|
|
445
483
|
|
|
446
484
|
const clampedTarget = this.clampTargetTemperature(this.state.targetTemperature);
|
|
447
485
|
if (this.supportsHeating()) {
|
|
448
|
-
this.HeaterCoolerService.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
486
|
+
safeUpdate(this.HeaterCoolerService.getCharacteristic(Characteristic.HeatingThresholdTemperature), clampedTarget);
|
|
449
487
|
}
|
|
450
488
|
if (this.supportsCooling()) {
|
|
451
|
-
this.HeaterCoolerService.getCharacteristic(Characteristic.CoolingThresholdTemperature)
|
|
489
|
+
safeUpdate(this.HeaterCoolerService.getCharacteristic(Characteristic.CoolingThresholdTemperature), clampedTarget);
|
|
452
490
|
}
|
|
453
491
|
|
|
454
492
|
if (this.swingModeValue) {
|
|
455
493
|
this.HeaterCoolerService.getCharacteristic(Characteristic.SwingMode).updateValue(this.state.swingMode ? 1 : 0);
|
|
456
494
|
}
|
|
457
495
|
if (this.supportsFanControl()) {
|
|
458
|
-
this.HeaterCoolerService.getCharacteristic(Characteristic.RotationSpeed)
|
|
459
|
-
.updateValue(fanModeToSpeed(this.state.fanMode, this.config.supportedFanModesList));
|
|
496
|
+
safeUpdate(this.HeaterCoolerService.getCharacteristic(Characteristic.RotationSpeed), fanModeToSpeed(this.state.fanMode, this.config.supportedFanModesList));
|
|
460
497
|
}
|
|
461
498
|
|
|
462
499
|
this.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
package/lib/esphome.js
CHANGED
|
@@ -7,6 +7,7 @@ const { bundleEntities } = require('./classifyEntity');
|
|
|
7
7
|
|
|
8
8
|
const RECONNECT_INTERVAL_MS = 5000;
|
|
9
9
|
const DEFAULT_DISCOVERY_TIMEOUT_S = 5;
|
|
10
|
+
const FIRST_STATE_TIMEOUT_MS = 5000;
|
|
10
11
|
|
|
11
12
|
const UPSTREAM_PLUGIN_NAME = 'homebridge-esphome-ac';
|
|
12
13
|
const LEGACY_PLATFORM_NAME = 'ESPHomeAC';
|
|
@@ -60,10 +61,23 @@ function detectOrphanedAccessories(platform) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function evictStaleSchemaAccessories(platform) {
|
|
65
|
+
if (!platform.staleAccessories || platform.staleAccessories.length === 0) return;
|
|
66
|
+
const stale = platform.staleAccessories.slice();
|
|
67
|
+
platform.staleAccessories = [];
|
|
68
|
+
platform.log(`Evicting ${stale.length} cached accessor${stale.length === 1 ? 'y' : 'ies'} from an older plugin schema. They will be re-registered fresh with stable UUIDs.`);
|
|
69
|
+
try {
|
|
70
|
+
platform.api.unregisterPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, stale);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
platform.log.error(`Failed to evict stale-schema accessories: ${err.message || err}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
async function init() {
|
|
64
77
|
const platform = this;
|
|
65
|
-
detectOrphanedAccessories(platform);
|
|
66
78
|
platform._clients = [];
|
|
79
|
+
evictStaleSchemaAccessories(platform);
|
|
80
|
+
detectOrphanedAccessories(platform);
|
|
67
81
|
|
|
68
82
|
const manualDevices = (platform.devices || []).map(d => ({
|
|
69
83
|
...d,
|
|
@@ -166,19 +180,47 @@ function spawnClient(platform, device) {
|
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
const displayName = device.name || (deviceInfo && deviceInfo.name) || device.host;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
|
|
184
|
+
const create = () => {
|
|
185
|
+
try {
|
|
186
|
+
const accessory = new DeviceAccessory({
|
|
187
|
+
device: { ...device, name: displayName },
|
|
188
|
+
deviceInfo,
|
|
189
|
+
entities: bundle,
|
|
190
|
+
platform,
|
|
191
|
+
});
|
|
192
|
+
platform.esphomeDevices[device.host] = accessory;
|
|
193
|
+
const count = Object.keys(bundle).length;
|
|
194
|
+
platform.log(`Initialized "${displayName}" with ${count} mapped entit${count === 1 ? 'y' : 'ies'}`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
platform.log.error(`Failed to initialize "${displayName}": ${err.message || err}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Wait for the climate's first state event before creating the accessory.
|
|
201
|
+
// HAP characteristic values must be defined when Apple Home pairs (otherwise → "out of compliance").
|
|
202
|
+
// 5s timeout fallback: if state never arrives, create with whatever defaults the device gives us.
|
|
203
|
+
if (bundle.climate.state && bundle.climate.state.mode != null) {
|
|
204
|
+
create();
|
|
205
|
+
return;
|
|
181
206
|
}
|
|
207
|
+
|
|
208
|
+
platform.log.easyDebug(`${displayName}: waiting for first climate state event before creating accessory…`);
|
|
209
|
+
let created = false;
|
|
210
|
+
const onState = () => {
|
|
211
|
+
if (created) return;
|
|
212
|
+
created = true;
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
create();
|
|
215
|
+
};
|
|
216
|
+
bundle.climate.once('state', onState);
|
|
217
|
+
const timer = setTimeout(() => {
|
|
218
|
+
if (created) return;
|
|
219
|
+
created = true;
|
|
220
|
+
bundle.climate.removeListener('state', onState);
|
|
221
|
+
platform.log.warn(`${displayName}: no climate state received within ${FIRST_STATE_TIMEOUT_MS / 1000}s — creating accessory with default values.`);
|
|
222
|
+
create();
|
|
223
|
+
}, FIRST_STATE_TIMEOUT_MS);
|
|
182
224
|
});
|
|
183
225
|
|
|
184
226
|
client.connect();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-slwf-01pro",
|
|
3
3
|
"description": "Homebridge plugin for the SMLIGHT SLWF-01Pro Wi-Fi dongle and other ESPHome Climate entities. Auto-discovery, multi-entity bundling, and full Midea-protocol AC support.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/nookied/homebridge-SLWF-01Pro.git"
|