diff --git a/.env.template b/.env.template deleted file mode 100644 index 532744e4..00000000 --- a/.env.template +++ /dev/null @@ -1,3 +0,0 @@ -CLIENT_ID="" -CLIENT_SECRET="" -REFRESH_TOKEN="" diff --git a/.github/renovate.json b/.github/renovate.json index 64ae4308..cfea8199 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,7 +2,7 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base", - "schedule:automergeMonthly", + "schedule:automergeMonthly" ], "packageRules": [ { diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 96a79e55..c2d27925 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -51,25 +51,6 @@ jobs: run: npm clean-install - name: Validate CSS Sources run: npm run validate:css - unit: - runs-on: ubuntu-latest - name: 'Unit Tests' - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.node-version }} - cache: "npm" - - name: Install Dependencies - run: npm clean-install - - name: Execute Unit Tests - run: npm run test - env: - CLIENT_ID: ${{ secrets.CLIENT_ID }} - CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} - REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }} package-lock-is-up-to-date: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 23ccfcc4..259ded2a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules report .env* .DS_Store +/token.json diff --git a/README.md b/README.md index 085e10b1..b4ec3bd3 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,7 @@ cd ~/MagicMirror/modules && git clone https://github.com/CFenner/MMM-Netatmo net :warning: Note that the checkout folder is named `netatmo` and not `MMM-Netatmo` as the repository. -Navigate into the module folder and install missing dependencies: - -```shell -cd netatmo && npm ci --production --ignore-scripts -``` +Since v2.1.0: **No special dependencies and no others commands are now needed!** ### Connection to Netatmo Service API diff --git a/helper.js b/helper.js index 95155370..b5dd4722 100644 --- a/helper.js +++ b/helper.js @@ -2,11 +2,12 @@ * Module: MMM-Netatmo * * By Christopher Fenner https://github.com/CFenner + * Review @bugsounet https://github.com/bugsounet * MIT Licensed. */ const fs = require('fs') const path = require('path') -const URLSearchParams = require('@ungap/url-search-params') +const moment = require('moment') module.exports = { notifications: { @@ -15,117 +16,235 @@ module.exports = { DATA: 'NETATMO_DATA', DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', }, - start: function () { + + start () { console.log('Netatmo helper started ...') this.token = null }, - authenticate: async function (config) { - const self = this - self.config = config + async authenticate () { 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) + params.append('refresh_token', this.refreshToken) + params.append('client_id', this.clientId) + params.append('client_secret', this.clientSecret) try { - const result = await fetch('https://' + self.config.apiBase + self.config.authEndpoint, { + const result = await fetch(`https://${this.config.apiBase}${this.config.authEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params, - }).then(response => response.json()) + }).then((response) => response.json()) if (result.error) { - throw new Error(result.error + ': ' + result.error_description) - } + throw new Error(`${result.error} : ${result.error_description}`) + }; + + this.token = result.access_token + this.token_expires_in = result.expires_in + this.refreshToken = result.refresh_token + console.log('Netatmo: Authenticated') + + // write in token file and provides for token refresh + this.writeToken(result) + if (result.expires_in) { + const expireAt = moment(Date.now() + (result.expires_in * 1000)).format('LLLL') + console.log(`Netatmo: New Token Expire ${expireAt}`) + setTimeout(() => this.authenticateRefresh(result.refresh_token), (result.expires_in - 60) * 1000) + }; - 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, { + // inform module AUTH ok + this.sendSocketNotification(this.notifications.AUTH_RESPONSE, { status: 'OK', }) } catch (error) { - console.log('error:', error) - self.sendSocketNotification(self.notifications.AUTH_RESPONSE, { + console.error('Netatmo:', error) + this.sendSocketNotification(this.notifications.AUTH_RESPONSE, { payloadReturn: error, status: 'NOTOK', message: error, }) - } + }; }, - loadData: async function (config) { - const self = this - self.config = config - if (self.config.mockData === true) { - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + async loadData () { + if (this.config.mockData === true) { + this.sendSocketNotification(this.notifications.DATA_RESPONSE, { payloadReturn: this.mockData(), status: 'OK', }) return - } - if (self.token === null || self.token === undefined) { - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + }; + + if (!this.token) { + this.sendSocketNotification(this.notifications.DATA_RESPONSE, { payloadReturn: 400, status: 'INVALID_TOKEN', message: 'token not set', }) return - } + }; try { - let result = await fetch('https://' + self.config.apiBase + self.config.dataEndpoint, { + let result = await fetch(`https://${this.config.apiBase}${this.config.dataEndpoint}`, { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${self.token}`, + Authorization: `Bearer ${this.token}`, }, }) if (result.status === 403) { - console.log('status code:', result.status, '\n', result.statusText) - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + console.warn(`Netatmo: status code: ${result.status} (${result.statusText})`) + this.sendSocketNotification(this.notifications.DATA_RESPONSE, { payloadReturn: result.statusText, status: 'INVALID_TOKEN', message: result, }) return - } + }; result = await result.json() if (result.error) { throw new Error(result.error.message) - } + }; - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + this.sendSocketNotification(this.notifications.DATA_RESPONSE, { payloadReturn: result.body.devices, status: 'OK', }) } catch (error) { - console.log('error:', error) - self.sendSocketNotification(self.notifications.DATA_RESPONSE, { + console.error('Netatmo:', error) + this.sendSocketNotification(this.notifications.DATA_RESPONSE, { payloadReturn: error, status: 'NOTOK', message: error, }) - } + }; }, - mockData: function () { + + mockData () { const sample = fs.readFileSync(path.join(__dirname, 'sample', 'sample.json'), 'utf8') return JSON.parse(sample) }, - socketNotificationReceived: function (notification, payload) { + + socketNotificationReceived (notification, payload) { switch (notification) { + case 'INIT': + this.Init(payload) + break case this.notifications.AUTH: - this.authenticate(payload) + this.authenticate() break case this.notifications.DATA: - this.loadData(payload) + this.loadData() break } }, + + Init (config) { + this.config = config + if (!this.config.clientId) { + console.error('Netatmo: clientId not set in config.') + return + } + this.clientId = this.config.clientId + + if (!this.config.clientSecret) { + console.error('Netatmo: clientSecret not set in config.') + return + } + this.clientSecret = this.config.clientSecret + + const refreshToken = this.readToken() + this.refreshToken = refreshToken || this.config.refresh_token + + if (!this.refreshToken) { + console.error('Netatmo: refresh_token not set in config.') + return + } + + console.log('Netatmo: Initialized') + this.authenticate() + }, + + /* from MMM-NetatmoThermostat + * @bugsounet + */ + readToken () { + const file = path.resolve(__dirname, './token.json') + // check presence of token.json + if (fs.existsSync(file)) { + console.log('Netatmo: using token.json file') + const tokenFile = JSON.parse(fs.readFileSync(file)) + const refreshToken = tokenFile.refresh_token + if (!refreshToken) { + console.error('Netatmo: Token not found in token.json file') + console.log('Netatmo: using refresh_token from config') + return null + } + return refreshToken + } + // Token file not used + console.log('Netatmo: using refresh_token from config') + return null + }, + + writeToken (token) { + try { + const file = path.resolve(__dirname, './token.json') + fs.writeFileSync(file, JSON.stringify(token)) + console.log('Netatmo: token.json was written successfully') + return token + } catch (error) { + ; + console.error('Netatmo: writeToken error', error.message) + return null + }; + }, + + // Refresh Token + async authenticateRefresh (refreshToken) { + console.log('Netatmo: Refresh Token') + const params = new URLSearchParams() + params.append('grant_type', 'refresh_token') + params.append('refresh_token', refreshToken) + params.append('client_id', this.clientId) + params.append('client_secret', this.clientSecret) + + try { + const result = await fetch(`https://${this.config.apiBase}${this.config.authEndpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params, + }).then((response) => response.json()) + + if (result.error) { + throw new Error(result.error) + }; + + this.writeToken(result) + this.token = result.access_token + console.log('Netatmo: TOKEN Updated') + + if (result.expires_in) { + const expireAt = moment(Date.now() + (result.expires_in * 1000)).format('LLLL') + console.log(`Netatmo: New Token Expire ${expireAt}`) + setTimeout(() => this.authenticateRefresh(result.refresh_token), (result.expires_in - 60) * 1000) + }; + + this.sendSocketNotification(this.notifications.AUTH_RESPONSE, { + status: 'OK', + }) + } catch (error) { + console.error('Netatmo:', error) + this.sendSocketNotification(this.notifications.AUTH_RESPONSE, { + payloadReturn: error, + status: 'NOTOK', + message: error, + }) + console.log('Netatmo: Retry login in 60 sec') + setTimeout(() => this.authenticate(this.config), 60 * 1000) + } + }, } diff --git a/helper.test.js b/helper.test.js deleted file mode 100644 index c0d65377..00000000 --- a/helper.test.js +++ /dev/null @@ -1,151 +0,0 @@ -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', async () => { - // prepare - expect(moduleUnderTest).not.toHaveProperty('token') - moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => {}) - await moduleUnderTest.authenticate({ - apiBase, - authEndpoint, - refresh_token: process.env.REFRESH_TOKEN, - clientId: process.env.CLIENT_ID, - clientSecret: process.env.CLIENT_SECRET, - }) - expect(moduleUnderTest).toHaveProperty('token') - 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 - await moduleUnderTest.loadData({ - apiBase, - dataEndpoint, - }) - // assert - expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() - }) - - test('with missing token', async () => { - // 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 - await moduleUnderTest.loadData({ - apiBase, - dataEndpoint, - }) - // assert - expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() - }) - - test('with invalid token', async () => { - moduleUnderTest.token = 'something' - // prepare - moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { - expect(type).toBe(moduleUnderTest.notifications.DATA_RESPONSE) - expect(payload).toHaveProperty('status', 'INVALID_TOKEN') - }) - // test - await moduleUnderTest.loadData({ - apiBase, - dataEndpoint, - }) - // assert - expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() - }) - }) - - describe('authentication', () => { - test('with refresh_token from config', async () => { - // 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 - await 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', async () => { - // 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 - await 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', async () => { - // 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 - await 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 3e85b8d9..8157fcde 100755 --- a/netatmo.js +++ b/netatmo.js @@ -64,24 +64,23 @@ Module.register('netatmo', { RAIN_PER_DAY: 'sum_rain_24', }, // init method - start: function () { - const self = this + start () { Log.info(`Starting module: ${this.name}`) - self.loaded = false - self.moduleList = [] + this.loaded = false + this.moduleList = [] - // get a new token at start-up. When receive, GET_CAMERA_EVENTS will be requested - setTimeout(function () { - self.sendSocketNotification(self.notifications.DATA, self.config) + // get a new token at start-up. + setTimeout(() => { + // best way is using initialize at start and if auth OK --> fetch data + this.sendSocketNotification('INIT', this.config) }, this.config.initialDelay * 1000) // set auto-update - setInterval(function () { - // request directly the data, with the previous token. When the token will become invalid (error 403), it will be requested again - self.sendSocketNotification(self.notifications.DATA, self.config) + setInterval(() => { + this.sendSocketNotification(this.notifications.DATA) }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000) }, - updateModuleList: function (stationList) { + updateModuleList (stationList) { let moduleList = [] for (const station of stationList) { @@ -111,7 +110,7 @@ Module.register('netatmo', { } this.moduleList = moduleList }, - getModule: function (module, stationName) { + getModule (module, stationName) { const result = {} result.name = module.module_name @@ -132,7 +131,7 @@ Module.register('netatmo', { name: measurement, value: this.getValue(measurement, 0), unit: this.getUnit(measurement), - icon: this.getIcon(measurement, 0) + ' flash red', + icon: `${this.getIcon(measurement, 0)} flash red`, label: this.translate(measurement.toUpperCase()), }) @@ -245,11 +244,13 @@ Module.register('netatmo', { } return result }, - getMeasurement: function (module, measurement, value) { + getMeasurement (module, measurement, value) { + /* eslint-disable no-param-reassign */ value = value || module.dashboard_data[measurement] if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) { value = value || 'undefined' } + /* eslint-enable no-param-reassign */ return { name: measurement, value: this.getValue(measurement, value), @@ -258,12 +259,12 @@ Module.register('netatmo', { label: this.translate(measurement.toUpperCase()), } }, - kebabCase: function (name) { + kebabCase (name) { return name.replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase() }, - getValue: function (measurement, value) { + getValue (measurement, value) { if (!value) { return value } switch (measurement) { case this.measurement.CO2: @@ -288,7 +289,7 @@ Module.register('netatmo', { return value.toFixed(0)// + ' m/s' case this.measurement.WIND_ANGLE: case this.measurement.GUST_ANGLE: - return this.getDirection(value) + ' | ' + value// + '°' + return `${this.getDirection(value)} | ${value}`// + '°' case this.measurement.TEMPERATURE_TREND: case this.measurement.PRESSURE_TREND: return this.translate(value.toUpperCase()) @@ -296,7 +297,7 @@ Module.register('netatmo', { return value } }, - getUnit: function (measurement) { + getUnit (measurement) { switch (measurement) { case this.measurement.CO2: return 'ppm' @@ -325,7 +326,7 @@ Module.register('netatmo', { return '' } }, - getDirection: function (value) { + getDirection (value) { if (value < 11.25) return 'N' if (value < 33.75) return 'NNE' if (value < 56.25) return 'NE' @@ -344,13 +345,13 @@ Module.register('netatmo', { if (value < 348.75) return 'NNW' return 'N' }, - getCO2Status: function (value) { + getCO2Status (value) { if (!value || value === 'undefined' || value < 0) return 'undefined' if (value >= this.config.thresholdCO2Bad) return 'bad' if (value >= this.config.thresholdCO2Average) return 'average' return 'good' }, - getIcon: function (dataType, value) { + getIcon (dataType, value) { switch (dataType) { // case this.measurement.CO2: // return 'fa-lungs' @@ -378,26 +379,26 @@ Module.register('netatmo', { return '' } }, - getTrendIcon: function (value) { + getTrendIcon (value) { if (value === 'stable') return 'fa-chevron-circle-right' if (value === 'down') return 'fa-chevron-circle-down' if (value === 'up') return 'fa-chevron-circle-up' if (value === 'undefined') return 'fa-times-circle' }, - getBatteryIcon: function (value) { + getBatteryIcon (value) { if (value > 80) return 'fa-battery-full' if (value > 60) return 'fa-battery-three-quarters' if (value > 40) return 'fa-battery-half' if (value > 20) return 'fa-battery-quarter' return 'fa-battery-empty flash red' }, - getStyles: function () { + getStyles () { return [`${this.name}.${this.config.design}.css`] }, - getTemplate: function () { + getTemplate () { return `${this.name}.${this.config.design}.njk` }, - getTemplateData: function () { + getTemplateData () { return { loaded: this.loaded, showLastMessage: this.config.showLastMessage, @@ -417,7 +418,7 @@ Module.register('netatmo', { labelLoading: this.translate('LOADING'), } }, - getTranslations: function () { + getTranslations () { return { en: 'l10n/en.json', // fallback language cs: 'l10n/cs.json', @@ -430,29 +431,31 @@ Module.register('netatmo', { sv: 'l10n/sv.json', } }, - socketNotificationReceived: function (notification, payload) { - const self = this - Log.debug('received ' + notification) + socketNotificationReceived (notification, payload) { + Log.debug(`Netatmo: received ${notification}`) switch (notification) { - case self.notifications.AUTH_RESPONSE: + case this.notifications.AUTH_RESPONSE: if (payload.status === 'OK') { - self.sendSocketNotification(self.notifications.DATA, self.config) + console.log('Netatmo: AUTH OK') + this.sendSocketNotification(this.notifications.DATA) } else { - console.log('AUTH FAILED ' + payload.message) + console.error(`Netatmo: AUTH FAILED ${payload.message}`) } break - case self.notifications.DATA_RESPONSE: + case this.notifications.DATA_RESPONSE: if (payload.status === 'OK') { console.log('Devices %o', payload.payloadReturn) const stationList = payload.payloadReturn - self.updateModuleList(stationList) - self.updateDom(self.config.animationSpeed) + this.updateModuleList(stationList) + this.updateDom(this.config.animationSpeed) } else if (payload.status === 'INVALID_TOKEN') { // node_module has no valid token, reauthenticate - console.log('DATA FAILED, refreshing token') - self.sendSocketNotification(self.notifications.AUTH, self.config) + console.error('DATA FAILED, refreshing token') + // i'm not agree with this... can have error 403 loop + // --> managed with node_helper + // this.sendSocketNotification(this.notifications.AUTH) } else { - console.log('DATA FAILED ' + payload.message) + console.error(`Netatmo: DATA FAILED ${payload.message}`) } break } diff --git a/node_helper.js b/node_helper.js index 3b9899da..3b9b38ac 100644 --- a/node_helper.js +++ b/node_helper.js @@ -5,6 +5,6 @@ * MIT Licensed. */ const NodeHelper = require('node_helper') -const helper = require('./helper') +const helper = require('./helper.js') module.exports = NodeHelper.create(helper) diff --git a/package-lock.json b/package-lock.json index 3daedefb..fa861ad6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,13 @@ { "name": "netatmo", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "netatmo", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", - "dependencies": { - "@ungap/url-search-params": "0.2.2" - }, "devDependencies": { "@snyk/protect": "1.1291.1", "dotenv": "16.3.1", @@ -1479,11 +1476,6 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/@ungap/url-search-params": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@ungap/url-search-params/-/url-search-params-0.2.2.tgz", - "integrity": "sha512-qQsguKXZVKdCixOHX9jqnX/K/1HekPDpGKyEcXHT+zR6EjGA7S4boSuelL4uuPv6YfhN0n8c4UxW+v/Z3gM2iw==" - }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", diff --git a/package.json b/package.json index 4c6be3ac..6cb1742c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netatmo", - "version": "2.0.0", + "version": "2.1.0", "description": "A module for the MagicMirror² to display information about your rooms climate from your Netatmo system.", "main": "netatmo.js", "scripts": { @@ -51,8 +51,5 @@ "stylelint": "16.6.1", "stylelint-config-standard": "36.0.1" }, - "snyk": true, - "dependencies": { - "@ungap/url-search-params": "0.2.2" - } + "snyk": true }