├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── shellies ├── index.js ├── lib ├── coap │ ├── index.js │ ├── message.js │ └── options.js ├── devices │ ├── base.js │ ├── index.js │ ├── shelly-1.js │ ├── shelly-1l.js │ ├── shelly-1pm.js │ ├── shelly-2.js │ ├── shelly-25.js │ ├── shelly-2led.js │ ├── shelly-3em.js │ ├── shelly-4pro.js │ ├── shelly-air.js │ ├── shelly-bulb-rgbw.js │ ├── shelly-bulb.js │ ├── shelly-button1-v2.js │ ├── shelly-button1.js │ ├── shelly-color.js │ ├── shelly-dimmer-w1.js │ ├── shelly-dimmer.js │ ├── shelly-dimmer2.js │ ├── shelly-door-window.js │ ├── shelly-door-window2.js │ ├── shelly-duo.js │ ├── shelly-em.js │ ├── shelly-flood.js │ ├── shelly-gas.js │ ├── shelly-hd.js │ ├── shelly-ht.js │ ├── shelly-i3.js │ ├── shelly-motion.js │ ├── shelly-motion2.js │ ├── shelly-plug-e.js │ ├── shelly-plug-s.js │ ├── shelly-plug-us.js │ ├── shelly-plug.js │ ├── shelly-rgbw.js │ ├── shelly-rgbw2.js │ ├── shelly-sense.js │ ├── shelly-smoke.js │ ├── shelly-smoke2.js │ ├── shelly-trv.js │ ├── shelly-uni.js │ ├── shelly-vintage.js │ └── unknown.js ├── http-request.js └── status-updates-listener.js ├── package-lock.json ├── package.json └── test ├── test-devices.js ├── test-shellies.js └── test-status-updates-listener.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true 4 | }, 5 | 6 | extends: [ 7 | 'eslint:recommended', 8 | 'standard' 9 | ], 10 | 11 | rules: { 12 | 'comma-dangle': ['error', 'only-multiline'], 13 | 14 | 'max-len': ['error', { 15 | code: 80 16 | }], 17 | 18 | 'space-before-function-paren': ['error', { 19 | anonymous: 'never', 20 | named: 'never', 21 | asyncArrow: 'always' 22 | }] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.7.0] - 2022-02-10 10 | ### Added 11 | - Add support for the Shelly TRV. 12 | 13 | ## [1.6.0] - 2021-08-08 14 | ### Fixed 15 | - **[Potentially breaking]** Change the data type of several device properties 16 | from `Boolean` to `Number` since their possible values have changed from 0/1 17 | to 0/1/-1, where -1 usually indicates that there is no valid reading. 18 | - Update dependencies to fix security vulnerabilities. 19 | 20 | ## [1.5.0] - 2021-02-28 21 | ### Added 22 | - Add support for unicast UDP messages. 23 | 24 | ## [1.4.0] - 2021-02-23 25 | ### Added 26 | - Add support for the Shelly Motion (thanks to @jghaanstra). 27 | 28 | ## [1.3.0] - 2021-01-12 29 | ### Added 30 | - Add support for the Shelly Button1 version 2. 31 | 32 | ## [1.2.0] - 2021-01-11 33 | ### Added 34 | - Add support for the Shelly Bulb RGBW (thanks to @jghaanstra). 35 | 36 | ## [1.1.1] - 2020-11-26 37 | ### Fixed 38 | - Fixed a bug with duplicate properties on the Shelly 1L. 39 | 40 | ## [1.1.0] - 2020-11-26 41 | ### Added 42 | - Add support for the Shelly 1L and the Shelly Uni (thanks to @jghaanstra). 43 | 44 | ### Changed 45 | - Update the coap library to version 0.24.0. 46 | 47 | ## [1.0.2] - 2020-09-04 48 | ### Fixed 49 | - Add a missing `setRelay()` method to `ShellyAir`. 50 | 51 | ## [1.0.1] - 2020-08-15 52 | ### Changed 53 | - **[Breaking]** Rename the `externalTemperature` property of `ShellyFlood` to 54 | `temperature`. 55 | 56 | ### Fixed 57 | - Add a missing `mode` argument to the `Shelly25` constructor. 58 | 59 | [Unreleased]: https://github.com/alexryd/node-shellies/compare/v1.7.0...HEAD 60 | [1.7.0]: https://github.com/alexryd/node-shellies/compare/v1.6.0...v1.7.0 61 | [1.6.0]: https://github.com/alexryd/node-shellies/compare/v1.5.0...v1.6.0 62 | [1.5.0]: https://github.com/alexryd/node-shellies/compare/v1.4.0...v1.5.0 63 | [1.4.0]: https://github.com/alexryd/node-shellies/compare/v1.3.0...v1.4.0 64 | [1.3.0]: https://github.com/alexryd/node-shellies/compare/v1.2.0...v1.3.0 65 | [1.2.0]: https://github.com/alexryd/node-shellies/compare/v1.1.1...v1.2.0 66 | [1.1.1]: https://github.com/alexryd/node-shellies/compare/v1.1.0...v1.1.1 67 | [1.1.0]: https://github.com/alexryd/node-shellies/compare/v1.0.2...v1.1.0 68 | [1.0.2]: https://github.com/alexryd/node-shellies/compare/v1.0.1...v1.0.2 69 | [1.0.1]: https://github.com/alexryd/node-shellies/compare/v1.0.0...v1.0.1 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Rydén 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-shellies 2 | [![NPM Version](https://img.shields.io/npm/v/shellies.svg)](https://www.npmjs.com/package/shellies) 3 | [![Build Status](https://travis-ci.org/alexryd/node-shellies.svg?branch=master)](https://travis-ci.org/alexryd/node-shellies) 4 | 5 | Handles communication with the first generation [Shelly](https://shelly.cloud) devices, using both 6 | [CoAP](http://coap.technology) and HTTP. 7 | 8 | For the next generation devices, see [node-shellies-ng](https://github.com/alexryd/node-shellies-ng). 9 | 10 | ## Features 11 | * Automatically detects Shelly devices (on the same network and subnet). 12 | * Automatically detects when the status of a device changes, such as when a 13 | relay is turned on or off. 14 | * Keeps track of devices and if they go offline (because no status update has 15 | been received in a given amount of time). 16 | 17 | ## Supported devices 18 | The following Shelly devices are supported: 19 | * [Shelly 1](https://shelly.cloud/shelly1-open-source/) 20 | * [Shelly 1L](https://shelly.cloud/products/shelly-1l-single-wire-smart-home-automation-relay/) 21 | * [Shelly 1PM](https://shelly.cloud/shelly-1pm-wifi-smart-relay-home-automation/) 22 | * Shelly 2 23 | * [Shelly 2.5](https://shelly.cloud/shelly-25-wifi-smart-relay-roller-shutter-home-automation/) 24 | * Shelly 2LED 25 | * [Shelly 3EM](https://shelly.cloud/shelly-3-phase-energy-meter-with-contactor-control-wifi-smart-home-automation/) 26 | * [Shelly 4Pro](https://shelly.cloud/shelly-4-pro/) 27 | * [Shelly Air](https://shelly.cloud/products/shelly-air-smart-home-air-purifier/) 28 | * [Shelly Bulb](https://shelly.cloud/shelly-bulb/) 29 | * [Shelly Button 1](https://shelly.cloud/products/shelly-button-1-smart-home-automation-device/) 30 | * Shelly Color 31 | * Shelly Dimmer 32 | * [Shelly Dimmer 2](https://shelly.cloud/products/shelly-dimmer-2-smart-home-light-contoller/) 33 | * Shelly Dimmer W1 34 | * Shelly Door/Window 35 | * [Shelly Door/Window 2](https://shelly.cloud/products/shelly-door-window-2-smart-home-automation-sensor/) 36 | * [Shelly Duo](https://shelly.cloud/wifi-smart-home-automation-shelly-duo/) 37 | * [Shelly EM](https://shelly.cloud/shelly-energy-meter-with-contactor-control-wifi-smart-home-automation/) 38 | * [Shelly Flood](https://shelly.cloud/shelly-flood-and-temperature-sensor-wifi-smart-home-automation/) 39 | * [Shelly Gas](https://shelly.cloud/products/shelly-gas-smart-home-automation-sensor/) 40 | * Shelly HD 41 | * [Shelly H&T](https://shelly.cloud/shelly-humidity-and-temperature/) 42 | * [Shelly i3](https://shelly.cloud/products/shelly-i3-smart-home-automation-device/) 43 | * [Shelly Motion](https://shelly.cloud/shelly-motion-smart-home-automation-sensor/) 1 44 | * [Shelly Plug](https://shelly.cloud/shelly-plug/) 45 | * [Shelly Plug S](https://shelly.cloud/shelly-plug-s/) 46 | * [Shelly Plug US](https://shelly.cloud/products/shelly-plug-us-smart-home-automation-device/) 47 | * Shelly RGBW 48 | * [Shelly RGBW2](https://shelly.cloud/wifi-smart-shelly-rgbw-2/) 49 | * [Shelly Sense](https://shelly.cloud/shelly-sense/) 50 | * Shelly Smoke 51 | * [Shelly Smoke 2](https://shelly.cloud/products/shelly-smoke-smart-home-automation-sensor/) 52 | * [Shelly TRV](https://shelly.cloud/shelly-thermostatic-radiator-valve/) 53 | * [Shelly Uni](https://shelly.cloud/products/shelly-uni-smart-home-automation-device/) 54 | * [Shelly Vintage](https://shelly.cloud/wifi-smart-home-automation-shelly-vintage/) 55 | 56 | ### Notes 57 | 1 Requires setting the `Internet & Security -> CoIoT -> Remote 58 | address` option on the Shelly device to the IP address of your device running 59 | node-shellies. 60 | 61 | ## Basic usage example 62 | ```javascript 63 | const shellies = require('shellies') 64 | 65 | shellies.on('discover', device => { 66 | // a new device has been discovered 67 | console.log('Discovered device with ID', device.id, 'and type', device.type) 68 | 69 | device.on('change', (prop, newValue, oldValue) => { 70 | // a property on the device has changed 71 | console.log(prop, 'changed from', oldValue, 'to', newValue) 72 | }) 73 | 74 | device.on('offline', () => { 75 | // the device went offline 76 | console.log('Device with ID', device.id, 'went offline') 77 | }) 78 | }) 79 | 80 | // start discovering devices and listening for status updates 81 | shellies.start() 82 | ``` 83 | -------------------------------------------------------------------------------- /bin/shellies: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | const colors = require('colors/safe') 5 | const commandLineCommands = require('command-line-commands') 6 | 7 | const packageVersion = require('../package.json').version 8 | const shellies = require('../index') 9 | 10 | const h = (txt, color = 'green') => { 11 | return colors[color](txt) + ' ' + colors.gray(new Date().toISOString()) 12 | } 13 | 14 | const v = (key, val) => { 15 | return colors.gray(key + ':') + ' ' + val 16 | } 17 | 18 | const listen = () => { 19 | shellies 20 | .on('discover', (device, unknown) => { 21 | if (!unknown) { 22 | console.log(h('[Device discovered]')) 23 | } else { 24 | console.log(h('[Unknown device]', 'yellow')) 25 | } 26 | console.log( 27 | v('Model', unknown ? device.type : device.modelName), 28 | v('ID', device.id), 29 | v('Host', device.host) 30 | ) 31 | 32 | for (const [prop, value] of device) { 33 | console.log( 34 | v('Property', prop), 35 | v('Value', value) 36 | ) 37 | } 38 | 39 | device.getSettings() 40 | .then(settings => { 41 | device.settings = settings 42 | }) 43 | .catch(error => { 44 | console.error(h('[Request failed]', 'red')) 45 | console.log('Failed to load settings for device with ID', device.id) 46 | console.log('Error:', error.status, error.message) 47 | }) 48 | 49 | device 50 | .on('change', (prop, newValue) => { 51 | if (prop === 'settings') { 52 | return 53 | } 54 | 55 | console.log(h('[Property changed]')) 56 | console.log( 57 | v('Property', prop), 58 | v('Value', newValue), 59 | v('Device ID', device.id) 60 | ) 61 | }) 62 | .on('offline', () => { 63 | console.log(h('[Device offline]', 'red')) 64 | console.log( 65 | v('Model', unknown ? device.type : device.modelName), 66 | v('ID', device.id), 67 | v('Host', device.host) 68 | ) 69 | }) 70 | .on('online', () => { 71 | console.log(h('[Device online]')) 72 | console.log( 73 | v('Model', unknown ? device.type : device.modelName), 74 | v('ID', device.id), 75 | v('Host', device.host) 76 | ) 77 | }) 78 | }) 79 | .on('stale', device => { 80 | console.log(h('[Device stale]', 'red')) 81 | console.log( 82 | v('Model', unknown ? device.type : device.modelName), 83 | v('ID', device.id), 84 | v('Host', device.host) 85 | ) 86 | }) 87 | .start() 88 | } 89 | 90 | const description = host => { 91 | shellies.Coap.getDescription(host) 92 | .then(msg => { 93 | const device = shellies.createDevice( 94 | msg.deviceType, 95 | msg.deviceId, 96 | msg.host 97 | ) 98 | 99 | const unknown = shellies.isUnknownDevice(device) 100 | if (unknown) { 101 | console.log(colors.yellow('[Unknown device]')) 102 | } 103 | 104 | console.log( 105 | v('Model', unknown ? device.type : device.modelName), 106 | v('ID', device.id), 107 | v('Host', device.host) 108 | ) 109 | console.log(v('Protocol revision', msg.protocolRevision)) 110 | 111 | console.log(v('Description', JSON.stringify(msg.payload))) 112 | }) 113 | } 114 | 115 | const status = host => { 116 | shellies.Coap.getStatus(host) 117 | .then(msg => { 118 | const device = shellies.createDevice( 119 | msg.deviceType, 120 | msg.deviceId, 121 | msg.host 122 | ) 123 | device.update(msg) 124 | device.ttl = 0 125 | 126 | const unknown = shellies.isUnknownDevice(device) 127 | if (unknown) { 128 | console.log(colors.yellow('[Unknown device]')) 129 | } 130 | 131 | console.log( 132 | v('Model', unknown ? device.type : device.modelName), 133 | v('ID', device.id), 134 | v('Host', device.host) 135 | ) 136 | 137 | if (!shellies.isUnknownDevice(device)) { 138 | for (const [prop, value] of device) { 139 | console.log( 140 | v('Property', prop), 141 | v('Value', value) 142 | ) 143 | } 144 | } 145 | 146 | console.log(v('Status', JSON.stringify(msg.payload))) 147 | }) 148 | } 149 | 150 | const commands = new Map() 151 | commands.set('listen', listen) 152 | commands.set('description', description) 153 | commands.set('status', status) 154 | 155 | try { 156 | const { command, argv } = commandLineCommands( 157 | Array.from(commands.keys()) 158 | ) 159 | 160 | commands.get(command).apply(this, argv) 161 | } catch (e) { 162 | if (e.name === 'INVALID_COMMAND') { 163 | console.log('node-shellies', packageVersion) 164 | console.log('') 165 | console.log('Valid commands:', Array.from(commands.keys()).join(', ')) 166 | } else { 167 | console.error(e) 168 | process.exit(1) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('eventemitter3') 2 | 3 | const Coap = require('./lib/coap') 4 | const devices = require('./lib/devices') 5 | const request = require('./lib/http-request') 6 | const StatusUpdatesListener = require('./lib/status-updates-listener') 7 | 8 | const deviceKey = (type, id) => `${type}#${id}` 9 | 10 | class Shellies extends EventEmitter { 11 | constructor() { 12 | super() 13 | 14 | this._devices = new Map() 15 | this._listener = new StatusUpdatesListener() 16 | 17 | this._listener 18 | .on('start', () => { this.emit('start') }) 19 | .on('stop', () => { this.emit('stop') }) 20 | .on('statusUpdate', this._statusUpdateHandler, this) 21 | 22 | this.staleTimeout = 0 23 | } 24 | 25 | get size() { 26 | return this._devices.size 27 | } 28 | 29 | get running() { 30 | return this._listener.listening 31 | } 32 | 33 | [Symbol.iterator]() { 34 | return this._devices.values() 35 | } 36 | 37 | _statusUpdateHandler(msg) { 38 | let device = this._devices.get(deviceKey(msg.deviceType, msg.deviceId)) 39 | 40 | if (device) { 41 | device.update(msg) 42 | } else { 43 | device = devices.create(msg.deviceType, msg.deviceId, msg.host) 44 | device.update(msg) 45 | this.emit('discover', device, this.isUnknownDevice(device)) 46 | this.addDevice(device) 47 | } 48 | } 49 | 50 | _deviceOfflineHandler(device) { 51 | if (!Number.isInteger(this.staleTimeout) || this.staleTimeout <= 0) { 52 | return 53 | } 54 | 55 | const timeout = setTimeout(() => { 56 | this.emit('stale', device) 57 | device.emit('stale', device) 58 | this.removeDevice(device) 59 | }, this.staleTimeout) 60 | 61 | const onlineHandler = () => { 62 | clearTimeout(timeout) 63 | device.once('offline', this._deviceOfflineHandler, this) 64 | this.removeListener('remove', removeHandler) 65 | } 66 | const removeHandler = d => { 67 | if (d === device) { 68 | clearTimeout(timeout) 69 | device.removeListener('online', onlineHandler) 70 | this.removeListener('remove', removeHandler) 71 | } 72 | } 73 | device.once('online', onlineHandler) 74 | this.on('remove', removeHandler) 75 | } 76 | 77 | setAuthCredentials(username, password) { 78 | request.auth(username, password) 79 | } 80 | 81 | async start(networkInterface = null) { 82 | await this._listener.start(networkInterface) 83 | } 84 | 85 | stop() { 86 | this._listener.stop() 87 | } 88 | 89 | getDevice(type, id) { 90 | return this._devices.get(deviceKey(type, id)) 91 | } 92 | 93 | hasDevice(device) { 94 | return this._devices.has(deviceKey(device.type, device.id)) 95 | } 96 | 97 | addDevice(device) { 98 | const key = deviceKey(device.type, device.id) 99 | if (this._devices.has(key)) { 100 | throw new Error( 101 | `Device with type ${device.type} and ID ${device.id} already added` 102 | ) 103 | } 104 | 105 | this._devices.set(key, device) 106 | if (!device.online) { 107 | this._deviceOfflineHandler(device) 108 | } else { 109 | device.once('offline', this._deviceOfflineHandler, this) 110 | } 111 | this.emit('add', device) 112 | } 113 | 114 | removeDevice(device) { 115 | if ((this._devices.delete(deviceKey(device.type, device.id)))) { 116 | device.removeListener('offline', this._deviceOfflineHandler, this) 117 | this.emit('remove', device) 118 | } 119 | } 120 | 121 | removeAllDevices() { 122 | for (const device of this) { 123 | this.removeDevice(device) 124 | } 125 | } 126 | 127 | isUnknownDevice(device) { 128 | return devices.isUnknown(device) 129 | } 130 | } 131 | 132 | const shellies = new Shellies() 133 | 134 | shellies.Coap = Coap 135 | shellies.createDevice = devices.create.bind(devices) 136 | shellies.request = request 137 | shellies.StatusUpdatesListener = StatusUpdatesListener 138 | 139 | module.exports = shellies 140 | -------------------------------------------------------------------------------- /lib/coap/index.js: -------------------------------------------------------------------------------- 1 | const coap = require('coap') 2 | 3 | const CoapMessage = require('./message') 4 | const CoapOptions = require('./options') 5 | 6 | const COAP_MULTICAST_ADDRESS = '224.0.1.187' 7 | 8 | CoapOptions.register() 9 | 10 | const agent = new coap.Agent() 11 | // Because of a bug in Shelly devices we need to override _nextToken() 12 | agent._nextToken = () => Buffer.alloc(0) 13 | 14 | const request = (host, pathname, opts) => { 15 | return new Promise((resolve, reject) => { 16 | coap.request( 17 | Object.assign({ 18 | host, 19 | pathname, 20 | agent, 21 | }, opts) 22 | ) 23 | .on('response', res => { 24 | resolve(new CoapMessage(res)) 25 | }) 26 | .on('error', err => { 27 | reject(err) 28 | }) 29 | .end() 30 | }) 31 | } 32 | 33 | const getDescription = host => request(host, '/cit/d') 34 | const getStatus = host => request(host, '/cit/s') 35 | 36 | const requestStatusUpdates = (statusUpdateHandler, timeout = 500) => { 37 | return new Promise((resolve, reject) => { 38 | const t = setTimeout(resolve, timeout + 10) 39 | 40 | coap.request({ 41 | host: COAP_MULTICAST_ADDRESS, 42 | method: 'GET', 43 | pathname: '/cit/s', 44 | multicast: true, 45 | multicastTimeout: timeout, 46 | agent, 47 | }) 48 | .on('response', res => { 49 | if (statusUpdateHandler) { 50 | statusUpdateHandler(new CoapMessage(res)) 51 | } 52 | }) 53 | .on('error', err => { 54 | clearTimeout(t) 55 | reject(err) 56 | }) 57 | .end() 58 | }) 59 | } 60 | 61 | const listenForStatusUpdates = (statusUpdateHandler, networkInterface) => { 62 | const server = coap.createServer({ 63 | multicastAddress: COAP_MULTICAST_ADDRESS, 64 | multicastInterface: networkInterface, 65 | }) 66 | 67 | // insert our own middleware right before requests are handled (the last step) 68 | server._middlewares.splice( 69 | Math.max(server._middlewares.length - 1, 0), 70 | 0, 71 | (req, next) => { 72 | // Unicast messages from Shelly devices will have the 2.05 code, which the 73 | // server will silently drop (since its a response code and not a request 74 | // code). To avoid this, we change it to 0.30 here. 75 | if (req.packet.code === '2.05') { 76 | req.packet.code = '0.30' 77 | } 78 | next() 79 | } 80 | ) 81 | 82 | server.on('request', req => { 83 | if (req.code === '0.30' && req.url === '/cit/s' && statusUpdateHandler) { 84 | statusUpdateHandler.call(server, new CoapMessage(req)) 85 | } 86 | }) 87 | 88 | return new Promise((resolve, reject) => { 89 | server.listen(err => { 90 | if (err) { 91 | reject(err, server) 92 | } else { 93 | resolve(server) 94 | } 95 | }) 96 | }) 97 | } 98 | 99 | module.exports = { 100 | getDescription, 101 | getStatus, 102 | listenForStatusUpdates, 103 | requestStatusUpdates, 104 | } 105 | -------------------------------------------------------------------------------- /lib/coap/message.js: -------------------------------------------------------------------------------- 1 | const CoapOptions = require('./options') 2 | 3 | class CoapMessage { 4 | constructor(msg) { 5 | this.msg = msg 6 | this.host = msg.rsinfo.address 7 | 8 | const headers = msg.headers 9 | 10 | if (headers[CoapOptions.GLOBAL_DEVID]) { 11 | const parts = headers[CoapOptions.GLOBAL_DEVID].split('#') 12 | this.deviceType = parts[0] 13 | this.deviceId = parts[1] 14 | this.protocolRevision = parts[2] 15 | } 16 | 17 | if (headers[CoapOptions.STATUS_VALIDITY]) { 18 | const validity = headers[CoapOptions.STATUS_VALIDITY] 19 | if ((validity & 0x1) === 0) { 20 | this.validFor = Math.floor(validity / 10) 21 | } else { 22 | this.validFor = validity * 4 23 | } 24 | } 25 | 26 | if (headers[CoapOptions.STATUS_SERIAL]) { 27 | this.serial = headers[CoapOptions.STATUS_SERIAL] 28 | } 29 | 30 | try { 31 | this.payload = JSON.parse(msg.payload.toString()) 32 | } catch (e) { 33 | this.payload = msg.payload.toString() 34 | } 35 | } 36 | } 37 | 38 | module.exports = CoapMessage 39 | -------------------------------------------------------------------------------- /lib/coap/options.js: -------------------------------------------------------------------------------- 1 | const coap = require('coap') 2 | 3 | const CoapOptions = { 4 | GLOBAL_DEVID: '3332', 5 | STATUS_VALIDITY: '3412', 6 | STATUS_SERIAL: '3420', 7 | 8 | register() { 9 | coap.registerOption( 10 | this.GLOBAL_DEVID, 11 | str => Buffer.from(str), 12 | buf => buf.toString() 13 | ) 14 | 15 | coap.registerOption( 16 | this.STATUS_VALIDITY, 17 | str => Buffer.alloc(2).writeUInt16BE(parseInt(str), 0), 18 | buf => buf.readUInt16BE(0) 19 | ) 20 | 21 | coap.registerOption( 22 | this.STATUS_SERIAL, 23 | str => Buffer.alloc(2).writeUInt16BE(parseInt(str), 0), 24 | buf => buf.readUInt16BE(0) 25 | ) 26 | }, 27 | } 28 | 29 | module.exports = CoapOptions 30 | -------------------------------------------------------------------------------- /lib/devices/base.js: -------------------------------------------------------------------------------- 1 | const defaults = require('superagent-defaults') 2 | const EventEmitter = require('eventemitter3') 3 | 4 | const request = require('../http-request') 5 | 6 | class Device extends EventEmitter { 7 | constructor(id, host) { 8 | super() 9 | 10 | this.id = id 11 | this.lastSeen = null 12 | this._online = false 13 | this._ttl = 0 14 | this._ttlTimer = null 15 | this._name = null 16 | this._props = new Map() 17 | this._request = null 18 | 19 | this._defineProperty('host', null, host) 20 | this._defineProperty( 21 | 'settings', 22 | null, 23 | null, 24 | this._settingsValidator.bind(this) 25 | ) 26 | } 27 | 28 | get type() { 29 | return this.constructor.deviceType 30 | } 31 | 32 | get modelName() { 33 | return this.constructor.deviceName 34 | } 35 | 36 | get online() { 37 | return this._online 38 | } 39 | 40 | set online(newValue) { 41 | if (!!newValue !== this._online) { 42 | this._online = !!newValue 43 | this.emit(this._online ? 'online' : 'offline', this) 44 | } 45 | } 46 | 47 | get ttl() { 48 | return this._ttl 49 | } 50 | 51 | set ttl(newValue) { 52 | if (this._ttlTimer !== null) { 53 | clearTimeout(this._ttlTimer) 54 | this._ttlTimer = null 55 | } 56 | 57 | this._ttl = newValue 58 | 59 | if (this._ttl > 0) { 60 | this._ttlTimer = setTimeout( 61 | () => { this.online = false }, 62 | this._ttl 63 | ) 64 | } 65 | } 66 | 67 | get name() { 68 | if (this._name) { 69 | return this._name 70 | } else if (this.settings) { 71 | return this.settings.name 72 | } 73 | return undefined 74 | } 75 | 76 | set name(value) { 77 | this._name = value 78 | } 79 | 80 | get request() { 81 | return this._request || request 82 | } 83 | 84 | * [Symbol.iterator]() { 85 | if (this._props.has('*')) { 86 | // adding the props to a new Set here to filter out duplicates 87 | for (const prop of new Set(this._props.get('*').values())) { 88 | yield [prop, this[prop]] 89 | } 90 | } 91 | 92 | if (this.mode && this._props.has(this.mode)) { 93 | // adding the props to a new Set here to filter out duplicates 94 | for (const prop of new Set(this._props.get(this.mode).values())) { 95 | yield [prop, this[prop]] 96 | } 97 | } 98 | } 99 | 100 | _defineProperty(name, ids = null, defaultValue = null, validator = null, 101 | mode = '*') { 102 | const key = `_${name}` 103 | 104 | Object.defineProperty(this, key, { 105 | value: defaultValue, 106 | writable: true, 107 | }) 108 | 109 | Object.defineProperty(this, name, { 110 | get() { return this[key] }, 111 | set(newValue) { 112 | const nv = validator ? validator(newValue) : newValue 113 | if (this[key] !== nv) { 114 | const oldValue = this[key] 115 | this[key] = nv 116 | this.emit('change', name, nv, oldValue, this) 117 | this.emit(`change:${name}`, nv, oldValue, this) 118 | } 119 | }, 120 | enumerable: true, 121 | }) 122 | 123 | if (ids !== null) { 124 | if (!Array.isArray(ids)) { 125 | ids = [ids] 126 | } 127 | 128 | for (const id of ids) { 129 | // make _props a two dimensional map of modes and properties 130 | let p = this._props.get(mode) 131 | if (!p) { 132 | p = new Map() 133 | this._props.set(mode, p) 134 | } 135 | p.set(id, name) 136 | } 137 | } 138 | } 139 | 140 | _getPropertyName(id, mode = '*') { 141 | const p = this._props 142 | if (mode !== '*' && p.has(mode) && p.get(mode).has(id)) { 143 | return p.get(mode).get(id) 144 | } else if (p.has('*')) { 145 | return p.get('*').get(id) 146 | } 147 | return undefined 148 | } 149 | 150 | _settingsValidator(settings) { 151 | // subclasses can override this 152 | return settings 153 | } 154 | 155 | update(msg) { 156 | if (msg.validFor) { 157 | this.ttl = msg.validFor * 1000 158 | } 159 | 160 | const updates = (msg.payload && msg.payload.G) || [] 161 | if (updates && !Array.isArray(updates)) { 162 | throw new Error( 163 | `Malformed status payload: ${JSON.stringify(msg.payload)}` 164 | ) 165 | } 166 | 167 | this._applyUpdate(msg, updates) 168 | 169 | this.online = true 170 | this.lastSeen = new Date() 171 | } 172 | 173 | _applyUpdate(msg, updates) { 174 | if (msg.host) { 175 | this.host = msg.host 176 | } 177 | 178 | for (const tuple of updates) { 179 | const prop = this._getPropertyName(tuple[1], this.mode) 180 | if (prop) { 181 | this[prop] = tuple[2] 182 | } 183 | } 184 | } 185 | 186 | setAuthCredentials(username, password) { 187 | if (this._request === null) { 188 | this._request = defaults(request) 189 | } 190 | this._request.auth(username, password) 191 | } 192 | 193 | async getSettings() { 194 | const res = await this.request.get(`${this.host}/settings`) 195 | return res.body 196 | } 197 | 198 | async getStatus() { 199 | const res = await this.request.get(`${this.host}/status`) 200 | return res.body 201 | } 202 | 203 | async reboot() { 204 | await this.request.get(`${this.host}/reboot`) 205 | } 206 | } 207 | 208 | class Switch extends Device { 209 | async setRelay(index, value) { 210 | await this.request 211 | .get(`${this.host}/relay/${index}`) 212 | .query({ turn: value ? 'on' : 'off' }) 213 | } 214 | } 215 | 216 | module.exports = { 217 | Device, 218 | Switch, 219 | } 220 | -------------------------------------------------------------------------------- /lib/devices/index.js: -------------------------------------------------------------------------------- 1 | const UnknownDevice = require('./unknown') 2 | 3 | // require all device classes 4 | const deviceClasses = require('require-all')({ 5 | dirname: __dirname, 6 | filter: fileName => { 7 | if (fileName === 'base.js' || fileName === 'index.js' || 8 | fileName === 'unknown.js') { 9 | return false 10 | } 11 | return fileName 12 | }, 13 | recursive: false, 14 | }) 15 | 16 | // construct a map of CoAP type identifiers to device classes 17 | const deviceTypeToClass = new Map() 18 | 19 | for (const DeviceClass of Object.values(deviceClasses)) { 20 | deviceTypeToClass.set( 21 | DeviceClass.deviceType, 22 | DeviceClass 23 | ) 24 | } 25 | 26 | module.exports = { 27 | /** 28 | * Creates a new device of the given type. 29 | * 30 | * @param {string} type - CoAP device type identifier. 31 | * @param {string} id - The device ID. 32 | * @param {string} host - The hostname of the device. 33 | * @param {string} mode - The intial device mode. 34 | */ 35 | create: (type, id, host, mode = undefined) => { 36 | const DeviceClass = deviceTypeToClass.get(type) 37 | if (DeviceClass) { 38 | return new DeviceClass(id, host, mode) 39 | } else { 40 | return new UnknownDevice(id, host, type) 41 | } 42 | }, 43 | 44 | /** 45 | * Returns true if the given device is of an unknown type; false otherwise. 46 | */ 47 | isUnknown: device => { 48 | return device instanceof UnknownDevice 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /lib/devices/shelly-1.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class Shelly1 extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [112, 1101], false, Boolean) 8 | 9 | this._defineProperty('input0', [118, 2101], 0, Number) 10 | this._defineProperty('inputEvent0', [2102], '', String) 11 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 12 | 13 | this._defineProperty('externalTemperature0', [119, 3101], null, Number) 14 | this._defineProperty('externalTemperature1', [3201], null, Number) 15 | this._defineProperty('externalTemperature2', [3301], null, Number) 16 | 17 | this._defineProperty('externalHumidity', [3103], null, Number) 18 | 19 | this._defineProperty('externalInput0', [3117], null, Number) 20 | } 21 | } 22 | 23 | Shelly1.deviceType = 'SHSW-1' 24 | Shelly1.deviceName = 'Shelly 1' 25 | 26 | module.exports = Shelly1 27 | -------------------------------------------------------------------------------- /lib/devices/shelly-1l.js: -------------------------------------------------------------------------------- 1 | const Shelly1 = require('./shelly-1') 2 | 3 | class Shelly1L extends Shelly1 { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('input1', [2201], 0, Number) 8 | this._defineProperty('inputEvent1', [2202], '', String) 9 | this._defineProperty('inputEventCounter1', [2203], 0, Number) 10 | 11 | this._defineProperty('power0', [4101], 0, Number) 12 | this._defineProperty('energyCounter0', [4103], 0, Number) 13 | 14 | this._defineProperty('deviceTemperature', [3104], 0, Number) 15 | this._defineProperty('overTemperature', [6101], false, Boolean) 16 | } 17 | } 18 | 19 | Shelly1L.deviceType = 'SHSW-L' 20 | Shelly1L.deviceName = 'Shelly 1L' 21 | 22 | module.exports = Shelly1L 23 | -------------------------------------------------------------------------------- /lib/devices/shelly-1pm.js: -------------------------------------------------------------------------------- 1 | const Shelly1 = require('./shelly-1') 2 | 3 | class Shelly1PM extends Shelly1 { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('power0', [111, 4101], 0, Number) 8 | this._defineProperty('energyCounter0', [4103], 0, Number) 9 | this._defineProperty('overPower', [6102], 0, Number) 10 | this._defineProperty('overPowerValue', [6109], 0, Number) 11 | 12 | this._defineProperty('deviceTemperature', [113, 3104], 0, Number) 13 | this._defineProperty('overTemperature', [115, 6101], 0, Number) 14 | } 15 | } 16 | 17 | Shelly1PM.deviceType = 'SHSW-PM' 18 | Shelly1PM.deviceName = 'Shelly 1PM' 19 | 20 | module.exports = Shelly1PM 21 | -------------------------------------------------------------------------------- /lib/devices/shelly-2.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring') 2 | 3 | const { Switch } = require('./base') 4 | 5 | class Shelly2 extends Switch { 6 | constructor(id, host, mode = 'relay') { 7 | super(id, host) 8 | 9 | this._defineProperty('relay0', [112, 1101], false, Boolean) 10 | this._defineProperty('relay1', [122, 1201], false, Boolean) 11 | 12 | this._defineProperty('input0', [118, 2101], 0, Number) 13 | this._defineProperty('inputEvent0', [2102], '', String) 14 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 15 | this._defineProperty('input1', [128, 2201], 0, Number) 16 | this._defineProperty('inputEvent1', [2202], '', String) 17 | this._defineProperty('inputEventCounter1', [2203], 0, Number) 18 | 19 | this._defineProperty('power0', [111, 4101, 4102], 0, Number) 20 | this._defineProperty('energyCounter0', [4103, 4104], 0, Number) 21 | 22 | this._defineProperty('mode', [9101], mode, String) 23 | 24 | this._defineProperty('overPower0', [6102], 0, Number, 'relay') 25 | this._defineProperty('overPower1', [6202], 0, Number, 'relay') 26 | this._defineProperty('overPowerValue', [6109], 0, Number, 'relay') 27 | 28 | this._defineProperty('rollerState', [1102], 'stop', String, 'roller') 29 | this._defineProperty('rollerPosition', [113, 1103], 0, Number, 'roller') 30 | this._defineProperty('rollerStopReason', [6103], '', String, 'roller') 31 | } 32 | 33 | _updateRollerState(mode) { 34 | if (mode === 'roller') { 35 | const swap = this.settings && this.settings.rollers && 36 | this.settings.rollers.length > 0 37 | ? !!this.settings.rollers[0].swap 38 | : false 39 | 40 | let state = 'stop' 41 | 42 | if (this.relay0) { 43 | state = swap ? 'close' : 'open' 44 | } else if (this.relay1) { 45 | state = swap ? 'open' : 'close' 46 | } 47 | 48 | this.rollerState = state 49 | } 50 | } 51 | 52 | _applyUpdate(msg, updates) { 53 | if (msg.protocolRevision === '1') { 54 | // if property 113 is part of the updates, we expect the device to be in 55 | // "roller" mode 56 | const r = updates.reduce((a, t) => { 57 | return a + (t[1] === 113 ? 1 : 0) 58 | }, 0) 59 | this.mode = r >= 1 ? 'roller' : 'relay' 60 | } 61 | 62 | super._applyUpdate(msg, updates) 63 | 64 | if (msg.protocolRevision === '1') { 65 | this._updateRollerState(this.mode) 66 | } 67 | } 68 | 69 | async setRollerState(state, duration = null) { 70 | const params = { go: state } 71 | if (duration > 0) { 72 | params.duration = duration 73 | } 74 | 75 | const qs = querystring.stringify(params) 76 | const res = await this.request.get(`${this.host}/roller/0?${qs}`) 77 | return res.body 78 | } 79 | 80 | async setRollerPosition(position) { 81 | const qs = querystring.stringify({ 82 | go: 'to_pos', 83 | roller_pos: position, 84 | }) 85 | const res = await this.request.get(`${this.host}/roller/0?${qs}`) 86 | return res.body 87 | } 88 | } 89 | 90 | Shelly2.deviceType = 'SHSW-21' 91 | Shelly2.deviceName = 'Shelly 2' 92 | 93 | module.exports = Shelly2 94 | -------------------------------------------------------------------------------- /lib/devices/shelly-25.js: -------------------------------------------------------------------------------- 1 | const Shelly2 = require('./shelly-2') 2 | 3 | class Shelly25 extends Shelly2 { 4 | constructor(id, host, mode = 'relay') { 5 | super(id, host, mode) 6 | 7 | this._defineProperty('power1', [121, 4201], 0, Number, 'relay') 8 | this._defineProperty('energyCounter1', [4203], 0, Number, 'relay') 9 | 10 | this._defineProperty('deviceTemperature', [115, 3104], 0, Number) 11 | this._defineProperty('overTemperature', [117, 6101], 0, Number) 12 | } 13 | } 14 | 15 | Shelly25.deviceType = 'SHSW-25' 16 | Shelly25.deviceName = 'Shelly 2.5' 17 | 18 | module.exports = Shelly25 19 | -------------------------------------------------------------------------------- /lib/devices/shelly-2led.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class Shelly2LED extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch0', [1101], false, Boolean) 8 | this._defineProperty('brightness0', [5101], 0, Number) 9 | 10 | this._defineProperty('switch1', [1201], false, Boolean) 11 | this._defineProperty('brightness1', [5201], 0, Number) 12 | } 13 | 14 | async setWhite(index, brightness, on) { 15 | await this.request 16 | .get(`${this.host}/light/${index}`) 17 | .query({ 18 | turn: on ? 'on' : 'off', 19 | brightness, 20 | }) 21 | } 22 | } 23 | 24 | Shelly2LED.deviceType = 'SH2LED-1' 25 | Shelly2LED.deviceName = 'Shelly 2LED' 26 | 27 | module.exports = Shelly2LED 28 | -------------------------------------------------------------------------------- /lib/devices/shelly-3em.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class Shelly3EM extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [112, 1101], false, Boolean) 8 | 9 | this._defineProperty('power0', [111, 4105], 0, Number) 10 | this._defineProperty('energyCounter0', [4106], 0, Number) 11 | this._defineProperty('energyReturned0', [4107], 0, Number) 12 | this._defineProperty('powerFactor0', [114, 4110], 0, Number) 13 | this._defineProperty('current0', [115, 4109], 0, Number) 14 | this._defineProperty('voltage0', [116, 4108], 0, Number) 15 | 16 | this._defineProperty('power1', [121, 4205], 0, Number) 17 | this._defineProperty('energyCounter1', [4206], 0, Number) 18 | this._defineProperty('energyReturned1', [4207], 0, Number) 19 | this._defineProperty('powerFactor1', [124, 4210], 0, Number) 20 | this._defineProperty('current1', [125, 4209], 0, Number) 21 | this._defineProperty('voltage1', [126, 4208], 0, Number) 22 | 23 | this._defineProperty('power2', [131, 4305], 0, Number) 24 | this._defineProperty('energyCounter2', [4306], 0, Number) 25 | this._defineProperty('energyReturned2', [4307], 0, Number) 26 | this._defineProperty('powerFactor2', [134, 4310], 0, Number) 27 | this._defineProperty('current2', [135, 4309], 0, Number) 28 | this._defineProperty('voltage2', [136, 4308], 0, Number) 29 | 30 | this._defineProperty('overPower', [6102], 0, Number) 31 | } 32 | } 33 | 34 | Shelly3EM.deviceType = 'SHEM-3' 35 | Shelly3EM.deviceName = 'Shelly 3EM' 36 | 37 | module.exports = Shelly3EM 38 | -------------------------------------------------------------------------------- /lib/devices/shelly-4pro.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class Shelly4Pro extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('power0', [111], 0, Number) 8 | this._defineProperty('relay0', [112], false, Boolean) 9 | this._defineProperty('power1', [121], 0, Number) 10 | this._defineProperty('relay1', [122], false, Boolean) 11 | this._defineProperty('power2', [131], 0, Number) 12 | this._defineProperty('relay2', [132], false, Boolean) 13 | this._defineProperty('power3', [141], 0, Number) 14 | this._defineProperty('relay3', [142], false, Boolean) 15 | } 16 | } 17 | 18 | Shelly4Pro.deviceType = 'SHSW-44' 19 | Shelly4Pro.deviceName = 'Shelly 4Pro' 20 | 21 | module.exports = Shelly4Pro 22 | -------------------------------------------------------------------------------- /lib/devices/shelly-air.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class ShellyAir extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [112, 1101], false, Boolean) 8 | 9 | this._defineProperty('totalWorkTime', [121, 1104], 0, Number) 10 | 11 | this._defineProperty('input0', [118, 2101], 0, Number) 12 | this._defineProperty('inputEvent0', [2102], '', String) 13 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 14 | 15 | this._defineProperty('power0', [111, 4101], 0, Number) 16 | this._defineProperty('energyCounter0', [211, 4103], 0, Number) 17 | this._defineProperty('overPower', [6102], 0, Number) 18 | this._defineProperty('overPowerValue', [6109], 0, Number) 19 | 20 | this._defineProperty('deviceTemperature', [113, 3104], 0, Number) 21 | this._defineProperty('overTemperature', [115, 6101], 0, Number) 22 | 23 | this._defineProperty('externalTemperature0', [119, 3101], null, Number) 24 | this._defineProperty('externalTemperature1', [3201], null, Number) 25 | this._defineProperty('externalTemperature2', [3301], null, Number) 26 | this._defineProperty('externalHumidity', [3103], null, Number) 27 | } 28 | } 29 | 30 | ShellyAir.deviceType = 'SHAIR-1' 31 | ShellyAir.deviceName = 'Shelly Air' 32 | 33 | module.exports = ShellyAir 34 | -------------------------------------------------------------------------------- /lib/devices/shelly-bulb-rgbw.js: -------------------------------------------------------------------------------- 1 | const ShellyBulb = require('./shelly-bulb') 2 | 3 | class ShellyBulbRGBW extends ShellyBulb {} 4 | 5 | ShellyBulbRGBW.deviceType = 'SHCB-1' 6 | ShellyBulbRGBW.deviceName = 'Shelly Bulb RGBW' 7 | 8 | module.exports = ShellyBulbRGBW 9 | -------------------------------------------------------------------------------- /lib/devices/shelly-bulb.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyBulb extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [181, 1101], false, Boolean) 8 | 9 | this._defineProperty('red', [111, 5105], 0, Number) 10 | this._defineProperty('green', [121, 5106], 0, Number) 11 | this._defineProperty('blue', [131, 5107], 0, Number) 12 | this._defineProperty('white', [141, 5108], 0, Number) 13 | 14 | this._defineProperty('gain', [151, 5102], 0, Number) 15 | this._defineProperty('brightness', [171, 5101], 0, Number) 16 | 17 | this._defineProperty('colorTemperature', [161, 5103], 0, Number) 18 | 19 | this._defineProperty('mode', [9101], 'color', String) 20 | } 21 | 22 | async setColor(opts) { 23 | const query = Object.assign({ mode: 'color' }, opts) 24 | if (Object.prototype.hasOwnProperty.call(query, 'switch')) { 25 | query.turn = query.switch ? 'on' : 'off' 26 | delete query.switch 27 | } 28 | 29 | await this.request 30 | .get(`${this.host}/light/0`) 31 | .query(query) 32 | } 33 | 34 | async setWhite(temperature, brightness, on) { 35 | await this.request 36 | .get(`${this.host}/light/0`) 37 | .query({ 38 | mode: 'white', 39 | turn: on ? 'on' : 'off', 40 | temp: temperature, 41 | brightness, 42 | }) 43 | } 44 | } 45 | 46 | ShellyBulb.deviceType = 'SHBLB-1' 47 | ShellyBulb.deviceName = 'Shelly Bulb' 48 | 49 | module.exports = ShellyBulb 50 | -------------------------------------------------------------------------------- /lib/devices/shelly-button1-v2.js: -------------------------------------------------------------------------------- 1 | const ShellyButton1 = require('./shelly-button1') 2 | 3 | class ShellyButton1V2 extends ShellyButton1 {} 4 | 5 | ShellyButton1V2.deviceType = 'SHBTN-2' 6 | ShellyButton1V2.deviceName = 'Shelly Button1' 7 | 8 | module.exports = ShellyButton1V2 9 | -------------------------------------------------------------------------------- /lib/devices/shelly-button1.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyButton1 extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('input0', [118], 0, Number) 8 | this._defineProperty('inputEvent0', [119, 2102], '', String) 9 | this._defineProperty('inputEventCounter0', [120, 2103], 0, Number) 10 | 11 | this._defineProperty('sensorError', [3115], false, Boolean) 12 | 13 | this._defineProperty('charging', [3112], 0, Number) 14 | this._defineProperty('battery', [77, 3111], 0, Number) 15 | 16 | this._defineProperty('wakeUpEvent', [9102], 'unknown', String) 17 | } 18 | } 19 | 20 | ShellyButton1.deviceType = 'SHBTN-1' 21 | ShellyButton1.deviceName = 'Shelly Button1' 22 | 23 | module.exports = ShellyButton1 24 | -------------------------------------------------------------------------------- /lib/devices/shelly-color.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyColor extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [181, 1101], false, Boolean) 8 | 9 | this._defineProperty('red', [111, 5105], 0, Number) 10 | this._defineProperty('green', [121, 5106], 0, Number) 11 | this._defineProperty('blue', [131, 5107], 0, Number) 12 | this._defineProperty('white', [141, 5108], 0, Number) 13 | 14 | this._defineProperty('gain', [151, 5102], 0, Number) 15 | this._defineProperty('brightness', [171, 5101], 0, Number) 16 | 17 | this._defineProperty('colorTemperature', [161, 5103], 0, Number) 18 | 19 | this._defineProperty('mode', [9101], 'color', String) 20 | } 21 | 22 | async setColor(opts) { 23 | const query = Object.assign({ mode: 'color' }, opts) 24 | if (Object.prototype.hasOwnProperty.call(query, 'switch')) { 25 | query.turn = query.switch ? 'on' : 'off' 26 | delete query.switch 27 | } 28 | 29 | await this.request 30 | .get(`${this.host}/light/0`) 31 | .query(query) 32 | } 33 | 34 | async setWhite(temperature, brightness, on) { 35 | await this.request 36 | .get(`${this.host}/light/0`) 37 | .query({ 38 | mode: 'white', 39 | turn: on ? 'on' : 'off', 40 | temp: temperature, 41 | brightness, 42 | }) 43 | } 44 | } 45 | 46 | ShellyColor.deviceType = 'SHCL-255' 47 | ShellyColor.deviceName = 'Shelly Color' 48 | 49 | module.exports = ShellyColor 50 | -------------------------------------------------------------------------------- /lib/devices/shelly-dimmer-w1.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyDimmerW1 extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [121, 1101], false, Boolean) 8 | 9 | this._defineProperty('brightness', [111, 5101], 0, Number) 10 | 11 | this._defineProperty('input0', [131, 2101], 0, Number) 12 | this._defineProperty('inputEvent0', [2102], '', String) 13 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 14 | 15 | this._defineProperty('loadError', [6104], false, Boolean) 16 | 17 | this._defineProperty('deviceTemperature', [3104], 0, Number) 18 | this._defineProperty('overTemperature', [6101], 0, Number) 19 | 20 | this._defineProperty('mode', [9101], 'white', String) 21 | } 22 | 23 | async setWhite(brightness, on) { 24 | await this.request 25 | .get(`${this.host}/light/0`) 26 | .query({ 27 | turn: on ? 'on' : 'off', 28 | brightness, 29 | }) 30 | } 31 | } 32 | 33 | ShellyDimmerW1.deviceType = 'SHDIMW-1' 34 | ShellyDimmerW1.deviceName = 'Shelly Dimmer W1' 35 | 36 | module.exports = ShellyDimmerW1 37 | -------------------------------------------------------------------------------- /lib/devices/shelly-dimmer.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyDimmer extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [121, 1101], false, Boolean) 8 | 9 | this._defineProperty('brightness', [111, 5101], 0, Number) 10 | 11 | this._defineProperty('input0', [131, 2101], 0, Number) 12 | this._defineProperty('inputEvent0', [2102], '', String) 13 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 14 | this._defineProperty('input1', [141, 2201], 0, Number) 15 | this._defineProperty('inputEvent1', [2202], '', String) 16 | this._defineProperty('inputEventCounter1', [2203], 0, Number) 17 | 18 | this._defineProperty('power0', [4101], 0, Number) 19 | this._defineProperty('energyCounter0', [4103], 0, Number) 20 | this._defineProperty('overPower', [6102], 0, Number) 21 | 22 | this._defineProperty('loadError', [6104], false, Boolean) 23 | 24 | this._defineProperty('deviceTemperature', [3104], 0, Number) 25 | this._defineProperty('overTemperature', [6101], 0, Number) 26 | 27 | this._defineProperty('mode', [9101], 'white', String) 28 | } 29 | 30 | async setWhite(brightness, on) { 31 | await this.request 32 | .get(`${this.host}/light/0`) 33 | .query({ 34 | turn: on ? 'on' : 'off', 35 | brightness, 36 | }) 37 | } 38 | } 39 | 40 | ShellyDimmer.deviceType = 'SHDM-1' 41 | ShellyDimmer.deviceName = 'Shelly Dimmer' 42 | 43 | module.exports = ShellyDimmer 44 | -------------------------------------------------------------------------------- /lib/devices/shelly-dimmer2.js: -------------------------------------------------------------------------------- 1 | const ShellyDimmer = require('./shelly-dimmer') 2 | 3 | class ShellyDimmer2 extends ShellyDimmer {} 4 | 5 | ShellyDimmer2.deviceType = 'SHDM-2' 6 | ShellyDimmer2.deviceName = 'Shelly Dimmer 2' 7 | 8 | module.exports = ShellyDimmer2 9 | -------------------------------------------------------------------------------- /lib/devices/shelly-door-window.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyDoorWindow extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('state', [55, 3108], 0, Number) 8 | this._defineProperty('tilt', [3109], 0, Number) 9 | this._defineProperty('vibration', [6110], 0, Number) 10 | this._defineProperty('illuminance', [66, 3106], 0, Number) 11 | this._defineProperty('illuminanceLevel', [3110], 'unknown', String) 12 | 13 | this._defineProperty('sensorError', [3115], false, Boolean) 14 | 15 | this._defineProperty('battery', [77, 3111], 0, Number) 16 | 17 | this._defineProperty('wakeUpEvent', [9102], 'unknown', String) 18 | } 19 | } 20 | 21 | ShellyDoorWindow.deviceType = 'SHDW-1' 22 | ShellyDoorWindow.deviceName = 'Shelly Door/Window' 23 | 24 | module.exports = ShellyDoorWindow 25 | -------------------------------------------------------------------------------- /lib/devices/shelly-door-window2.js: -------------------------------------------------------------------------------- 1 | const ShellyDoorWindow = require('./shelly-door-window') 2 | 3 | class ShellyDoorWindow2 extends ShellyDoorWindow { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [3101], 0, Number) 8 | } 9 | } 10 | 11 | ShellyDoorWindow2.deviceType = 'SHDW-2' 12 | ShellyDoorWindow2.deviceName = 'Shelly Door/Window 2' 13 | 14 | module.exports = ShellyDoorWindow2 15 | -------------------------------------------------------------------------------- /lib/devices/shelly-duo.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyDuo extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [121, 1101], false, Boolean) 8 | 9 | this._defineProperty('brightness', [111, 5101], 0, Number) 10 | this._defineProperty('colorTemperature', [131, 5103], 0, Number) 11 | this._defineProperty('whiteLevel', [5104], 0, Number) 12 | 13 | this._defineProperty('power0', [141, 4101], 0, Number) 14 | this._defineProperty('energyCounter0', [211, 4103], 0, Number) 15 | } 16 | 17 | async setWhite(temperature, brightness, on) { 18 | const white = (temperature - 2700) / (6500 - 2700) * 100 19 | 20 | await this.request 21 | .get(`${this.host}/light/0`) 22 | .query({ 23 | turn: on ? 'on' : 'off', 24 | temp: temperature, 25 | white: Math.max(Math.min(Math.round(white), 100), 0), 26 | brightness, 27 | }) 28 | } 29 | } 30 | 31 | ShellyDuo.deviceType = 'SHBDUO-1' 32 | ShellyDuo.deviceName = 'Shelly Duo' 33 | 34 | module.exports = ShellyDuo 35 | -------------------------------------------------------------------------------- /lib/devices/shelly-em.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class ShellyEM extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [112, 1101], false, Boolean) 8 | 9 | this._defineProperty('power0', [111, 4105], 0, Number) 10 | this._defineProperty('energyCounter0', [4106], 0, Number) 11 | this._defineProperty('energyReturned0', [4107], 0, Number) 12 | this._defineProperty('voltage0', [116, 4108], 0, Number) 13 | 14 | this._defineProperty('power1', [121, 4205], 0, Number) 15 | this._defineProperty('energyCounter1', [4206], 0, Number) 16 | this._defineProperty('energyReturned1', [4207], 0, Number) 17 | this._defineProperty('voltage1', [126, 4208], 0, Number) 18 | 19 | this._defineProperty('overPower', [6102], 0, Number) 20 | } 21 | } 22 | 23 | ShellyEM.deviceType = 'SHEM' 24 | ShellyEM.deviceName = 'Shelly EM' 25 | 26 | module.exports = ShellyEM 27 | -------------------------------------------------------------------------------- /lib/devices/shelly-flood.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyFlood extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [33, 3101], 0, Number) 8 | this._defineProperty('flood', [23, 6106], 0, Number) 9 | 10 | this._defineProperty('sensorError', [3115], false, Boolean) 11 | 12 | this._defineProperty('battery', [3111], 0, Number) 13 | 14 | this._defineProperty('wakeUpEvent', [9102], 'unknown', String) 15 | } 16 | } 17 | 18 | ShellyFlood.deviceType = 'SHWT-1' 19 | ShellyFlood.deviceName = 'Shelly Flood' 20 | 21 | module.exports = ShellyFlood 22 | -------------------------------------------------------------------------------- /lib/devices/shelly-gas.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyGas extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('sensorOperation', [118, 3113], 'unknown', String) 8 | this._defineProperty('selfTest', [120, 3114], '', String) 9 | this._defineProperty('gas', [119, 6108], 'unknown', String) 10 | this._defineProperty('concentration', [122, 3107], 0, Number) 11 | this._defineProperty('valve', [1105], 'unknown', String) 12 | } 13 | } 14 | 15 | ShellyGas.deviceType = 'SHGS-1' 16 | ShellyGas.deviceName = 'Shelly Gas' 17 | 18 | module.exports = ShellyGas 19 | -------------------------------------------------------------------------------- /lib/devices/shelly-hd.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class ShellyHD extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('power0', [111], 0, Number) 8 | this._defineProperty('relay0', [112], false, Boolean) 9 | this._defineProperty('power1', [121], 0, Number) 10 | this._defineProperty('relay1', [122], false, Boolean) 11 | } 12 | } 13 | 14 | ShellyHD.deviceType = 'SHSW-22' 15 | ShellyHD.deviceName = 'Shelly HD' 16 | 17 | module.exports = ShellyHD 18 | -------------------------------------------------------------------------------- /lib/devices/shelly-ht.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyHT extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [33, 3101], 0, Number) 8 | this._defineProperty('humidity', [44, 3103], 0, Number) 9 | 10 | this._defineProperty('sensorError', [3115], false, Boolean) 11 | 12 | this._defineProperty('battery', [77, 3111], 0, Number) 13 | 14 | this._defineProperty('wakeUpEvent', [9102], 'unknown', String) 15 | } 16 | } 17 | 18 | ShellyHT.deviceType = 'SHHT-1' 19 | ShellyHT.deviceName = 'Shelly H&T' 20 | 21 | module.exports = ShellyHT 22 | -------------------------------------------------------------------------------- /lib/devices/shelly-i3.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyI3 extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('input0', [118, 2101], 0, Number) 8 | this._defineProperty('inputEvent0', [119, 2102], '', String) 9 | this._defineProperty('inputEventCounter0', [120, 2103], 0, Number) 10 | 11 | this._defineProperty('input1', [128, 2201], 0, Number) 12 | this._defineProperty('inputEvent1', [129, 2202], '', String) 13 | this._defineProperty('inputEventCounter1', [130, 2203], 0, Number) 14 | 15 | this._defineProperty('input2', [138, 2301], 0, Number) 16 | this._defineProperty('inputEvent2', [139, 2302], '', String) 17 | this._defineProperty('inputEventCounter2', [140, 2303], 0, Number) 18 | } 19 | } 20 | 21 | ShellyI3.deviceType = 'SHIX3-1' 22 | ShellyI3.deviceName = 'Shelly i3' 23 | 24 | module.exports = ShellyI3 25 | -------------------------------------------------------------------------------- /lib/devices/shelly-motion.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyMotion extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('motion', [6107], 0, Number) 8 | this._defineProperty('vibration', [6110], 0, Number) 9 | this._defineProperty('illuminance', [3106], 0, Number) 10 | this._defineProperty('battery', [3111], 0, Number) 11 | } 12 | } 13 | 14 | ShellyMotion.deviceType = 'SHMOS-01' 15 | ShellyMotion.deviceName = 'Shelly Motion' 16 | 17 | module.exports = ShellyMotion 18 | -------------------------------------------------------------------------------- /lib/devices/shelly-motion2.js: -------------------------------------------------------------------------------- 1 | const ShellyMotion = require('./shelly-motion') 2 | 3 | class ShellyMotion2 extends ShellyMotion { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [33, 3101], 0, Number) 8 | } 9 | } 10 | 11 | ShellyMotion2.deviceType = 'SHMOS-02' 12 | ShellyMotion2.deviceName = 'Shelly Motion 2' 13 | 14 | module.exports = ShellyMotion2 15 | -------------------------------------------------------------------------------- /lib/devices/shelly-plug-e.js: -------------------------------------------------------------------------------- 1 | const ShellyPlug = require('./shelly-plug') 2 | 3 | class ShellyPlugE extends ShellyPlug {} 4 | 5 | ShellyPlugE.deviceType = 'SHPLG2-1' 6 | ShellyPlugE.deviceName = 'Shelly Plug' 7 | 8 | module.exports = ShellyPlugE 9 | -------------------------------------------------------------------------------- /lib/devices/shelly-plug-s.js: -------------------------------------------------------------------------------- 1 | const ShellyPlug = require('./shelly-plug') 2 | 3 | class ShellyPlugS extends ShellyPlug { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('deviceTemperature', [113, 3104], 0, Number) 8 | this._defineProperty('overTemperature', [115, 6101], 0, Number) 9 | } 10 | } 11 | 12 | ShellyPlugS.deviceType = 'SHPLG-S' 13 | ShellyPlugS.deviceName = 'Shelly Plug S' 14 | 15 | module.exports = ShellyPlugS 16 | -------------------------------------------------------------------------------- /lib/devices/shelly-plug-us.js: -------------------------------------------------------------------------------- 1 | const ShellyPlug = require('./shelly-plug') 2 | 3 | class ShellyPlugUS extends ShellyPlug {} 4 | 5 | ShellyPlugUS.deviceType = 'SHPLG-U1' 6 | ShellyPlugUS.deviceName = 'Shelly Plug US' 7 | 8 | module.exports = ShellyPlugUS 9 | -------------------------------------------------------------------------------- /lib/devices/shelly-plug.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class ShellyPlug extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [112, 1101], false, Boolean) 8 | 9 | this._defineProperty('power0', [111, 4101], 0, Number) 10 | this._defineProperty('energyCounter0', [4103], 0, Number) 11 | 12 | this._defineProperty('overPower', [6102], 0, Number) 13 | this._defineProperty('overPowerValue', [6109], 0, Number) 14 | } 15 | } 16 | 17 | ShellyPlug.deviceType = 'SHPLG-1' 18 | ShellyPlug.deviceName = 'Shelly Plug' 19 | 20 | module.exports = ShellyPlug 21 | -------------------------------------------------------------------------------- /lib/devices/shelly-rgbw.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyRGBW extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [181, 1101], false, Boolean) 8 | 9 | this._defineProperty('red', [111, 5105], 0, Number) 10 | this._defineProperty('green', [121, 5106], 0, Number) 11 | this._defineProperty('blue', [131, 5107], 0, Number) 12 | this._defineProperty('white', [141, 5108], 0, Number) 13 | 14 | this._defineProperty('gain', [151, 5102], 0, Number) 15 | this._defineProperty('brightness', [171, 5101], 0, Number) 16 | 17 | this._defineProperty('colorTemperature', [161, 5103], 0, Number) 18 | 19 | this._defineProperty('mode', [9101], 'color', String) 20 | } 21 | 22 | async setColor(opts) { 23 | const query = Object.assign({ mode: 'color' }, opts) 24 | if (Object.prototype.hasOwnProperty.call(query, 'switch')) { 25 | query.turn = query.switch ? 'on' : 'off' 26 | delete query.switch 27 | } 28 | 29 | await this.request 30 | .get(`${this.host}/light/0`) 31 | .query(query) 32 | } 33 | 34 | async setWhite(temperature, brightness, on) { 35 | await this.request 36 | .get(`${this.host}/light/0`) 37 | .query({ 38 | mode: 'white', 39 | turn: on ? 'on' : 'off', 40 | temp: temperature, 41 | brightness, 42 | }) 43 | } 44 | } 45 | 46 | ShellyRGBW.deviceType = 'SHRGBWW-01' 47 | ShellyRGBW.deviceName = 'Shelly RGBW' 48 | 49 | module.exports = ShellyRGBW 50 | -------------------------------------------------------------------------------- /lib/devices/shelly-rgbw2.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyRGBW2 extends Device { 4 | constructor(id, host, mode = 'color') { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [161, 1101], false, Boolean, 'color') 8 | 9 | this._defineProperty('red', [111, 5105], 0, Number, 'color') 10 | this._defineProperty('green', [121, 5106], 0, Number, 'color') 11 | this._defineProperty('blue', [131, 5107], 0, Number, 'color') 12 | this._defineProperty('white', [141, 5108], 0, Number, 'color') 13 | this._defineProperty('gain', [151, 5102], 0, Number, 'color') 14 | 15 | this._defineProperty('switch0', [151, 1101], false, Boolean, 'white') 16 | this._defineProperty('brightness0', [111, 5101], 0, Number, 'white') 17 | this._defineProperty('switch1', [161, 1201], false, Boolean, 'white') 18 | this._defineProperty('brightness1', [121, 5201], 0, Number, 'white') 19 | this._defineProperty('switch2', [171, 1301], false, Boolean, 'white') 20 | this._defineProperty('brightness2', [131, 5301], 0, Number, 'white') 21 | this._defineProperty('switch3', [181, 1401], false, Boolean, 'white') 22 | this._defineProperty('brightness3', [141, 5401], 0, Number, 'white') 23 | 24 | this._defineProperty('power0', [4101], 0, Number) 25 | this._defineProperty('energyCounter0', [4103], 0, Number) 26 | this._defineProperty('power1', [4201], 0, Number, 'white') 27 | this._defineProperty('energyCounter1', [4203], 0, Number, 'white') 28 | this._defineProperty('power2', [4301], 0, Number, 'white') 29 | this._defineProperty('energyCounter2', [4303], 0, Number, 'white') 30 | this._defineProperty('power3', [4401], 0, Number, 'white') 31 | this._defineProperty('energyCounter3', [4403], 0, Number, 'white') 32 | 33 | this._defineProperty('overPower', [6102], 0, Number) 34 | 35 | this._defineProperty('input0', [118, 2101], 0, Number) 36 | this._defineProperty('inputEvent0', [2102], '', String) 37 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 38 | 39 | this._defineProperty('mode', [9101], mode, String) 40 | } 41 | 42 | _applyUpdate(msg, updates) { 43 | if (msg.protocolRevision === '1') { 44 | // if properties 171 and 181 are part of the updates, we expect the device 45 | // to be in "white" mode 46 | const r = updates.reduce((a, t) => { 47 | return a + (t[1] === 171 || t[1] === 181 ? 1 : 0) 48 | }, 0) 49 | this.mode = r >= 2 ? 'white' : 'color' 50 | } 51 | 52 | super._applyUpdate(msg, updates) 53 | } 54 | 55 | async setColor(opts) { 56 | const query = Object.assign({}, opts) 57 | if (Object.prototype.hasOwnProperty.call(query, 'switch')) { 58 | query.turn = query.switch ? 'on' : 'off' 59 | delete query.switch 60 | } 61 | 62 | await this.request 63 | .get(`${this.host}/color/0`) 64 | .query(query) 65 | } 66 | 67 | async setWhite(index, brightness, on) { 68 | await this.request 69 | .get(`${this.host}/white/${index}`) 70 | .query({ 71 | turn: on ? 'on' : 'off', 72 | brightness, 73 | }) 74 | } 75 | } 76 | 77 | ShellyRGBW2.deviceType = 'SHRGBW2' 78 | ShellyRGBW2.deviceName = 'Shelly RGBW2' 79 | 80 | module.exports = ShellyRGBW2 81 | -------------------------------------------------------------------------------- /lib/devices/shelly-sense.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellySense extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [33, 3101], 0, Number) 8 | this._defineProperty('humidity', [44, 3103], 0, Number) 9 | this._defineProperty('motion', [11, 6107], 0, Number) 10 | this._defineProperty('illuminance', [66, 3106], 0, Number) 11 | 12 | this._defineProperty('charging', [22, 3112], 0, Number) 13 | this._defineProperty('battery', [77, 3111], 0, Number) 14 | } 15 | } 16 | 17 | ShellySense.deviceType = 'SHSEN-1' 18 | ShellySense.deviceName = 'Shelly Sense' 19 | 20 | module.exports = ShellySense 21 | -------------------------------------------------------------------------------- /lib/devices/shelly-smoke.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellySmoke extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [3101], 0, Number) 8 | this._defineProperty('smoke', [6105], 0, Number) 9 | 10 | this._defineProperty('sensorError', [3115], false, Boolean) 11 | 12 | this._defineProperty('battery', [77, 3111], 0, Number) 13 | 14 | this._defineProperty('wakeUpEvent', [9102], 'unknown', String) 15 | } 16 | } 17 | 18 | ShellySmoke.deviceType = 'SHSM-01' 19 | ShellySmoke.deviceName = 'Shelly Smoke' 20 | 21 | module.exports = ShellySmoke 22 | -------------------------------------------------------------------------------- /lib/devices/shelly-smoke2.js: -------------------------------------------------------------------------------- 1 | const ShellySmoke = require('./shelly-smoke') 2 | 3 | class ShellySmoke2 extends ShellySmoke { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('humidity', [3103], 0, Number) 8 | } 9 | } 10 | 11 | ShellySmoke2.deviceType = 'SHSM-02' 12 | ShellySmoke2.deviceName = 'Shelly Smoke 2' 13 | 14 | module.exports = ShellySmoke2 15 | -------------------------------------------------------------------------------- /lib/devices/shelly-trv.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyTRV extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('temperature', [3101], 0, Number) 8 | this._defineProperty('targetTemperature', [3103], 0, Number) 9 | this._defineProperty('battery', [3111], 0, Number) 10 | this._defineProperty('sensorError', [3115], false, Boolean) 11 | this._defineProperty('valveError', [3116], false, Boolean) 12 | this._defineProperty('mode', [3117], 0, Number) 13 | this._defineProperty('status', [3118], false, Boolean) 14 | this._defineProperty('valvePosition', [3121], 0, Number) 15 | } 16 | 17 | async setTargetTemperature(temperature) { 18 | await this.request 19 | .get(`${this.host}/settings`) 20 | .query({ 21 | target_t: temperature, 22 | }) 23 | } 24 | } 25 | 26 | ShellyTRV.deviceType = 'SHTRV-01' 27 | ShellyTRV.deviceName = 'Shelly TRV' 28 | 29 | module.exports = ShellyTRV 30 | -------------------------------------------------------------------------------- /lib/devices/shelly-uni.js: -------------------------------------------------------------------------------- 1 | const { Switch } = require('./base') 2 | 3 | class ShellyUni extends Switch { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('relay0', [1101], false, Boolean) 8 | this._defineProperty('relay1', [1201], false, Boolean) 9 | 10 | this._defineProperty('input0', [2101], 0, Number) 11 | this._defineProperty('inputEvent0', [2102], '', String) 12 | this._defineProperty('inputEventCounter0', [2103], 0, Number) 13 | this._defineProperty('input1', [2201], 0, Number) 14 | this._defineProperty('inputEvent1', [2202], '', String) 15 | this._defineProperty('inputEventCounter1', [2203], 0, Number) 16 | 17 | this._defineProperty('externalTemperature0', [3101], null, Number) 18 | this._defineProperty('externalTemperature1', [3201], null, Number) 19 | this._defineProperty('externalTemperature2', [3301], null, Number) 20 | this._defineProperty('externalTemperature3', [3401], null, Number) 21 | this._defineProperty('externalTemperature4', [3501], null, Number) 22 | 23 | this._defineProperty('voltage0', [3118], 0, Number) 24 | 25 | this._defineProperty('externalHumidity', [3103], null, Number) 26 | } 27 | } 28 | 29 | ShellyUni.deviceType = 'SHUNI-1' 30 | ShellyUni.deviceName = 'Shelly Uni' 31 | 32 | module.exports = ShellyUni 33 | -------------------------------------------------------------------------------- /lib/devices/shelly-vintage.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class ShellyVintage extends Device { 4 | constructor(id, host) { 5 | super(id, host) 6 | 7 | this._defineProperty('switch', [121, 1101], false, Boolean) 8 | 9 | this._defineProperty('brightness', [111, 5101], 0, Number) 10 | 11 | this._defineProperty('power0', [141, 4101], 0, Number) 12 | this._defineProperty('energyCounter0', [211, 4103], 0, Number) 13 | } 14 | 15 | async setWhite(brightness, on) { 16 | await this.request 17 | .get(`${this.host}/light/0`) 18 | .query({ 19 | turn: on ? 'on' : 'off', 20 | brightness, 21 | }) 22 | } 23 | } 24 | 25 | ShellyVintage.deviceType = 'SHVIN-1' 26 | ShellyVintage.deviceName = 'Shelly Vintage' 27 | 28 | module.exports = ShellyVintage 29 | -------------------------------------------------------------------------------- /lib/devices/unknown.js: -------------------------------------------------------------------------------- 1 | const { Device } = require('./base') 2 | 3 | class UnknownDevice extends Device { 4 | constructor(id, host, type) { 5 | super(id, host) 6 | 7 | this._type = type 8 | 9 | this._defineProperty('payload', -99, null) 10 | } 11 | 12 | get type() { 13 | return this._type 14 | } 15 | 16 | _applyUpdate(msg, updates) { 17 | if (msg.host) { 18 | this.host = msg.host 19 | } 20 | 21 | this.payload = JSON.stringify(updates) 22 | } 23 | } 24 | 25 | UnknownDevice.deviceType = 'UNKNOWN' 26 | UnknownDevice.deviceName = 'Unknown Device' 27 | 28 | module.exports = UnknownDevice 29 | -------------------------------------------------------------------------------- /lib/http-request.js: -------------------------------------------------------------------------------- 1 | const defaults = require('superagent-defaults') 2 | 3 | const packageJson = require('../package.json') 4 | 5 | const request = defaults() 6 | .set('User-Agent', `node-shellies ${packageJson.version}`) 7 | .timeout(10000) 8 | 9 | module.exports = request 10 | -------------------------------------------------------------------------------- /lib/status-updates-listener.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('eventemitter3') 2 | 3 | const coap = require('./coap') 4 | 5 | class StatusUpdatesListener extends EventEmitter { 6 | constructor() { 7 | super() 8 | 9 | this._server = null 10 | this._listening = false 11 | } 12 | 13 | get listening() { 14 | return this._listening 15 | } 16 | 17 | async start(networkInterface = null) { 18 | if (!this._listening) { 19 | try { 20 | this._listening = true 21 | 22 | const handler = msg => { 23 | // ignore updates without valid device info 24 | if (msg.deviceType && msg.deviceId) { 25 | this.emit('statusUpdate', msg) 26 | } 27 | } 28 | 29 | await coap.requestStatusUpdates(handler) 30 | 31 | this._server = await coap.listenForStatusUpdates( 32 | handler, 33 | networkInterface 34 | ) 35 | this.emit('start') 36 | } catch (e) { 37 | this._listening = false 38 | throw e 39 | } 40 | } 41 | 42 | return this 43 | } 44 | 45 | stop() { 46 | if (this._listening) { 47 | this._server.close() 48 | this._server = null 49 | this._listening = false 50 | this.emit('stop') 51 | } 52 | 53 | return this 54 | } 55 | } 56 | 57 | module.exports = StatusUpdatesListener 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shellies", 3 | "version": "1.7.0", 4 | "description": "Handles communication with Shelly devices", 5 | "main": "index.js", 6 | "bin": "bin/shellies", 7 | "scripts": { 8 | "test": "mocha --exit", 9 | "eslint": "eslint ." 10 | }, 11 | "pre-commit": [ 12 | "eslint", 13 | "test" 14 | ], 15 | "keywords": [ 16 | "shelly", 17 | "coap", 18 | "http" 19 | ], 20 | "author": "Alexander Rydén", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/alexryd/node-shellies.git" 25 | }, 26 | "dependencies": { 27 | "coap": "^1.2.2", 28 | "colors": "^1.4.0", 29 | "command-line-commands": "^3.0.2", 30 | "eventemitter3": "^5.0.0", 31 | "require-all": "^3.0.0", 32 | "superagent": "^8.0.6", 33 | "superagent-defaults": "^0.1.14" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^8.29.0", 37 | "eslint-config-standard": "^17.0.0", 38 | "eslint-plugin-import": "^2.26.0", 39 | "eslint-plugin-node": "^11.1.0", 40 | "eslint-plugin-promise": "^6.1.1", 41 | "mocha": "^10.1.0", 42 | "pre-commit": "^1.2.2", 43 | "should": "^13.2.3", 44 | "sinon": "^15.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/test-devices.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const should = require('should') 3 | const sinon = require('sinon') 4 | 5 | const { Device } = require('../lib/devices/base') 6 | const devices = require('../lib/devices') 7 | const request = require('../lib/http-request') 8 | const UnknownDevice = require('../lib/devices/unknown') 9 | 10 | describe('devices', function() { 11 | describe('.create()', function() { 12 | it('should return instances of Device for known device types', function() { 13 | devices.create( 14 | 'SHSW-1', 15 | 'ABC123', 16 | '192.168.1.2' 17 | ).should.be.instanceof(Device) 18 | }) 19 | 20 | it('should return an UnknownDevice for unknown device types', function() { 21 | devices.create( 22 | 'UNKNOWN-1', 23 | 'ABC123', 24 | '192.168.1.2' 25 | ).should.be.instanceof(UnknownDevice) 26 | }) 27 | }) 28 | 29 | describe('.isUnknown()', function() { 30 | it('should return true for unknown devices', function() { 31 | devices.isUnknown( 32 | devices.create( 33 | 'UNKNOWN-1', 34 | 'ABC123', 35 | '192.168.1.2' 36 | ) 37 | ).should.be.true() 38 | }) 39 | 40 | it('should return false for known devices', function() { 41 | devices.isUnknown( 42 | devices.create( 43 | 'SHSW-1', 44 | 'ABC123', 45 | '192.168.1.2' 46 | ) 47 | ).should.be.false() 48 | }) 49 | }) 50 | }) 51 | 52 | describe('Device', function() { 53 | let device = null 54 | 55 | beforeEach(function() { 56 | device = new Device('ABC123', '192.168.1.2') 57 | }) 58 | 59 | describe('#settings', function() { 60 | it('should not fail when assigned a new value', function() { 61 | const settings = {} 62 | should(() => { device.settings = settings }).not.throw() 63 | device.settings.should.equal(settings) 64 | }) 65 | }) 66 | 67 | describe('#online', function() { 68 | it('should be false by default', function() { 69 | device.online.should.equal(false) 70 | }) 71 | 72 | it('should emit `online` and `offline` events upon changes', function() { 73 | const onlineHandler = sinon.fake() 74 | const offlineHandler = sinon.fake() 75 | device.on('online', onlineHandler).on('offline', offlineHandler) 76 | 77 | device.online = true 78 | onlineHandler.calledOnce.should.equal(true) 79 | onlineHandler.calledWith(device).should.equal(true) 80 | offlineHandler.called.should.equal(false) 81 | 82 | device.online = true 83 | onlineHandler.calledOnce.should.equal(true) 84 | offlineHandler.called.should.equal(false) 85 | 86 | device.online = false 87 | onlineHandler.calledOnce.should.equal(true) 88 | offlineHandler.calledOnce.should.equal(true) 89 | offlineHandler.calledWith(device).should.equal(true) 90 | 91 | device.online = false 92 | onlineHandler.calledOnce.should.equal(true) 93 | offlineHandler.calledOnce.should.equal(true) 94 | }) 95 | }) 96 | 97 | describe('#ttl', function() { 98 | it('should set `online` to false after the given time', function() { 99 | const clock = sinon.useFakeTimers() 100 | 101 | device.online = true 102 | device.ttl = 1000 103 | clock.tick(500) 104 | device.online.should.equal(true) 105 | clock.tick(500) 106 | device.online.should.equal(false) 107 | 108 | clock.restore() 109 | }) 110 | 111 | it('should not set `online` to false when set to 0', function() { 112 | const clock = sinon.useFakeTimers() 113 | 114 | device.online = true 115 | device.ttl = 1000 116 | device.ttl = 0 117 | device.online.should.equal(true) 118 | clock.tick(1000) 119 | device.online.should.equal(true) 120 | 121 | clock.restore() 122 | }) 123 | }) 124 | 125 | describe('#name', function() { 126 | it('should return the name when one is set', function() { 127 | device.settings = { name: 'foo' } 128 | device.name = 'bar' 129 | device.name.should.equal('bar') 130 | }) 131 | 132 | it('should return the name from the settings', function() { 133 | device.settings = { name: 'foo' } 134 | device.name.should.equal('foo') 135 | }) 136 | 137 | it('should return undefined when no settings have been loaded', function() { 138 | should(device.name).be.undefined() 139 | }) 140 | }) 141 | 142 | describe('#request', function() { 143 | it('should not return null when _request is not set', function() { 144 | should(device._request).be.null() 145 | device.request.should.be.ok() 146 | }) 147 | 148 | it('should return _request when it is set', function() { 149 | const r = {} 150 | device._request = r 151 | device.request.should.equal(r) 152 | }) 153 | }) 154 | 155 | describe('#_defineProperty()', function() { 156 | it('should define a property', function() { 157 | device._defineProperty('foo') 158 | Object.prototype.hasOwnProperty.call(device, 'foo').should.equal(true) 159 | should(device.foo).be.null() 160 | device.foo = 'bar' 161 | device.foo.should.equal('bar') 162 | }) 163 | 164 | it('should associate the property with the given ID', function() { 165 | device._defineProperty('foo', 1) 166 | device._props.get('*').get(1).should.equal('foo') 167 | }) 168 | 169 | it('should associate the property with the given IDs', function() { 170 | device._defineProperty('foo', [1, 212, 33]) 171 | device._props.get('*').get(1).should.equal('foo') 172 | device._props.get('*').get(212).should.equal('foo') 173 | device._props.get('*').get(33).should.equal('foo') 174 | }) 175 | 176 | it('should not associate the property when no ID is given', function() { 177 | device._defineProperty('foo', null) 178 | device._props.size.should.equal(0) 179 | }) 180 | 181 | it('should associate the property with the given mode', function() { 182 | device._defineProperty('foo', 1, null, null, 'bar') 183 | device._props.get('bar').get(1).should.equal('foo') 184 | }) 185 | 186 | it('should properly set the default value', function() { 187 | device._defineProperty('foo', null, 'bar') 188 | device.foo.should.equal('bar') 189 | }) 190 | 191 | it('should invoke the validator when setting a value', function() { 192 | const validator = val => val.toUpperCase() 193 | device._defineProperty('foo', null, null, validator) 194 | device.foo = 'bar' 195 | device.foo.should.equal('BAR') 196 | }) 197 | 198 | it('should emit `change` events when the property changes', function() { 199 | const changeHandler = sinon.fake() 200 | const changeFooHandler = sinon.fake() 201 | device.on('change', changeHandler).on('change:foo', changeFooHandler) 202 | 203 | device._defineProperty('foo') 204 | device.foo = 'bar' 205 | 206 | changeHandler.calledOnce.should.equal(true) 207 | changeHandler.calledWith('foo', 'bar', null, device).should.equal(true) 208 | changeFooHandler.calledOnce.should.equal(true) 209 | changeFooHandler.calledWith('bar', null, device).should.equal(true) 210 | }) 211 | }) 212 | 213 | describe('#_getPropertyName()', function() { 214 | it('should return the name of the property with the given ID', function() { 215 | device._defineProperty('foo', 1) 216 | device._getPropertyName(1).should.equal('foo') 217 | }) 218 | 219 | it('should return undefined for unknown IDs', function() { 220 | should(device._getPropertyName(1)).be.undefined() 221 | }) 222 | 223 | it('should respect the given mode', function() { 224 | device._defineProperty('foo', 1, null, null, 'bar') 225 | device._getPropertyName(1, 'bar').should.equal('foo') 226 | }) 227 | 228 | it('should ignore properties associated with other modes', function() { 229 | device._defineProperty('foo', 1, null, null, 'bar') 230 | should(device._getPropertyName(1, 'baz')).be.undefined() 231 | }) 232 | }) 233 | 234 | describe('#[Symbol.iterator]()', function() { 235 | it('should return an iterator', function() { 236 | device.should.be.iterable() 237 | device[Symbol.iterator]().should.be.iterator() 238 | }) 239 | 240 | it('should iterate through properties with IDs', function() { 241 | device._defineProperty('foo', 1) 242 | device._defineProperty('bar') 243 | device._defineProperty('baz', 2) 244 | 245 | const seenProps = new Set() 246 | 247 | for (const [key, value] of device) { // eslint-disable-line no-unused-vars 248 | seenProps.add(key) 249 | } 250 | 251 | seenProps.has('foo').should.be.true() 252 | seenProps.has('bar').should.be.false() 253 | seenProps.has('baz').should.be.true() 254 | }) 255 | 256 | it('should only include properties for the current mode', function() { 257 | device._defineProperty('foo', 1) 258 | device._defineProperty('bar', 2, null, null, 'mode1') 259 | device._defineProperty('baz', 3, null, null, 'mode2') 260 | device.mode = 'mode2' 261 | 262 | const seenProps = new Set() 263 | 264 | for (const [key, value] of device) { // eslint-disable-line no-unused-vars 265 | seenProps.add(key) 266 | } 267 | 268 | seenProps.has('foo').should.be.true() 269 | seenProps.has('bar').should.be.false() 270 | seenProps.has('baz').should.be.true() 271 | }) 272 | }) 273 | 274 | describe('#update()', function() { 275 | it('should set `online` to true', function() { 276 | device.online = false 277 | device.update({}) 278 | device.online.should.equal(true) 279 | }) 280 | 281 | it('should not set `ttl` when `validFor` is not specified', function() { 282 | device.ttl = 0 283 | device.update({}) 284 | device.ttl.should.equal(0) 285 | }) 286 | 287 | it('should set `ttl` when `validFor` is specified', function() { 288 | const msg = { 289 | validFor: 37, 290 | } 291 | 292 | device.ttl = 0 293 | device.update(msg) 294 | device.ttl.should.equal(msg.validFor * 1000) 295 | }) 296 | 297 | it('should set `lastSeen`', function() { 298 | should(device.lastSeen).be.null() 299 | device.update({}) 300 | device.lastSeen.should.not.be.null() 301 | }) 302 | }) 303 | 304 | describe('#_applyUpdate()', function() { 305 | it('should update the host', function() { 306 | const changeHostHandler = sinon.fake() 307 | const msg = { 308 | host: '192.168.1.3', 309 | } 310 | 311 | device.on('change:host', changeHostHandler) 312 | device._applyUpdate(msg, []) 313 | 314 | changeHostHandler.calledOnce.should.equal(true) 315 | changeHostHandler.calledWith(msg.host).should.equal(true) 316 | }) 317 | 318 | it('should update the properties from to the payload', function() { 319 | const changeFooHandler = sinon.fake() 320 | const payload = [ 321 | [0, 1, 2], 322 | ] 323 | 324 | device._defineProperty('foo', 1) 325 | device.on('change:foo', changeFooHandler) 326 | device._applyUpdate({}, payload) 327 | 328 | changeFooHandler.calledOnce.should.equal(true) 329 | changeFooHandler.calledWith(payload[0][2]).should.equal(true) 330 | }) 331 | }) 332 | 333 | describe('#setAuthCredentials()', function() { 334 | it('should create a request object if none exists', function() { 335 | should(device._request).be.null() 336 | device.setAuthCredentials('foo', 'bar') 337 | device._request.should.be.ok() 338 | }) 339 | }) 340 | }) 341 | 342 | describe('Shelly2', function() { 343 | let device = null 344 | 345 | beforeEach(function() { 346 | device = devices.create('SHSW-21', 'ABC123', '192.168.1.2') 347 | }) 348 | 349 | afterEach(function() { 350 | sinon.restore() 351 | }) 352 | 353 | describe('#_updateRollerState()', function() { 354 | it('should properly update the roller state', function() { 355 | device.relay0.should.be.false() 356 | device.relay1.should.be.false() 357 | device.rollerState.should.equal('stop') 358 | 359 | device._updateRollerState('roller') 360 | device.rollerState.should.equal('stop') 361 | 362 | device.relay0 = true 363 | device._updateRollerState('roller') 364 | device.rollerState.should.equal('open') 365 | 366 | device.relay0 = false 367 | device.relay1 = true 368 | device._updateRollerState('roller') 369 | device.rollerState.should.equal('close') 370 | 371 | device.relay1 = false 372 | device._updateRollerState('roller') 373 | device.rollerState.should.equal('stop') 374 | 375 | device.settings = { mode: 'roller', rollers: [{ swap: true }] } 376 | 377 | device.relay0 = true 378 | device._updateRollerState('roller') 379 | device.rollerState.should.equal('close') 380 | 381 | device.relay0 = false 382 | device.relay1 = true 383 | device._updateRollerState('roller') 384 | device.rollerState.should.equal('open') 385 | 386 | device.relay1 = false 387 | device._updateRollerState('roller') 388 | device.rollerState.should.equal('stop') 389 | }) 390 | 391 | it('should do nothing when mode is not \'roller\'', function() { 392 | device.relay0 = true 393 | device._updateRollerState('relay') 394 | device.rollerState.should.equal('stop') 395 | }) 396 | }) 397 | 398 | describe('#_applyUpdate()', function() { 399 | it('should set mode to "roller" when property 113 is present', function() { 400 | device.mode.should.equal('relay') 401 | device._applyUpdate( 402 | { protocolRevision: '1' }, 403 | [[0, 112, 0], [0, 113, 0], [0, 122, 1]] 404 | ) 405 | device.mode.should.equal('roller') 406 | }) 407 | 408 | it('should set mode to "relay" when property 113 is absent', function() { 409 | device.mode = 'roller' 410 | device._applyUpdate( 411 | { protocolRevision: '1' }, 412 | [[0, 112, 0], [0, 122, 1]] 413 | ) 414 | device.mode.should.equal('relay') 415 | }) 416 | 417 | it('should invoke _updateRollerState()', function() { 418 | const _updateRollerState = sinon.stub(device, '_updateRollerState') 419 | device._applyUpdate({ protocolRevision: '1' }, []) 420 | _updateRollerState.called.should.be.true() 421 | _updateRollerState.calledWith(device.mode).should.be.true() 422 | }) 423 | }) 424 | 425 | describe('#setRollerState()', function() { 426 | let get = null 427 | 428 | beforeEach(function() { 429 | get = sinon.stub(request, 'get') 430 | }) 431 | 432 | it('should request a URL with a proper query string', function() { 433 | get.resolves({}) 434 | 435 | device.setRollerState('open') 436 | get.calledOnce.should.be.true() 437 | get.calledWith(`${device.host}/roller/0?go=open`).should.be.true() 438 | 439 | device.setRollerState('close', 20) 440 | get.calledTwice.should.be.true() 441 | get.calledWith(`${device.host}/roller/0?go=close&duration=20`) 442 | .should.be.true() 443 | }) 444 | 445 | it('should resolve with the request body', function() { 446 | const body = {} 447 | get.resolves({ body }) 448 | 449 | return device.setRollerState('open').should.be.fulfilledWith(body) 450 | }) 451 | 452 | it('should reject failed requests', function() { 453 | get.rejects() 454 | device.setRollerState('open').should.be.rejected() 455 | }) 456 | }) 457 | 458 | describe('#setRollerPosition()', function() { 459 | let get = null 460 | 461 | beforeEach(function() { 462 | get = sinon.stub(request, 'get') 463 | }) 464 | 465 | it('should request a URL with a proper query string', function() { 466 | get.resolves({}) 467 | 468 | device.setRollerPosition(55) 469 | get.calledOnce.should.be.true() 470 | get.calledWith(`${device.host}/roller/0?go=to_pos&roller_pos=55`) 471 | .should.be.true() 472 | }) 473 | 474 | it('should resolve with the request body', function() { 475 | const body = {} 476 | get.resolves({ body }) 477 | 478 | return device.setRollerPosition(20).should.be.fulfilledWith(body) 479 | }) 480 | 481 | it('should reject failed requests', function() { 482 | get.rejects() 483 | device.setRollerPosition(20).should.be.rejected() 484 | }) 485 | }) 486 | }) 487 | 488 | describe('ShellyRGBW2', function() { 489 | let device = null 490 | 491 | beforeEach(function() { 492 | device = devices.create('SHRGBW2', 'ABC123', '192.168.1.2') 493 | }) 494 | 495 | afterEach(function() { 496 | sinon.restore() 497 | }) 498 | 499 | describe('#_applyUpdate()', function() { 500 | it( 501 | 'should set mode to "white" when properties 171 and 181 are present', 502 | function() { 503 | device.mode.should.equal('color') 504 | device._applyUpdate( 505 | { protocolRevision: '1' }, 506 | [[0, 112, 0], [0, 171, 0], [0, 181, 1]] 507 | ) 508 | device.mode.should.equal('white') 509 | } 510 | ) 511 | 512 | it( 513 | 'should set mode to "color" when properties 171 and 181 are absent', 514 | function() { 515 | device.mode = 'white' 516 | device._applyUpdate( 517 | { protocolRevision: '1' }, 518 | [[0, 112, 0], [0, 122, 1]] 519 | ) 520 | device.mode.should.equal('color') 521 | 522 | device.mode = 'white' 523 | device._applyUpdate( 524 | { protocolRevision: '1' }, 525 | [[0, 112, 0], [0, 171, 0], [0, 122, 1]] 526 | ) 527 | device.mode.should.equal('color') 528 | 529 | device.mode = 'white' 530 | device._applyUpdate( 531 | { protocolRevision: '1' }, 532 | [[0, 112, 0], [0, 181, 0], [0, 122, 1]] 533 | ) 534 | device.mode.should.equal('color') 535 | } 536 | ) 537 | }) 538 | }) 539 | -------------------------------------------------------------------------------- /test/test-shellies.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const should = require('should') 3 | const sinon = require('sinon') 4 | 5 | const devices = require('../lib/devices') 6 | const shellies = require('../index') 7 | const UnknownDevice = require('../lib/devices/unknown') 8 | 9 | describe('shellies', function() { 10 | let device = null 11 | 12 | beforeEach(function() { 13 | device = shellies.createDevice('SHSW-1', 'ABC123', '192.168.1.2') 14 | }) 15 | 16 | afterEach(function() { 17 | shellies._devices.clear() 18 | shellies.removeAllListeners() 19 | }) 20 | 21 | it('should emit `start` when the status listener starts', function() { 22 | const startHandler = sinon.fake() 23 | shellies.on('start', startHandler) 24 | 25 | shellies._listener.emit('start') 26 | startHandler.calledOnce.should.equal(true) 27 | }) 28 | 29 | it('should emit `stop` when the status listener stops', function() { 30 | const stopHandler = sinon.fake() 31 | shellies.on('stop', stopHandler) 32 | 33 | shellies._listener.emit('stop') 34 | stopHandler.calledOnce.should.equal(true) 35 | }) 36 | 37 | it('should emit `discover` when a new device is found', function() { 38 | const discoverHandler = sinon.fake() 39 | shellies.on('discover', discoverHandler) 40 | 41 | const msg = { 42 | deviceType: 'SHSW-1', 43 | deviceId: 'ABC123', 44 | host: '192.168.1.2', 45 | } 46 | shellies._listener.emit('statusUpdate', msg) 47 | 48 | discoverHandler.calledOnce.should.equal(true) 49 | discoverHandler.lastCall.args[0].type.should.equal(msg.deviceType) 50 | discoverHandler.lastCall.args[0].id.should.equal(msg.deviceId) 51 | discoverHandler.lastCall.args[0].host.should.equal(msg.host) 52 | discoverHandler.lastCall.args[1].should.be.false() 53 | 54 | const msg2 = { 55 | deviceType: 'SHSW-1', 56 | deviceId: 'ABC124', 57 | host: '192.168.1.3', 58 | } 59 | shellies._listener.emit('statusUpdate', msg2) 60 | 61 | discoverHandler.calledTwice.should.equal(true) 62 | discoverHandler.lastCall.args[0].type.should.equal(msg2.deviceType) 63 | discoverHandler.lastCall.args[0].id.should.equal(msg2.deviceId) 64 | discoverHandler.lastCall.args[0].host.should.equal(msg2.host) 65 | discoverHandler.lastCall.args[1].should.be.false() 66 | }) 67 | 68 | it('should not emit `discover` when a known device is found', function() { 69 | const discoverHandler = sinon.fake() 70 | shellies.on('discover', discoverHandler) 71 | 72 | shellies.addDevice(device) 73 | 74 | const msg = { 75 | deviceType: device.type, 76 | deviceId: device.id, 77 | host: device.host, 78 | } 79 | shellies._listener.emit('statusUpdate', msg) 80 | 81 | discoverHandler.calledOnce.should.equal(false) 82 | }) 83 | 84 | it('should emit `add` when a new device is discovered', function() { 85 | const addHandler = sinon.fake() 86 | shellies.on('add', addHandler) 87 | 88 | const msg = { 89 | deviceType: 'SHSW-1', 90 | deviceId: 'ABC123', 91 | host: '192.168.1.2', 92 | } 93 | shellies._listener.emit('statusUpdate', msg) 94 | 95 | addHandler.calledOnce.should.equal(true) 96 | }) 97 | 98 | it('should emit `discover` when an unknown device type is found', function() { 99 | const discoverHandler = sinon.fake() 100 | shellies.on('discover', discoverHandler) 101 | 102 | const msg = { 103 | deviceType: 'UNKNOWN-1', 104 | deviceId: 'ABC123', 105 | host: '192.168.1.2', 106 | } 107 | shellies._listener.emit('statusUpdate', msg) 108 | 109 | discoverHandler.calledOnce.should.equal(true) 110 | discoverHandler.lastCall.args[0].should.be.instanceof(UnknownDevice) 111 | discoverHandler.lastCall.args[1].should.be.true() 112 | }) 113 | 114 | it('should not emit `stale` when `staleTimeout` is disabled', function() { 115 | const clock = sinon.useFakeTimers() 116 | const staleHandler = sinon.fake() 117 | const deviceStaleHandler = sinon.fake() 118 | shellies.on('stale', staleHandler) 119 | device.on('stale', deviceStaleHandler) 120 | 121 | shellies.staleTimeout = 0 122 | device.online = true 123 | shellies.addDevice(device) 124 | device.online = false 125 | 126 | staleHandler.called.should.equal(false) 127 | clock.tick(99999999999) 128 | staleHandler.calledOnce.should.equal(false) 129 | deviceStaleHandler.calledOnce.should.equal(false) 130 | 131 | clock.restore() 132 | }) 133 | 134 | it('should emit `stale` when a device becomes stale', function() { 135 | const clock = sinon.useFakeTimers() 136 | const staleHandler = sinon.fake() 137 | const deviceStaleHandler = sinon.fake() 138 | shellies.on('stale', staleHandler) 139 | device.on('stale', deviceStaleHandler) 140 | 141 | shellies.staleTimeout = 1000 142 | device.online = true 143 | shellies.addDevice(device) 144 | device.online = false 145 | 146 | staleHandler.called.should.equal(false) 147 | clock.tick(shellies.staleTimeout) 148 | staleHandler.calledOnce.should.equal(true) 149 | staleHandler.calledWith(device).should.equal(true) 150 | deviceStaleHandler.calledOnce.should.equal(true) 151 | deviceStaleHandler.calledWith(device).should.equal(true) 152 | 153 | clock.restore() 154 | }) 155 | 156 | it('should emit `stale` for devices that are already offline', function() { 157 | const clock = sinon.useFakeTimers() 158 | const staleHandler = sinon.fake() 159 | shellies.on('stale', staleHandler) 160 | 161 | shellies.staleTimeout = 1000 162 | shellies.addDevice(device) 163 | 164 | staleHandler.called.should.be.false() 165 | clock.tick(shellies.staleTimeout) 166 | staleHandler.calledOnce.should.be.true() 167 | staleHandler.calledWith(device).should.be.true() 168 | 169 | clock.restore() 170 | }) 171 | 172 | it('should remove stale devices', function() { 173 | const clock = sinon.useFakeTimers() 174 | const removeHandler = sinon.fake() 175 | shellies.on('remove', removeHandler) 176 | 177 | shellies.staleTimeout = 1000 178 | shellies.addDevice(device) 179 | device.emit('offline', device) 180 | 181 | shellies.size.should.equal(1) 182 | clock.tick(shellies.staleTimeout) 183 | shellies.size.should.equal(0) 184 | removeHandler.calledOnce.should.equal(true) 185 | removeHandler.calledWith(device).should.equal(true) 186 | 187 | clock.restore() 188 | }) 189 | 190 | it('should not emit `stale` for devices that are online', function() { 191 | const clock = sinon.useFakeTimers() 192 | const staleHandler = sinon.fake() 193 | shellies.on('stale', staleHandler) 194 | 195 | shellies.staleTimeout = 1000 196 | device.online = true 197 | shellies.addDevice(device) 198 | device.online = false 199 | device.online = true 200 | 201 | staleHandler.called.should.be.false() 202 | clock.tick(shellies.staleTimeout) 203 | staleHandler.called.should.be.false() 204 | 205 | clock.restore() 206 | }) 207 | 208 | describe('#start()', function() { 209 | it( 210 | 'should pass the network interface to the status update listener', 211 | function() { 212 | const start = sinon.stub(shellies._listener, 'start') 213 | const networkInterface = '127.0.0.1' 214 | 215 | shellies.start(networkInterface) 216 | 217 | start.calledOnce.should.be.true() 218 | start.calledWith(networkInterface).should.be.true() 219 | } 220 | ) 221 | }) 222 | 223 | describe('#addDevice()', function() { 224 | it('should add the device to the list of devices', function() { 225 | shellies.size.should.equal(0) 226 | shellies.addDevice(device) 227 | shellies.size.should.equal(1) 228 | }) 229 | 230 | it('should emit an `add` event', function() { 231 | const addHandler = sinon.fake() 232 | 233 | shellies.on('add', addHandler) 234 | shellies.addDevice(device) 235 | 236 | addHandler.calledOnce.should.equal(true) 237 | addHandler.calledWith(device).should.equal(true) 238 | }) 239 | 240 | it('should throw an error when a device is added twice', function() { 241 | shellies.addDevice(device) 242 | 243 | should(() => shellies.addDevice(device)).throw() 244 | should(() => { 245 | shellies.addDevice( 246 | shellies.createDevice('SHSW-1', 'ABC123', '192.168.1.3') 247 | ) 248 | }).throw() 249 | }) 250 | 251 | it('should not throw an error when two devices are added', function() { 252 | shellies.addDevice(device) 253 | should(() => { 254 | shellies.addDevice( 255 | shellies.createDevice('SHSW-1', 'ABC124', '192.168.1.2') 256 | ) 257 | }).not.throw() 258 | }) 259 | }) 260 | 261 | describe('#getDevice()', function() { 262 | it('should return undefined when the device is not found', function() { 263 | should(shellies.getDevice('SHSW-1', 'ABC123')).equal(undefined) 264 | }) 265 | 266 | it('should return the device when it is found', function() { 267 | shellies.addDevice(device) 268 | 269 | shellies.getDevice(device.type, device.id).should.equal(device) 270 | }) 271 | }) 272 | 273 | describe('#hasDevice()', function() { 274 | it('should return false when the device is not found', function() { 275 | shellies.hasDevice(device).should.equal(false) 276 | }) 277 | 278 | it('should return true when the device is found', function() { 279 | shellies.addDevice(device) 280 | 281 | shellies.hasDevice(device).should.equal(true) 282 | shellies.hasDevice( 283 | shellies.createDevice('SHSW-1', 'ABC123', '192.168.1.3') 284 | ).should.equal(true) 285 | }) 286 | }) 287 | 288 | describe('#removeDevice()', function() { 289 | it('should remove the device from the list of devices', function() { 290 | shellies.addDevice(device) 291 | shellies.size.should.equal(1) 292 | shellies.removeDevice(device) 293 | shellies.size.should.equal(0) 294 | }) 295 | 296 | it('should remove all event listeners from the device', function() { 297 | shellies.addDevice(device) 298 | shellies.removeDevice(device) 299 | 300 | device.eventNames().length.should.equal(0) 301 | }) 302 | 303 | it('should emit a `remove` event when a device is removed', function() { 304 | const removeHandler = sinon.fake() 305 | 306 | shellies.addDevice(device) 307 | shellies.on('remove', removeHandler) 308 | shellies.removeDevice(device) 309 | 310 | removeHandler.calledOnce.should.equal(true) 311 | removeHandler.calledWith(device).should.equal(true) 312 | }) 313 | 314 | it( 315 | 'should not emit a `remove` event when no device is removed', 316 | function() { 317 | const removeHandler = sinon.fake() 318 | 319 | shellies.on('remove', removeHandler) 320 | shellies.removeDevice(device) 321 | 322 | removeHandler.called.should.equal(false) 323 | } 324 | ) 325 | }) 326 | 327 | describe('#[Symbol.iterator]()', function() { 328 | it('should return an iterator', function() { 329 | shellies.should.be.iterable() 330 | shellies[Symbol.iterator]().should.be.iterator() 331 | }) 332 | 333 | it('should iterate through the list of devices', function() { 334 | const device2 = shellies.createDevice('SHSW-1', 'ABC124', '192.168.1.3') 335 | const device3 = shellies.createDevice('SHSW-1', 'ABC125', '192.168.1.4') 336 | const iterator = shellies[Symbol.iterator]() 337 | 338 | shellies.addDevice(device) 339 | shellies.addDevice(device2) 340 | shellies.addDevice(device3) 341 | 342 | iterator.next().value.should.equal(device) 343 | iterator.next().value.should.equal(device2) 344 | iterator.next().value.should.equal(device3) 345 | }) 346 | }) 347 | 348 | describe('#setAuthCredentials()', function() { 349 | it('should set the authentication credentials', function() { 350 | const auth = sinon.fake() 351 | sinon.replace(shellies.request, 'auth', auth) 352 | 353 | shellies.setAuthCredentials('foo', 'bar') 354 | 355 | auth.calledOnce.should.equal(true) 356 | auth.calledWith('foo', 'bar').should.equal(true) 357 | }) 358 | }) 359 | 360 | describe('#isUnknownDevice()', function() { 361 | it('should return true for unknown devices', function() { 362 | shellies.isUnknownDevice( 363 | devices.create( 364 | 'UNKNOWN-1', 365 | 'ABC123', 366 | '192.168.1.2' 367 | ) 368 | ).should.be.true() 369 | }) 370 | 371 | it('should return false for known devices', function() { 372 | shellies.isUnknownDevice( 373 | devices.create( 374 | 'SHSW-1', 375 | 'ABC123', 376 | '192.168.1.2' 377 | ) 378 | ).should.be.false() 379 | }) 380 | }) 381 | }) 382 | -------------------------------------------------------------------------------- /test/test-status-updates-listener.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const sinon = require('sinon') 3 | 4 | const coap = require('../lib/coap') 5 | const StatusUpdatesListener = require('../lib/status-updates-listener') 6 | 7 | describe('StatusUpdatesListener', function() { 8 | const fakeServer = { close: () => {} } 9 | let listener = null 10 | 11 | beforeEach(function() { 12 | sinon.stub(coap, 'listenForStatusUpdates').resolves(fakeServer) 13 | sinon.stub(coap, 'requestStatusUpdates').resolves() 14 | sinon.stub(fakeServer, 'close') 15 | 16 | listener = new StatusUpdatesListener() 17 | }) 18 | 19 | afterEach(function() { 20 | sinon.restore() 21 | listener.removeAllListeners() 22 | listener = null 23 | }) 24 | 25 | describe('#start()', function() { 26 | it( 27 | 'should pass the network interface to listenForStatusUpdates()', 28 | async function() { 29 | const networkInterface = '127.0.0.1' 30 | await listener.start(networkInterface) 31 | coap.listenForStatusUpdates.firstCall.args[1] 32 | .should.equal(networkInterface) 33 | } 34 | ) 35 | 36 | it('should emit a `start` event', async function() { 37 | const startHandler = sinon.fake() 38 | listener.on('start', startHandler) 39 | 40 | await listener.start() 41 | 42 | startHandler.calledOnce.should.equal(true) 43 | }) 44 | 45 | it('should set `listening` to true', async function() { 46 | listener.listening.should.equal(false) 47 | await listener.start() 48 | listener.listening.should.equal(true) 49 | }) 50 | 51 | it('should handle updates from requestStatusUpdates()', async function() { 52 | const statusUpdateHandler = sinon.fake() 53 | listener.on('statusUpdate', statusUpdateHandler) 54 | 55 | const msg = { 56 | deviceType: 'SHSW-1', 57 | deviceId: 'ABC123', 58 | host: '192.168.1.2', 59 | } 60 | coap.requestStatusUpdates.callsArgWith(0, msg) 61 | 62 | await listener.start() 63 | 64 | statusUpdateHandler.calledOnce.should.equal(true) 65 | statusUpdateHandler.calledWith(msg).should.equal(true) 66 | }) 67 | 68 | it( 69 | 'should ignore invalid updates from requestStatusUpdates()', 70 | async function() { 71 | const statusUpdateHandler = sinon.fake() 72 | listener.on('statusUpdate', statusUpdateHandler) 73 | 74 | const msg = {} 75 | coap.requestStatusUpdates.callsArgWith(0, msg) 76 | 77 | await listener.start() 78 | 79 | statusUpdateHandler.calledOnce.should.equal(false) 80 | } 81 | ) 82 | 83 | it('should handle errors from requestStatusUpdates()', function() { 84 | const err = new Error('Fake error') 85 | coap.requestStatusUpdates.rejects(err) 86 | 87 | return listener.start().should.be.rejectedWith(err) 88 | }) 89 | 90 | it('should handle updates from listenForStatusUpdates()', async function() { 91 | const statusUpdateHandler = sinon.fake() 92 | listener.on('statusUpdate', statusUpdateHandler) 93 | 94 | const msg = { 95 | deviceType: 'SHSW-1', 96 | deviceId: 'ABC123', 97 | host: '192.168.1.2', 98 | } 99 | coap.listenForStatusUpdates.callsArgWith(0, msg) 100 | 101 | await listener.start() 102 | 103 | statusUpdateHandler.calledOnce.should.equal(true) 104 | statusUpdateHandler.calledWith(msg).should.equal(true) 105 | 106 | coap.listenForStatusUpdates.firstCall.args[0](msg) 107 | 108 | statusUpdateHandler.calledTwice.should.equal(true) 109 | statusUpdateHandler.calledWith(msg).should.equal(true) 110 | }) 111 | 112 | it( 113 | 'should ignore invalid updates from listenForStatusUpdates()', 114 | async function() { 115 | const statusUpdateHandler = sinon.fake() 116 | listener.on('statusUpdate', statusUpdateHandler) 117 | 118 | const msg = {} 119 | coap.listenForStatusUpdates.callsArgWith(0, msg) 120 | 121 | await listener.start() 122 | 123 | statusUpdateHandler.calledOnce.should.equal(false) 124 | } 125 | ) 126 | 127 | it('should handle errors from listenForStatusUpdates()', function() { 128 | const err = new Error('Fake error') 129 | coap.listenForStatusUpdates.rejects(err) 130 | 131 | return listener.start().should.be.rejectedWith(err) 132 | }) 133 | 134 | it('should do nothing when already listening', async function() { 135 | await listener.start() 136 | await listener.start() 137 | 138 | coap.listenForStatusUpdates.calledOnce.should.equal(true) 139 | coap.requestStatusUpdates.calledOnce.should.equal(true) 140 | }) 141 | }) 142 | 143 | describe('#stop()', function() { 144 | it('should stop the server', async function() { 145 | await listener.start() 146 | listener.stop() 147 | 148 | fakeServer.close.calledOnce.should.equal(true) 149 | }) 150 | 151 | it('should emit a `stop` event', async function() { 152 | const stopHandler = sinon.fake() 153 | listener.on('stop', stopHandler) 154 | 155 | await listener.start() 156 | listener.stop() 157 | 158 | stopHandler.calledOnce.should.equal(true) 159 | }) 160 | 161 | it('should set `listening` to false', async function() { 162 | await listener.start() 163 | 164 | listener.listening.should.equal(true) 165 | listener.stop() 166 | listener.listening.should.equal(false) 167 | }) 168 | 169 | it('should do nothing when not listening', async function() { 170 | const stopHandler = sinon.fake() 171 | listener.on('stop', stopHandler) 172 | 173 | listener.stop() 174 | 175 | stopHandler.called.should.equal(false) 176 | 177 | await listener.start() 178 | listener.stop() 179 | listener.stop() 180 | 181 | stopHandler.calledOnce.should.equal(true) 182 | }) 183 | }) 184 | }) 185 | --------------------------------------------------------------------------------