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
70
dist/cjs/Alex2Node.js
vendored
70
dist/cjs/Alex2Node.js
vendored
|
|
@ -1,4 +1,37 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
|
|
@ -11,6 +44,8 @@ const validate_js_1 = require("./device/validate.js");
|
|||
const events_1 = require("events");
|
||||
const enums_js_1 = require("./compat/enums.js");
|
||||
const types_js_1 = require("./registry/types.js");
|
||||
const topics = __importStar(require("./topics.js"));
|
||||
const transport_js_1 = require("./transport.js");
|
||||
exports.DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883";
|
||||
// What addDevice() copies from the definition to the device
|
||||
const DESCRIBED = [
|
||||
|
|
@ -39,6 +74,8 @@ class Alex2MQTT extends events_1.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 transport_js_1.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();
|
||||
|
|
@ -76,8 +113,6 @@ class Alex2MQTT extends events_1.EventEmitter {
|
|||
...(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");
|
||||
|
|
@ -100,12 +135,12 @@ class Alex2MQTT extends events_1.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);
|
||||
});
|
||||
}
|
||||
|
|
@ -179,15 +214,13 @@ class Alex2MQTT extends events_1.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_js_1.default(this.client, this.rootTopic, name, endpointId, (categories ?? []));
|
||||
const device = new Device_js_1.default(null, this.rootTopic, name, endpointId, (categories ?? []));
|
||||
for (const [field, value] of Object.entries(described)) {
|
||||
if (!DESCRIBED.includes(field))
|
||||
throw new types_js_1.DeclarationError({ endpointId }, `${field} is not a field of an endpoint`);
|
||||
|
|
@ -202,9 +235,6 @@ class Alex2MQTT extends events_1.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`;
|
||||
|
|
@ -216,7 +246,7 @@ class Alex2MQTT extends events_1.EventEmitter {
|
|||
}
|
||||
this.log(`Creating new device with endpoint: ${endpointId}`);
|
||||
const normalizedCategory = Array.isArray(displayCategory) ? displayCategory : [displayCategory || enums_js_1.DisplayCategory.LIGHT];
|
||||
const device = new Device_js_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory);
|
||||
const device = new Device_js_1.default(null, this.rootTopic, name, endpointId, normalizedCategory);
|
||||
this.register(device, false);
|
||||
return device;
|
||||
}
|
||||
|
|
@ -224,6 +254,7 @@ class Alex2MQTT extends events_1.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. */
|
||||
|
|
@ -231,16 +262,21 @@ class Alex2MQTT extends events_1.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();
|
||||
}
|
||||
|
|
|
|||
51
dist/cjs/compat/AlexaErrorResponse.js
vendored
51
dist/cjs/compat/AlexaErrorResponse.js
vendored
|
|
@ -1,16 +1,51 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AlexaErrorResponse = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
const build_js_1 = require("../messages/build.js");
|
||||
const errors_js_1 = require("../messages/errors.js");
|
||||
const topics = __importStar(require("../topics.js"));
|
||||
const transport_js_1 = require("../transport.js");
|
||||
/** An ErrorResponse as 1.x builds it: device.getErrorMessage(token), setErrorMessage(), send(). */
|
||||
class AlexaErrorResponse {
|
||||
constructor(correlationToken, rootTopic, endpointId, mqttClient) {
|
||||
constructor(correlationToken, rootTopic, endpointId, publisher) {
|
||||
this.correlationToken = correlationToken;
|
||||
this.rootTopic = rootTopic;
|
||||
this.endpointId = endpointId;
|
||||
this.mqttClient = mqttClient;
|
||||
this.publisher = publisher;
|
||||
// Sent when setErrorMessage() was not called: 1.x published an empty payload, which is no ErrorResponse to Alexa
|
||||
this.error = errors_js_1.AlexaErrors.of("INTERNAL_ERROR", "the device answered with an error and did not say which");
|
||||
// One id for the message, however often toJSON() is called
|
||||
|
|
@ -39,16 +74,8 @@ class AlexaErrorResponse {
|
|||
}
|
||||
/** Resolves with the topic published to, or "" when the publish failed. Never rejects. */
|
||||
send(sendAsync = false) {
|
||||
const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; // "alexaResponce" is how Alex2MQTT and Alex2ESP spell the topic
|
||||
return new Promise((resolve) => {
|
||||
this.mqttClient.publish(topic, JSON.stringify(this.toJSON()), (err) => {
|
||||
if (!err)
|
||||
return resolve(topic);
|
||||
if (this.onPublishError)
|
||||
this.onPublishError(err); // -> the bridge's "error" event (when somebody listens)
|
||||
resolve(""); // 1.5.1 rejected here, and an un-caught send() then killed the host on any broker hiccup
|
||||
});
|
||||
});
|
||||
const answer = sendAsync ? topics.deferred : topics.response;
|
||||
return (0, transport_js_1.send)(this.publisher, answer(this.rootTopic, this.endpointId), () => this.toJSON(), this.onPublishError);
|
||||
}
|
||||
}
|
||||
exports.AlexaErrorResponse = AlexaErrorResponse;
|
||||
|
|
|
|||
60
dist/cjs/compat/AlexaStatusMessage.js
vendored
60
dist/cjs/compat/AlexaStatusMessage.js
vendored
|
|
@ -1,9 +1,44 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AlexaStatusMessage = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
const build_js_1 = require("../messages/build.js");
|
||||
const StateBuilder_js_1 = require("../messages/StateBuilder.js");
|
||||
const topics = __importStar(require("../topics.js"));
|
||||
const transport_js_1 = require("../transport.js");
|
||||
const enums_js_1 = require("./enums.js");
|
||||
// The 1.x helpers report every temperature in Celsius, whatever scale the device works in
|
||||
function celsius(scale, value) {
|
||||
|
|
@ -14,11 +49,11 @@ function celsius(scale, value) {
|
|||
* device.getChangeReport(), the add...Prop() calls, send(). The values are not checked.
|
||||
*/
|
||||
class AlexaStatusMessage {
|
||||
constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false, changeCause = null) {
|
||||
constructor(correlationToken, rootTopic, endpointId, publisher, isResponse = false, isDeferred = false, changeCause = null) {
|
||||
this.correlationToken = correlationToken;
|
||||
this.rootTopic = rootTopic;
|
||||
this.endpointId = endpointId;
|
||||
this.mqttClient = mqttClient;
|
||||
this.publisher = publisher;
|
||||
this.isResponse = isResponse;
|
||||
this.isDeferred = isDeferred;
|
||||
this.changeCause = changeCause;
|
||||
|
|
@ -109,24 +144,9 @@ class AlexaStatusMessage {
|
|||
* to). Never rejects (1.5.1 did, so an un-caught send() could kill the host).
|
||||
*/
|
||||
send(sendAsync = false) {
|
||||
const topic = this.changeCause
|
||||
? `${this.rootTopic}/changeReport`
|
||||
: `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; // "alexaResponce" is how Alex2MQTT and Alex2ESP spell the topic
|
||||
return new Promise((resolve) => {
|
||||
const failed = (err) => {
|
||||
if (this.onPublishError)
|
||||
this.onPublishError(err); // -> the bridge's "error" event (when somebody listens)
|
||||
resolve(""); // 1.5.1 rejected here, and an un-caught send() then killed the host on any broker hiccup
|
||||
};
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.stringify(this.toJSON());
|
||||
}
|
||||
catch (err) {
|
||||
return failed(err);
|
||||
}
|
||||
this.mqttClient.publish(topic, payload, (err) => (err ? failed(err) : resolve(topic)));
|
||||
});
|
||||
const answer = sendAsync ? topics.deferred : topics.response;
|
||||
const topic = this.changeCause ? topics.changeReport(this.rootTopic) : answer(this.rootTopic, this.endpointId);
|
||||
return (0, transport_js_1.send)(this.publisher, topic, () => this.toJSON(), this.onPublishError);
|
||||
}
|
||||
}
|
||||
exports.AlexaStatusMessage = AlexaStatusMessage;
|
||||
|
|
|
|||
69
dist/cjs/device/Device.js
vendored
69
dist/cjs/device/Device.js
vendored
|
|
@ -1,4 +1,37 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const events_1 = require("events");
|
||||
const AlexaErrorResponse_js_1 = require("../compat/AlexaErrorResponse.js");
|
||||
|
|
@ -10,12 +43,14 @@ const Alexa_js_1 = require("../registry/interfaces/Alexa.js");
|
|||
const EndpointHealth_js_1 = require("../registry/interfaces/EndpointHealth.js");
|
||||
const schema_js_1 = require("../registry/schema.js");
|
||||
const types_js_1 = require("../registry/types.js");
|
||||
const topics = __importStar(require("../topics.js"));
|
||||
const transport_js_1 = require("../transport.js");
|
||||
const Capability_js_1 = require("./Capability.js");
|
||||
const validate_js_1 = require("./validate.js");
|
||||
class Device extends events_1.EventEmitter {
|
||||
constructor(mqttClient, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") {
|
||||
/** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */
|
||||
constructor(client, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") {
|
||||
super();
|
||||
this.mqttClient = mqttClient;
|
||||
this.rootTopic = rootTopic;
|
||||
this.name = name;
|
||||
this.endpointId = endpointId;
|
||||
|
|
@ -39,10 +74,12 @@ class Device extends events_1.EventEmitter {
|
|||
*/
|
||||
this.endpointHealth = false;
|
||||
this.capabilities = [];
|
||||
}
|
||||
/** Internal (1.5.2): Alex2MQTT.connect() re-binds every registered device to its new broker client after a disconnect(). */
|
||||
setMqttClient(client) {
|
||||
this.mqttClient = client;
|
||||
/**
|
||||
* What the device and the messages it builds publish through. The bridge sets it when the device is registered
|
||||
* and clears it when the device is unregistered; a MemoryPublisher here tests a device without a broker. While it
|
||||
* is null every send() resolves "" and reports why.
|
||||
*/
|
||||
this.publisher = null;
|
||||
}
|
||||
getName() {
|
||||
return this.name;
|
||||
|
|
@ -66,12 +103,12 @@ class Device extends events_1.EventEmitter {
|
|||
return this.description;
|
||||
}
|
||||
getErrorMessage(correlationToken) {
|
||||
const msg = new AlexaErrorResponse_js_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.mqttClient);
|
||||
const msg = new AlexaErrorResponse_js_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.publisher);
|
||||
msg.onPublishError = this.onPublishError;
|
||||
return msg;
|
||||
}
|
||||
getStatusMessage(correlationToken, isResponse = false, isDeferred = false) {
|
||||
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred);
|
||||
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.publisher, isResponse, isDeferred);
|
||||
msg.onPublishError = this.onPublishError;
|
||||
return msg;
|
||||
}
|
||||
|
|
@ -82,7 +119,7 @@ class Device extends events_1.EventEmitter {
|
|||
* Without a changed property send() publishes nothing and resolves with "": Alex2MQTT would drop the report.
|
||||
*/
|
||||
getChangeReport(cause = "PHYSICAL_INTERACTION") {
|
||||
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, false, false, cause);
|
||||
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.publisher, false, false, cause);
|
||||
msg.onPublishError = this.onPublishError;
|
||||
return msg;
|
||||
}
|
||||
|
|
@ -91,17 +128,9 @@ class Device extends events_1.EventEmitter {
|
|||
* (1.5.1). Resolves with the topic published to, or "" when the publish failed (never rejects, 1.5.2).
|
||||
*/
|
||||
sendSceneResponse(correlationToken, activated, cause = "VOICE_INTERACTION", sendAsync = false) {
|
||||
const payload = (0, build_js_1.sceneEvent)({ endpointId: this.endpointId, correlationToken, activated, cause });
|
||||
const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`;
|
||||
return new Promise((resolve) => {
|
||||
this.mqttClient.publish(topic, JSON.stringify(payload), (err) => {
|
||||
if (!err)
|
||||
return resolve(topic);
|
||||
if (this.onPublishError)
|
||||
this.onPublishError(err); // -> the bridge's "error" event (when somebody listens)
|
||||
resolve(""); // 1.5.1 rejected here, and an un-caught send() then killed the host on any broker hiccup
|
||||
});
|
||||
});
|
||||
const answer = sendAsync ? topics.deferred : topics.response;
|
||||
const build = () => (0, build_js_1.sceneEvent)({ endpointId: this.endpointId, correlationToken, activated, cause });
|
||||
return (0, transport_js_1.send)(this.publisher, answer(this.rootTopic, this.endpointId), build, this.onPublishError);
|
||||
}
|
||||
/** The capabilities the device declared, in the order it declared them. */
|
||||
getCapabilities() {
|
||||
|
|
|
|||
6
dist/cjs/index.js
vendored
6
dist/cjs/index.js
vendored
|
|
@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AlexaErrorResponse = exports.AlexaStatusMessage = exports.ThermostatMode = exports.TemperatureSensorScale = exports.PowerState = exports.DisplayCategory = exports.AlexaInterfaceType = exports.AlexaErrorType = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterface = exports.property = exports.StateBuilder = exports.MessageError = exports.AlexaErrors = exports.AlexaError = exports.messages = exports.DisplayCategories = exports.States = exports.Actions = exports.Units = exports.Assets = exports.SemanticsBuilder = exports.semantics = exports.text = exports.asset = exports.EndpointHealth = exports.PowerController = exports.ToggleController = exports.TemperatureSensor = exports.RangeController = exports.ModeController = exports.BrightnessController = exports.Alexa = exports.SchemaError = exports.DeclarationError = exports.registry = exports.Capability = exports.Device = exports.DEFAULT_HOST = exports.Alex2MQTT = void 0;
|
||||
exports.AlexaErrorResponse = exports.AlexaStatusMessage = exports.ThermostatMode = exports.TemperatureSensorScale = exports.PowerState = exports.DisplayCategory = exports.AlexaInterfaceType = exports.AlexaErrorType = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterface = exports.MemoryPublisher = exports.topics = exports.property = exports.StateBuilder = exports.MessageError = exports.AlexaErrors = exports.AlexaError = exports.messages = exports.DisplayCategories = exports.States = exports.Actions = exports.Units = exports.Assets = exports.SemanticsBuilder = exports.semantics = exports.text = exports.asset = exports.EndpointHealth = exports.PowerController = exports.ToggleController = exports.TemperatureSensor = exports.RangeController = exports.ModeController = exports.BrightnessController = exports.Alexa = exports.SchemaError = exports.DeclarationError = exports.registry = exports.Capability = exports.Device = exports.DEFAULT_HOST = exports.Alex2MQTT = void 0;
|
||||
// Relative specifiers carry ".js": Node's ES module loader resolves no extension, and TypeScript maps it back to the .ts.
|
||||
var Alex2Node_js_1 = require("./Alex2Node.js");
|
||||
Object.defineProperty(exports, "Alex2MQTT", { enumerable: true, get: function () { return __importDefault(Alex2Node_js_1).default; } });
|
||||
|
|
@ -80,6 +80,10 @@ Object.defineProperty(exports, "AlexaErrors", { enumerable: true, get: function
|
|||
Object.defineProperty(exports, "MessageError", { enumerable: true, get: function () { return index_js_5.MessageError; } });
|
||||
Object.defineProperty(exports, "StateBuilder", { enumerable: true, get: function () { return index_js_5.StateBuilder; } });
|
||||
Object.defineProperty(exports, "property", { enumerable: true, get: function () { return index_js_5.property; } });
|
||||
// Publishing: the topics of the Alex2MQTT contract, and a publisher that needs no broker for tests
|
||||
exports.topics = __importStar(require("./topics.js"));
|
||||
var transport_js_1 = require("./transport.js");
|
||||
Object.defineProperty(exports, "MemoryPublisher", { enumerable: true, get: function () { return transport_js_1.MemoryPublisher; } });
|
||||
// 1.x
|
||||
var AlexaInterface_js_1 = require("./compat/AlexaInterface.js");
|
||||
Object.defineProperty(exports, "AlexaInterface", { enumerable: true, get: function () { return AlexaInterface_js_1.AlexaInterface; } });
|
||||
|
|
|
|||
17
dist/cjs/topics.js
vendored
Normal file
17
dist/cjs/topics.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"use strict";
|
||||
// The topics of the Alex2MQTT contract, named once. The backend and Alex2ESP use the same names, so none of them
|
||||
// can change here alone.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.changeReport = exports.deferred = exports.response = exports.discoverReply = void 0;
|
||||
/** Where the bridge answers a discovery request with its endpoints. */
|
||||
const discoverReply = (root) => `${root}/discover_r`;
|
||||
exports.discoverReply = discoverReply;
|
||||
/** Where the answer to a directive goes. "alexaResponce" is how the backend and Alex2ESP spell it. */
|
||||
const response = (root, endpointId) => `${root}/${endpointId}/alexaResponce`;
|
||||
exports.response = response;
|
||||
/** Where the answer goes once a DeferredResponse was sent for the directive. */
|
||||
const deferred = (root, endpointId) => `${root}/${endpointId}/deferredResponse`;
|
||||
exports.deferred = deferred;
|
||||
/** Where every ChangeReport of the root goes: the backend adds the user's token and posts it to Alexa. */
|
||||
const changeReport = (root) => `${root}/changeReport`;
|
||||
exports.changeReport = changeReport;
|
||||
83
dist/cjs/transport.js
vendored
Normal file
83
dist/cjs/transport.js
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MemoryPublisher = exports.MqttPublisher = void 0;
|
||||
exports.send = send;
|
||||
const asError = (err) => (err instanceof Error ? err : new Error(String(err)));
|
||||
/**
|
||||
* Publishes to the broker through the client the bridge has at the time: a new one after disconnect() and connect(),
|
||||
* none before connect() and after disconnect(). Without a client the publish is refused. With a client that lost
|
||||
* its connection mqtt.js keeps the message and sends it after the reconnect, and the publish resolves then.
|
||||
*/
|
||||
class MqttPublisher {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
publish(topic, message) {
|
||||
const client = this.client();
|
||||
return new Promise((resolve) => {
|
||||
const failed = (err) => resolve({ ok: false, topic, error: asError(err) });
|
||||
if (!client) {
|
||||
failed(new Error(`nothing was published to ${topic}: the bridge is not connected, call connect() first`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
client.publish(topic, JSON.stringify(message), (err) => (err ? failed(err) : resolve({ ok: true, topic })));
|
||||
}
|
||||
catch (err) {
|
||||
failed(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.MqttPublisher = MqttPublisher;
|
||||
/**
|
||||
* Keeps what it is asked to publish, for tests and dry runs without a broker:
|
||||
*
|
||||
* const sent = new MemoryPublisher();
|
||||
* device.publisher = sent;
|
||||
* await device.getStatusMessage(token, true).addPowerControllerProp(PowerController.ON).send();
|
||||
* sent.published[0] // { topic: "<root>/<endpointId>/alexaResponce", message: { event, context } }
|
||||
*/
|
||||
class MemoryPublisher {
|
||||
constructor() {
|
||||
/** Oldest first. The message is what a subscriber gets after JSON.parse. */
|
||||
this.published = [];
|
||||
/** Set it and every publish fails with this error, as a publish to a broker that is gone does. */
|
||||
this.failWith = null;
|
||||
}
|
||||
publish(topic, message) {
|
||||
if (this.failWith)
|
||||
return Promise.resolve({ ok: false, topic, error: this.failWith });
|
||||
try {
|
||||
this.published.push({ topic, message: JSON.parse(JSON.stringify(message)) });
|
||||
}
|
||||
catch (err) {
|
||||
return Promise.resolve({ ok: false, topic, error: asError(err) });
|
||||
}
|
||||
return Promise.resolve({ ok: true, topic });
|
||||
}
|
||||
}
|
||||
exports.MemoryPublisher = MemoryPublisher;
|
||||
/**
|
||||
* send() as 1.x promises it: resolves with the topic, or with "" when nothing was published, and never rejects
|
||||
* (1.5.1 rejected, and a send() nobody caught killed the host on any broker hiccup). What went wrong goes to report:
|
||||
* the message cannot be built, the device is on no bridge, or the publish failed.
|
||||
*/
|
||||
async function send(publisher, topic, build, report) {
|
||||
let result;
|
||||
try {
|
||||
const message = build();
|
||||
if (!publisher) {
|
||||
throw new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`);
|
||||
}
|
||||
result = await publisher.publish(topic, message);
|
||||
}
|
||||
catch (err) {
|
||||
result = { ok: false, topic, error: asError(err) };
|
||||
}
|
||||
if (result.ok)
|
||||
return topic;
|
||||
if (report)
|
||||
report(result.error);
|
||||
return "";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue