Giter VIP home page Giter VIP logo

st-schema-nodejs's Introduction

st-schema-nodejs

ST Schema helper library for NodeJS

Installing the module

npm install st-schema

Connector app structure

const connector = new SchemaConnector()
  .discoveryHandler((accessToken, response) => {
    /**
     * Discovery request. Respond with a list of devices. Called after installation of the
     * connector and every six hours after that.
     * @accessToken External cloud access token
     * @response {DiscoveryResponse} Discovery response object
     */
  })
  .stateRefreshHandler((accessToken, response) => {
    /**
     * State refresh request. Respond with the current states of all devices. Called after
     * device discovery runs.
     * @accessToken External cloud access token
     * @response {StateRefreshResponse} StateRefresh response object
     */
  })
  .commandHandler((accessToken, response, devices) => {
    /**
     * Device command request. Control the devices and respond with new device states
     * @accessToken External cloud access token
     * @response {CommandResponse} CommandResponse response object
     * @devices {array} List of ST device commands
     */
  })
  .callbackAccessHandler((accessToken, callbackAuthentication, callbackUrls) => {
    /**
     * Create access and refresh tokens to allow SmartThings to be informed of device state
     * changes as they happen. 
     * @accessToken External cloud access token
     * @callbackAuthentication ST access and refresh tokens for proactive state callbacks
     * @callbackUrls Callback and refresh token URLs
     */
  })
  .integrationDeletedHandler(accessToken => {
    /**
     * Called when the connector is removed from SmartThings. You may want clean up access
     * tokens and other data when that happend.
     * @accessToken External cloud access token
     */
  });

Minimal loopback connector example

This simple connector creates a one dimmer device named Test Dimmer. There's no physical device involved. The connector command handler simply returns the state value corresponding to the issued command. The current state of the device is stored in memory, so if the server is restarted the states will revert to their initial value. This implementation does not implement proactive state callbacks.

connector.js

const {SchemaConnector, DeviceErrorTypes} = require('st-schema')
const deviceStates = { switch: 'off', level: 100}
const connector = new SchemaConnector()
  .discoveryHandler((accessToken, response) => {
    response.addDevice('external-device-1', 'Test Dimmer', 'c2c-dimmer')
      .manufacturerName('Example Connector')
      .modelName('Virtual Dimmer');
  })
  .stateRefreshHandler((accessToken, response) => {
    response.addDevice('external-device-1', [
      {
        component: 'main',
        capability: 'st.switch',
        attribute: 'switch',
        value: deviceStates.switch
      },
      {
        component: 'main',
        capability: 'st.switchLevel',
        attribute: 'level',
        value: deviceStates.level
      }
    ])
  })
  .commandHandler((accessToken, response, devices) => {
    for (const device of devices) {
      const deviceResponse = response.addDevice(device.externalDeviceId);
      for (cmd of device.commands) {
        const state = {
          component: cmd.component,
          capability: cmd.capability
        };
        if (cmd.capability === 'st.switchLevel' && cmd.command === 'setLevel') {
          state.attribute = 'level';
          state.value = deviceStates.level = cmd.arguments[0];
          deviceResponse.addState(state);

        } else if (cmd.capability === 'st.switch') {
          state.attribute = 'switch';
          state.value = deviceStates.switch = cmd.command === 'on' ? 'on' : 'off';
          deviceResponse.addState(state);

        } else {
          deviceResponse.setError(
            `Command '${cmd.command} of capability '${cmd.capability}' not supported`,
            DeviceErrorTypes.CAPABILITY_NOT_SUPPORTED)
        }
      }
    }
  });

module.exports = connector

Running as a web-service

To run the above connector as a web service using the Express framework create a server like this one. Note that a real application would need to validate the access token passed in each request. This example only checks for the presence of the token.

server.js

const express = require('express');
const connector = require('./connector');
const server = express();
const port = 3000;
server.use(express.json());

server.post('/', (req, res) => {
  if (accessTokenIsValid(req)) {
    connector.handleHttpCallback(req, res)
  }
});

function accessTokenIsValid(req) {
  // Replace with proper validation of issued access token
  if (req.body.authentication.token) {
    return true;
  }
  res.status(401).send('Unauthorized')
  return false;
}

server.listen(port);
console.log(`Server listening on http://127.0.0.1:${port}`);

Running as an AWS Lambda

To run the connector as an AWS lambda use a handler like this one.

index.js

const connector = require('./connector');
exports.handle = async (evt, context, callback) => {
    return connector.handleLambdaCallback(evt, context, callback);
};

Proactive state callbacks

Sensors and devices that can be controlled other than through the SmartThings mobile app can change state at any time. To ensure that the SmartThings platform is made aware of these state changes right away callsbacks can be implemented to call into the SmartThings cloud. These callbacks are secured via a token exchange dependent on the client ID and secret defined for the ST Schema connector in the Developer Workspace. The following example is a minimal implementation of a connector that supports these callback. It builds on the previous example by implementing the callbacks and exposing a web-service endpoint for executing device commands.

app.js

The connector app is now initialized with the ST Schema connector's client ID and secret, which are available from the Developer workspace. It also declares an accessTokens map to contain the list of connectors that need to be called when device state changes. Note that this simple implementation stores the connectors in memory, so restarting the server will cause them to be lost. The app also has new callbackAccessHandler and integrationDeletedHandler handlers defined to add and remove entries from the accessTokens map.

const {SchemaConnector} = require('st-schema');
const deviceStates = {switch: 'off', level: 100};
const accessTokens = {};
const connector = new SchemaConnector()
  .clientId(process.env.ST_CLIENT_ID)
  .clientSecret(process.env.ST_CLIENT_SECRET)
  .discoveryHandler((accessToken, response) => {
    response.addDevice('external-device-1', 'Test Dimmer', 'c2c-dimmer')
      .manufacturerName('Example Connector')
      .modelName('Virtual Dimmer');
  })
  .stateRefreshHandler((accessToken, response) => {
    response.addDevice('external-device-1', [
      {
        component: 'main',
        capability: 'st.switch',
        attribute: 'switch',
        value: deviceStates.switch
      },
      {
        component: 'main',
        capability: 'st.switchLevel',
        attribute: 'level',
        value: deviceStates.level
      }
    ])
  })
  .commandHandler((accessToken, response, devices) => {
    for (const device of devices) {
      const deviceResponse = response.addDevice(device.externalDeviceId);
      for (cmd of device.commands) {
        const state = {
          component: cmd.component,
          capability: cmd.capability
        };
        if (cmd.capability === 'st.switchLevel' && cmd.command === 'setLevel') {
          state.attribute = 'level';
          state.value = deviceStates.level = cmd.arguments[0];
          deviceResponse.addState(state);

        } else if (cmd.capability === 'st.switch') {
          state.attribute = 'switch';
          state.value = deviceStates.switch = cmd.command === 'on' ? 'on' : 'off';
          deviceResponse.addState(state);

        } else {
          deviceResponse.setError(
            `Command '${cmd.command} of capability '${cmd.capability}' not supported`,
            DeviceErrorTypes.CAPABILITY_NOT_SUPPORTED)
        }
      }
    }
  })
  .callbackAccessHandler((accessToken, callbackAuthentication, callbackUrls) => {
    accessTokens[accessToken] = {
      callbackAuthentication,
      callbackUrls
    }
  })

  .integrationDeletedHandler(accessToken => {
    delete accessTokens[accessToken]
  });

module.exports = {
  connector: connector,
  deviceStates: deviceStates,
  accessTokens: accessTokens
};

server.js

The web server is modified to add a new /command endpoint for turning on and off the switch. It expects a JSON body of the form {"attribute": "switch", "value": "on"}.

"use strict";
require('dotenv').config();
const express = require('express');
const {StateUpdateRequest} = require('st-schema');
const {connector, deviceStates, accessTokens} = require('./app');
const server = express();
const port = 3001;
server.use(express.json());

server.post('/', (req, res) => {
  if (accessTokenIsValid(req)) {
    connector.handleHttpCallback(req, res)
  }
});

server.post('/command', (req, res) => {
  deviceStates[req.body.attribute] = req.body.value;
  for (const accessToken of Object.keys(accessTokens)) {
    const item = accessTokens[accessToken];
    const updateRequest = new StateUpdateRequest(process.env.ST_CLIENT_ID, process.env.ST_CLIENT_SECRET);
    const deviceState = [
      {
        externalDeviceId: 'external-device-1',
        states: [
          {
            component: 'main',
            capability: req.body.attribute === 'level' ? 'st.switchLevel' : 'st.switch',
            attribute: req.body.attribute,
            value: req.body.value
          }
        ]
      }
    ];
    updateRequest.updateState(item.callbackUrls, item.callbackAuthentication, deviceState)
  }
  res.send({});
  res.end()
});


function accessTokenIsValid(req) {
  // Replace with proper validation of issued access token
  if (req.body.authentication && req.body.authentication.token) {
    return true;
  }
  res.status(401).send('Unauthorized');
  return false;
}

server.listen(port);
console.log(`Server listening on http://127.0.0.1:${port}`);

st-schema-nodejs's People

Contributors

aarondchu avatar anthony-nguyen-st avatar anujatupe-smartthings avatar atrovato avatar bc-luke avatar bflorian avatar deeptigaonkar avatar dependabot[bot] avatar j2fong avatar juano2310 avatar workingmonk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

st-schema-nodejs's Issues

Question. connector's state refresh polling time

Hi,

I would like to know the reason of 6 hours polling time.
why schema connector works per 6 hours for polling ? what's the background of this number ?

Connector app structure
const connector = new SchemaConnector()
.discoveryHandler((accessToken, response) => {
/**
* Discovery request. Respond with a list of devices. Called after installation of the
* connector and every six hours after that.
* @accesstoken External cloud access token
* @response {DiscoveryResponse} Discovery response object
*/
})

Response's 'setError' should take precedence on any other response

If setError is called, the Response should ignore further calls to other convenience methods and it should take precedence.

If called from a loop, the method can then be used to ignore previous or future calls to other methods without a need to prevent them using a check on isError.

devices.forEach(device => {
  if (/* something */) {
    response.setError(`Something happened, let's leave it at that.`);
  }
});

deviceCookie missing in Device class

There seem to be no support in st-schema-nodejs for adding deviceCookie as part of discoveryResponse. Please refer properties supported currently in the "class Device: in DiscoveryDevice.js
/*

Please update st-schema-nodejs lib with support for deviceCookie.

Add support for 'interactionResult' interaction

If you run the framework without handling the certain phase, such as the discovery phase, your connector will print out the following message:

ERROR: {"headers":{"schema":"st-schema","version":"1.0","interactionType":"interactionResult","requestId":"C29856C5-B6F8-4374-ABD0-E42A41C9F1CF"},"globalError":{"detail":"Unsupported interactionType: 'interactionResult'","errorEnum":"INVALID-INTERACTION-TYPE"}}

This seems to occur when a required interaction does not have a corresponding & responding handler.

request object is undefined in lambda.js

When I tested lifx-connector(https://github.com/SmartThingsCommunity/st-schema-connectors/tree/master/lifx-connector) in aws lambda, request object is undefined.

await opts[interactionType](request, response);

So I met below error message.

ERROR MESSAGE START

2019-05-26T23:53:19.544Z 32ad93e2-0f34-49e9-837f-6c4993444593 interactionType discoveryRequest
2019-05-26T23:53:19.545Z 32ad93e2-0f34-49e9-837f-6c4993444593 ERROR IN ST-SCHEMA HELPER TypeError: Cannot read property 'authentication' of undefined
at Object.discoveryRequest (/var/task/index.js:9:84)
at /var/task/node_modules/st-schema/lib/lambda.js:82:34 await opts[interactionType](request, response);

ERROR MESSAGE END

I think below line is needed before line 80.
request = event;

What do you think?

State Refresh Handler does not make devices from request available

According to the SmartThings documentation, the state refresh incoming request contains the list of devices which require a state update:
https://smartthings.developer.samsung.com/docs/devices/smartthings-schema/smartthings-schema-reference.html#State-Refresh

However, this framework's state refresh handler fails to make these available to the dev:

.stateRefreshHandler((accessToken, response) => {
    /**
     * State refresh request. Respond with the current states of all devices. Called after
     * device discovery runs.
     * @accessToken External cloud access token
     * @response {StateRefreshResponse} StateRefresh response object
     */
  })

Unlike the handler for commands, which does:

.commandHandler((accessToken, response, devices) => {})

Check if body.authentication exists

Hi,
in the SchemaConnector file, for each interaction type, the token is directly taken from the body.authentication, but if it don't exist, the error TypeError: Cannot read property 'token' of undefined is thrown.

Accord to SmartThings documentation, this case can throw a specific error :

{
...
  "globalError": {
    "errorEnum": "BAD-REQUEST",
    "detail": "missing st-schema authentication"
  },
...
}

It will improve the schema compliance.

Add SmartThings API framework dependency

This SDK could benefit from having a SmartThings API framework as a dependency to simplify making calls from the connector to SmartThings, such as when trying to update the state of a device outside of a connector interaction.

This could simply be a suggestion in the README, as these calls are not a requirement for integrations to operate, thus the package could be optional.

Unfortunately, there is no public SmartThings API framework at the moment (which isnt outdated). The one from https://github.com/SmartThingsCommunity/smartapp-sdk-nodejs should be extracted, and both SDKs should make it a dependency (separately maintained).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.