bridge: publish through a Publisher; devices no longer hold the broker client
src/transport.ts has the Publisher interface, MqttPublisher (the client the bridge has at the time) and MemoryPublisher (tests without a broker); src/topics.ts names the four topics the library publishes to. Device, AlexaStatusMessage, AlexaErrorResponse and sendSceneResponse shared three copies of the publish-and-report code: they now call one send() that resolves the topic or "" and never rejects. registerDevice() and addDevice() work before connect(); a send() without a connection resolves "" and the "error" event says to call connect(). unregisterDevice() and clearDevices() take the publisher from the device. Device.setMqttClient() is gone, the first constructor argument of Device is ignored, and the message classes take a Publisher where they took the client. Tests: 108 -> 113. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
46fa06728c
commit
6789a1a077
39 changed files with 839 additions and 246 deletions
37
dist/esm/Alex2Node.js
vendored
37
dist/esm/Alex2Node.js
vendored
|
|
@ -5,6 +5,8 @@ import { checkEndpoint } from "./device/validate.js";
|
|||
import { EventEmitter } from "events";
|
||||
import { DisplayCategory } from "./compat/enums.js";
|
||||
import { DeclarationError } from "./registry/types.js";
|
||||
import * as topics from "./topics.js";
|
||||
import { MqttPublisher } from "./transport.js";
|
||||
export const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883";
|
||||
// What addDevice() copies from the definition to the device
|
||||
const DESCRIBED = [
|
||||
|
|
@ -33,6 +35,8 @@ class Alex2MQTT extends EventEmitter {
|
|||
this.rootTopic = rootTopic;
|
||||
this.debugLogging = debugLogging;
|
||||
this.client = null;
|
||||
// One for the life of the bridge: the devices keep it over disconnect() and connect()
|
||||
this.publisher = new MqttPublisher(() => this.client);
|
||||
this.devices = [];
|
||||
// The lines of check() that were logged. Discovery comes every few minutes: a line is logged once.
|
||||
this.logged = new Set();
|
||||
|
|
@ -70,8 +74,6 @@ class Alex2MQTT extends EventEmitter {
|
|||
...(this.options.mqtt || {}),
|
||||
};
|
||||
this.client = mqtt.connect(this.MqttHost, options);
|
||||
for (const d of this.devices)
|
||||
d.setMqttClient(this.client); // after a disconnect(): the devices publish through the new client
|
||||
this.client.on("connect", () => {
|
||||
this.connected = true;
|
||||
this.log("Connected to MQTT broker");
|
||||
|
|
@ -94,12 +96,12 @@ class Alex2MQTT extends EventEmitter {
|
|||
this.log("Discovery request received, getting device json...");
|
||||
const deviceArray = this.describeDevices();
|
||||
this.lastDiscoveryAt = new Date().toISOString();
|
||||
this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => {
|
||||
if (err) {
|
||||
this.fail(err);
|
||||
void this.publisher.publish(topics.discoverReply(this.rootTopic), deviceArray).then((result) => {
|
||||
if (!result.ok) {
|
||||
this.fail(result.error);
|
||||
return;
|
||||
}
|
||||
this.log(`Discovery payloads published to ${topic + "_r"}`, deviceArray);
|
||||
this.log(`Discovery payloads published to ${result.topic}`, deviceArray);
|
||||
this.emit("discover", deviceArray.length);
|
||||
});
|
||||
}
|
||||
|
|
@ -173,15 +175,13 @@ class Alex2MQTT extends EventEmitter {
|
|||
*
|
||||
* Throws a DeclarationError when Alexa would reject the endpoint (an endpointId with a slash, a name with
|
||||
* punctuation) or when the endpointId is registered already. Discovery lists Alexa.EndpointHealth for the device
|
||||
* unless endpointHealth is false.
|
||||
* unless endpointHealth is false. A device can be declared before connect(): what it sends before the bridge has
|
||||
* a connection is not published, and the "error" event says so.
|
||||
*/
|
||||
addDevice(definition) {
|
||||
if (!this.client) {
|
||||
throw new Error("Must call connect before creating devices");
|
||||
}
|
||||
const { endpointId, name, categories, endpointHealth, ...described } = definition;
|
||||
// Without categories the device would take LIGHT, the default of registerDevice: here it is a mistake
|
||||
const device = new Device(this.client, this.rootTopic, name, endpointId, (categories ?? []));
|
||||
const device = new Device(null, this.rootTopic, name, endpointId, (categories ?? []));
|
||||
for (const [field, value] of Object.entries(described)) {
|
||||
if (!DESCRIBED.includes(field))
|
||||
throw new DeclarationError({ endpointId }, `${field} is not a field of an endpoint`);
|
||||
|
|
@ -196,9 +196,6 @@ class Alex2MQTT extends EventEmitter {
|
|||
return device;
|
||||
}
|
||||
registerDevice(name, endpointId, displayCategory) {
|
||||
if (!this.client) {
|
||||
throw new Error("Must call connect before creating devices");
|
||||
}
|
||||
const existing = this.devices.find((d) => d.endpointId === endpointId);
|
||||
if (existing) { // 1.5.2: endpointIds are unique per root topic; 1.5.1 added a second device that never got a directive
|
||||
const warning = `warning: registerDevice("${endpointId}") is already registered as "${existing.name}", returning that device`;
|
||||
|
|
@ -210,7 +207,7 @@ class Alex2MQTT extends EventEmitter {
|
|||
}
|
||||
this.log(`Creating new device with endpoint: ${endpointId}`);
|
||||
const normalizedCategory = Array.isArray(displayCategory) ? displayCategory : [displayCategory || DisplayCategory.LIGHT];
|
||||
const device = new Device(this.client, this.rootTopic, name, endpointId, normalizedCategory);
|
||||
const device = new Device(null, this.rootTopic, name, endpointId, normalizedCategory);
|
||||
this.register(device, false);
|
||||
return device;
|
||||
}
|
||||
|
|
@ -218,6 +215,7 @@ class Alex2MQTT extends EventEmitter {
|
|||
device.alexaInterface = this.options.alexaInterface !== false;
|
||||
device.endpointHealth = endpointHealth;
|
||||
device.onPublishError = (err) => this.fail(err); // a failed publish is an "error" event (when listened to), never a rejected send()
|
||||
device.publisher = this.publisher;
|
||||
this.devices.push(device);
|
||||
}
|
||||
/** Forget a device (its listeners with it). Returns false when there was none. */
|
||||
|
|
@ -225,16 +223,21 @@ class Alex2MQTT extends EventEmitter {
|
|||
const i = this.devices.findIndex((d) => d.endpointId === endpointId);
|
||||
if (i === -1)
|
||||
return false;
|
||||
this.devices[i].removeAllListeners();
|
||||
this.release(this.devices[i]);
|
||||
this.devices.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
/** Forget every device. */
|
||||
clearDevices() {
|
||||
for (const d of this.devices)
|
||||
d.removeAllListeners();
|
||||
this.release(d);
|
||||
this.devices = [];
|
||||
}
|
||||
// A device the bridge forgot hears no directive and publishes nothing
|
||||
release(device) {
|
||||
device.removeAllListeners();
|
||||
device.publisher = null;
|
||||
}
|
||||
getDevices() {
|
||||
return this.devices.slice();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue