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 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.2",
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) this.conn.getPort(this.conn.ports.status);
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
- if (!acc) {
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
  }