From 3a9e70674409f24a693822f2b6a330957eda768a Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 15 Aug 2023 00:50:36 +0200 Subject: [PATCH] fix: netatmo api authentication (#182) * fix issue * fix issue * change test names * add compose file * ignore docker folders * add compose description * fix markdownlint findings * add config * rename test * remove old files --- .github/workflows/validation.yml | 3 +- .gitignore | 2 + README.md | 8 +- compose/.gitignore | 4 + compose/README.md | 24 +++++ compose/config/config.js.template | 61 +++++++++++ compose/docker-compose.yml | 21 ++++ helper.js | 132 ++++++++++++++++++++++++ helper.test.js | 142 +++++++++++++++++++++++++ netatmo.js | 2 +- node_helper.js | 4 +- node_helper_impl.js | 165 ------------------------------ node_helper_impl.test.js | 11 -- package-lock.json | 65 +++++++++++- package.json | 7 +- 15 files changed, 463 insertions(+), 188 deletions(-) create mode 100644 compose/.gitignore create mode 100644 compose/README.md create mode 100644 compose/config/config.js.template create mode 100644 compose/docker-compose.yml create mode 100644 helper.js create mode 100644 helper.test.js delete mode 100644 node_helper_impl.js delete mode 100644 node_helper_impl.test.js diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index c3f2c5c..0f82cca 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -71,6 +71,5 @@ jobs: cache: "npm" - name: Install Dependencies run: npm clean-install - - name: Validate JS Sources + - name: Execute Unit Tests run: npm run test - diff --git a/.gitignore b/.gitignore index 3f1bc52..eb83b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # Ignore all node modules. node_modules report +.env +.DS_Store diff --git a/README.md b/README.md index ac387bf..267e4b2 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ cd netatmo && npm ci --production --ignore-scripts ### Connection to Netatmo Service API -To be able to access your data, you need to have an Netatmo Application. Create your personal app in the [Netatmo developer portal][dev-portal] and you will get an `APP_ID` and an `APP_SECRET` which you will need to enter in your [mirror configuration](#configuration). +To be able to access your data, you need to have an Netatmo Application. Create your personal app in the [Netatmo developer portal][dev-portal] and you will get an `APP_ID` and an `APP_SECRET` which you will need to enter in your [mirror configuration](#configuration). On the same page, scroll to *Token Generator* and create a token with the `read_station` scope. During that process you will grant your previously created Netatmo app access to your Netatmo weather station. You will actually not need the `access_token`, but the `refresh_token`. This will also go into your mirror configuration. #### Sample Data @@ -55,8 +55,7 @@ To run the module properly, you need to add the following data to your config.js config: { clientId: '', // your app id clientSecret: '', // your app secret - username: '', // your netatmo username - password: '', // your netatmo password + refresh_token: '', // your generated refresh token } } ``` @@ -69,8 +68,7 @@ The following properties can be configured: |---|---|---|---| |`clientId`|The ID of your Netatmo [application][dev-portal].||yes| |`clientSecret`|The app secret of your Netatmo [application][dev-portal].||yes| -|`username`|Username for your Netatmo weather station.||yes| -|`password`|Password for your Netatmo weather station.||yes| +|`refresh_token`|Generated refresh token for your Netatmo app and Netatmo instance.||yes| |`refreshInterval`|How often does the content needs to be updated (minutes)? Data is updated by netatmo every 10 minutes|`3`|no| |`moduleOrder`|The rendering order of your weather modules, ommit a module to hide the output. **Example:** `["Kitchen","Kid's Bedroom","Garage","Garden"]` Be aware that you need to use the module names that you set in the netatmo configuration.||no| |`dataOrder`|The rendering order of the data types of a module, ommit a data type to hide the output. **Example:** `["Noise","Pressure","CO2","Humidity","Temperature","Rain"]`||no| diff --git a/compose/.gitignore b/compose/.gitignore new file mode 100644 index 0000000..ab85fb4 --- /dev/null +++ b/compose/.gitignore @@ -0,0 +1,4 @@ +config/** +css/ +modules/ +!config/config.js.template diff --git a/compose/README.md b/compose/README.md new file mode 100644 index 0000000..b73492b --- /dev/null +++ b/compose/README.md @@ -0,0 +1,24 @@ +# Integration Testing with Docker Compose + +To test the module in a MagicMirror instance: + +- run `npm run docker:server` to start MagicMirror Docker +- run `npm run docker:clone` to clone the module into the modules folder +- use `docker exec -it mm bash` and `git checkout ` to load a specific branch +- run `npm run docker:install` to install the modules dependencies +- add the module config to the `config/config.js` + + ```yaml + { + module: 'netatmo', + position: 'bottom_left', + header: 'Netatmo', + config: { + clientId: '', + clientSecret: '', + refresh_token: '', + }, + }, + ``` + +- open MagicMirror ui at [`http://0.0.0.0:8080`](http://0.0.0.0:8080) diff --git a/compose/config/config.js.template b/compose/config/config.js.template new file mode 100644 index 0000000..1abc2a5 --- /dev/null +++ b/compose/config/config.js.template @@ -0,0 +1,61 @@ +/* MagicMirror² Config Sample + * + * By Michael Teeuw https://michaelteeuw.nl + * MIT Licensed. + * + * For more information on how you can configure this file + * see https://docs.magicmirror.builders/configuration/introduction.html + * and https://docs.magicmirror.builders/modules/configuration.html + * + * You can use environment variables using a `config.js.template` file instead of `config.js` + * which will be converted to `config.js` while starting. For more information + * see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables + */ +let config = { + address: "0.0.0.0", // Address to listen on, can be: + // - "localhost", "127.0.0.1", "::1" to listen on loopback interface + // - another specific IPv4/6 to listen on a specific interface + // - "0.0.0.0", "::" to listen on any interface + // Default, when address config is left out or empty, is "localhost" + port: 8080, + basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy + // you must set the sub path here. basePath must end with a / + ipWhitelist: [], // Set [] to allow all IP addresses + // or add a specific IPv4 of 192.168.1.5 : + // ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"], + // or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format : + // ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"], + + useHttps: false, // Support HTTPS or not, default "false" will use HTTP + httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true + httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true + + language: "en", + locale: "en-US", + logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging + timeFormat: 24, + units: "metric", + + modules: [ + { + module: "alert", + }, + { + module: 'netatmo', + position: 'bottom_left', + header: 'Netatmo', + config: { + clientId: '${CLIENT_ID}', + clientSecret: '${CLIENT_SECRET}', + refresh_token: '${REFRESH_TOKEN}', + }, + }, + { + module: "clock", + position: "top_left" + }, + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") {module.exports = config;} diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..8ee2b95 --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' + +services: + magicmirror: + container_name: mm + image: karsten13/magicmirror:fat + volumes: + - ./config:/opt/magic_mirror/config + - ./modules:/opt/magic_mirror/modules + - ./css:/opt/magic_mirror/css + environment: + TZ: Europe/Berlin + MM_SHOW_CURSOR: "true" + env_file: ../.env + ports: + - 8080:8080 + restart: unless-stopped + command: + - npm + - run + - server diff --git a/helper.js b/helper.js new file mode 100644 index 0000000..19be9f2 --- /dev/null +++ b/helper.js @@ -0,0 +1,132 @@ +/* Magic Mirror + * Module: MagicMirror-Netatmo-Module + * + * By Christopher Fenner https://github.com/CFenner + * MIT Licensed. + */ +const fs = require('fs') +const path = require('path') +const fetch = require('sync-fetch') +const URLSearchParams = require('@ungap/url-search-params') + +module.exports = { + notifications: { + AUTH: 'NETATMO_AUTH', + AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', + DATA: 'NETATMO_DATA', + DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', + }, + start: function () { + console.log('Netatmo helper started ...') + this.token = null + }, + authenticate: function (config) { + const self = this + self.config = config + + const params = new URLSearchParams() + params.append('grant_type', 'refresh_token') + params.append('refresh_token', self.refresh_token || self.config.refresh_token) + params.append('client_id', self.config.clientId) + params.append('client_secret', self.config.clientSecret) + + try { + const result = fetch('https://' + self.config.apiBase + self.config.authEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params, + }).json() + + if (result.error) { + throw new Error(result.error + ': ' + result.error_description) + } + + console.log('UPDATING TOKEN ' + result.access_token) + self.token = result.access_token + self.token_expires_in = result.expires_in + self.refresh_token = result.refresh_token + // we got a new token, save it to main file to allow it to request the datas + self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { + status: 'OK', + }) + } catch (error) { + console.log('error:', error) + self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { + payloadReturn: error, + status: 'NOTOK', + message: error, + }) + } + }, + loadData: function (config) { + const self = this + self.config = config + + if (self.config.mockData === true) { + self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + payloadReturn: this.mockData(), + status: 'OK', + }) + return + } + if (self.token === null || self.token === undefined) { + self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + payloadReturn: 400, + status: 'INVALID_TOKEN', + message: 'token not set', + }) + return + } + + try { + let result = fetch('https://' + self.config.apiBase + self.config.dataEndpoint, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${self.token}`, + }, + }) + + if (result.status === 403) { + console.log('status code:', result.status, '\n', result.statusText) + self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + payloadReturn: result.statusText, + status: 'INVALID_TOKEN', + message: result, + }) + return + } + + result = result.json() + + if (result.error) { + throw new Error(result.error.message) + } + + self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + payloadReturn: result.body.devices, + status: 'OK', + }) + } catch (error) { + console.log('error:', error) + self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + payloadReturn: error, + status: 'NOTOK', + message: error, + }) + } + }, + mockData: function () { + const sample = fs.readFileSync(path.join(__dirname, 'sample', 'sample.json'), 'utf8') + return JSON.parse(sample) + }, + socketNotificationReceived: function (notification, payload) { + switch (notification) { + case this.notifications.AUTH: + this.authenticate(payload) + break + case this.notifications.DATA: + this.loadData(payload) + break + } + }, +} diff --git a/helper.test.js b/helper.test.js new file mode 100644 index 0000000..f8b935f --- /dev/null +++ b/helper.test.js @@ -0,0 +1,142 @@ +require('dotenv').config() +const moduleUnderTest = require('./helper.js') + +const apiBase = 'api.netatmo.com' +const authEndpoint = '/oauth2/token' +const dataEndpoint = '/api/getstationsdata' + +describe('helper', () => { + afterEach(() => { + delete moduleUnderTest.token + delete moduleUnderTest.token_expires_in + delete moduleUnderTest.refresh_token + delete moduleUnderTest.sendSocketNotification + }) + describe('data', () => { + test('existing token', () => { + moduleUnderTest.token = process.env.TOKEN + // prepare + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.DATA_RESPONSE) + expect(payload).toHaveProperty('status', 'OK') + expect(payload).toHaveProperty('payloadReturn') + expect(payload.payloadReturn).toHaveLength(2) + }) + expect(moduleUnderTest).toHaveProperty('token') + // test + moduleUnderTest.loadData({ + apiBase, + dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('with missing token', () => { + // moduleUnderTest.token = process.env.TOKEN + // prepare + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.DATA_RESPONSE) + expect(payload).toHaveProperty('status', 'INVALID_TOKEN') + }) + // test + moduleUnderTest.loadData({ + apiBase, + dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('with invalid token', () => { + moduleUnderTest.token = 'something' + // prepare + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.DATA_RESPONSE) + expect(payload).toHaveProperty('status', 'INVALID_TOKEN') + }) + // test + moduleUnderTest.loadData({ + apiBase, + dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + }) + + describe('authentication', () => { + test('with refresh_token from config', () => { + // prepare + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) + expect(payload).toHaveProperty('status', 'OK') + }) + expect(moduleUnderTest).not.toHaveProperty('token') + expect(moduleUnderTest).not.toHaveProperty('token_expires_in') + expect(moduleUnderTest).not.toHaveProperty('refresh_token') + // test + moduleUnderTest.authenticate({ + apiBase, + authEndpoint, + refresh_token: process.env.REFRESH_TOKEN, + clientId: process.env.CLIENT_ID, + clientSecret: process.env.CLIENT_SECRET, + }) + // assert + expect(moduleUnderTest).toHaveProperty('token') + expect(moduleUnderTest).toHaveProperty('token_expires_in') + expect(moduleUnderTest).toHaveProperty('refresh_token') + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('with refresh_token from object', () => { + // prepare + moduleUnderTest.refresh_token = process.env.REFRESH_TOKEN + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) + expect(payload).toHaveProperty('status', 'OK') + }) + expect(moduleUnderTest).not.toHaveProperty('token') + expect(moduleUnderTest).not.toHaveProperty('token_expires_in') + expect(moduleUnderTest).toHaveProperty('refresh_token') + // test + moduleUnderTest.authenticate({ + apiBase, + authEndpoint, + refresh_token: '', + clientId: process.env.CLIENT_ID, + clientSecret: process.env.CLIENT_SECRET, + }) + // assert + expect(moduleUnderTest).toHaveProperty('token') + expect(moduleUnderTest).toHaveProperty('token_expires_in') + expect(moduleUnderTest).toHaveProperty('refresh_token') + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('without refresh_token', () => { + // prepare + moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { + expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) + expect(payload).toHaveProperty('status', 'NOTOK') + }) + expect(moduleUnderTest).not.toHaveProperty('token') + expect(moduleUnderTest).not.toHaveProperty('token_expires_in') + expect(moduleUnderTest).not.toHaveProperty('refresh_token') + // test + moduleUnderTest.authenticate({ + apiBase, + authEndpoint, + refresh_token: '', + clientId: process.env.CLIENT_ID, + clientSecret: process.env.CLIENT_SECRET, + }) + // assert + expect(moduleUnderTest).not.toHaveProperty('token') + expect(moduleUnderTest).not.toHaveProperty('token_expires_in') + expect(moduleUnderTest).not.toHaveProperty('refresh_token') + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + }) +}) diff --git a/netatmo.js b/netatmo.js index 81247fa..73ac3ab 100755 --- a/netatmo.js +++ b/netatmo.js @@ -4,7 +4,7 @@ * By Christopher Fenner http://github.com/CFenner * MIT Licensed. */ -/* global Module, Log */ +/* global Module */ Module.register('netatmo', { // default config defaults: { diff --git a/node_helper.js b/node_helper.js index 3fb6fa9..fc7da9f 100644 --- a/node_helper.js +++ b/node_helper.js @@ -5,6 +5,6 @@ * MIT Licensed. */ const NodeHelper = require('node_helper') -const api = require('./api') +const helper = require('./helper') -module.exports = NodeHelper.create(api) +module.exports = NodeHelper.create(helper) diff --git a/node_helper_impl.js b/node_helper_impl.js deleted file mode 100644 index 44481bf..0000000 --- a/node_helper_impl.js +++ /dev/null @@ -1,165 +0,0 @@ -/* Magic Mirror - * Module: MagicMirror-Netatmo-Module - * - * By Christopher Fenner https://github.com/CFenner - * MIT Licensed. - */ -const fs = require('fs') -const path = require('path') -const https = require('https') -const URLSearchParams = require('@ungap/url-search-params') - -module.exports = { - notifications: { - AUTH: 'NETATMO_AUTH', - AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', - DATA: 'NETATMO_DATA', - DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', - }, - start: function () { - console.log('Netatmo helper started ...') - this.token = null - this.token_time = null - }, - authenticate: function (config) { - const self = this - self.config = config - - // TODO: only update if token is invalid - - const req = https.request({ - hostname: self.config.apiBase, - path: self.config.authEndpoint, - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }, self.callbackAuthenticate.bind(self)) - - req.on('error', function (e) { - console.log('There is a problem with your request:', e.message) - self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { - // instanceID: self.config.instanceID, - payloadReturn: e.message, - }) - }) - - req.write(new URLSearchParams({ - scope: 'read_station', - grant_type: 'password', - username: self.config.username, - password: self.config.password, - client_id: self.config.clientId, - client_secret: self.config.clientSecret, - }).toString()) - - req.end() - }, - loadData: function (config) { - const self = this - self.config = config - if (self.config.mockData === true) { - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: this.mockData(), - status: 'OK', - }) - return - } - if (self.token === null) { - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: 400, - status: 'INVALID_TOKEN', - message: 'token not set', - }) - return - } - - const req = https.request({ - hostname: self.config.apiBase, - path: self.config.dataEndpoint, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${self.token}`, - }, - }, self.callbackData.bind(self)) - - req.on('error', function (e) { - console.log('There is a problem with your request:', e.message) - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: e.message, - status: 'NOTOK', - message: e.message, - }) - }) - req.end() - }, - mockData: function () { - const sample = fs.readFileSync(path.join(__dirname, 'sample', 'sample.json'), 'utf8') - return JSON.parse(sample) - }, - callbackAuthenticate: function (response) { - const self = this - let result = '' - - response.on('error', function (e) { console.log('error', e) }) - response.on('data', function (chunk) { result += chunk }) - response.on('end', function () { - result = JSON.parse(result) - if (response.statusCode === 200) { - console.log('UPDATING TOKEN ' + result.access_token) - self.token = result.access_token - self.token_time = new Date() - // we got a new token, save it to main file to allow it to request the datas - self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { - status: 'OK', - }) - } else { - console.log('status code:', response.statusCode, '\n', result) - self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { - // instanceID: self.config.instanceID, - payloadReturn: response.statusCode, - status: 'NOTOK', - message: result, - }) - } - }) - }, - callbackData: function (response) { - const self = this - let result = '' - - response.on('error', function (e) { console.log('error', e) }) - response.on('data', function (chunk) { result += chunk }) - response.on('end', function () { - result = JSON.parse(result) - if (response.statusCode === 200) { - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: result.body.devices, - status: 'OK', - }) - } else if (response.statusCode === 403) { - console.log('status code:', response.statusCode, '\n', result) - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: response.statusCode, - status: 'INVALID_TOKEN', - message: result, - }) - } else { - console.log('status code:', response.statusCode, '\n', result) - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { - payloadReturn: response.statusCode, - status: 'NOTOK', - message: result, - }) - } - }) - }, - socketNotificationReceived: function (notification, payload) { - switch (notification) { - case this.notifications.AUTH: - this.authenticate(payload) - break - case this.notifications.DATA: - this.loadData(payload) - break - } - }, -} diff --git a/node_helper_impl.test.js b/node_helper_impl.test.js deleted file mode 100644 index 47a3374..0000000 --- a/node_helper_impl.test.js +++ /dev/null @@ -1,11 +0,0 @@ -const moduleName = 'node_helper_impl' -const moduleUnderTest = require('./' + moduleName + '.js') - -describe(moduleName, () => { - test('test notifications map', () => { - expect(moduleUnderTest.notifications).toHaveProperty('AUTH', 'NETATMO_AUTH') - expect(moduleUnderTest.notifications).toHaveProperty('AUTH_RESPONSE', 'NETATMO_AUTH_RESPONSE') - expect(moduleUnderTest.notifications).toHaveProperty('DATA', 'NETATMO_DATA') - expect(moduleUnderTest.notifications).toHaveProperty('DATA_RESPONSE', 'NETATMO_DATA_RESPONSE') - }) -}) diff --git a/package-lock.json b/package-lock.json index af99de0..6074d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@ungap/url-search-params": "^0.2.2" + "@ungap/url-search-params": "^0.2.2", + "sync-fetch": "^0.5.2" }, "devDependencies": { "@snyk/protect": "^1.1200.0", + "dotenv": "^16.3.1", "eslint": "^8.7.0", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.25.4", @@ -2401,6 +2403,18 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.490", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz", @@ -5099,6 +5113,25 @@ "ms": "^2.1.1" } }, + "node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6450,6 +6483,17 @@ "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", "dev": true }, + "node_modules/sync-fetch": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.5.2.tgz", + "integrity": "sha512-6gBqqkHrYvkH65WI2bzrDwrIKmt3U10s4Exnz3dYuE5Ah62FIfNv/F63inrNhu2Nyh3GH5f42GKU3RrSJoaUyQ==", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -6535,6 +6579,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", @@ -6768,6 +6817,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b0ca9d6..5725d35 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "fix:js": "eslint --fix **/*.js", "test": "npm run test:js", "test:js": "jest *.test.js", + "docker:server": "docker compose --file compose/docker-compose.yml up", + "docker:clone": "git clone https://github.com/CFenner/MMM-Netatmo.git compose/modules/netatmo", + "docker:install": "npm clean-install --production --ignore-scripts --prefix compose/modules/netatmo", "snyk-protect": "snyk-protect", "prepublish": "npm run snyk-protect" }, @@ -29,6 +32,7 @@ "homepage": "https://github.com/CFenner/MagicMirror-Netatmo-Module#readme", "devDependencies": { "@snyk/protect": "^1.1200.0", + "dotenv": "^16.3.1", "eslint": "^8.7.0", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.25.4", @@ -43,6 +47,7 @@ }, "snyk": true, "dependencies": { - "@ungap/url-search-params": "^0.2.2" + "@ungap/url-search-params": "^0.2.2", + "sync-fetch": "^0.5.2" } }