homebridge-navilink 0.1.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +202 -0
  3. package/README.md +161 -0
  4. package/SECURITY.md +73 -0
  5. package/config.schema.json +138 -0
  6. package/dist/api/channel.d.ts +94 -0
  7. package/dist/api/channel.js +334 -0
  8. package/dist/api/http.d.ts +68 -0
  9. package/dist/api/http.js +205 -0
  10. package/dist/api/identity.d.ts +51 -0
  11. package/dist/api/identity.js +80 -0
  12. package/dist/api/index.d.ts +17 -0
  13. package/dist/api/index.js +33 -0
  14. package/dist/api/mqtt-codec.d.ts +181 -0
  15. package/dist/api/mqtt-codec.js +377 -0
  16. package/dist/api/mqtt.d.ts +178 -0
  17. package/dist/api/mqtt.js +450 -0
  18. package/dist/api/protocol.d.ts +212 -0
  19. package/dist/api/protocol.js +286 -0
  20. package/dist/api/rest.d.ts +139 -0
  21. package/dist/api/rest.js +317 -0
  22. package/dist/api/sigv4.d.ts +70 -0
  23. package/dist/api/sigv4.js +123 -0
  24. package/dist/api/topics.d.ts +75 -0
  25. package/dist/api/topics.js +90 -0
  26. package/dist/devices/base-accessory.d.ts +127 -0
  27. package/dist/devices/base-accessory.js +232 -0
  28. package/dist/devices/dhw-accessory.d.ts +62 -0
  29. package/dist/devices/dhw-accessory.js +100 -0
  30. package/dist/devices/fault-accessory.d.ts +31 -0
  31. package/dist/devices/fault-accessory.js +73 -0
  32. package/dist/devices/heating-accessory.d.ts +63 -0
  33. package/dist/devices/heating-accessory.js +128 -0
  34. package/dist/devices/host.d.ts +77 -0
  35. package/dist/devices/host.js +23 -0
  36. package/dist/devices/index.d.ts +17 -0
  37. package/dist/devices/index.js +33 -0
  38. package/dist/devices/power-accessory.d.ts +32 -0
  39. package/dist/devices/power-accessory.js +86 -0
  40. package/dist/devices/probe-accessory.d.ts +45 -0
  41. package/dist/devices/probe-accessory.js +108 -0
  42. package/dist/devices/recirculation-accessory.d.ts +47 -0
  43. package/dist/devices/recirculation-accessory.js +122 -0
  44. package/dist/devices/thermostat-accessory.d.ts +129 -0
  45. package/dist/devices/thermostat-accessory.js +372 -0
  46. package/dist/discovery.d.ts +99 -0
  47. package/dist/discovery.js +423 -0
  48. package/dist/index.d.ts +11 -0
  49. package/dist/index.js +15 -0
  50. package/dist/platform.d.ts +120 -0
  51. package/dist/platform.js +425 -0
  52. package/dist/session.d.ts +243 -0
  53. package/dist/session.js +717 -0
  54. package/dist/settings.d.ts +218 -0
  55. package/dist/settings.js +240 -0
  56. package/dist/types/index.d.ts +265 -0
  57. package/dist/types/index.js +58 -0
  58. package/dist/ui-api.d.ts +27 -0
  59. package/dist/ui-api.js +39 -0
  60. package/dist/utils/context.d.ts +32 -0
  61. package/dist/utils/context.js +74 -0
  62. package/dist/utils/errors.d.ts +69 -0
  63. package/dist/utils/errors.js +118 -0
  64. package/dist/utils/index.d.ts +15 -0
  65. package/dist/utils/index.js +31 -0
  66. package/dist/utils/redact.d.ts +85 -0
  67. package/dist/utils/redact.js +210 -0
  68. package/dist/utils/serial.d.ts +18 -0
  69. package/dist/utils/serial.js +24 -0
  70. package/dist/utils/temperature.d.ts +114 -0
  71. package/dist/utils/temperature.js +150 -0
  72. package/dist/utils/timing.d.ts +68 -0
  73. package/dist/utils/timing.js +91 -0
  74. package/dist/utils/validators.d.ts +96 -0
  75. package/dist/utils/validators.js +353 -0
  76. package/docs/FEATURES.md +80 -0
  77. package/docs/PROTOCOL.md +200 -0
  78. package/docs/README-DETAILED.md +312 -0
  79. package/homebridge-ui/public/index.html +123 -0
  80. package/homebridge-ui/public/index.js +503 -0
  81. package/homebridge-ui/server.js +174 -0
  82. package/package.json +87 -0
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 tbaur
4
+ *
5
+ * Licensed under the Apache License, Version 2.0
6
+ * See LICENSE file for full license text
7
+ *
8
+ * @fileoverview MQTT 3.1.1 packet encoding and decoding, for the subset AWS
9
+ * IoT needs.
10
+ *
11
+ * Writing this rather than taking a dependency was a deliberate call, and the
12
+ * reasoning belongs next to the code it justifies.
13
+ *
14
+ * `mqtt.js` rebuilds the WebSocket path from its own parsed options, which
15
+ * discards the SigV4 query string, so the upgrade is rejected or hangs. The
16
+ * documented way round is `transformWsUrl`, a hook that exists to undo what the
17
+ * library just did. That is a bad sign for the one part of this plugin where a
18
+ * subtle failure means a user's heating silently stops responding.
19
+ *
20
+ * AWS IoT speaks 3.1.1, this plugin needs seven packet types, and the wire
21
+ * format is a fixed header, a variable-length integer and length-prefixed
22
+ * strings. It is a few hundred lines with no I/O in it, which makes it
23
+ * exhaustively testable without a socket. The family this plugin belongs to
24
+ * already hand-rolls a capped XML reader for the same reason. The dependency
25
+ * it replaces brings roughly twenty transitive packages into a plugin that
26
+ * handles a cloud password, which the security posture would rather not carry.
27
+ *
28
+ * What is deliberately absent: QoS 2 (AWS IoT does not support it), retained
29
+ * message handling beyond the flag, topic aliases and MQTT 5 properties. If a
30
+ * future NaviLink protocol needs any of them, add it here with a test rather
31
+ * than reaching back for the library.
32
+ *
33
+ * Section references are to the OASIS MQTT 3.1.1 specification.
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PacketDecoder = exports.MAX_PACKET_BYTES = exports.PacketType = void 0;
37
+ exports.describeConnackReturnCode = describeConnackReturnCode;
38
+ exports.isSubscribeFailure = isSubscribeFailure;
39
+ exports.encodeRemainingLength = encodeRemainingLength;
40
+ exports.encodeConnect = encodeConnect;
41
+ exports.encodePublish = encodePublish;
42
+ exports.encodePuback = encodePuback;
43
+ exports.encodeSubscribe = encodeSubscribe;
44
+ exports.encodePingreq = encodePingreq;
45
+ exports.encodeDisconnect = encodeDisconnect;
46
+ const errors_1 = require("../utils/errors");
47
+ /** Packet type codes (§2.2.1). */
48
+ exports.PacketType = {
49
+ CONNECT: 1,
50
+ CONNACK: 2,
51
+ PUBLISH: 3,
52
+ PUBACK: 4,
53
+ SUBSCRIBE: 8,
54
+ SUBACK: 9,
55
+ PINGREQ: 12,
56
+ PINGRESP: 13,
57
+ DISCONNECT: 14,
58
+ };
59
+ /**
60
+ * Largest packet accepted from the broker.
61
+ *
62
+ * A status frame is a couple of kilobytes. 256 KiB is generous while keeping a
63
+ * malfunctioning or hostile broker from growing the heap: the remaining-length
64
+ * field can encode 256 MB, and a decoder that trusts it will happily buffer
65
+ * that much before noticing.
66
+ */
67
+ exports.MAX_PACKET_BYTES = 262_144;
68
+ /** CONNACK return codes (§3.2.2.3). */
69
+ const CONNACK_REASONS = {
70
+ 0: 'accepted',
71
+ 1: 'unacceptable protocol version',
72
+ 2: 'client identifier rejected',
73
+ 3: 'server unavailable',
74
+ 4: 'bad username or password',
75
+ 5: 'not authorised',
76
+ };
77
+ /** Human-readable text for a CONNACK return code. */
78
+ function describeConnackReturnCode(code) {
79
+ return CONNACK_REASONS[code] ?? `unknown return code ${code}`;
80
+ }
81
+ /** True when a SUBACK return code means the subscription was refused (§3.9.3). */
82
+ function isSubscribeFailure(code) {
83
+ return code === 0x80;
84
+ }
85
+ // --- Encoding --------------------------------------------------------------
86
+ /**
87
+ * Encode the remaining-length field (§2.2.3).
88
+ *
89
+ * A base-128 varint, little-endian, with the top bit as the continuation flag
90
+ * and a hard limit of four bytes.
91
+ */
92
+ function encodeRemainingLength(length) {
93
+ if (!Number.isInteger(length) || length < 0 || length > 268_435_455) {
94
+ throw new errors_1.ProtocolError(`remaining length ${length} is outside the encodable range`);
95
+ }
96
+ const bytes = [];
97
+ let value = length;
98
+ do {
99
+ let byte = value % 128;
100
+ value = Math.floor(value / 128);
101
+ if (value > 0) {
102
+ byte = byte | 0x80;
103
+ }
104
+ bytes.push(byte);
105
+ } while (value > 0);
106
+ return bytes;
107
+ }
108
+ /** Length-prefixed UTF-8, as every MQTT string is (§1.5.3). */
109
+ function encodeString(value) {
110
+ const bytes = Buffer.from(value, 'utf8');
111
+ if (bytes.length > 0xFFFF) {
112
+ throw new errors_1.ProtocolError(`string of ${bytes.length} bytes exceeds the MQTT field limit`);
113
+ }
114
+ return [bytes.length >> 8, bytes.length & 0xFF, ...bytes];
115
+ }
116
+ function packet(type, flags, body) {
117
+ return Uint8Array.from([
118
+ (type << 4) | flags,
119
+ ...encodeRemainingLength(body.length),
120
+ ...body,
121
+ ]);
122
+ }
123
+ /**
124
+ * Build a CONNECT packet (§3.1).
125
+ *
126
+ * AWS IoT authenticates from the SigV4 signature on the WebSocket URL, so the
127
+ * username and password fields are not credentials here. The username is
128
+ * optional. The verified US endpoint accepts a CONNECT without one, so this
129
+ * plugin omits it rather than inventing an SDK marker we have not recorded.
130
+ */
131
+ function encodeConnect(options) {
132
+ const { clientId, keepaliveSec, cleanSession, username, password, will } = options;
133
+ let flags = 0;
134
+ if (cleanSession) {
135
+ flags |= 0x02;
136
+ }
137
+ if (will !== undefined) {
138
+ flags |= 0x04;
139
+ flags |= (will.qos & 0x03) << 3;
140
+ if (will.retain) {
141
+ flags |= 0x20;
142
+ }
143
+ }
144
+ if (username !== undefined) {
145
+ flags |= 0x80;
146
+ }
147
+ if (password !== undefined) {
148
+ flags |= 0x40;
149
+ }
150
+ const body = [
151
+ ...encodeString('MQTT'),
152
+ // Protocol level 4 is 3.1.1 (§3.1.2.2).
153
+ 0x04,
154
+ flags,
155
+ (keepaliveSec >> 8) & 0xFF,
156
+ keepaliveSec & 0xFF,
157
+ ...encodeString(clientId),
158
+ ];
159
+ if (will !== undefined) {
160
+ body.push(...encodeString(will.topic), ...encodeString(will.payload));
161
+ }
162
+ if (username !== undefined) {
163
+ body.push(...encodeString(username));
164
+ }
165
+ if (password !== undefined) {
166
+ body.push(...encodeString(password));
167
+ }
168
+ return packet(exports.PacketType.CONNECT, 0, body);
169
+ }
170
+ /**
171
+ * Build a PUBLISH packet (§3.3).
172
+ *
173
+ * `packetId` is required for QoS 1 and forbidden for QoS 0, which the
174
+ * specification states and this enforces: a QoS 1 publish with no identifier
175
+ * cannot be acknowledged, so the caller would wait for a PUBACK that can never
176
+ * be matched.
177
+ */
178
+ function encodePublish(input) {
179
+ const { topic, payload, qos, packetId, retain = false } = input;
180
+ if (qos === 1 && packetId === undefined) {
181
+ throw new errors_1.ProtocolError('a QoS 1 publish needs a packet identifier');
182
+ }
183
+ const flags = (qos << 1) | (retain ? 1 : 0);
184
+ const body = [...encodeString(topic)];
185
+ if (qos === 1 && packetId !== undefined) {
186
+ body.push((packetId >> 8) & 0xFF, packetId & 0xFF);
187
+ }
188
+ body.push(...Buffer.from(payload, 'utf8'));
189
+ return packet(exports.PacketType.PUBLISH, flags, body);
190
+ }
191
+ /** Build a PUBACK, acknowledging a QoS 1 message the broker sent us (§3.4). */
192
+ function encodePuback(packetId) {
193
+ return packet(exports.PacketType.PUBACK, 0, [(packetId >> 8) & 0xFF, packetId & 0xFF]);
194
+ }
195
+ /**
196
+ * Build a SUBSCRIBE packet (§3.8).
197
+ *
198
+ * The fixed-header flags are required to be `0b0010`; a broker must treat
199
+ * anything else as a protocol violation and close the connection.
200
+ */
201
+ function encodeSubscribe(packetId, subscriptions) {
202
+ if (subscriptions.length === 0) {
203
+ throw new errors_1.ProtocolError('a subscribe needs at least one topic filter');
204
+ }
205
+ const body = [(packetId >> 8) & 0xFF, packetId & 0xFF];
206
+ for (const subscription of subscriptions) {
207
+ body.push(...encodeString(subscription.topic), subscription.qos);
208
+ }
209
+ return packet(exports.PacketType.SUBSCRIBE, 0x02, body);
210
+ }
211
+ /** Build a PINGREQ (§3.12). */
212
+ function encodePingreq() {
213
+ return packet(exports.PacketType.PINGREQ, 0, []);
214
+ }
215
+ /** Build a DISCONNECT (§3.14), which tells the broker not to send our will. */
216
+ function encodeDisconnect() {
217
+ return packet(exports.PacketType.DISCONNECT, 0, []);
218
+ }
219
+ /**
220
+ * Read a remaining-length varint from `buffer` at `offset`.
221
+ *
222
+ * Returns undefined when the field is not yet complete, which is normal: a
223
+ * WebSocket frame boundary can fall anywhere, including inside this field.
224
+ */
225
+ function readRemainingLength(buffer, offset) {
226
+ let multiplier = 1;
227
+ let value = 0;
228
+ for (let index = 0; index < 4; index += 1) {
229
+ const byte = buffer[offset + index];
230
+ if (byte === undefined) {
231
+ return undefined;
232
+ }
233
+ value += (byte & 0x7F) * multiplier;
234
+ if ((byte & 0x80) === 0) {
235
+ return { value, bytes: index + 1 };
236
+ }
237
+ multiplier *= 128;
238
+ }
239
+ throw new errors_1.ProtocolError('remaining length field is longer than four bytes');
240
+ }
241
+ function readUint16(buffer, offset) {
242
+ const high = buffer[offset];
243
+ const low = buffer[offset + 1];
244
+ if (high === undefined || low === undefined) {
245
+ throw new errors_1.ProtocolError('packet ended inside a two-byte field');
246
+ }
247
+ return (high << 8) | low;
248
+ }
249
+ function readString(buffer, offset) {
250
+ const length = readUint16(buffer, offset);
251
+ const start = offset + 2;
252
+ const end = start + length;
253
+ if (end > buffer.length) {
254
+ throw new errors_1.ProtocolError('packet ended inside a string');
255
+ }
256
+ return { value: Buffer.from(buffer.subarray(start, end)).toString('utf8'), next: end };
257
+ }
258
+ function decodePublish(flags, body) {
259
+ const qos = (flags >> 1) & 0x03;
260
+ if (qos > 1) {
261
+ // QoS 2 needs a four-packet handshake this client does not implement. AWS
262
+ // IoT does not offer it, so receiving one means something is badly wrong
263
+ // and pretending otherwise would drop the message silently.
264
+ throw new errors_1.ProtocolError(`QoS ${qos} is not supported`);
265
+ }
266
+ const { value: topic, next } = readString(body, 0);
267
+ let offset = next;
268
+ let packetId;
269
+ if (qos === 1) {
270
+ packetId = readUint16(body, offset);
271
+ offset += 2;
272
+ }
273
+ const result = {
274
+ type: exports.PacketType.PUBLISH,
275
+ topic,
276
+ payload: body.subarray(offset),
277
+ qos: qos,
278
+ retain: (flags & 0x01) === 1,
279
+ dup: (flags & 0x08) !== 0,
280
+ };
281
+ if (packetId !== undefined) {
282
+ result.packetId = packetId;
283
+ }
284
+ return result;
285
+ }
286
+ function decodeBody(type, flags, body) {
287
+ switch (type) {
288
+ case exports.PacketType.CONNACK: {
289
+ if (body.length < 2) {
290
+ throw new errors_1.ProtocolError('CONNACK is shorter than two bytes');
291
+ }
292
+ return {
293
+ type: exports.PacketType.CONNACK,
294
+ sessionPresent: ((body[0] ?? 0) & 0x01) === 1,
295
+ returnCode: body[1] ?? 0,
296
+ };
297
+ }
298
+ case exports.PacketType.PUBLISH:
299
+ return decodePublish(flags, body);
300
+ case exports.PacketType.PUBACK:
301
+ return { type: exports.PacketType.PUBACK, packetId: readUint16(body, 0) };
302
+ case exports.PacketType.SUBACK:
303
+ return {
304
+ type: exports.PacketType.SUBACK,
305
+ packetId: readUint16(body, 0),
306
+ returnCodes: [...body.subarray(2)],
307
+ };
308
+ case exports.PacketType.PINGRESP:
309
+ return { type: exports.PacketType.PINGRESP };
310
+ default:
311
+ throw new errors_1.ProtocolError(`unexpected packet type ${type} from the broker`);
312
+ }
313
+ }
314
+ /**
315
+ * Reassembles packets from a byte stream.
316
+ *
317
+ * Needed because MQTT framing and WebSocket framing are unrelated. One
318
+ * WebSocket message can carry several MQTT packets, one MQTT packet can be
319
+ * split across several WebSocket messages, and a split can fall inside the
320
+ * length field itself. A decoder that assumed one frame is one packet works
321
+ * perfectly on a quiet connection and corrupts under load, which is the worst
322
+ * possible failure schedule.
323
+ */
324
+ class PacketDecoder {
325
+ buffer = new Uint8Array(0);
326
+ /**
327
+ * Add received bytes and return every complete packet now available.
328
+ *
329
+ * Throws {@link ProtocolError} on a malformed stream. The caller is expected
330
+ * to treat that as fatal for the connection: once framing is lost there is no
331
+ * way to resynchronise, because MQTT has no frame delimiter to scan for.
332
+ */
333
+ push(chunk) {
334
+ this.buffer = this.buffer.length === 0
335
+ ? chunk
336
+ : concat(this.buffer, chunk);
337
+ const packets = [];
338
+ let offset = 0;
339
+ for (;;) {
340
+ const header = this.buffer[offset];
341
+ if (header === undefined) {
342
+ break;
343
+ }
344
+ const length = readRemainingLength(this.buffer, offset + 1);
345
+ if (length === undefined) {
346
+ break;
347
+ }
348
+ if (length.value > exports.MAX_PACKET_BYTES) {
349
+ throw new errors_1.ProtocolError(`packet of ${length.value} bytes exceeds the ${exports.MAX_PACKET_BYTES} byte limit`);
350
+ }
351
+ const start = offset + 1 + length.bytes;
352
+ const end = start + length.value;
353
+ if (end > this.buffer.length) {
354
+ break;
355
+ }
356
+ packets.push(decodeBody(header >> 4, header & 0x0F, this.buffer.subarray(start, end)));
357
+ offset = end;
358
+ }
359
+ // Retained rather than copied when nothing was consumed, so a large packet
360
+ // arriving in many small frames does not re-copy its prefix each time.
361
+ if (offset > 0) {
362
+ this.buffer = this.buffer.subarray(offset);
363
+ }
364
+ return packets;
365
+ }
366
+ /** Bytes held pending the rest of a packet. Exposed for tests and diagnostics. */
367
+ get pending() {
368
+ return this.buffer.length;
369
+ }
370
+ }
371
+ exports.PacketDecoder = PacketDecoder;
372
+ function concat(left, right) {
373
+ const merged = new Uint8Array(left.length + right.length);
374
+ merged.set(left, 0);
375
+ merged.set(right, left.length);
376
+ return merged;
377
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Copyright (c) 2026 tbaur
3
+ *
4
+ * Licensed under the Apache License, Version 2.0
5
+ * See LICENSE file for full license text
6
+ *
7
+ * @fileoverview One MQTT-over-WebSocket connection to AWS IoT.
8
+ *
9
+ * This is the transport and nothing else: it knows about packets, packet
10
+ * identifiers, keepalive and the socket. It knows nothing about NaviLink
11
+ * topics, commands or appliances, which live in `protocol.ts` and `session.ts`.
12
+ * The split is what lets the whole of this file be tested against a stand-in
13
+ * socket with no network and no clock.
14
+ *
15
+ * Three behaviours here are load-bearing and easy to leave out.
16
+ *
17
+ * **An unanswered ping ends the connection.** A WebSocket can stay open with
18
+ * nothing behind it: a NAT that dropped the mapping, a broker that vanished
19
+ * without a close frame. Nothing surfaces that except a ping that goes
20
+ * unanswered, and without this check the plugin reports yesterday's setpoint
21
+ * as current state for as long as the socket stays nominally open.
22
+ *
23
+ * **A failure is terminal for the connection, never for the process.** Every
24
+ * error path ends in {@link fail}, which settles anything waiting, reports
25
+ * once, and hands control to the owner to decide about reconnecting. Nothing
26
+ * here retries: a transport that reconnects itself competes with the session
27
+ * above it that also wants to.
28
+ *
29
+ * **The signed URL is never logged.** It carries the session token. The one
30
+ * thing this file prints about its destination is the endpoint host.
31
+ */
32
+ import type { PluginLogger } from '../types';
33
+ import { type Subscription, type WillMessage } from './mqtt-codec';
34
+ import { type IotCredentials } from './sigv4';
35
+ /** Callbacks a socket implementation must drive. */
36
+ export interface SocketHandlers {
37
+ onOpen(): void;
38
+ onMessage(data: Uint8Array): void;
39
+ onClose(reason: string): void;
40
+ onError(error: Error): void;
41
+ }
42
+ /** The socket operations this transport needs. */
43
+ export interface MessageSocket {
44
+ send(data: Uint8Array): void;
45
+ close(): void;
46
+ }
47
+ /** Opens a WebSocket. Injectable so tests never touch the network. */
48
+ export type SocketFactory = (url: string, handlers: SocketHandlers) => MessageSocket;
49
+ /** A message delivered by the broker. */
50
+ export interface MqttMessage {
51
+ topic: string;
52
+ payload: string;
53
+ }
54
+ /** Collaborators for one connection. */
55
+ export interface MqttConnectionOptions {
56
+ log: PluginLogger;
57
+ credentials: IotCredentials;
58
+ clientId: string;
59
+ will?: WillMessage;
60
+ /**
61
+ * Username presented in CONNECT.
62
+ *
63
+ * Optional. AWS IoT authenticates from the URL signature. The verified
64
+ * endpoint accepts a CONNECT without a username, so the session leaves this
65
+ * unset rather than inventing an SDK marker.
66
+ */
67
+ username?: string;
68
+ /** Sign the `host` header with `:443`. The fallback; see {@link presignIotWebsocketUrl}. */
69
+ signHostWithPort?: boolean;
70
+ openSocket?: SocketFactory;
71
+ /** Injected in tests so keepalive can be driven without real time. */
72
+ setTimer?: (handler: () => void, ms: number) => unknown;
73
+ clearTimer?: (handle: unknown) => void;
74
+ }
75
+ /** The default socket factory: the runtime's own WebSocket. */
76
+ export declare const openWebSocket: SocketFactory;
77
+ /**
78
+ * One live MQTT session.
79
+ *
80
+ * Single use. Once it has failed or been closed it stays that way; the owner
81
+ * builds a new one to reconnect. That keeps the state machine to two states
82
+ * and means a stale reference cannot silently resurrect a connection whose
83
+ * credentials have since expired.
84
+ */
85
+ export declare class MqttConnection {
86
+ private readonly options;
87
+ private readonly decoder;
88
+ private readonly pending;
89
+ private socket;
90
+ private nextPacketId;
91
+ private connected;
92
+ private closed;
93
+ /** Set once, and the reason every later operation rejects with. */
94
+ private failure;
95
+ private connectSettle;
96
+ private keepaliveTimer;
97
+ private pingTimeoutTimer;
98
+ private onMessageHandler;
99
+ private onCloseHandler;
100
+ constructor(options: MqttConnectionOptions);
101
+ /** True while the broker has accepted us and nothing has gone wrong. */
102
+ get isConnected(): boolean;
103
+ /** Register the handler for inbound application messages. */
104
+ onMessage(handler: (message: MqttMessage) => void): void;
105
+ /**
106
+ * Register the handler for the connection ending.
107
+ *
108
+ * Called exactly once, for any reason including a clean {@link close}, so an
109
+ * owner has one place to decide whether to reconnect.
110
+ */
111
+ onClose(handler: (error: Error) => void): void;
112
+ /**
113
+ * Open the socket and complete the MQTT handshake.
114
+ *
115
+ * Resolves when CONNACK reports acceptance. Rejects on a refused CONNACK, a
116
+ * socket that will not open, or a handshake that does not finish inside
117
+ * {@link MQTT_CONNECT_TIMEOUT_MS}.
118
+ */
119
+ connect(): Promise<void>;
120
+ /**
121
+ * Subscribe, and confirm the broker granted every filter.
122
+ *
123
+ * A refusal is treated as fatal rather than logged and carried on with. A
124
+ * NaviLink session that is subscribed to some of its response topics is
125
+ * worse than one that failed: state arrives for some accessories and not
126
+ * others, which reads as a hardware fault rather than a permissions problem.
127
+ */
128
+ subscribe(subscriptions: readonly Subscription[]): Promise<void>;
129
+ /**
130
+ * Publish at QoS 1 and wait for the broker's acknowledgement.
131
+ *
132
+ * QoS 1 rather than 0 because a control command that vanished in transit
133
+ * must not be reported to HomeKit as applied. Note the limit of what this
134
+ * proves: a PUBACK means the *broker* has the message, not that the
135
+ * appliance has acted on it. The appliance's answer arrives separately, as a
136
+ * status frame or a rejection on the failure topic.
137
+ */
138
+ publish(topic: string, payload: string, timeoutMs: number): Promise<void>;
139
+ /**
140
+ * Close cleanly.
141
+ *
142
+ * DISCONNECT before closing the socket is what stops the broker publishing
143
+ * our last will. The will exists to tell the gateway we have gone away
144
+ * unexpectedly, so firing it on an orderly shutdown would be a lie, and on
145
+ * a Homebridge restart, a lie the gateway acts on.
146
+ */
147
+ close(): void;
148
+ private handleOpen;
149
+ private handleData;
150
+ private handlePacket;
151
+ private handleConnack;
152
+ private handlePublish;
153
+ /** Send a packet and wait for the acknowledgement carrying the same id. */
154
+ private exchange;
155
+ private settle;
156
+ /**
157
+ * Allocate a packet identifier.
158
+ *
159
+ * One to 65535, wrapping, skipping any still in flight. Zero is reserved by
160
+ * the specification. The scan is bounded so a session that somehow filled
161
+ * the space fails loudly instead of looping.
162
+ */
163
+ private takePacketId;
164
+ private scheduleKeepalive;
165
+ private send;
166
+ /** Send without caring whether it worked, for teardown and keepalive paths. */
167
+ private trySend;
168
+ /**
169
+ * End the connection, settling everything waiting on it.
170
+ *
171
+ * Idempotent, because several paths can reach it at once: a socket error is
172
+ * routinely followed by a close event, and a ping timeout can race the close
173
+ * it predicted.
174
+ */
175
+ private fail;
176
+ private setTimer;
177
+ private clearTimer;
178
+ }