1.5.2: send() never rejects (status, error, scene, change report: resolves the topic or "" and reports the failure through the bridge's error event when listened to - 1.5.1 rejected, and an un-caught .send() killed the host on any broker hiccup); types ship (declaration: true, dist/*.d.ts tracked; addSupportedModes takes {value, modeResources}, ActionMapping payload optional, addHealthProp accepts EndpointHealth or the string); disconnect()/connect() re-binds devices to the new client; registerDevice returns the existing device on a duplicate endpointId (warning via the log hook, console.warn without one); ThermostatController discovery lists targetSetpoint and drops adaptiveRecoveryStatus; the UNSUPORTED INTERFACE TYPE stderr spam goes through the log hook. Examples: require("alex2node"), an error listener in each, EndpointHealth.OK, neutral endpoint ids, BlindControl reads correlationToken from the header, the thermostat reports Fahrenheit as Fahrenheit, ExamplePowerController is power-only again + new ExamplePowerControllerWithBrightness. readme (install from Forgejo, 1.5.2 changelog, table syntax), LICENSE (MIT); tests for the non-rejecting send, reconnect, duplicate, thermostat discovery and the shipped declaration signatures (8/8 on an in-process broker).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 04:58:39 +00:00
parent 717af632de
commit 275f00f8a7
35 changed files with 1011 additions and 176 deletions

17
dist/Alex2Node.js vendored
View file

@ -8,6 +8,7 @@ const mqtt_1 = __importDefault(require("mqtt"));
const Device_1 = __importDefault(require("./Device"));
const events_1 = require("events");
const DisplayCategory_1 = require("./DisplayCategory");
const AlexaInterface_1 = require("./AlexaInterface");
exports.DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883";
/**
* The Alexa-to-MQTT bridge: one broker connection for a user's root topic, a set of registered devices, discovery and
@ -60,6 +61,8 @@ class Alex2MQTT extends events_1.EventEmitter {
return;
const options = Object.assign({ username: this.username, password: this.password, reconnectPeriod: 1000, connectTimeout: 10000 }, (this.options.mqtt || {}));
this.client = mqtt_1.default.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");
@ -82,6 +85,10 @@ class Alex2MQTT extends events_1.EventEmitter {
this.log("Discovery request received, getting device json...");
const deviceArray = this.devices.map((device) => device.getJSON());
this.lastDiscoveryAt = new Date().toISOString();
for (const device of this.devices) // a capability the library has no property list for goes out with supported: []
for (const cap of device.getCapabilities())
if (cap.getProps().length === 0 && cap.getType() !== AlexaInterface_1.AlexaInterfaceType.SCENE_CONTROLLER)
this.log(`${device.endpointId}: no property list known for ${cap.getTypeString()}, discovery lists it with no supported properties`);
this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => {
if (err) {
this.fail(err);
@ -137,6 +144,15 @@ class Alex2MQTT extends events_1.EventEmitter {
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`;
if (this.options.log)
this.options.log(warning);
else
console.warn(`[Alex2Node.ts] ${warning}`); // visible without a log hook too
return existing;
}
this.log(`Creating new device with endpoint: ${endpointId}`);
const normalizedCategory = displayCategory === null
? [DisplayCategory_1.DisplayCategory.LIGHT]
@ -144,6 +160,7 @@ class Alex2MQTT extends events_1.EventEmitter {
? displayCategory
: [displayCategory || DisplayCategory_1.DisplayCategory.LIGHT];
const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory);
device.onPublishError = (err) => this.fail(err); // a failed publish is an "error" event (when listened to), never a rejected send()
this.devices.push(device);
return device;
}