homebridge-slwf-01pro 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,102 @@ This package is a maintained fork of [`homebridge-esphome-ac`](https://github.co
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.0] — 2026-05-06
11
+
12
+ 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.
13
+
14
+ ### ⚠️ Breaking — accessories will re-pair
15
+
16
+ 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.
17
+
18
+ The plugin **automatically evicts** old-schema cached accessories on startup (no manual cleanup required) and registers fresh ones with the new UUIDs.
19
+
20
+ ### Why
21
+
22
+ 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:
23
+
24
+ - Upstream registers UUID `abc-123` for "Air Conditioner"
25
+ - Our fork tries to register UUID `abc-123` too
26
+ - HomeKit rejects the second one (UUID collision within a bridge)
27
+
28
+ 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.
29
+
30
+ 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.
31
+
32
+ ### Added
33
+
34
+ - **`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.
35
+ - **`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.
36
+ - **`UUID_NAMESPACE = 'homebridge-slwf-01pro'`** constant in `lib/DeviceAccessory.js`. Prefixed onto the device id before hashing.
37
+
38
+ ### Changed
39
+
40
+ - **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.
41
+ - **`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.
42
+
43
+ ### Migration
44
+
45
+ For anyone on `homebridge-slwf-01pro@0.2.x` or earlier:
46
+ 1. `sudo npm install -g homebridge-slwf-01pro@latest`
47
+ 2. Restart Homebridge.
48
+ 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.
49
+ 4. Re-add the new accessories to your Apple Home rooms / scenes / automations.
50
+
51
+ For anyone migrating from upstream: same as 0.2.0 (config.json edit `"ESPHomeAC"` → `"SLWFOnePro"`), plus the same UUID-driven re-pairing.
52
+
53
+ ---
54
+
55
+ ## [0.2.0] — 2026-05-06
56
+
57
+ Clean separation from upstream `homebridge-esphome-ac`. Breaking config change (one-line edit) but eliminates all namespace collision risk.
58
+
59
+ ### ⚠️ Breaking — config.json edit required
60
+
61
+ The Homebridge platform identifier has been renamed from `"ESPHomeAC"` (shared with upstream) to **`"SLWFOnePro"`** (ours alone). Update your `config.json`:
62
+
63
+ ```diff
64
+ - "platform": "ESPHomeAC",
65
+ + "platform": "SLWFOnePro",
66
+ ```
67
+
68
+ Restart Homebridge after the edit. The plugin detects orphaned cached accessories from the old identifier and logs a clear warning with cleanup instructions on first start.
69
+
70
+ ### Why
71
+
72
+ Before 0.2.0, the platform identifier was shared with upstream `homebridge-esphome-ac` for "drop-in migration" compatibility. In practice this caused two real problems:
73
+
74
+ 1. **Cache collisions** — when both plugins were ever installed in the same Homebridge (even temporarily during a migration), accessories cached under upstream's plugin name became orphans that no live plugin claims, but the platform identifier overlap made it ambiguous which plugin "owned" them.
75
+ 2. **Confusing logs** — Homebridge's startup log lists all installed platforms by `<plugin>.<platformName>`. Both plugins claiming `ESPHomeAC` made the dependency graph unreadable.
76
+
77
+ After 0.2.0, the two plugins are completely independent: separate npm names, separate platform identifiers, no shared state. Both can coexist on the same Homebridge without interfering.
78
+
79
+ ### Added
80
+
81
+ - **`detectOrphanedAccessories(platform)` in `lib/esphome.js`.** Best-effort scan of the bridge's `cachedAccessories.<bridgeId>` files at startup. Warns about:
82
+ - Accessories cached under upstream `homebridge-esphome-ac`
83
+ - Accessories cached under our plugin name but the **legacy** `ESPHomeAC` platform identifier (i.e. pre-0.2.0 entries left after the rename)
84
+ Both warnings include explicit cleanup paths (Homebridge UI step + filesystem path). Best-effort — wrapped in try/catch, never throws, falls through silently if the cache directory doesn't exist or files are malformed.
85
+
86
+ ### Changed
87
+
88
+ - **Platform identifier `ESPHomeAC` → `SLWFOnePro`** in `index.js` `PLATFORM_NAME`, `config.schema.json` `pluginAlias`, `config-sample.json`, README, CLAUDE.md, QA_TESTS.md.
89
+ - **Upstream git remote removed** (`git remote remove upstream`). The fork is intentionally divergent. CLAUDE.md fork rules updated; if anyone needs upstream history they can re-add the remote ad-hoc.
90
+
91
+ ### Migration
92
+
93
+ For anyone on `homebridge-slwf-01pro@0.1.x`:
94
+ 1. `sudo npm install -g homebridge-slwf-01pro@latest`
95
+ 2. Edit `config.json`: `"platform": "ESPHomeAC"` → `"platform": "SLWFOnePro"` (and the `name` field if you used the default).
96
+ 3. Restart Homebridge.
97
+ 4. Log will warn about the orphaned 0.1.x cached accessories. Clean via Homebridge UI → Settings → Remove Single Cached Accessory.
98
+
99
+ For anyone migrating from upstream `homebridge-esphome-ac`:
100
+ 1. `sudo npm uninstall -g homebridge-esphome-ac && sudo npm install -g homebridge-slwf-01pro`
101
+ 2. Same `config.json` edit as above.
102
+ 3. Restart. Log warns about upstream's orphaned cache entries. Clean via UI.
103
+
104
+ ---
105
+
10
106
  ## [0.1.2] — 2026-05-06
11
107
 
12
108
  Critical bug fix surfaced by the first real-hardware test of 0.1.0/0.1.1.
package/README.md CHANGED
@@ -56,8 +56,8 @@ The simplest config — auto-discover everything on the local network:
56
56
  {
57
57
  "platforms": [
58
58
  {
59
- "platform": "ESPHomeAC",
60
- "name": "ESPHomeAC",
59
+ "platform": "SLWFOnePro",
60
+ "name": "SLWFOnePro",
61
61
  "autoDiscover": true
62
62
  }
63
63
  ]
@@ -70,8 +70,8 @@ Or fully manual (required for encrypted devices and any device not on the same b
70
70
  {
71
71
  "platforms": [
72
72
  {
73
- "platform": "ESPHomeAC",
74
- "name": "ESPHomeAC",
73
+ "platform": "SLWFOnePro",
74
+ "name": "SLWFOnePro",
75
75
  "debug": false,
76
76
  "devices": [
77
77
  {
@@ -93,8 +93,8 @@ You can mix both — listed devices in `devices[]` take precedence; auto-discove
93
93
 
94
94
  | Key | Required | Default | Notes |
95
95
  |---|---|---|---|
96
- | `platform` | yes | — | Must be exactly `"ESPHomeAC"` (the platform identifier — same as the upstream plugin so existing configs migrate without edits) |
97
- | `name` | no | `ESPHomeAC` | Display name in Homebridge logs |
96
+ | `platform` | yes | — | Must be exactly `"SLWFOnePro"` (the platform identifier — distinct from upstream `homebridge-esphome-ac`'s `"ESPHomeAC"` to guarantee no namespace collision when both plugins are installed). Pre-0.2.0 configs using `"ESPHomeAC"` need a one-line edit. |
97
+ | `name` | no | `SLWFOnePro` | Display name in Homebridge logs |
98
98
  | `debug` | no | `false` | Surface ESPHome state-change chatter to the main log instead of `log.debug` |
99
99
  | `autoDiscover` | no | `false` | mDNS-browse for ESPHome devices on the local network and create accessories automatically. Encrypted devices still need a manual `devices[]` entry — the Noise key is not broadcast. |
100
100
  | `discoveryTimeout` | no | `5` | Seconds to wait for mDNS responses before continuing. |
@@ -184,17 +184,37 @@ If the device advertises any swing mode beyond OFF, a HomeKit Swing toggle is ex
184
184
 
185
185
  ## Migration
186
186
 
187
- If you're moving from the upstream `homebridge-esphome-ac`:
187
+ ### From upstream `homebridge-esphome-ac`
188
188
 
189
- ```bash
190
- sudo npm uninstall -g homebridge-esphome-ac
191
- sudo npm install -g homebridge-slwf-01pro
192
- sudo systemctl restart homebridge # or: hb-service restart
193
- ```
189
+ Both the npm package name AND the Homebridge platform identifier are different from upstream — that's deliberate, and it means the two plugins never share cache or namespace. Migration is two short steps:
190
+
191
+ 1. **Replace the package**:
192
+ ```bash
193
+ sudo npm uninstall -g homebridge-esphome-ac
194
+ sudo npm install -g homebridge-slwf-01pro
195
+ ```
196
+
197
+ 2. **Edit `config.json`** — change `"platform": "ESPHomeAC"` to `"platform": "SLWFOnePro"`:
198
+ ```jsonc
199
+ {
200
+ "platforms": [
201
+ {
202
+ "platform": "SLWFOnePro", // ← was "ESPHomeAC"
203
+ "name": "SLWFOnePro", // ← rename to match (or keep your custom name)
204
+ "autoDiscover": true,
205
+ "devices": [ /* ...same as before... */ ]
206
+ }
207
+ ]
208
+ }
209
+ ```
210
+
211
+ 3. **Restart Homebridge**: `sudo systemctl restart homebridge` (or via UI).
212
+
213
+ The plugin will detect any cached accessories left over from upstream and **log a warning** with cleanup instructions on startup. Clean them via Homebridge UI → Settings → Remove Single Cached Accessory, or stop the (child) bridge and delete the relevant `cachedAccessories.<bridgeId>` file. The new accessories from this plugin will re-pair automatically on the next restart with stable UUIDs derived from each device's ESPHome `unique_id` (or from the MAC + `object_id` when `unique_id` isn't set).
194
214
 
195
- **Your `config.json` does not need changes.** The platform identifier (`"platform": "ESPHomeAC"`) is unchanged for compatibility — only the npm package name differs.
215
+ ### From a pre-0.2.0 release of this plugin
196
216
 
197
- If accessories appear duplicated after the migration, clear Homebridge's cached accessories from the UI (Settings Remove Single Cached Accessory) for the orphaned ones from the old plugin. The new plugin will re-pair them automatically on the next restart with stable UUIDs derived from each device's ESPHome `entity.config.uniqueId`.
217
+ If you upgraded from `homebridge-slwf-01pro@0.1.x`, the platform identifier rename is the only change you need to apply (step 2 above). The plugin will detect old `"ESPHomeAC"` cached entries and log a warning the same way.
198
218
 
199
219
  ## Known SLWF-01Pro quirks
200
220
 
@@ -9,8 +9,8 @@
9
9
  "description": "Sample config for the homebridge-SLWF-01Pro fork. Pick auto-discovery OR manual devices, or both.",
10
10
  "platforms": [
11
11
  {
12
- "platform": "ESPHomeAC",
13
- "name": "ESPHomeAC",
12
+ "platform": "SLWFOnePro",
13
+ "name": "SLWFOnePro",
14
14
  "debug": false,
15
15
  "autoDiscover": true,
16
16
  "discoveryTimeout": 5,
@@ -1,5 +1,5 @@
1
1
  {
2
- "pluginAlias": "ESPHomeAC",
2
+ "pluginAlias": "SLWFOnePro",
3
3
  "pluginType": "platform",
4
4
  "singular": true,
5
5
  "headerDisplay": "Homebridge plugin for ESPHome AC controllers (SLWF-01Pro Wi-Fi dongle and other ESPHome `Climate` entities). Supports auto-discovery, humidity / outdoor-temperature / power sensors, beeper / display switches, and DRY / FAN_ONLY mode tiles.",
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const ESPHome = require('./lib/esphome');
2
2
 
3
3
  const PLUGIN_NAME = 'homebridge-slwf-01pro';
4
- const PLATFORM_NAME = 'ESPHomeAC';
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
  }
@@ -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;
@@ -84,12 +87,13 @@ class DeviceAccessory {
84
87
 
85
88
  this.swingModeValue = pickDefaultSwingValue(this.config.supportedSwingModesList);
86
89
 
87
- this.UUID = this.api.hap.uuid.generate(this.id);
90
+ this.UUID = this.api.hap.uuid.generate(`${UUID_NAMESPACE}:${this.id}`);
88
91
  this.accessory = platform.accessories.find(acc => acc.UUID === this.UUID);
89
92
 
90
93
  if (!this.accessory) {
91
94
  this.log(`Creating new ESPHome AC accessory: "${this.name}"`);
92
95
  this.accessory = new this.api.platformAccessory(this.name, this.UUID);
96
+ this.accessory.context.schemaVersion = ACCESSORY_SCHEMA_VERSION;
93
97
  this.accessory.context.deviceId = this.id;
94
98
  this.accessory.context.host = this.host;
95
99
  this.accessory.context.lastTargetState = chooseInitialTargetMode(this.state.mode);
@@ -97,6 +101,7 @@ class DeviceAccessory {
97
101
  this.api.registerPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, [this.accessory]);
98
102
  } else {
99
103
  this.log(`ESPHome device "${this.name}" reconnected.`);
104
+ this.accessory.context.schemaVersion = ACCESSORY_SCHEMA_VERSION;
100
105
  this.accessory.context.host = this.host;
101
106
  if (PRIMARY_MODES.includes(this.state.mode)) {
102
107
  this.accessory.context.lastTargetState = this.state.mode;
package/lib/esphome.js CHANGED
@@ -1,3 +1,5 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const { Client } = require('@2colors/esphome-native-api');
2
4
  const DeviceAccessory = require('./DeviceAccessory');
3
5
  const { discoverDevices } = require('./discovery');
@@ -6,14 +8,75 @@ const { bundleEntities } = require('./classifyEntity');
6
8
  const RECONNECT_INTERVAL_MS = 5000;
7
9
  const DEFAULT_DISCOVERY_TIMEOUT_S = 5;
8
10
 
11
+ const UPSTREAM_PLUGIN_NAME = 'homebridge-esphome-ac';
12
+ const LEGACY_PLATFORM_NAME = 'ESPHomeAC';
13
+
9
14
  function normalizeHost(value) {
10
15
  if (!value) return '';
11
16
  return value.toString().toLowerCase().replace(/\.local\.?$/, '').replace(/\.$/, '');
12
17
  }
13
18
 
19
+ function detectOrphanedAccessories(platform) {
20
+ try {
21
+ const cacheDir = path.join(platform.api.user.persistPath(), 'accessories');
22
+ if (!fs.existsSync(cacheDir)) return;
23
+
24
+ const files = fs.readdirSync(cacheDir).filter(f => f.startsWith('cachedAccessories.'));
25
+ let upstreamCount = 0;
26
+ let legacyPlatformCount = 0;
27
+
28
+ for (const file of files) {
29
+ let content;
30
+ try {
31
+ content = JSON.parse(fs.readFileSync(path.join(cacheDir, file), 'utf8'));
32
+ } catch (_e) {
33
+ continue;
34
+ }
35
+ if (!Array.isArray(content)) continue;
36
+ for (const acc of content) {
37
+ if (!acc) continue;
38
+ if (acc.plugin === UPSTREAM_PLUGIN_NAME) upstreamCount++;
39
+ else if (acc.plugin === platform.PLUGIN_NAME && acc.platform === LEGACY_PLATFORM_NAME) legacyPlatformCount++;
40
+ }
41
+ }
42
+
43
+ if (upstreamCount > 0) {
44
+ platform.log.warn(
45
+ `Detected ${upstreamCount} cached accessor${upstreamCount === 1 ? 'y' : 'ies'} from upstream "${UPSTREAM_PLUGIN_NAME}". ` +
46
+ `These are orphans (this plugin is "${platform.PLUGIN_NAME}", different name). ` +
47
+ `Clean up via Homebridge UI → Settings → Remove Single Cached Accessory, ` +
48
+ `or stop Homebridge and delete cachedAccessories.* in ${cacheDir}, then restart.`
49
+ );
50
+ }
51
+ if (legacyPlatformCount > 0) {
52
+ platform.log.warn(
53
+ `Detected ${legacyPlatformCount} cached accessor${legacyPlatformCount === 1 ? 'y' : 'ies'} using the legacy "${LEGACY_PLATFORM_NAME}" platform identifier (pre-0.2.0). ` +
54
+ `The platform was renamed to "SLWFOnePro" in v0.2.0; the old entries are orphans. ` +
55
+ `Update your config.json (\`"platform": "SLWFOnePro"\`) and clean up via Homebridge UI → Settings → Remove Single Cached Accessory.`
56
+ );
57
+ }
58
+ } catch (err) {
59
+ platform.log.easyDebug(`Orphan detection failed (non-fatal): ${err.message || err}`);
60
+ }
61
+ }
62
+
63
+ function evictStaleSchemaAccessories(platform) {
64
+ if (!platform.staleAccessories || platform.staleAccessories.length === 0) return;
65
+ const stale = platform.staleAccessories.slice();
66
+ platform.staleAccessories = [];
67
+ 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.`);
68
+ try {
69
+ platform.api.unregisterPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, stale);
70
+ } catch (err) {
71
+ platform.log.error(`Failed to evict stale-schema accessories: ${err.message || err}`);
72
+ }
73
+ }
74
+
14
75
  async function init() {
15
76
  const platform = this;
16
77
  platform._clients = [];
78
+ evictStaleSchemaAccessories(platform);
79
+ detectOrphanedAccessories(platform);
17
80
 
18
81
  const manualDevices = (platform.devices || []).map(d => ({
19
82
  ...d,
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.1.2",
4
+ "version": "0.3.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/nookied/homebridge-SLWF-01Pro.git"