homebridge-philips-airplus-cloud 0.3.2 → 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 +5 -0
- package/package.json +1 -1
- package/src/accessory.js +25 -1
- package/src/connection.js +13 -0
- package/src/platform.js +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
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
|
+
|
|
5
10
|
## 0.3.2 — 2026-10-11
|
|
6
11
|
|
|
7
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.
|
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
|
@@ -72,17 +72,27 @@ class PhilipsAirPlusPlatform {
|
|
|
72
72
|
const hbUuid = this.api.hap.uuid.generate(`${C.PLATFORM_NAME}:${dev.uuid}${identity ? `:${identity}` : ''}`);
|
|
73
73
|
keep.add(hbUuid);
|
|
74
74
|
let acc = this.cached.get(hbUuid);
|
|
75
|
-
|
|
75
|
+
const isNew = !acc;
|
|
76
|
+
if (isNew) {
|
|
76
77
|
acc = new this.api.platformAccessory(dev.name, hbUuid, this.api.hap.Categories.AIR_PURIFIER);
|
|
77
78
|
acc.context.uuid = dev.uuid;
|
|
78
79
|
if (dev.model) acc.context.model = dev.model;
|
|
79
|
-
this.log.info(`Adding ${dev.name}`);
|
|
80
|
-
this.api.registerPlatformAccessories(C.PLUGIN_NAME, C.PLATFORM_NAME, [acc]);
|
|
81
80
|
} else if (!acc.context.model && dev.model) {
|
|
82
81
|
acc.context.model = dev.model;
|
|
83
82
|
}
|
|
84
83
|
const handler = new PurifierAccessory(this, acc, { account: this.account });
|
|
85
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
|
+
}
|
|
86
96
|
handler.conn.on('authExpired', () => this._authExpired());
|
|
87
97
|
handler.start();
|
|
88
98
|
}
|