diff --git a/dist/cjs/Alex2Node.js b/dist/cjs/Alex2Node.js index 0225dda..dd5fe96 100644 --- a/dist/cjs/Alex2Node.js +++ b/dist/cjs/Alex2Node.js @@ -278,6 +278,7 @@ class Alex2MQTT extends events_1.EventEmitter { register(device, endpointHealth) { device.alexaInterface = this.options.alexaInterface !== false; device.endpointHealth = endpointHealth; + device.log = (message) => this.log(message); device.onPublishError = (err) => this.fail(err); // a failed publish is an "error" event (when listened to), never a rejected send() // Through the dispatcher, which lets one answer to a directive pass device.publisher = this.dispatcher.publisher; diff --git a/dist/cjs/device/Device.js b/dist/cjs/device/Device.js index 486c70c..d125d3f 100644 --- a/dist/cjs/device/Device.js +++ b/dist/cjs/device/Device.js @@ -48,8 +48,15 @@ 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"); +// The 1.x calls that were reported as no longer doing anything: each is said once in a process +const reported = new Set(); class Device extends events_1.EventEmitter { - /** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */ + /** + * client is ignored and null is what to pass: in 1.x it was the broker client, and a device could only be built + * after connect(). A device that is given one says once that it publishes through its bridge. + * + * @deprecated Removed in 3.0, with the device made by bridge.addDevice() or bridge.registerDevice(). + */ constructor(client, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { super(); this.rootTopic = rootTopic; @@ -81,6 +88,31 @@ class Device extends events_1.EventEmitter { * is null every send() resolves "" and reports why. */ this.publisher = null; + if (client !== null && client !== undefined) { + this.deprecated("ALEX2NODE_DEVICE_CLIENT", "new Device(client, ...) ignores the client since 2.0: nothing is published through it. " + + "Make the device with addDevice() or registerDevice() of the bridge, or set device.publisher"); + } + } + /** + * Does nothing. In 1.5.2 the bridge gave its devices the new broker client after disconnect() and connect(); + * they now publish through the bridge and need no client. + * + * @deprecated Removed in 3.0. Remove the call, or set device.publisher to publish somewhere else. + */ + setMqttClient(_client) { + this.deprecated("ALEX2NODE_SET_MQTT_CLIENT", "device.setMqttClient() does nothing since 2.0: a device publishes through its bridge, over disconnect() and " + + "connect() too. Remove the call, or set device.publisher to publish somewhere else"); + } + // Through the log hook of the bridge. A device on no bridge has none and warns as Node does for a deprecation: + // once on stderr, to process.on("warning"), and not at all under --no-deprecation. + deprecated(code, message) { + if (reported.has(code)) + return; + reported.add(code); + if (this.log) + this.log(`warning: ${message}`); + else + process.emitWarning(message, { type: "DeprecationWarning", code }); } getName() { return this.name; diff --git a/dist/esm/Alex2Node.js b/dist/esm/Alex2Node.js index 5c0be0d..c0bcb2a 100644 --- a/dist/esm/Alex2Node.js +++ b/dist/esm/Alex2Node.js @@ -239,6 +239,7 @@ class Alex2MQTT extends EventEmitter { register(device, endpointHealth) { device.alexaInterface = this.options.alexaInterface !== false; device.endpointHealth = endpointHealth; + device.log = (message) => this.log(message); device.onPublishError = (err) => this.fail(err); // a failed publish is an "error" event (when listened to), never a rejected send() // Through the dispatcher, which lets one answer to a directive pass device.publisher = this.dispatcher.publisher; diff --git a/dist/esm/device/Device.d.ts b/dist/esm/device/Device.d.ts index 9e6befb..bf12866 100644 --- a/dist/esm/device/Device.d.ts +++ b/dist/esm/device/Device.d.ts @@ -73,14 +73,29 @@ declare class Device extends EventEmitter { * is null every send() resolves "" and reports why. */ publisher: Publisher | null; + /** Where the device says that a call of 1.x no longer does anything. The bridge sets it to its log hook. */ + log?: (message: string) => void; /** Set by state(). */ stateProvider?: Fill; /** Set by onReportState(). */ reportStateHandler?: DirectiveHandler>; /** Set by onDirective(). */ directiveHandler?: DirectiveHandler; - /** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */ + /** + * client is ignored and null is what to pass: in 1.x it was the broker client, and a device could only be built + * after connect(). A device that is given one says once that it publishes through its bridge. + * + * @deprecated Removed in 3.0, with the device made by bridge.addDevice() or bridge.registerDevice(). + */ constructor(client: unknown, rootTopic: string, name: string, endpointId: string, displayCategory: Array | null, description?: string, manufacturerName?: string, manufacturer?: string, model?: string); + /** + * Does nothing. In 1.5.2 the bridge gave its devices the new broker client after disconnect() and connect(); + * they now publish through the bridge and need no client. + * + * @deprecated Removed in 3.0. Remove the call, or set device.publisher to publish somewhere else. + */ + setMqttClient(_client: unknown): void; + private deprecated; getName(): string; setName(name: string): void; getEndpointId(): string; diff --git a/dist/esm/device/Device.js b/dist/esm/device/Device.js index 1a49bbd..cd6882a 100644 --- a/dist/esm/device/Device.js +++ b/dist/esm/device/Device.js @@ -13,8 +13,15 @@ import * as topics from "../topics.js"; import { send } from "../transport.js"; import { Capability, commonOptions } from "./Capability.js"; import { checkCapability, checkCapabilityCount, checkEndpoint } from "./validate.js"; +// The 1.x calls that were reported as no longer doing anything: each is said once in a process +const reported = new Set(); class Device extends EventEmitter { - /** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */ + /** + * client is ignored and null is what to pass: in 1.x it was the broker client, and a device could only be built + * after connect(). A device that is given one says once that it publishes through its bridge. + * + * @deprecated Removed in 3.0, with the device made by bridge.addDevice() or bridge.registerDevice(). + */ constructor(client, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { super(); this.rootTopic = rootTopic; @@ -46,6 +53,31 @@ class Device extends EventEmitter { * is null every send() resolves "" and reports why. */ this.publisher = null; + if (client !== null && client !== undefined) { + this.deprecated("ALEX2NODE_DEVICE_CLIENT", "new Device(client, ...) ignores the client since 2.0: nothing is published through it. " + + "Make the device with addDevice() or registerDevice() of the bridge, or set device.publisher"); + } + } + /** + * Does nothing. In 1.5.2 the bridge gave its devices the new broker client after disconnect() and connect(); + * they now publish through the bridge and need no client. + * + * @deprecated Removed in 3.0. Remove the call, or set device.publisher to publish somewhere else. + */ + setMqttClient(_client) { + this.deprecated("ALEX2NODE_SET_MQTT_CLIENT", "device.setMqttClient() does nothing since 2.0: a device publishes through its bridge, over disconnect() and " + + "connect() too. Remove the call, or set device.publisher to publish somewhere else"); + } + // Through the log hook of the bridge. A device on no bridge has none and warns as Node does for a deprecation: + // once on stderr, to process.on("warning"), and not at all under --no-deprecation. + deprecated(code, message) { + if (reported.has(code)) + return; + reported.add(code); + if (this.log) + this.log(`warning: ${message}`); + else + process.emitWarning(message, { type: "DeprecationWarning", code }); } getName() { return this.name; diff --git a/dist/types/device/Device.d.ts b/dist/types/device/Device.d.ts index 9e6befb..bf12866 100644 --- a/dist/types/device/Device.d.ts +++ b/dist/types/device/Device.d.ts @@ -73,14 +73,29 @@ declare class Device extends EventEmitter { * is null every send() resolves "" and reports why. */ publisher: Publisher | null; + /** Where the device says that a call of 1.x no longer does anything. The bridge sets it to its log hook. */ + log?: (message: string) => void; /** Set by state(). */ stateProvider?: Fill; /** Set by onReportState(). */ reportStateHandler?: DirectiveHandler>; /** Set by onDirective(). */ directiveHandler?: DirectiveHandler; - /** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */ + /** + * client is ignored and null is what to pass: in 1.x it was the broker client, and a device could only be built + * after connect(). A device that is given one says once that it publishes through its bridge. + * + * @deprecated Removed in 3.0, with the device made by bridge.addDevice() or bridge.registerDevice(). + */ constructor(client: unknown, rootTopic: string, name: string, endpointId: string, displayCategory: Array | null, description?: string, manufacturerName?: string, manufacturer?: string, model?: string); + /** + * Does nothing. In 1.5.2 the bridge gave its devices the new broker client after disconnect() and connect(); + * they now publish through the bridge and need no client. + * + * @deprecated Removed in 3.0. Remove the call, or set device.publisher to publish somewhere else. + */ + setMqttClient(_client: unknown): void; + private deprecated; getName(): string; setName(name: string): void; getEndpointId(): string; diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index c08b4b1..2d584db 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -279,6 +279,7 @@ class Alex2MQTT extends EventEmitter { private register(device: Device, endpointHealth: boolean): void { device.alexaInterface = this.options.alexaInterface !== false; device.endpointHealth = endpointHealth; + device.log = (message) => this.log(message); device.onPublishError = (err) => this.fail(err); // a failed publish is an "error" event (when listened to), never a rejected send() // Through the dispatcher, which lets one answer to a directive pass device.publisher = this.dispatcher.publisher; diff --git a/src/device/Device.ts b/src/device/Device.ts index 24cb789..c768999 100644 --- a/src/device/Device.ts +++ b/src/device/Device.ts @@ -57,6 +57,9 @@ type DeclarationArguments = {} extends Declaration ? [options?: Declaration] : [options: Declaration]; +// The 1.x calls that were reported as no longer doing anything: each is said once in a process +const reported = new Set(); + class Device extends EventEmitter { public softwareVersion = "1.0.0"; public serialNumber = "Alex2Node"; @@ -82,6 +85,8 @@ class Device extends EventEmitter { * is null every send() resolves "" and reports why. */ public publisher: Publisher | null = null; + /** Where the device says that a call of 1.x no longer does anything. The bridge sets it to its log hook. */ + public log?: (message: string) => void; /** Set by state(). */ public stateProvider?: Fill; /** Set by onReportState(). */ @@ -89,7 +94,12 @@ class Device extends EventEmitter { /** Set by onDirective(). */ public directiveHandler?: DirectiveHandler; - /** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */ + /** + * client is ignored and null is what to pass: in 1.x it was the broker client, and a device could only be built + * after connect(). A device that is given one says once that it publishes through its bridge. + * + * @deprecated Removed in 3.0, with the device made by bridge.addDevice() or bridge.registerDevice(). + */ constructor( client: unknown, private rootTopic: string, @@ -102,6 +112,36 @@ class Device extends EventEmitter { public model: string = "Alex2Node_v1.0.0" ) { super(); + if (client !== null && client !== undefined) { + this.deprecated( + "ALEX2NODE_DEVICE_CLIENT", + "new Device(client, ...) ignores the client since 2.0: nothing is published through it. " + + "Make the device with addDevice() or registerDevice() of the bridge, or set device.publisher" + ); + } + } + + /** + * Does nothing. In 1.5.2 the bridge gave its devices the new broker client after disconnect() and connect(); + * they now publish through the bridge and need no client. + * + * @deprecated Removed in 3.0. Remove the call, or set device.publisher to publish somewhere else. + */ + setMqttClient(_client: unknown): void { + this.deprecated( + "ALEX2NODE_SET_MQTT_CLIENT", + "device.setMqttClient() does nothing since 2.0: a device publishes through its bridge, over disconnect() and " + + "connect() too. Remove the call, or set device.publisher to publish somewhere else" + ); + } + + // Through the log hook of the bridge. A device on no bridge has none and warns as Node does for a deprecation: + // once on stderr, to process.on("warning"), and not at all under --no-deprecation. + private deprecated(code: string, message: string): void { + if (reported.has(code)) return; + reported.add(code); + if (this.log) this.log(`warning: ${message}`); + else process.emitWarning(message, { type: "DeprecationWarning", code }); } getName(): string { diff --git a/test/compat/deprecations.test.js b/test/compat/deprecations.test.js new file mode 100644 index 0000000..841265b --- /dev/null +++ b/test/compat/deprecations.test.js @@ -0,0 +1,46 @@ +"use strict"; +// What 1.x took and 2.0 ignores: said once, through the log hook of the bridge or as a warning of the process. +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { Alex2MQTT, Device, MemoryPublisher, PowerController } = require("alex2node"); + +test("new Device(client, ...): the client is ignored, and the process is warned once", async () => { + const warnings = []; + const listen = (warning) => warnings.push(warning); + process.on("warning", listen); + try { + new Device(null, "root", "Lamp", "lamp-1", ["LIGHT"]); + new Device(undefined, "root", "Lamp", "lamp-2", ["LIGHT"]); + const client = { publish() { throw new Error("the client of 1.x is not published through"); } }; + const lamp = new Device(client, "root", "Lamp", "lamp-3", ["LIGHT"]); + new Device(client, "root", "Lamp", "lamp-4", ["LIGHT"]); + // A warning is emitted on the next tick + await new Promise((resolve) => setImmediate(resolve)); + + assert.deepEqual(warnings.map(({ name, code }) => ({ name, code })), [{ name: "DeprecationWarning", code: "ALEX2NODE_DEVICE_CLIENT" }]); + assert.match(warnings[0].message, /^new Device\(client, \.\.\.\) ignores the client since 2\.0: .* or set device\.publisher$/); + assert.equal(lamp.publisher, null); + assert.equal(await lamp.getStatusMessage("ct", true).addPowerControllerProp(PowerController.ON).send(), ""); + } finally { + process.off("warning", listen); + } +}); + +test("setMqttClient(): does nothing, and the log hook of the bridge hears it once", async () => { + const sent = new MemoryPublisher(); + const logged = []; + const bridge = new Alex2MQTT("user", "password", "root", false, { publisher: sent, log: (message) => logged.push(message) }); + const lamp = bridge.registerDevice("Lamp", "lamp-1", null); + const plug = bridge.registerDevice("Plug", "plug-1", null); + + lamp.setMqttClient({ publish() { throw new Error("the client of 1.x is not published through"); } }); + plug.setMqttClient(null); + lamp.setMqttClient(null); + + assert.deepEqual(logged.filter((line) => line.includes("setMqttClient")), [ + "warning: device.setMqttClient() does nothing since 2.0: a device publishes through its bridge, over disconnect() and " + + "connect() too. Remove the call, or set device.publisher to publish somewhere else", + ]); + assert.equal(await lamp.getStatusMessage("ct", true).addPowerControllerProp(PowerController.ON).send(), "root/lamp-1/alexaResponce"); + assert.equal(sent.published.length, 1); +});