├── .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 | [](https://www.npmjs.com/package/shellies)
3 | [](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 |
--------------------------------------------------------------------------------