diff --git a/README.md b/README.md index 74e95c46..d5722579 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,11 @@ import { TradfriClient, Accessory, AccessoryTypes } from "node-tradfri-client"; // connect const tradfri = new TradfriClient("gw-abcdef012345"); -await tradfri.connect(identity, psk); +try { + await tradfri.connect(identity, psk); +} catch (e) { + // handle error - see below for details +} ``` ### Make a lightbulb blink @@ -194,16 +198,20 @@ try { ``` The returned `identity` and `psk` **have to be stored** for future connections to the gateway. To comply with IKEA's requests, the security code **must not be stored** permanently in your application. -The call throws an error if it wasn't successful which you should handle. The error `e` should be of type `TradfriError` and gives further information why the authentication failed. To check that, add `TradfriError` and `TradfriErrorCodes` to the list of imports and check as follows: +If the authentication was not successful, this method throws (or rather rejects with) an error which you should handle. The error `e` should be of type `TradfriError` and gives further information why the authentication failed. To check that, add `TradfriError` and `TradfriErrorCodes` to the list of imports and check as follows: ```TS if (e instanceof TradfriError) { switch (e.code) { - case TradfriErrorCodes.ConnectionFailed: { - // Gateway unreachable or security code wrong + case TradfriErrorCodes.ConnectionTimedOut: { + // The gateway is unreachable or did not respond in time } case TradfriErrorCodes.AuthenticationFailed: { - // Something went wrong with the authentication. - // It might be that this library has to be updated to be compatible with a new firmware. + // The security code is wrong or something else went wrong with the authentication. + // Check the error message for details. It might be that this library has to be updated + // to be compatible with a new firmware. + } + case TradfriErrorCodes.ConnectionFailed: { + // An unknown error happened while trying to connect } } } @@ -212,9 +220,24 @@ if (e instanceof TradfriError) { ### Connecting to the gateway When you have a valid identity and psk, you can connect to the gateway using the `connect` method: ```TS -const success = await tradfri.connect(identity, psk); +try { + await tradfri.connect(identity, psk); +} catch (e: TradfriError) { + // handle error + switch (e.code) { + case TradfriErrorCodes.ConnectionTimedOut: { + // The gateway is unreachable or did not respond in time + } + case TradfriErrorCodes.AuthenticationFailed: { + // The provided credentials are not valid. You need to re-authenticate using `authenticate()`. + } + case TradfriErrorCodes.ConnectionFailed: { + // An unknown error happened while trying to connect + } + } +} ``` -If the connection was unsuccessful, either the gateway was unreachable or the identity/psk pair isn't valid. +**NOTE:** As of v0.6.0, this no longer resolves with `false` if the connection was unsuccessful. Instead, it throws (or rejects with) a `TradfriError` which contains details about why the connection failed. ### Pinging the gateway ```TS @@ -507,7 +530,9 @@ A DeviceInfo object contains general information about a device. It has the foll ## Changelog -#### __WORK IN PROGRESS__ +#### __WORK IN PROGRESS__ - WARNING: BREAKING CHANGES! +* (AlCalzone) **BREAKING**: The `connect()` method now either resolves with `true` or rejects with an error detailing why the connection failed. +* (AlCalzone) The error thrown by `authentication()` now correctly reflects why the authentication failed. * (AlCalzone) Swallow `"DTLS handshake timed out"` promise rejections and emit an `"error"` instead #### 0.10.1 (2018-03-15) diff --git a/build/tradfri-client.d.ts b/build/tradfri-client.d.ts index b129a077..1e7a57e8 100644 --- a/build/tradfri-client.d.ts +++ b/build/tradfri-client.d.ts @@ -58,7 +58,7 @@ export declare class TradfriClient extends EventEmitter implements OperationProv * @param identity A previously negotiated identity. * @param psk The pre-shared key belonging to the identity. */ - connect(identity: string, psk: string): Promise; + connect(identity: string, psk: string): Promise; /** * Try to establish a connection to the configured gateway. * @param identity The DTLS identity to use diff --git a/build/tradfri-client.js b/build/tradfri-client.js index b7ebfccb..01cf0f9f 100644 --- a/build/tradfri-client.js +++ b/build/tradfri-client.js @@ -54,7 +54,14 @@ class TradfriClient extends events_1.EventEmitter { * @param psk The pre-shared key belonging to the identity. */ connect(identity, psk) { - return this.tryToConnect(identity, psk); + return __awaiter(this, void 0, void 0, function* () { + switch (yield this.tryToConnect(identity, psk)) { + case true: return true; + case "auth failed": throw new tradfri_error_1.TradfriError("The provided credentials are not valid. Please re-authenticate!", tradfri_error_1.TradfriErrorCodes.AuthenticationFailed); + case "timeout": throw new tradfri_error_1.TradfriError("The gateway did not respond in time.", tradfri_error_1.TradfriErrorCodes.ConnectionTimedOut); + case "error": throw new tradfri_error_1.TradfriError("An unknown error occured while connecting to the gateway", tradfri_error_1.TradfriErrorCodes.ConnectionFailed); + } + }); } /** * Try to establish a connection to the configured gateway. @@ -71,7 +78,12 @@ class TradfriClient extends events_1.EventEmitter { }); logger_1.log(`Attempting connection. Identity = ${identity}, psk = ${psk}`, "debug"); const result = yield node_coap_client_1.CoapClient.tryToConnect(this.requestBase); - logger_1.log(`Connection ${result ? "" : "un"}successful`, "debug"); + if (result === true) { + logger_1.log("Connection successful", "debug"); + } + else { + logger_1.log("Connection failed. Reason: " + result, "debug"); + } return result; }); } @@ -85,9 +97,11 @@ class TradfriClient extends events_1.EventEmitter { return __awaiter(this, void 0, void 0, function* () { // first, check try to connect with the security code logger_1.log("authenticate() > trying to connect with the security code", "debug"); - if (!(yield this.tryToConnect("Client_identity", securityCode))) { - // that didn't work, so the code is wrong - throw new tradfri_error_1.TradfriError("The security code is wrong", tradfri_error_1.TradfriErrorCodes.ConnectionFailed); + switch (yield this.tryToConnect("Client_identity", securityCode)) { + case true: break; // all good + case "auth failed": throw new tradfri_error_1.TradfriError("The security code is wrong", tradfri_error_1.TradfriErrorCodes.AuthenticationFailed); + case "timeout": throw new tradfri_error_1.TradfriError("The gateway did not respond in time.", tradfri_error_1.TradfriErrorCodes.ConnectionTimedOut); + case "error": throw new tradfri_error_1.TradfriError("An unknown error occured while connecting to the gateway", tradfri_error_1.TradfriErrorCodes.ConnectionFailed); } // generate a new identity const identity = `tradfri_${Date.now()}`; diff --git a/package.json b/package.json index 2fde1f1b..c945abdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-tradfri-client", - "version": "0.10.1", + "version": "0.11.0", "description": "Library to talk to IKEA Trådfri Gateways without external binaries", "keywords": [ "coap", @@ -63,7 +63,7 @@ "dependencies": { "bonjour": "^3.5.0", "debug": "^3.1.0", - "node-coap-client": "^0.5.5", + "node-coap-client": "^0.6.0", "reflect-metadata": "^0.1.12" }, "scripts": { @@ -104,4 +104,4 @@ "instrument": true }, "readme": "README.md" -} \ No newline at end of file +} diff --git a/src/tradfri-client.test.ts b/src/tradfri-client.test.ts index aa0904f6..e79b4362 100644 --- a/src/tradfri-client.test.ts +++ b/src/tradfri-client.test.ts @@ -59,14 +59,10 @@ describe("tradfri-client => infrastructure => ", () => { const identity = "IDENTITY"; const psk = "PSK"; - it("should reset the CoAP client, provide new security params and return the result from tryToConnect", async () => { - // test if both possible responses are passed through + it("should reset the CoAP client, provide new security params and resolve with true on success", async () => { fakeCoap.tryToConnect.returns(Promise.resolve(true)); await tradfri.connect(identity, psk).should.become(true); - fakeCoap.tryToConnect.returns(Promise.resolve(false)); - await tradfri.connect(identity, psk).should.become(false); - fakeCoap.reset.should.have.been.called; fakeCoap.setSecurityParams.should.have.been.called; fakeCoap.setSecurityParams.getCall(0).args[1].should.deep.equal({ @@ -76,6 +72,33 @@ describe("tradfri-client => infrastructure => ", () => { fakeCoap.tryToConnect.resetBehavior(); }); + + it("should reject with a `TradfriError` with code ConnectionTimedOut when the connection times out", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("timeout")); + await tradfri.connect(identity, psk).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.ConnectionTimedOut); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); + + it("should reject with a `TradfriError` with code AuthenticationFailed when the credentials are wrong", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("auth failed")); + await tradfri.connect(identity, psk).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.AuthenticationFailed); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); + + it("should reject with a `TradfriError` with code ConnectionFailed when some other error happens", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("error")); + await tradfri.connect(identity, psk).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.ConnectionFailed); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); }); describe("authenticate => ", () => { @@ -90,11 +113,6 @@ describe("tradfri-client => infrastructure => ", () => { fakeCoap.request.resetBehavior(); }); - it(`detects failure to connect with "Client_identity" as a wrong security code`, async () => { - fakeCoap.tryToConnect.returns(Promise.resolve(false)); - await tradfri.authenticate(null).should.be.rejectedWith("security code"); - }); - it(`should call coap.request with the correct endpoint and payload and return the identity and psk`, async () => { fakeCoap.tryToConnect.returns(Promise.resolve(true)); fakeCoap.request.returns(Promise.resolve(authResponse)); @@ -114,6 +132,33 @@ describe("tradfri-client => infrastructure => ", () => { ); }); + it("should reject with a `TradfriError` with code ConnectionTimedOut when the authentication times out", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("timeout")); + await tradfri.authenticate(dummyIdentity).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.ConnectionTimedOut); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); + + it("should reject with a `TradfriError` with code AuthenticationFailed when the security code was wrong", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("auth failed")); + await tradfri.authenticate(dummyIdentity).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.AuthenticationFailed); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); + + it("should reject with a `TradfriError` with code ConnectionFailed when some other error happens", async () => { + fakeCoap.tryToConnect.returns(Promise.resolve("error")); + await tradfri.authenticate(dummyIdentity).should.be.rejected.then(err => { + expect(err).to.be.an.instanceof(TradfriError); + expect(err.code).to.equal(TradfriErrorCodes.ConnectionFailed); + }); + fakeCoap.tryToConnect.resetBehavior(); + }); + it(`if coap.request returns an error, throw AuthenticationFailed`, async () => { fakeCoap.tryToConnect.returns(Promise.resolve(true)); fakeCoap.request.returns(Promise.resolve(failedAuthResponse)); @@ -122,7 +167,6 @@ describe("tradfri-client => infrastructure => ", () => { expect((err as TradfriError).code).to.equal(TradfriErrorCodes.AuthenticationFailed); }); }); - }); describe("ping =>", () => { diff --git a/src/tradfri-client.ts b/src/tradfri-client.ts index 4a32d4e1..09b7b003 100644 --- a/src/tradfri-client.ts +++ b/src/tradfri-client.ts @@ -1,6 +1,6 @@ // load external modules import { EventEmitter } from "events"; -import { CoapClient as coap, CoapResponse, RequestMethod } from "node-coap-client"; +import { CoapClient as coap, CoapResponse, ConnectionResult, RequestMethod } from "node-coap-client"; // load internal modules import { Accessory, AccessoryTypes } from "./lib/accessory"; @@ -105,8 +105,22 @@ export class TradfriClient extends EventEmitter implements OperationProvider { * @param identity A previously negotiated identity. * @param psk The pre-shared key belonging to the identity. */ - public connect(identity: string, psk: string): Promise { - return this.tryToConnect(identity, psk); + public async connect(identity: string, psk: string): Promise { + switch (await this.tryToConnect(identity, psk)) { + case true: return true; + case "auth failed": throw new TradfriError( + "The provided credentials are not valid. Please re-authenticate!", + TradfriErrorCodes.AuthenticationFailed, + ); + case "timeout": throw new TradfriError( + "The gateway did not respond in time.", + TradfriErrorCodes.ConnectionTimedOut, + ); + case "error": throw new TradfriError( + "An unknown error occured while connecting to the gateway", + TradfriErrorCodes.ConnectionFailed, + ); + } } /** @@ -115,7 +129,7 @@ export class TradfriClient extends EventEmitter implements OperationProvider { * @param psk The pre-shared key to use * @returns true if the connection attempt was successful, otherwise false. */ - private async tryToConnect(identity: string, psk: string): Promise { + private async tryToConnect(identity: string, psk: string): Promise { // initialize CoAP client coap.reset(); @@ -125,7 +139,11 @@ export class TradfriClient extends EventEmitter implements OperationProvider { log(`Attempting connection. Identity = ${identity}, psk = ${psk}`, "debug"); const result = await coap.tryToConnect(this.requestBase); - log(`Connection ${result ? "" : "un"}successful`, "debug"); + if (result === true) { + log("Connection successful", "debug"); + } else { + log("Connection failed. Reason: " + result, "debug"); + } return result; } @@ -138,10 +156,23 @@ export class TradfriClient extends EventEmitter implements OperationProvider { public async authenticate(securityCode: string): Promise<{identity: string, psk: string}> { // first, check try to connect with the security code log("authenticate() > trying to connect with the security code", "debug"); - if (!await this.tryToConnect("Client_identity", securityCode)) { - // that didn't work, so the code is wrong - throw new TradfriError("The security code is wrong", TradfriErrorCodes.ConnectionFailed); + switch (await this.tryToConnect("Client_identity", securityCode)) { + case true: break; // all good + + case "auth failed": throw new TradfriError( + "The security code is wrong", + TradfriErrorCodes.AuthenticationFailed, + ); + case "timeout": throw new TradfriError( + "The gateway did not respond in time.", + TradfriErrorCodes.ConnectionTimedOut, + ); + case "error": throw new TradfriError( + "An unknown error occured while connecting to the gateway", + TradfriErrorCodes.ConnectionFailed, + ); } + // generate a new identity const identity = `tradfri_${Date.now()}`; log(`authenticating with identity "${identity}"`, "debug");