homebridge-philips-airplus-cloud 0.3.1 → 0.3.3
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 +9 -0
- package/README.md +1 -0
- package/config.schema.json +8 -1
- package/package.json +1 -1
- package/src/accessory.js +25 -1
- package/src/connection.js +13 -0
- package/src/platform.js +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
The Home Assistant integration in `custom_components/` is unchanged from upstream and keeps its own history.
|
|
4
4
|
|
|
5
|
+
## 0.3.3 — 2026-10-11
|
|
6
|
+
|
|
7
|
+
- Fix: the plugin could stop updating when the cloud connection stayed open but stopped delivering data. If the purifier sends nothing for three status polls (at least 6 minutes), the connection is now rebuilt.
|
|
8
|
+
- Fix: a new accessory is announced to HomeKit only once all its services exist, so the Home app sees the labelled, linked components from the start.
|
|
9
|
+
|
|
10
|
+
## 0.3.2 — 2026-10-11
|
|
11
|
+
|
|
12
|
+
- Option `homekitIdentity`: gives the purifier a new HomeKit identity when the Home app keeps a stuck record of it (rooms and names cannot be changed). The previous accessory is removed automatically.
|
|
13
|
+
|
|
5
14
|
## 0.3.1 — 2026-10-11
|
|
6
15
|
|
|
7
16
|
- Fix: on a fresh install the purifier started with the reduced profile ("Model AC0651 is not in models.yaml") because the cloud device list gives the model without its `/10` suffix. The short code now matches its `models.yaml` entry, and an unknown code waits for the exact one reported by the device.
|
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ The refresh token rotates on every use; the plugin keeps the current one in `<ho
|
|
|
83
83
|
| `displayLight` | `true` | Show the display light |
|
|
84
84
|
| `displayLightProperty` | — | Property key of the display light, if it is not detected (see the `Status properties reported by the device` log line) |
|
|
85
85
|
| `logDeviceData` | `false` | Diagnostics: at startup, log every port and the cloud shadow the purifier publishes |
|
|
86
|
+
| `homekitIdentity` | — | Change to any new value to give the purifier a new HomeKit identity (use only if the Home app refuses to move or rename it; reassign the room afterwards) |
|
|
86
87
|
| `filterChangeThreshold` | `10` | Filter life (%) at or below which HomeKit shows "replace filter" |
|
|
87
88
|
| `pollInterval` | `120` | Seconds between safety-net status polls |
|
|
88
89
|
|
package/config.schema.json
CHANGED
|
@@ -122,6 +122,12 @@
|
|
|
122
122
|
"type": "boolean",
|
|
123
123
|
"default": false,
|
|
124
124
|
"description": "At startup, ask the purifier for all its ports and cloud shadow and write them to the log. Turn off again afterwards."
|
|
125
|
+
},
|
|
126
|
+
"homekitIdentity": {
|
|
127
|
+
"title": "HomeKit identity (advanced)",
|
|
128
|
+
"type": "string",
|
|
129
|
+
"placeholder": "e.g. 2",
|
|
130
|
+
"description": "Only if the Home app refuses to move or rename the purifier: type any new value (e.g. 2, then 3 next time) and restart. Home will see the purifier as a new accessory; reassign its room and check your scenes afterwards."
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
},
|
|
@@ -165,7 +171,8 @@
|
|
|
165
171
|
"displayLightProperty",
|
|
166
172
|
"filterChangeThreshold",
|
|
167
173
|
"pollInterval",
|
|
168
|
-
"logDeviceData"
|
|
174
|
+
"logDeviceData",
|
|
175
|
+
"homekitIdentity"
|
|
169
176
|
]
|
|
170
177
|
}
|
|
171
178
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-philips-airplus-cloud",
|
|
3
3
|
"displayName": "Philips Air+",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"description": "Homebridge plugin for Philips Air+ / PureProtect air purifiers via the Philips (Versuni) cloud — the same MQTT protocol used by the Air+ app and Google Home.",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"license": "MIT",
|
package/src/accessory.js
CHANGED
|
@@ -91,9 +91,28 @@ class PurifierAccessory {
|
|
|
91
91
|
start() {
|
|
92
92
|
this.conn.start();
|
|
93
93
|
const pollS = Math.max(30, Number(this.platform.config.pollInterval) || C.STATUS_POLL_INTERVAL_S);
|
|
94
|
+
// Watchdog: the cloud connection can stay open but stop delivering data.
|
|
95
|
+
// Every poll asks the device for its status, so if nothing has arrived for
|
|
96
|
+
// three polls (at least 6 minutes) the connection is rebuilt.
|
|
97
|
+
const silenceMs = Math.max(3 * pollS, 360) * 1000;
|
|
94
98
|
this.pollTimer = setInterval(async () => {
|
|
95
99
|
await this.conn.refreshIfNeeded();
|
|
96
|
-
if (this.conn.connected)
|
|
100
|
+
if (!this.conn.connected) return;
|
|
101
|
+
const silent = Date.now() - this.conn.lastMessageAt;
|
|
102
|
+
if (silent > silenceMs) {
|
|
103
|
+
// Warn once; if the purifier is simply unplugged keep retrying quietly.
|
|
104
|
+
const msg = `No data from the purifier for ${Math.round(silent / 60000)} min; reconnecting to the Philips cloud`;
|
|
105
|
+
if (this._silenceWarned) this.log.debug(msg);
|
|
106
|
+
else this.log.warn(msg);
|
|
107
|
+
this._silenceWarned = true;
|
|
108
|
+
this.conn.reconnect();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (this._silenceWarned) {
|
|
112
|
+
this._silenceWarned = false;
|
|
113
|
+
this.log.info('Receiving data from the purifier again');
|
|
114
|
+
}
|
|
115
|
+
this.conn.getPort(this.conn.ports.status);
|
|
97
116
|
}, pollS * 1000);
|
|
98
117
|
this.filterTimer = setInterval(() => {
|
|
99
118
|
if (this.conn.connected) this.conn.getPort(this.conn.ports.filter_read);
|
|
@@ -125,6 +144,11 @@ class PurifierAccessory {
|
|
|
125
144
|
this._setInfo();
|
|
126
145
|
this._pushAll();
|
|
127
146
|
this.log.info(`Using model profile "${this.model.name}" (${this.speeds.length} manual speeds${this.hasAuto ? ' + Auto' : ''})`);
|
|
147
|
+
if (this.onModelReady) {
|
|
148
|
+
const cb = this.onModelReady;
|
|
149
|
+
this.onModelReady = null;
|
|
150
|
+
cb();
|
|
151
|
+
}
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
_prop(name) {
|
package/src/connection.js
CHANGED
|
@@ -70,6 +70,16 @@ class DeviceConnection extends EventEmitter {
|
|
|
70
70
|
this.attempt = 0;
|
|
71
71
|
this.timer = null;
|
|
72
72
|
this.ports = { ...C.DEFAULT_PORTS };
|
|
73
|
+
this.lastMessageAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Drop the current connection and connect again with fresh credentials. */
|
|
77
|
+
reconnect() {
|
|
78
|
+
if (this.stopped) return;
|
|
79
|
+
this.connected = false;
|
|
80
|
+
this.attempt = 0;
|
|
81
|
+
clearTimeout(this.timer);
|
|
82
|
+
this._connect();
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
setPorts(ports) {
|
|
@@ -161,12 +171,15 @@ class DeviceConnection extends EventEmitter {
|
|
|
161
171
|
if (client !== this.client) return;
|
|
162
172
|
this.connected = true;
|
|
163
173
|
this.attempt = 0;
|
|
174
|
+
this.lastMessageAt = Date.now();
|
|
164
175
|
client.subscribe(C.TOPIC_STATUS(this.deviceId), { qos: 0 });
|
|
165
176
|
this.log.info('Connected to Philips cloud');
|
|
166
177
|
this.emit('connected');
|
|
167
178
|
});
|
|
168
179
|
|
|
169
180
|
client.on('message', (topic, payload) => {
|
|
181
|
+
if (client !== this.client) return;
|
|
182
|
+
this.lastMessageAt = Date.now();
|
|
170
183
|
let messages;
|
|
171
184
|
try {
|
|
172
185
|
messages = parseConcatenatedJson(payload.toString('utf8'));
|
package/src/platform.js
CHANGED
|
@@ -66,20 +66,33 @@ class PhilipsAirPlusPlatform {
|
|
|
66
66
|
|
|
67
67
|
const keep = new Set();
|
|
68
68
|
for (const dev of wanted) {
|
|
69
|
-
|
|
69
|
+
// homekitIdentity: changing it gives the accessory a brand-new HomeKit
|
|
70
|
+
// identity, for when the Home app has a stuck record of the old one.
|
|
71
|
+
const identity = String(this.config.homekitIdentity || '').trim();
|
|
72
|
+
const hbUuid = this.api.hap.uuid.generate(`${C.PLATFORM_NAME}:${dev.uuid}${identity ? `:${identity}` : ''}`);
|
|
70
73
|
keep.add(hbUuid);
|
|
71
74
|
let acc = this.cached.get(hbUuid);
|
|
72
|
-
|
|
75
|
+
const isNew = !acc;
|
|
76
|
+
if (isNew) {
|
|
73
77
|
acc = new this.api.platformAccessory(dev.name, hbUuid, this.api.hap.Categories.AIR_PURIFIER);
|
|
74
78
|
acc.context.uuid = dev.uuid;
|
|
75
79
|
if (dev.model) acc.context.model = dev.model;
|
|
76
|
-
this.log.info(`Adding ${dev.name}`);
|
|
77
|
-
this.api.registerPlatformAccessories(C.PLUGIN_NAME, C.PLATFORM_NAME, [acc]);
|
|
78
80
|
} else if (!acc.context.model && dev.model) {
|
|
79
81
|
acc.context.model = dev.model;
|
|
80
82
|
}
|
|
81
83
|
const handler = new PurifierAccessory(this, acc, { account: this.account });
|
|
82
84
|
this.handlers.set(hbUuid, handler);
|
|
85
|
+
if (isNew) {
|
|
86
|
+
// Announce a new accessory to HomeKit only once all its services exist:
|
|
87
|
+
// the Home app records names and links when it first sees an accessory,
|
|
88
|
+
// and services added to an already published accessory may show unlabelled.
|
|
89
|
+
const register = () => {
|
|
90
|
+
this.log.info(`Adding ${dev.name}`);
|
|
91
|
+
this.api.registerPlatformAccessories(C.PLUGIN_NAME, C.PLATFORM_NAME, [acc]);
|
|
92
|
+
};
|
|
93
|
+
if (handler.model) register();
|
|
94
|
+
else handler.onModelReady = register;
|
|
95
|
+
}
|
|
83
96
|
handler.conn.on('authExpired', () => this._authExpired());
|
|
84
97
|
handler.start();
|
|
85
98
|
}
|