├── .eslintrc.js ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── server.js └── views ├── error.ejs ├── index.ejs └── scenes.ejs /.eslintrc.js: -------------------------------------------------------------------------------- 1 | (module.exports = { 2 | 'root': true, 3 | 'plugins': [ 4 | 'standard', 5 | 'dollar-sign', 6 | 'jquery', 7 | ], 8 | 'env': { 9 | 'browser': true, 10 | 'commonjs': true, 11 | 'es6': true, 12 | 'node': true 13 | }, 14 | 'extends': [ 15 | 'eslint:recommended', 16 | ], 17 | 'globals': { 18 | 'Atomics': 'readonly', 19 | 'SharedArrayBuffer': 'readonly' 20 | }, 21 | 'parserOptions': { 22 | 'ecmaVersion': 2018 23 | }, 24 | 'rules': { 25 | 'indent': ['error', 2], 26 | 'quotes': ['error', 'single'], 27 | 'linebreak-style': ['error', 'unix'], 28 | 'curly': ['error', 'all'], 29 | 'comma-dangle': 'off', 30 | 'no-console': 'off', 31 | 'no-undef': 'off', 32 | 'no-process-exit': 'error', 33 | 'no-template-curly-in-string': 'error', 34 | 'require-await': 'off', 35 | 'semi': ['error', 'always'], 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Intellij 9 | .idea 10 | *.iml 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | .env.* 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple API Access App Example 2 | 3 | ## Overview 4 | 5 | This simple NodeJS Express app illustrates how to create an _API Access_ SmartApp that connects to your SmartThings 6 | account with OAuth2 and allows you to execute manually run routines (which are called "scenes" in the API). 7 | It's a very simple app that stores the access and refresh tokens in session state. It uses the 8 | [express-session](https://www.npmjs.com/package/express-session#compatible-session-stores) in-memory session store, 9 | so you will lose your session data 10 | when you restart the server, but you can use another 11 | [compatible session store](https://www.npmjs.com/package/express-session#compatible-session-stores) 12 | to make the session persist between server 13 | restarts. This example uses the 14 | [@SmartThings/SmartApp](https://www.npmjs.com/package/@smartthings/smartapp) SDK NPM module for making the 15 | API calls to list and execute scenes. 16 | 17 | ## Files and directories 18 | 19 | - public 20 | - stylesheets -- stylesheets used by the web pages 21 | - views 22 | - error.ejs -- error page 23 | - index.ejs -- initial page with link to connect to SmartThings 24 | - scenes.ejs -- page that displays scenes and allows them to be executed 25 | - server.js -- the Express server and SmartApp 26 | - .env -- file you create with app client ID and client secret 27 | 28 | ## Getting Started 29 | 30 | ### Prerequisites 31 | - A [SmartThings](https://smartthings.com) account with at least one location and manually run routines created 32 | 33 | - The [SmartThings CLI](https://github.com/SmartThingsCommunity/smartthings-cli#readme) installed on your computer 34 | 35 | - [Node.js](https://nodejs.org/en/) and [npm](https://www.npmjs.com/) installed on your computer 36 | 37 | ## Instructions 38 | 39 | ### 1. Clone [this GitHub repository](https://github.com/SmartThingsCommunity/api-app-minimal-example-js), cd into the directory, and install the Node modules with NPM: 40 | ```$bash 41 | git clone https://github.com/SmartThingsCommunity/api-app-minimal-example-js.git 42 | cd api-app-minimal-example-js 43 | npm install 44 | ``` 45 | 46 | ### 2. Register your app with SmartThings using the CLI 47 | 48 | Start with the `smartthings apps:create` command to create a new app. You will be prompted for the required 49 | information. The following is an example of the output from the command: 50 | 51 | ```bash 52 | ~ % smartthings apps:create 53 | ? What kind of app do you want to create? (Currently, only OAuth-In apps are supported.) OAuth-In App 54 | 55 | More information on writing SmartApps can be found at 56 | https://developer.smartthings.com/docs/connected-services/smartapp-basics 57 | 58 | ? Display Name My API App 59 | ? Description Allows scenes to be executed 60 | ? Icon Image URL (optional) 61 | ? Target URL (optional) 62 | 63 | More information on OAuth 2 Scopes can be found at: 64 | https://www.oauth.com/oauth2-servers/scope/ 65 | 66 | To determine which scopes you need for the application, see documentation for the individual endpoints you will use in your app: 67 | https://developer.smartthings.com/docs/api/public/ 68 | 69 | ? Select Scopes. r:locations:*, r:scenes:*, x:scenes:* 70 | ? Add or edit Redirect URIs. Add Redirect URI. 71 | ? Redirect URI (? for help) http://localhost:3000/oauth/callback 72 | ? Add or edit Redirect URIs. Finish editing Redirect URIs. 73 | ? Choose an action. Finish and create OAuth-In SmartApp. 74 | Basic App Data: 75 | ───────────────────────────────────────────────────────────────── 76 | Display Name My API App 77 | App Id 037bcd6c-xxxx-xxxx-xxxx-xxxxxxxxxxxx 78 | App Name amyapiapp-a8b20801-xxxx-xxxx-xxxx-xxxxxxxxxxxx 79 | Description Allows scenes to be executed 80 | Single Instance true 81 | Classifications CONNECTED_SERVICE 82 | App Type API_ONLY 83 | ───────────────────────────────────────────────────────────────── 84 | 85 | 86 | OAuth Info (you will not be able to see the OAuth info again so please save it now!): 87 | ─────────────────────────────────────────────────────────── 88 | OAuth Client Id 689f9823-xxxx-xxxx-xxxx-xxxxxxxxxxxx 89 | OAuth Client Secret 3a2c39d8-xxxx-xxxx-xxxx-xxxxxxxxxxxx 90 | ─────────────────────────────────────────────────────────── 91 | ```` 92 | 93 | ### 3. Create a `.env` file in the root directory of the project 94 | 95 | Add the `PORT`, `SERVER_URL`, `APP_ID`, `CLIENT_ID`, and `CLIENT_SECRET` properties from the output of the `smartthings apps:create` command. For example: 96 | 97 | ```$bash 98 | PORT=3000 99 | SERVER_URL=http://localhost:3000 100 | APP_ID=037bcd6c-xxxx-xxxx-xxxx-xxxxxxxxxxxx 101 | CLIENT_ID=689f9823-xxxx-xxxx-xxxx-xxxxxxxxxxxx 102 | CLIENT_SECRET=3a2c39d8-xxxx-xxxx-xxxx-xxxxxxxxxxxx 103 | ``` 104 | 105 | ### 4. Start your server: 106 | ```$bash 107 | node server.js 108 | ``` 109 | 110 | ### 5. Connect your app to SmartThings 111 | 112 | Go to http://localhost:3000, log in with your SmartThings account credentials, and 113 | choose a location. You should see a page with the location name as a header and button for 114 | each scene in that location. Clicking the button should execute the scene. If you don't see 115 | any buttons you may need to create some scenes using the SmartThings mobile app. 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-app-minimal-example-js", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "lint": "eslint .", 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "@smartthings/smartapp": "^4.3.4", 11 | "dotenv": "^16.0.1", 12 | "ejs": "~3.1.8", 13 | "encodeurl": "^1.0.2", 14 | "express": "~4.19.2", 15 | "express-session": "^1.17.3", 16 | "morgan": "~1.10.0", 17 | "nanoid": "^3.3.7" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^8.55.0", 21 | "eslint-config-standard": "^17.1.0", 22 | "eslint-plugin-dollar-sign": "^1.0.2", 23 | "eslint-plugin-jquery": "^1.5.1", 24 | "eslint-plugin-standard": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | const express = require('express'); 5 | const session = require('express-session'); 6 | const path = require('path'); 7 | const morgan = require('morgan'); 8 | const encodeUrl = require('encodeurl'); 9 | const { nanoid } = require('nanoid'); 10 | 11 | const SmartApp = require('@smartthings/smartapp'); 12 | 13 | const port = process.env.PORT || 3000; 14 | const serverUrl = process.env.SERVER_URL || 'http://localhost:3000'; 15 | const redirectUri = `${serverUrl}/oauth/callback`; 16 | const scope = encodeUrl('r:locations:* r:scenes:* x:scenes:*'); 17 | const appId = process.env.APP_ID; 18 | const clientId = process.env.CLIENT_ID; 19 | const clientSecret = process.env.CLIENT_SECRET; 20 | 21 | /* SmartThings API */ 22 | const smartApp = new SmartApp() 23 | .appId(appId) 24 | .clientId(clientId) 25 | .clientSecret(clientSecret) 26 | .redirectUri(redirectUri); 27 | 28 | /* Webserver setup */ 29 | const server = express(); 30 | server.set('views', path.join(__dirname, 'views')); 31 | server.set('view engine', 'ejs'); 32 | server.use(morgan('dev')); 33 | server.use(express.json()); 34 | server.use(express.urlencoded({extended: false})); 35 | server.use(session({ 36 | secret: 'oauth example secret', 37 | resave: false, 38 | saveUninitialized: true, 39 | cookie: {secure: false} 40 | })); 41 | server.use(express.static(path.join(__dirname, 'public'))); 42 | 43 | /* Main page. Shows link to SmartThings if not authenticated and list of scenes afterwards */ 44 | server.get('/', function (req, res) { 45 | if (req.session.smartThings) { 46 | // Context cookie found, use it to list scenes 47 | const data = req.session.smartThings; 48 | smartApp.withContext(data).then(ctx => { 49 | ctx.api.scenes.list().then(scenes => { 50 | res.render('scenes', { 51 | installedAppId: data.installedAppId, 52 | locationName: data.locationName, 53 | errorMessage: '', 54 | scenes: scenes 55 | }); 56 | }).catch(error => { 57 | res.render('scenes', { 58 | installedAppId: data.installedAppId, 59 | locationName: data.locationName, 60 | errorMessage: `${error.message}`, 61 | scenes: {items:[]} 62 | }); 63 | }); 64 | }); 65 | } 66 | else { 67 | // No context cookie. Display link to authenticate with SmartThings 68 | const state = nanoid(24); 69 | req.session.smartThingsState = state; 70 | res.render('index', { 71 | url: `https://api.smartthings.com/oauth/authorize?client_id=${clientId}&scope=${scope}&response_type=code&redirect_uri=${redirectUri}&state=${state}` 72 | }); 73 | } 74 | }); 75 | 76 | /* Uninstalls app and clears context cookie */ 77 | server.get('/logout', async function(req, res) { 78 | const ctx = await smartApp.withContext(req.session.smartThings); 79 | await ctx.api.installedApps.delete(); 80 | req.session.destroy(() => { 81 | res.redirect('/'); 82 | }); 83 | }); 84 | 85 | /* Executes a scene */ 86 | server.post('/scenes/:sceneId', function (req, res) { 87 | smartApp.withContext(req.session.smartThings).then(ctx => { 88 | ctx.api.scenes.execute(req.params.sceneId).then(result => { 89 | res.send(result); 90 | }); 91 | }); 92 | }); 93 | 94 | /* Handles OAuth redirect */ 95 | server.get('/oauth/callback', async (req, res) => { 96 | 97 | // Validate the state 98 | if (req.query.state !== req.session.smartThingsState) { 99 | res.status(400).send('Invalid state'); 100 | return; 101 | } 102 | 103 | // Exchange the code for the auth token. Returns an API context that can be used for subsequent calls 104 | const ctx = await smartApp.handleOAuthCallback(req); 105 | 106 | // Get the location name 107 | const location = await ctx.api.locations.get(); 108 | 109 | // Set the cookie with the context, including the location ID and name 110 | req.session.smartThings = { 111 | locationId: ctx.locationId, 112 | locationName: location.name, 113 | installedAppId: ctx.installedAppId, 114 | authToken: ctx.authToken, 115 | refreshToken: ctx.refreshToken 116 | }; 117 | 118 | // Redirect back to the main mage 119 | res.redirect('/'); 120 | }); 121 | 122 | server.listen(port); 123 | console.log(`\nWebsite URL -- Use this URL to log into SmartThings and connect this app to your account:\n${serverUrl}\n`); 124 | console.log(`Redirect URI -- Copy this value into the "Redirection URI(s)" field in the Developer Workspace:\n${redirectUri}`); 125 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 |
<%= error.stack %>4 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
10 | Connect to SmartThings 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /views/scenes.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |<%= errorMessage %>
21 |29 | Logout 30 |
31 | 32 | 33 | --------------------------------------------------------------------------------