From aa4d300d28f30837c29a5a117d2d68e55f3a6263 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Aug 2023 20:41:19 +0200 Subject: [PATCH 01/10] fix issue --- .github/workflows/validation.yml | 3 +- .gitignore | 2 + README.md | 8 +- helper.js | 132 +++++++++ helper.test.js | 151 ++++++++++ module.js | 458 +++++++++++++++++++++++++++++++ netatmo.js | 454 +----------------------------- node_helper.js | 4 +- package-lock.json | 65 ++++- package.json | 4 +- 10 files changed, 819 insertions(+), 462 deletions(-) create mode 100644 helper.js create mode 100644 helper.test.js create mode 100755 module.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/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..cd21f44 --- /dev/null +++ b/helper.test.js @@ -0,0 +1,151 @@ +const moduleUnderTest = require('./helper.js') + +const netatmo = require('./module.js') + +require('dotenv').config() + +describe('helper', () => { + beforeEach(() => { + }) + + 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: netatmo.defaults.apiBase, + dataEndpoint: netatmo.defaults.dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('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: netatmo.defaults.apiBase, + dataEndpoint: netatmo.defaults.dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + + test('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: netatmo.defaults.apiBase, + dataEndpoint: netatmo.defaults.dataEndpoint, + }) + // assert + expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() + }) + }) + + describe('authentication', () => { + test('verify 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') + }) + + 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: netatmo.defaults.apiBase, + authEndpoint: netatmo.defaults.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: netatmo.defaults.apiBase, + authEndpoint: netatmo.defaults.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('test authenticate fail', () => { + // 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: netatmo.defaults.apiBase, + authEndpoint: netatmo.defaults.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/module.js b/module.js new file mode 100755 index 0000000..b0f9b93 --- /dev/null +++ b/module.js @@ -0,0 +1,458 @@ +/* Magic Mirror + * Module: Netatmo + * + * By Christopher Fenner http://github.com/CFenner + * MIT Licensed. + */ +/* global Log */ +module.exports = { + // default config + defaults: { + initialDelay: 0, + updateInterval: 3, // every 3 minutes, refresh interval on netatmo is 10 minutes + animationSpeed: 1000, + design: 'classic', // or bubbles + horizontal: true, + lastMessageThreshold: 600, // in seconds (10 minutes) + showLastMessage: true, + showBattery: true, + showRadio: true, + showWiFi: true, + showTrend: true, + showMeasurementIcon: true, + showMeasurementLabel: true, + showStationName: true, + showModuleNameOnTop: false, + apiBase: 'api.netatmo.com', + authEndpoint: '/oauth2/token', + dataEndpoint: '/api/getstationsdata', + fontClassModuleName: 'xsmall', + fontClassPrimary: 'large', + fontClassSecondary: 'xsmall', + fontClassMeasurement: 'xsmall', + thresholdCO2Average: 800, + thresholdCO2Bad: 1800, + mockData: false, + }, + notifications: { + AUTH: 'NETATMO_AUTH', + AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', + DATA: 'NETATMO_DATA', + DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', + }, + moduleType: { + MAIN: 'NAMain', + INDOOR: 'NAModule4', + OUTDOOR: 'NAModule1', + RAIN: 'NAModule3', + WIND: 'NAModule2', + }, + measurement: { + CO2: 'CO2', + HUMIDITY: 'Humidity', + TEMPERATURE: 'Temperature', + TEMPERATURE_TREND: 'temp_trend', + PRESSURE: 'Pressure', + PRESSURE_TREND: 'pressure_trend', + NOISE: 'Noise', + WIND_STRENGTH: 'WindStrength', + WIND_ANGLE: 'WindAngle', + GUST_STRENGTH: 'GustStrength', + GUST_ANGLE: 'GustAngle', + RAIN: 'Rain', + RAIN_PER_HOUR: 'sum_rain_1', + RAIN_PER_DAY: 'sum_rain_24', + }, + // init method + start: function () { + const self = this + Log.info(`Starting module: ${this.name}`) + self.loaded = false + self.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) + }, 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) + }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000) + }, + updateModuleList: function (stationList) { + let moduleList = [] + + for (const station of stationList) { + moduleList.push(this.getModule(station, station.home_name)) + + station.modules.forEach(function (module) { + moduleList.push(this.getModule(module, station.home_name)) + }.bind(this)) + + if (station.reachable) { this.lastUpdate = station.dashboard_data.time_utc } + } + this.loaded = true + if (JSON.stringify(this.moduleList) === JSON.stringify(moduleList)) { + return + } + // reorder modules + if (this.config.moduleOrder && this.config.moduleOrder.length > 0) { + const reorderedModuleList = [] + for (const moduleName of this.config.moduleOrder) { + for (const module of moduleList) { + if (module.name === moduleName) { + reorderedModuleList.push(module) + } + } + } + moduleList = reorderedModuleList + } + this.moduleList = moduleList + }, + getModule: function (module, stationName) { + const result = {} + + result.name = module.module_name + if (this.config.showStationName) { + result.name = `${stationName} - ${result.name}` + } + result.measurementList = [] + + if (!module.reachable) { + let measurement = '' + if (module.type === this.moduleType.MAIN) { + measurement = 'wifi' + } else { + measurement = 'radio' + } + + result.measurementList.push({ + name: measurement, + value: this.getValue(measurement, 0), + unit: this.getUnit(measurement), + icon: this.getIcon(measurement, 0) + ' flash red', + label: this.translate(measurement.toUpperCase()), + }) + + return result + } + + // TODO check module.reachable + let primaryType = '' + let primaryValue = '' + let secondaryType = '' + let secondaryValue = '' + + // add module sensor measurements + switch (module.type) { + case this.moduleType.MAIN: + result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE)) + if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE_TREND)) } + result.measurementList.push(this.getMeasurement(module, this.measurement.NOISE)) + // break; fallthrough + case this.moduleType.INDOOR: + if (this.config.design === 'bubbles') { + secondaryType = this.measurement.CO2 + secondaryValue = module.dashboard_data[secondaryType] + result.secondary = { + value: this.getValue(secondaryType, secondaryValue), + unit: this.getUnit(secondaryType), + class: this.kebabCase(secondaryType), + visualClass: this.getCO2Status(secondaryValue), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.CO2)) + } + // break; fallthrough + case this.moduleType.OUTDOOR: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.TEMPERATURE + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE)) + } + if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE_TREND)) } + result.measurementList.push(this.getMeasurement(module, this.measurement.HUMIDITY)) + break + case this.moduleType.WIND: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.WIND_STRENGTH + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + secondaryType = this.measurement.WIND_ANGLE + secondaryValue = module.dashboard_data[secondaryType] + result.secondary = { + value: this.getValue(secondaryType, secondaryValue), + unit: this.getUnit(secondaryType), + class: this.kebabCase(secondaryType), + visualClass: 'xlarge wi wi-direction-up', + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_STRENGTH)) + result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_ANGLE)) + } + // $('
').addClass('visual xlarge wi wi-direction-up').css('transform', 'rotate(' + value + 'deg)') + result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_STRENGTH)) + result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_ANGLE)) + break + case this.moduleType.RAIN: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.RAIN + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN)) + } + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_HOUR)) + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_DAY)) + break + default: + break + } + // add module specific measurements + if (module.type === this.moduleType.MAIN) { + if (this.config.showWiFi) { result.measurementList.push(this.getMeasurement(module, 'wifi', module.wifi_status)) } + } else { + if (this.config.showRadio) { result.measurementList.push(this.getMeasurement(module, 'radio', module.rf_status)) } + if (this.config.showBattery) { result.measurementList.push(this.getMeasurement(module, 'battery', module.battery_percent)) } + } + // reorder measurements + if (this.config.dataOrder && this.config.dataOrder.length > 0) { + const reorderedMeasurementList = [] + for (const measurementName of this.config.dataOrder) { + for (const measurement of result.measurementList) { + if (measurement.name === measurementName) { + reorderedMeasurementList.push(measurement) + } + } + } + result.measurementList = reorderedMeasurementList + } + return result + }, + getMeasurement: function (module, measurement, value) { + value = value || module.dashboard_data[measurement] + if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) { + value = value || 'undefined' + } + return { + name: measurement, + value: this.getValue(measurement, value), + unit: this.getUnit(measurement), + icon: this.getIcon(measurement, value), + label: this.translate(measurement.toUpperCase()), + } + }, + kebabCase: function (name) { + return name.replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase() + }, + getValue: function (measurement, value) { + if (!value) { return value } + switch (measurement) { + case this.measurement.CO2: + return value.toFixed(0)// + ' ppm' + case this.measurement.NOISE: + return value.toFixed(0)// + ' dB' + case this.measurement.HUMIDITY: + case 'battery': + case 'wifi': + case 'radio': + return value.toFixed(0)// + '%' + case this.measurement.PRESSURE: + return value.toFixed(0)// + ' mbar' + case this.measurement.TEMPERATURE: + return value.toFixed(1)// + '°C' + case this.measurement.RAIN: + case this.measurement.RAIN_PER_HOUR: + case this.measurement.RAIN_PER_DAY: + return value.toFixed(1)// + ' mm/h' + case this.measurement.WIND_STRENGTH: + case this.measurement.GUST_STRENGTH: + return value.toFixed(0)// + ' m/s' + case this.measurement.WIND_ANGLE: + case this.measurement.GUST_ANGLE: + return this.getDirection(value) + ' | ' + value// + '°' + case this.measurement.TEMPERATURE_TREND: + case this.measurement.PRESSURE_TREND: + return this.translate(value.toUpperCase()) + default: + return value + } + }, + getUnit: function (measurement) { + switch (measurement) { + case this.measurement.CO2: + return 'ppm' + case this.measurement.NOISE: + return 'dB' + case this.measurement.HUMIDITY: + case 'battery': + case 'wifi': + case 'radio': + return '%' + case this.measurement.PRESSURE: + return 'mbar' + case this.measurement.TEMPERATURE: + return '°C' + case this.measurement.RAIN: + case this.measurement.RAIN_PER_HOUR: + case this.measurement.RAIN_PER_DAY: + return 'mm/h' + case this.measurement.WIND_STRENGTH: + case this.measurement.GUST_STRENGTH: + return 'm/s' + case this.measurement.WIND_ANGLE: + case this.measurement.GUST_ANGLE: + return '°' + default: + return '' + } + }, + getDirection: function (value) { + if (value < 11.25) return 'N' + if (value < 33.75) return 'NNE' + if (value < 56.25) return 'NE' + if (value < 78.75) return 'ENE' + if (value < 101.25) return 'E' + if (value < 123.75) return 'ESE' + if (value < 146.25) return 'SE' + if (value < 168.75) return 'SSE' + if (value < 191.25) return 'S' + if (value < 213.75) return 'SSW' + if (value < 236.25) return 'SW' + if (value < 258.75) return 'WSW' + if (value < 281.25) return 'W' + if (value < 303.75) return 'WNW' + if (value < 326.25) return 'NW' + if (value < 348.75) return 'NNW' + return 'N' + }, + getCO2Status: function (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) { + switch (dataType) { + // case this.measurement.CO2: + // return 'fa-lungs' + case this.measurement.NOISE: + return 'fa-volume-up' + case this.measurement.HUMIDITY: + return 'fa-tint' + case this.measurement.PRESSURE: + return 'fa-tachometer-alt' + case this.measurement.GUST_STRENGTH: + case this.measurement.WIND_STRENGTH: + return 'fa-wind' + // case this.measurement.GUST_ANGLE: + // case this.measurement.WIND_ANGLE: + case this.measurement.PRESSURE_TREND: + case this.measurement.TEMPERATURE_TREND: + return this.getTrendIcon(value) + case 'wifi': + return 'fa-wifi' + case 'radio': + return 'fa-broadcast-tower' + case 'battery': + return this.getBatteryIcon(value) + default: + return '' + } + }, + getTrendIcon: function (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) { + 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 () { + return [`${this.name}.${this.config.design}.css`] + }, + getTemplate: function () { + return `${this.name}.${this.config.design}.njk` + }, + getTemplateData: function () { + return { + loaded: this.loaded, + showLastMessage: this.config.showLastMessage, + showBattery: this.config.showBattery, + showRadio: this.config.showRadio, + showWiFi: this.config.showWiFi, + showTrend: this.config.showTrend, + showMeasurementIcon: this.config.showMeasurementIcon, + showMeasurementLabel: this.config.showMeasurementLabel, + showModuleNameOnTop: this.config.showModuleNameOnTop, + horizontal: this.config.horizontal, + moduleList: this.moduleList, + fontClassModuleName: this.config.fontClassModuleName, + fontClassPrimary: this.config.fontClassPrimary, + fontClassSecondary: this.config.fontClassSecondary, + fontClassMeasurement: this.config.fontClassMeasurement, + labelLoading: this.translate('LOADING'), + } + }, + getTranslations: function () { + return { + cs: 'l10n/cs.json', + de: 'l10n/de.json', + en: 'l10n/en.json', + fr: 'l10n/fr.json', + hu: 'l10n/hu.json', + nb: 'l10n/nb.json', + nn: 'l10n/nn.json', + } + }, + socketNotificationReceived: function (notification, payload) { + const self = this + Log.debug('received ' + notification) + switch (notification) { + case self.notifications.AUTH_RESPONSE: + if (payload.status === 'OK') { + self.sendSocketNotification(self.notifications.DATA, self.config) + } else { + console.log('AUTH FAILED ' + payload.message) + } + break + case self.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) + } 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) + } else { + console.log('DATA FAILED ' + payload.message) + } + break + } + }, +} diff --git a/netatmo.js b/netatmo.js index 81247fa..d9aad60 100755 --- a/netatmo.js +++ b/netatmo.js @@ -4,455 +4,7 @@ * By Christopher Fenner http://github.com/CFenner * MIT Licensed. */ -/* global Module, Log */ -Module.register('netatmo', { - // default config - defaults: { - initialDelay: 0, - updateInterval: 3, // every 3 minutes, refresh interval on netatmo is 10 minutes - animationSpeed: 1000, - design: 'classic', // or bubbles - horizontal: true, - lastMessageThreshold: 600, // in seconds (10 minutes) - showLastMessage: true, - showBattery: true, - showRadio: true, - showWiFi: true, - showTrend: true, - showMeasurementIcon: true, - showMeasurementLabel: true, - showStationName: true, - showModuleNameOnTop: false, - apiBase: 'api.netatmo.com', - authEndpoint: '/oauth2/token', - dataEndpoint: '/api/getstationsdata', - fontClassModuleName: 'xsmall', - fontClassPrimary: 'large', - fontClassSecondary: 'xsmall', - fontClassMeasurement: 'xsmall', - thresholdCO2Average: 800, - thresholdCO2Bad: 1800, - mockData: false, - }, - notifications: { - AUTH: 'NETATMO_AUTH', - AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', - DATA: 'NETATMO_DATA', - DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', - }, - moduleType: { - MAIN: 'NAMain', - INDOOR: 'NAModule4', - OUTDOOR: 'NAModule1', - RAIN: 'NAModule3', - WIND: 'NAModule2', - }, - measurement: { - CO2: 'CO2', - HUMIDITY: 'Humidity', - TEMPERATURE: 'Temperature', - TEMPERATURE_TREND: 'temp_trend', - PRESSURE: 'Pressure', - PRESSURE_TREND: 'pressure_trend', - NOISE: 'Noise', - WIND_STRENGTH: 'WindStrength', - WIND_ANGLE: 'WindAngle', - GUST_STRENGTH: 'GustStrength', - GUST_ANGLE: 'GustAngle', - RAIN: 'Rain', - RAIN_PER_HOUR: 'sum_rain_1', - RAIN_PER_DAY: 'sum_rain_24', - }, - // init method - start: function () { - const self = this - Log.info(`Starting module: ${this.name}`) - self.loaded = false - self.moduleList = [] +const module = require('./module') - // get a new token at start-up. When receive, GET_CAMERA_EVENTS will be requested - setTimeout(function () { - self.sendSocketNotification(self.notifications.DATA, self.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) - }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000) - }, - updateModuleList: function (stationList) { - let moduleList = [] - - for (const station of stationList) { - moduleList.push(this.getModule(station, station.home_name)) - - station.modules.forEach(function (module) { - moduleList.push(this.getModule(module, station.home_name)) - }.bind(this)) - - if (station.reachable) { this.lastUpdate = station.dashboard_data.time_utc } - } - this.loaded = true - if (JSON.stringify(this.moduleList) === JSON.stringify(moduleList)) { - return - } - // reorder modules - if (this.config.moduleOrder && this.config.moduleOrder.length > 0) { - const reorderedModuleList = [] - for (const moduleName of this.config.moduleOrder) { - for (const module of moduleList) { - if (module.name === moduleName) { - reorderedModuleList.push(module) - } - } - } - moduleList = reorderedModuleList - } - this.moduleList = moduleList - }, - getModule: function (module, stationName) { - const result = {} - - result.name = module.module_name - if (this.config.showStationName) { - result.name = `${stationName} - ${result.name}` - } - result.measurementList = [] - - if (!module.reachable) { - let measurement = '' - if (module.type === this.moduleType.MAIN) { - measurement = 'wifi' - } else { - measurement = 'radio' - } - - result.measurementList.push({ - name: measurement, - value: this.getValue(measurement, 0), - unit: this.getUnit(measurement), - icon: this.getIcon(measurement, 0) + ' flash red', - label: this.translate(measurement.toUpperCase()), - }) - - return result - } - - // TODO check module.reachable - let primaryType = '' - let primaryValue = '' - let secondaryType = '' - let secondaryValue = '' - - // add module sensor measurements - switch (module.type) { - case this.moduleType.MAIN: - result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE)) - if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE_TREND)) } - result.measurementList.push(this.getMeasurement(module, this.measurement.NOISE)) - // break; fallthrough - case this.moduleType.INDOOR: - if (this.config.design === 'bubbles') { - secondaryType = this.measurement.CO2 - secondaryValue = module.dashboard_data[secondaryType] - result.secondary = { - value: this.getValue(secondaryType, secondaryValue), - unit: this.getUnit(secondaryType), - class: this.kebabCase(secondaryType), - visualClass: this.getCO2Status(secondaryValue), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.CO2)) - } - // break; fallthrough - case this.moduleType.OUTDOOR: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.TEMPERATURE - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE)) - } - if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE_TREND)) } - result.measurementList.push(this.getMeasurement(module, this.measurement.HUMIDITY)) - break - case this.moduleType.WIND: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.WIND_STRENGTH - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - secondaryType = this.measurement.WIND_ANGLE - secondaryValue = module.dashboard_data[secondaryType] - result.secondary = { - value: this.getValue(secondaryType, secondaryValue), - unit: this.getUnit(secondaryType), - class: this.kebabCase(secondaryType), - visualClass: 'xlarge wi wi-direction-up', - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_STRENGTH)) - result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_ANGLE)) - } - // $('
').addClass('visual xlarge wi wi-direction-up').css('transform', 'rotate(' + value + 'deg)') - result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_STRENGTH)) - result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_ANGLE)) - break - case this.moduleType.RAIN: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.RAIN - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN)) - } - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_HOUR)) - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_DAY)) - break - default: - break - } - // add module specific measurements - if (module.type === this.moduleType.MAIN) { - if (this.config.showWiFi) { result.measurementList.push(this.getMeasurement(module, 'wifi', module.wifi_status)) } - } else { - if (this.config.showRadio) { result.measurementList.push(this.getMeasurement(module, 'radio', module.rf_status)) } - if (this.config.showBattery) { result.measurementList.push(this.getMeasurement(module, 'battery', module.battery_percent)) } - } - // reorder measurements - if (this.config.dataOrder && this.config.dataOrder.length > 0) { - const reorderedMeasurementList = [] - for (const measurementName of this.config.dataOrder) { - for (const measurement of result.measurementList) { - if (measurement.name === measurementName) { - reorderedMeasurementList.push(measurement) - } - } - } - result.measurementList = reorderedMeasurementList - } - return result - }, - getMeasurement: function (module, measurement, value) { - value = value || module.dashboard_data[measurement] - if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) { - value = value || 'undefined' - } - return { - name: measurement, - value: this.getValue(measurement, value), - unit: this.getUnit(measurement), - icon: this.getIcon(measurement, value), - label: this.translate(measurement.toUpperCase()), - } - }, - kebabCase: function (name) { - return name.replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase() - }, - getValue: function (measurement, value) { - if (!value) { return value } - switch (measurement) { - case this.measurement.CO2: - return value.toFixed(0)// + ' ppm' - case this.measurement.NOISE: - return value.toFixed(0)// + ' dB' - case this.measurement.HUMIDITY: - case 'battery': - case 'wifi': - case 'radio': - return value.toFixed(0)// + '%' - case this.measurement.PRESSURE: - return value.toFixed(0)// + ' mbar' - case this.measurement.TEMPERATURE: - return value.toFixed(1)// + '°C' - case this.measurement.RAIN: - case this.measurement.RAIN_PER_HOUR: - case this.measurement.RAIN_PER_DAY: - return value.toFixed(1)// + ' mm/h' - case this.measurement.WIND_STRENGTH: - case this.measurement.GUST_STRENGTH: - return value.toFixed(0)// + ' m/s' - case this.measurement.WIND_ANGLE: - case this.measurement.GUST_ANGLE: - return this.getDirection(value) + ' | ' + value// + '°' - case this.measurement.TEMPERATURE_TREND: - case this.measurement.PRESSURE_TREND: - return this.translate(value.toUpperCase()) - default: - return value - } - }, - getUnit: function (measurement) { - switch (measurement) { - case this.measurement.CO2: - return 'ppm' - case this.measurement.NOISE: - return 'dB' - case this.measurement.HUMIDITY: - case 'battery': - case 'wifi': - case 'radio': - return '%' - case this.measurement.PRESSURE: - return 'mbar' - case this.measurement.TEMPERATURE: - return '°C' - case this.measurement.RAIN: - case this.measurement.RAIN_PER_HOUR: - case this.measurement.RAIN_PER_DAY: - return 'mm/h' - case this.measurement.WIND_STRENGTH: - case this.measurement.GUST_STRENGTH: - return 'm/s' - case this.measurement.WIND_ANGLE: - case this.measurement.GUST_ANGLE: - return '°' - default: - return '' - } - }, - getDirection: function (value) { - if (value < 11.25) return 'N' - if (value < 33.75) return 'NNE' - if (value < 56.25) return 'NE' - if (value < 78.75) return 'ENE' - if (value < 101.25) return 'E' - if (value < 123.75) return 'ESE' - if (value < 146.25) return 'SE' - if (value < 168.75) return 'SSE' - if (value < 191.25) return 'S' - if (value < 213.75) return 'SSW' - if (value < 236.25) return 'SW' - if (value < 258.75) return 'WSW' - if (value < 281.25) return 'W' - if (value < 303.75) return 'WNW' - if (value < 326.25) return 'NW' - if (value < 348.75) return 'NNW' - return 'N' - }, - getCO2Status: function (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) { - switch (dataType) { - // case this.measurement.CO2: - // return 'fa-lungs' - case this.measurement.NOISE: - return 'fa-volume-up' - case this.measurement.HUMIDITY: - return 'fa-tint' - case this.measurement.PRESSURE: - return 'fa-tachometer-alt' - case this.measurement.GUST_STRENGTH: - case this.measurement.WIND_STRENGTH: - return 'fa-wind' - // case this.measurement.GUST_ANGLE: - // case this.measurement.WIND_ANGLE: - case this.measurement.PRESSURE_TREND: - case this.measurement.TEMPERATURE_TREND: - return this.getTrendIcon(value) - case 'wifi': - return 'fa-wifi' - case 'radio': - return 'fa-broadcast-tower' - case 'battery': - return this.getBatteryIcon(value) - default: - return '' - } - }, - getTrendIcon: function (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) { - 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 () { - return [`${this.name}.${this.config.design}.css`] - }, - getTemplate: function () { - return `${this.name}.${this.config.design}.njk` - }, - getTemplateData: function () { - return { - loaded: this.loaded, - showLastMessage: this.config.showLastMessage, - showBattery: this.config.showBattery, - showRadio: this.config.showRadio, - showWiFi: this.config.showWiFi, - showTrend: this.config.showTrend, - showMeasurementIcon: this.config.showMeasurementIcon, - showMeasurementLabel: this.config.showMeasurementLabel, - showModuleNameOnTop: this.config.showModuleNameOnTop, - horizontal: this.config.horizontal, - moduleList: this.moduleList, - fontClassModuleName: this.config.fontClassModuleName, - fontClassPrimary: this.config.fontClassPrimary, - fontClassSecondary: this.config.fontClassSecondary, - fontClassMeasurement: this.config.fontClassMeasurement, - labelLoading: this.translate('LOADING'), - } - }, - getTranslations: function () { - return { - cs: 'l10n/cs.json', - de: 'l10n/de.json', - en: 'l10n/en.json', - fr: 'l10n/fr.json', - hu: 'l10n/hu.json', - nb: 'l10n/nb.json', - nn: 'l10n/nn.json', - } - }, - socketNotificationReceived: function (notification, payload) { - const self = this - Log.debug('received ' + notification) - switch (notification) { - case self.notifications.AUTH_RESPONSE: - if (payload.status === 'OK') { - self.sendSocketNotification(self.notifications.DATA, self.config) - } else { - console.log('AUTH FAILED ' + payload.message) - } - break - case self.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) - } 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) - } else { - console.log('DATA FAILED ' + payload.message) - } - break - } - }, -}) +/* global Module */ +Module.register('netatmo', module) 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/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..eec8578 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,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 +44,7 @@ }, "snyk": true, "dependencies": { - "@ungap/url-search-params": "^0.2.2" + "@ungap/url-search-params": "^0.2.2", + "sync-fetch": "^0.5.2" } } From b16ee1911e389d4ad6208c784fd26d10a1d47c5f Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Aug 2023 22:35:32 +0200 Subject: [PATCH 02/10] fix issue --- helper.test.js | 34 ++-- module.js | 458 ------------------------------------------------- netatmo.js | 454 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 467 insertions(+), 479 deletions(-) delete mode 100755 module.js diff --git a/helper.test.js b/helper.test.js index cd21f44..48e627e 100644 --- a/helper.test.js +++ b/helper.test.js @@ -1,13 +1,11 @@ +require('dotenv').config() const moduleUnderTest = require('./helper.js') -const netatmo = require('./module.js') - -require('dotenv').config() +const apiBase = 'api.netatmo.com' +const authEndpoint = '/oauth2/token' +const dataEndpoint = '/api/getstationsdata' describe('helper', () => { - beforeEach(() => { - }) - afterEach(() => { delete moduleUnderTest.token delete moduleUnderTest.token_expires_in @@ -27,8 +25,8 @@ describe('helper', () => { expect(moduleUnderTest).toHaveProperty('token') // test moduleUnderTest.loadData({ - apiBase: netatmo.defaults.apiBase, - dataEndpoint: netatmo.defaults.dataEndpoint, + apiBase, + dataEndpoint, }) // assert expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() @@ -43,8 +41,8 @@ describe('helper', () => { }) // test moduleUnderTest.loadData({ - apiBase: netatmo.defaults.apiBase, - dataEndpoint: netatmo.defaults.dataEndpoint, + apiBase, + dataEndpoint, }) // assert expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() @@ -59,8 +57,8 @@ describe('helper', () => { }) // test moduleUnderTest.loadData({ - apiBase: netatmo.defaults.apiBase, - dataEndpoint: netatmo.defaults.dataEndpoint, + apiBase, + dataEndpoint, }) // assert expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() @@ -86,8 +84,8 @@ describe('helper', () => { expect(moduleUnderTest).not.toHaveProperty('refresh_token') // test moduleUnderTest.authenticate({ - apiBase: netatmo.defaults.apiBase, - authEndpoint: netatmo.defaults.authEndpoint, + apiBase, + authEndpoint, refresh_token: process.env.REFRESH_TOKEN, clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, @@ -111,8 +109,8 @@ describe('helper', () => { expect(moduleUnderTest).toHaveProperty('refresh_token') // test moduleUnderTest.authenticate({ - apiBase: netatmo.defaults.apiBase, - authEndpoint: netatmo.defaults.authEndpoint, + apiBase, + authEndpoint, refresh_token: '', clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, @@ -135,8 +133,8 @@ describe('helper', () => { expect(moduleUnderTest).not.toHaveProperty('refresh_token') // test moduleUnderTest.authenticate({ - apiBase: netatmo.defaults.apiBase, - authEndpoint: netatmo.defaults.authEndpoint, + apiBase, + authEndpoint, refresh_token: '', clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, diff --git a/module.js b/module.js deleted file mode 100755 index b0f9b93..0000000 --- a/module.js +++ /dev/null @@ -1,458 +0,0 @@ -/* Magic Mirror - * Module: Netatmo - * - * By Christopher Fenner http://github.com/CFenner - * MIT Licensed. - */ -/* global Log */ -module.exports = { - // default config - defaults: { - initialDelay: 0, - updateInterval: 3, // every 3 minutes, refresh interval on netatmo is 10 minutes - animationSpeed: 1000, - design: 'classic', // or bubbles - horizontal: true, - lastMessageThreshold: 600, // in seconds (10 minutes) - showLastMessage: true, - showBattery: true, - showRadio: true, - showWiFi: true, - showTrend: true, - showMeasurementIcon: true, - showMeasurementLabel: true, - showStationName: true, - showModuleNameOnTop: false, - apiBase: 'api.netatmo.com', - authEndpoint: '/oauth2/token', - dataEndpoint: '/api/getstationsdata', - fontClassModuleName: 'xsmall', - fontClassPrimary: 'large', - fontClassSecondary: 'xsmall', - fontClassMeasurement: 'xsmall', - thresholdCO2Average: 800, - thresholdCO2Bad: 1800, - mockData: false, - }, - notifications: { - AUTH: 'NETATMO_AUTH', - AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', - DATA: 'NETATMO_DATA', - DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', - }, - moduleType: { - MAIN: 'NAMain', - INDOOR: 'NAModule4', - OUTDOOR: 'NAModule1', - RAIN: 'NAModule3', - WIND: 'NAModule2', - }, - measurement: { - CO2: 'CO2', - HUMIDITY: 'Humidity', - TEMPERATURE: 'Temperature', - TEMPERATURE_TREND: 'temp_trend', - PRESSURE: 'Pressure', - PRESSURE_TREND: 'pressure_trend', - NOISE: 'Noise', - WIND_STRENGTH: 'WindStrength', - WIND_ANGLE: 'WindAngle', - GUST_STRENGTH: 'GustStrength', - GUST_ANGLE: 'GustAngle', - RAIN: 'Rain', - RAIN_PER_HOUR: 'sum_rain_1', - RAIN_PER_DAY: 'sum_rain_24', - }, - // init method - start: function () { - const self = this - Log.info(`Starting module: ${this.name}`) - self.loaded = false - self.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) - }, 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) - }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000) - }, - updateModuleList: function (stationList) { - let moduleList = [] - - for (const station of stationList) { - moduleList.push(this.getModule(station, station.home_name)) - - station.modules.forEach(function (module) { - moduleList.push(this.getModule(module, station.home_name)) - }.bind(this)) - - if (station.reachable) { this.lastUpdate = station.dashboard_data.time_utc } - } - this.loaded = true - if (JSON.stringify(this.moduleList) === JSON.stringify(moduleList)) { - return - } - // reorder modules - if (this.config.moduleOrder && this.config.moduleOrder.length > 0) { - const reorderedModuleList = [] - for (const moduleName of this.config.moduleOrder) { - for (const module of moduleList) { - if (module.name === moduleName) { - reorderedModuleList.push(module) - } - } - } - moduleList = reorderedModuleList - } - this.moduleList = moduleList - }, - getModule: function (module, stationName) { - const result = {} - - result.name = module.module_name - if (this.config.showStationName) { - result.name = `${stationName} - ${result.name}` - } - result.measurementList = [] - - if (!module.reachable) { - let measurement = '' - if (module.type === this.moduleType.MAIN) { - measurement = 'wifi' - } else { - measurement = 'radio' - } - - result.measurementList.push({ - name: measurement, - value: this.getValue(measurement, 0), - unit: this.getUnit(measurement), - icon: this.getIcon(measurement, 0) + ' flash red', - label: this.translate(measurement.toUpperCase()), - }) - - return result - } - - // TODO check module.reachable - let primaryType = '' - let primaryValue = '' - let secondaryType = '' - let secondaryValue = '' - - // add module sensor measurements - switch (module.type) { - case this.moduleType.MAIN: - result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE)) - if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE_TREND)) } - result.measurementList.push(this.getMeasurement(module, this.measurement.NOISE)) - // break; fallthrough - case this.moduleType.INDOOR: - if (this.config.design === 'bubbles') { - secondaryType = this.measurement.CO2 - secondaryValue = module.dashboard_data[secondaryType] - result.secondary = { - value: this.getValue(secondaryType, secondaryValue), - unit: this.getUnit(secondaryType), - class: this.kebabCase(secondaryType), - visualClass: this.getCO2Status(secondaryValue), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.CO2)) - } - // break; fallthrough - case this.moduleType.OUTDOOR: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.TEMPERATURE - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE)) - } - if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE_TREND)) } - result.measurementList.push(this.getMeasurement(module, this.measurement.HUMIDITY)) - break - case this.moduleType.WIND: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.WIND_STRENGTH - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - secondaryType = this.measurement.WIND_ANGLE - secondaryValue = module.dashboard_data[secondaryType] - result.secondary = { - value: this.getValue(secondaryType, secondaryValue), - unit: this.getUnit(secondaryType), - class: this.kebabCase(secondaryType), - visualClass: 'xlarge wi wi-direction-up', - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_STRENGTH)) - result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_ANGLE)) - } - // $('
').addClass('visual xlarge wi wi-direction-up').css('transform', 'rotate(' + value + 'deg)') - result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_STRENGTH)) - result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_ANGLE)) - break - case this.moduleType.RAIN: - if (this.config.design === 'bubbles') { - primaryType = this.measurement.RAIN - primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' - result.primary = { - value: this.getValue(primaryType, primaryValue), - unit: this.getUnit(primaryType), - class: this.kebabCase(primaryType), - } - } else { - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN)) - } - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_HOUR)) - result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_DAY)) - break - default: - break - } - // add module specific measurements - if (module.type === this.moduleType.MAIN) { - if (this.config.showWiFi) { result.measurementList.push(this.getMeasurement(module, 'wifi', module.wifi_status)) } - } else { - if (this.config.showRadio) { result.measurementList.push(this.getMeasurement(module, 'radio', module.rf_status)) } - if (this.config.showBattery) { result.measurementList.push(this.getMeasurement(module, 'battery', module.battery_percent)) } - } - // reorder measurements - if (this.config.dataOrder && this.config.dataOrder.length > 0) { - const reorderedMeasurementList = [] - for (const measurementName of this.config.dataOrder) { - for (const measurement of result.measurementList) { - if (measurement.name === measurementName) { - reorderedMeasurementList.push(measurement) - } - } - } - result.measurementList = reorderedMeasurementList - } - return result - }, - getMeasurement: function (module, measurement, value) { - value = value || module.dashboard_data[measurement] - if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) { - value = value || 'undefined' - } - return { - name: measurement, - value: this.getValue(measurement, value), - unit: this.getUnit(measurement), - icon: this.getIcon(measurement, value), - label: this.translate(measurement.toUpperCase()), - } - }, - kebabCase: function (name) { - return name.replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase() - }, - getValue: function (measurement, value) { - if (!value) { return value } - switch (measurement) { - case this.measurement.CO2: - return value.toFixed(0)// + ' ppm' - case this.measurement.NOISE: - return value.toFixed(0)// + ' dB' - case this.measurement.HUMIDITY: - case 'battery': - case 'wifi': - case 'radio': - return value.toFixed(0)// + '%' - case this.measurement.PRESSURE: - return value.toFixed(0)// + ' mbar' - case this.measurement.TEMPERATURE: - return value.toFixed(1)// + '°C' - case this.measurement.RAIN: - case this.measurement.RAIN_PER_HOUR: - case this.measurement.RAIN_PER_DAY: - return value.toFixed(1)// + ' mm/h' - case this.measurement.WIND_STRENGTH: - case this.measurement.GUST_STRENGTH: - return value.toFixed(0)// + ' m/s' - case this.measurement.WIND_ANGLE: - case this.measurement.GUST_ANGLE: - return this.getDirection(value) + ' | ' + value// + '°' - case this.measurement.TEMPERATURE_TREND: - case this.measurement.PRESSURE_TREND: - return this.translate(value.toUpperCase()) - default: - return value - } - }, - getUnit: function (measurement) { - switch (measurement) { - case this.measurement.CO2: - return 'ppm' - case this.measurement.NOISE: - return 'dB' - case this.measurement.HUMIDITY: - case 'battery': - case 'wifi': - case 'radio': - return '%' - case this.measurement.PRESSURE: - return 'mbar' - case this.measurement.TEMPERATURE: - return '°C' - case this.measurement.RAIN: - case this.measurement.RAIN_PER_HOUR: - case this.measurement.RAIN_PER_DAY: - return 'mm/h' - case this.measurement.WIND_STRENGTH: - case this.measurement.GUST_STRENGTH: - return 'm/s' - case this.measurement.WIND_ANGLE: - case this.measurement.GUST_ANGLE: - return '°' - default: - return '' - } - }, - getDirection: function (value) { - if (value < 11.25) return 'N' - if (value < 33.75) return 'NNE' - if (value < 56.25) return 'NE' - if (value < 78.75) return 'ENE' - if (value < 101.25) return 'E' - if (value < 123.75) return 'ESE' - if (value < 146.25) return 'SE' - if (value < 168.75) return 'SSE' - if (value < 191.25) return 'S' - if (value < 213.75) return 'SSW' - if (value < 236.25) return 'SW' - if (value < 258.75) return 'WSW' - if (value < 281.25) return 'W' - if (value < 303.75) return 'WNW' - if (value < 326.25) return 'NW' - if (value < 348.75) return 'NNW' - return 'N' - }, - getCO2Status: function (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) { - switch (dataType) { - // case this.measurement.CO2: - // return 'fa-lungs' - case this.measurement.NOISE: - return 'fa-volume-up' - case this.measurement.HUMIDITY: - return 'fa-tint' - case this.measurement.PRESSURE: - return 'fa-tachometer-alt' - case this.measurement.GUST_STRENGTH: - case this.measurement.WIND_STRENGTH: - return 'fa-wind' - // case this.measurement.GUST_ANGLE: - // case this.measurement.WIND_ANGLE: - case this.measurement.PRESSURE_TREND: - case this.measurement.TEMPERATURE_TREND: - return this.getTrendIcon(value) - case 'wifi': - return 'fa-wifi' - case 'radio': - return 'fa-broadcast-tower' - case 'battery': - return this.getBatteryIcon(value) - default: - return '' - } - }, - getTrendIcon: function (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) { - 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 () { - return [`${this.name}.${this.config.design}.css`] - }, - getTemplate: function () { - return `${this.name}.${this.config.design}.njk` - }, - getTemplateData: function () { - return { - loaded: this.loaded, - showLastMessage: this.config.showLastMessage, - showBattery: this.config.showBattery, - showRadio: this.config.showRadio, - showWiFi: this.config.showWiFi, - showTrend: this.config.showTrend, - showMeasurementIcon: this.config.showMeasurementIcon, - showMeasurementLabel: this.config.showMeasurementLabel, - showModuleNameOnTop: this.config.showModuleNameOnTop, - horizontal: this.config.horizontal, - moduleList: this.moduleList, - fontClassModuleName: this.config.fontClassModuleName, - fontClassPrimary: this.config.fontClassPrimary, - fontClassSecondary: this.config.fontClassSecondary, - fontClassMeasurement: this.config.fontClassMeasurement, - labelLoading: this.translate('LOADING'), - } - }, - getTranslations: function () { - return { - cs: 'l10n/cs.json', - de: 'l10n/de.json', - en: 'l10n/en.json', - fr: 'l10n/fr.json', - hu: 'l10n/hu.json', - nb: 'l10n/nb.json', - nn: 'l10n/nn.json', - } - }, - socketNotificationReceived: function (notification, payload) { - const self = this - Log.debug('received ' + notification) - switch (notification) { - case self.notifications.AUTH_RESPONSE: - if (payload.status === 'OK') { - self.sendSocketNotification(self.notifications.DATA, self.config) - } else { - console.log('AUTH FAILED ' + payload.message) - } - break - case self.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) - } 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) - } else { - console.log('DATA FAILED ' + payload.message) - } - break - } - }, -} diff --git a/netatmo.js b/netatmo.js index d9aad60..73ac3ab 100755 --- a/netatmo.js +++ b/netatmo.js @@ -4,7 +4,455 @@ * By Christopher Fenner http://github.com/CFenner * MIT Licensed. */ -const module = require('./module') - /* global Module */ -Module.register('netatmo', module) +Module.register('netatmo', { + // default config + defaults: { + initialDelay: 0, + updateInterval: 3, // every 3 minutes, refresh interval on netatmo is 10 minutes + animationSpeed: 1000, + design: 'classic', // or bubbles + horizontal: true, + lastMessageThreshold: 600, // in seconds (10 minutes) + showLastMessage: true, + showBattery: true, + showRadio: true, + showWiFi: true, + showTrend: true, + showMeasurementIcon: true, + showMeasurementLabel: true, + showStationName: true, + showModuleNameOnTop: false, + apiBase: 'api.netatmo.com', + authEndpoint: '/oauth2/token', + dataEndpoint: '/api/getstationsdata', + fontClassModuleName: 'xsmall', + fontClassPrimary: 'large', + fontClassSecondary: 'xsmall', + fontClassMeasurement: 'xsmall', + thresholdCO2Average: 800, + thresholdCO2Bad: 1800, + mockData: false, + }, + notifications: { + AUTH: 'NETATMO_AUTH', + AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE', + DATA: 'NETATMO_DATA', + DATA_RESPONSE: 'NETATMO_DATA_RESPONSE', + }, + moduleType: { + MAIN: 'NAMain', + INDOOR: 'NAModule4', + OUTDOOR: 'NAModule1', + RAIN: 'NAModule3', + WIND: 'NAModule2', + }, + measurement: { + CO2: 'CO2', + HUMIDITY: 'Humidity', + TEMPERATURE: 'Temperature', + TEMPERATURE_TREND: 'temp_trend', + PRESSURE: 'Pressure', + PRESSURE_TREND: 'pressure_trend', + NOISE: 'Noise', + WIND_STRENGTH: 'WindStrength', + WIND_ANGLE: 'WindAngle', + GUST_STRENGTH: 'GustStrength', + GUST_ANGLE: 'GustAngle', + RAIN: 'Rain', + RAIN_PER_HOUR: 'sum_rain_1', + RAIN_PER_DAY: 'sum_rain_24', + }, + // init method + start: function () { + const self = this + Log.info(`Starting module: ${this.name}`) + self.loaded = false + self.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) + }, 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) + }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000) + }, + updateModuleList: function (stationList) { + let moduleList = [] + + for (const station of stationList) { + moduleList.push(this.getModule(station, station.home_name)) + + station.modules.forEach(function (module) { + moduleList.push(this.getModule(module, station.home_name)) + }.bind(this)) + + if (station.reachable) { this.lastUpdate = station.dashboard_data.time_utc } + } + this.loaded = true + if (JSON.stringify(this.moduleList) === JSON.stringify(moduleList)) { + return + } + // reorder modules + if (this.config.moduleOrder && this.config.moduleOrder.length > 0) { + const reorderedModuleList = [] + for (const moduleName of this.config.moduleOrder) { + for (const module of moduleList) { + if (module.name === moduleName) { + reorderedModuleList.push(module) + } + } + } + moduleList = reorderedModuleList + } + this.moduleList = moduleList + }, + getModule: function (module, stationName) { + const result = {} + + result.name = module.module_name + if (this.config.showStationName) { + result.name = `${stationName} - ${result.name}` + } + result.measurementList = [] + + if (!module.reachable) { + let measurement = '' + if (module.type === this.moduleType.MAIN) { + measurement = 'wifi' + } else { + measurement = 'radio' + } + + result.measurementList.push({ + name: measurement, + value: this.getValue(measurement, 0), + unit: this.getUnit(measurement), + icon: this.getIcon(measurement, 0) + ' flash red', + label: this.translate(measurement.toUpperCase()), + }) + + return result + } + + // TODO check module.reachable + let primaryType = '' + let primaryValue = '' + let secondaryType = '' + let secondaryValue = '' + + // add module sensor measurements + switch (module.type) { + case this.moduleType.MAIN: + result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE)) + if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE_TREND)) } + result.measurementList.push(this.getMeasurement(module, this.measurement.NOISE)) + // break; fallthrough + case this.moduleType.INDOOR: + if (this.config.design === 'bubbles') { + secondaryType = this.measurement.CO2 + secondaryValue = module.dashboard_data[secondaryType] + result.secondary = { + value: this.getValue(secondaryType, secondaryValue), + unit: this.getUnit(secondaryType), + class: this.kebabCase(secondaryType), + visualClass: this.getCO2Status(secondaryValue), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.CO2)) + } + // break; fallthrough + case this.moduleType.OUTDOOR: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.TEMPERATURE + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE)) + } + if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE_TREND)) } + result.measurementList.push(this.getMeasurement(module, this.measurement.HUMIDITY)) + break + case this.moduleType.WIND: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.WIND_STRENGTH + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + secondaryType = this.measurement.WIND_ANGLE + secondaryValue = module.dashboard_data[secondaryType] + result.secondary = { + value: this.getValue(secondaryType, secondaryValue), + unit: this.getUnit(secondaryType), + class: this.kebabCase(secondaryType), + visualClass: 'xlarge wi wi-direction-up', + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_STRENGTH)) + result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_ANGLE)) + } + // $('
').addClass('visual xlarge wi wi-direction-up').css('transform', 'rotate(' + value + 'deg)') + result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_STRENGTH)) + result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_ANGLE)) + break + case this.moduleType.RAIN: + if (this.config.design === 'bubbles') { + primaryType = this.measurement.RAIN + primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : '' + result.primary = { + value: this.getValue(primaryType, primaryValue), + unit: this.getUnit(primaryType), + class: this.kebabCase(primaryType), + } + } else { + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN)) + } + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_HOUR)) + result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_DAY)) + break + default: + break + } + // add module specific measurements + if (module.type === this.moduleType.MAIN) { + if (this.config.showWiFi) { result.measurementList.push(this.getMeasurement(module, 'wifi', module.wifi_status)) } + } else { + if (this.config.showRadio) { result.measurementList.push(this.getMeasurement(module, 'radio', module.rf_status)) } + if (this.config.showBattery) { result.measurementList.push(this.getMeasurement(module, 'battery', module.battery_percent)) } + } + // reorder measurements + if (this.config.dataOrder && this.config.dataOrder.length > 0) { + const reorderedMeasurementList = [] + for (const measurementName of this.config.dataOrder) { + for (const measurement of result.measurementList) { + if (measurement.name === measurementName) { + reorderedMeasurementList.push(measurement) + } + } + } + result.measurementList = reorderedMeasurementList + } + return result + }, + getMeasurement: function (module, measurement, value) { + value = value || module.dashboard_data[measurement] + if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) { + value = value || 'undefined' + } + return { + name: measurement, + value: this.getValue(measurement, value), + unit: this.getUnit(measurement), + icon: this.getIcon(measurement, value), + label: this.translate(measurement.toUpperCase()), + } + }, + kebabCase: function (name) { + return name.replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase() + }, + getValue: function (measurement, value) { + if (!value) { return value } + switch (measurement) { + case this.measurement.CO2: + return value.toFixed(0)// + ' ppm' + case this.measurement.NOISE: + return value.toFixed(0)// + ' dB' + case this.measurement.HUMIDITY: + case 'battery': + case 'wifi': + case 'radio': + return value.toFixed(0)// + '%' + case this.measurement.PRESSURE: + return value.toFixed(0)// + ' mbar' + case this.measurement.TEMPERATURE: + return value.toFixed(1)// + '°C' + case this.measurement.RAIN: + case this.measurement.RAIN_PER_HOUR: + case this.measurement.RAIN_PER_DAY: + return value.toFixed(1)// + ' mm/h' + case this.measurement.WIND_STRENGTH: + case this.measurement.GUST_STRENGTH: + return value.toFixed(0)// + ' m/s' + case this.measurement.WIND_ANGLE: + case this.measurement.GUST_ANGLE: + return this.getDirection(value) + ' | ' + value// + '°' + case this.measurement.TEMPERATURE_TREND: + case this.measurement.PRESSURE_TREND: + return this.translate(value.toUpperCase()) + default: + return value + } + }, + getUnit: function (measurement) { + switch (measurement) { + case this.measurement.CO2: + return 'ppm' + case this.measurement.NOISE: + return 'dB' + case this.measurement.HUMIDITY: + case 'battery': + case 'wifi': + case 'radio': + return '%' + case this.measurement.PRESSURE: + return 'mbar' + case this.measurement.TEMPERATURE: + return '°C' + case this.measurement.RAIN: + case this.measurement.RAIN_PER_HOUR: + case this.measurement.RAIN_PER_DAY: + return 'mm/h' + case this.measurement.WIND_STRENGTH: + case this.measurement.GUST_STRENGTH: + return 'm/s' + case this.measurement.WIND_ANGLE: + case this.measurement.GUST_ANGLE: + return '°' + default: + return '' + } + }, + getDirection: function (value) { + if (value < 11.25) return 'N' + if (value < 33.75) return 'NNE' + if (value < 56.25) return 'NE' + if (value < 78.75) return 'ENE' + if (value < 101.25) return 'E' + if (value < 123.75) return 'ESE' + if (value < 146.25) return 'SE' + if (value < 168.75) return 'SSE' + if (value < 191.25) return 'S' + if (value < 213.75) return 'SSW' + if (value < 236.25) return 'SW' + if (value < 258.75) return 'WSW' + if (value < 281.25) return 'W' + if (value < 303.75) return 'WNW' + if (value < 326.25) return 'NW' + if (value < 348.75) return 'NNW' + return 'N' + }, + getCO2Status: function (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) { + switch (dataType) { + // case this.measurement.CO2: + // return 'fa-lungs' + case this.measurement.NOISE: + return 'fa-volume-up' + case this.measurement.HUMIDITY: + return 'fa-tint' + case this.measurement.PRESSURE: + return 'fa-tachometer-alt' + case this.measurement.GUST_STRENGTH: + case this.measurement.WIND_STRENGTH: + return 'fa-wind' + // case this.measurement.GUST_ANGLE: + // case this.measurement.WIND_ANGLE: + case this.measurement.PRESSURE_TREND: + case this.measurement.TEMPERATURE_TREND: + return this.getTrendIcon(value) + case 'wifi': + return 'fa-wifi' + case 'radio': + return 'fa-broadcast-tower' + case 'battery': + return this.getBatteryIcon(value) + default: + return '' + } + }, + getTrendIcon: function (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) { + 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 () { + return [`${this.name}.${this.config.design}.css`] + }, + getTemplate: function () { + return `${this.name}.${this.config.design}.njk` + }, + getTemplateData: function () { + return { + loaded: this.loaded, + showLastMessage: this.config.showLastMessage, + showBattery: this.config.showBattery, + showRadio: this.config.showRadio, + showWiFi: this.config.showWiFi, + showTrend: this.config.showTrend, + showMeasurementIcon: this.config.showMeasurementIcon, + showMeasurementLabel: this.config.showMeasurementLabel, + showModuleNameOnTop: this.config.showModuleNameOnTop, + horizontal: this.config.horizontal, + moduleList: this.moduleList, + fontClassModuleName: this.config.fontClassModuleName, + fontClassPrimary: this.config.fontClassPrimary, + fontClassSecondary: this.config.fontClassSecondary, + fontClassMeasurement: this.config.fontClassMeasurement, + labelLoading: this.translate('LOADING'), + } + }, + getTranslations: function () { + return { + cs: 'l10n/cs.json', + de: 'l10n/de.json', + en: 'l10n/en.json', + fr: 'l10n/fr.json', + hu: 'l10n/hu.json', + nb: 'l10n/nb.json', + nn: 'l10n/nn.json', + } + }, + socketNotificationReceived: function (notification, payload) { + const self = this + Log.debug('received ' + notification) + switch (notification) { + case self.notifications.AUTH_RESPONSE: + if (payload.status === 'OK') { + self.sendSocketNotification(self.notifications.DATA, self.config) + } else { + console.log('AUTH FAILED ' + payload.message) + } + break + case self.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) + } 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) + } else { + console.log('DATA FAILED ' + payload.message) + } + break + } + }, +}) From 0f302863c4c716d26cfa6dc790c8c13748216f9b Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Aug 2023 22:49:49 +0200 Subject: [PATCH 03/10] change test names --- helper.test.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/helper.test.js b/helper.test.js index 48e627e..6225efe 100644 --- a/helper.test.js +++ b/helper.test.js @@ -32,7 +32,7 @@ describe('helper', () => { expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() }) - test('missing token', () => { + test('with missing token', () => { // moduleUnderTest.token = process.env.TOKEN // prepare moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { @@ -48,7 +48,7 @@ describe('helper', () => { expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() }) - test('invalid token', () => { + test('with invalid token', () => { moduleUnderTest.token = 'something' // prepare moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { @@ -66,14 +66,7 @@ describe('helper', () => { }) describe('authentication', () => { - test('verify 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') - }) - - test('with refresh_token from config', () => { + test('and data with refresh_token from config', () => { // prepare moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) @@ -122,7 +115,7 @@ describe('helper', () => { expect(moduleUnderTest.sendSocketNotification).toHaveBeenCalled() }) - test('test authenticate fail', () => { + test('without refresh_token', () => { // prepare moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) From 5e2cd2a24a1c2f62792fb71126e6d30ab80efae5 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Aug 2023 23:58:06 +0200 Subject: [PATCH 04/10] add compose file --- compose/docker-compose.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 compose/docker-compose.yml diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..91ec983 --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,20 @@ +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" + ports: + - 8080:8080 + restart: unless-stopped + command: + - npm + - run + - server From 14566f419a4234b4726af9aa73e72ace780803b6 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:00:37 +0200 Subject: [PATCH 05/10] ignore docker folders --- compose/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 compose/.gitignore diff --git a/compose/.gitignore b/compose/.gitignore new file mode 100644 index 0000000..3a62afd --- /dev/null +++ b/compose/.gitignore @@ -0,0 +1,3 @@ +config/ +css/ +modules/ From cca2cbd997ced301af89d1857b147aa75f9495ef Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:14:50 +0200 Subject: [PATCH 06/10] add compose description --- compose/README.md | 24 ++++++++++++++++++++++++ package.json | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 compose/README.md diff --git a/compose/README.md b/compose/README.md new file mode 100644 index 0000000..f0afe41 --- /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` + + ``` + { + module: 'netatmo', + position: 'bottom_left', + header: 'Netatmo', + config: { + clientId: '', + clientSecret: '', + refresh_token: '', + }, + }, + ``` + +- open MagicMirror ui at http://0.0.0.0:8080 diff --git a/package.json b/package.json index eec8578..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" }, From 0c138d99777c30b0da32cf2b1ae24ec45b1d655c Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:18:23 +0200 Subject: [PATCH 07/10] fix markdownlint findings --- compose/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/README.md b/compose/README.md index f0afe41..b73492b 100644 --- a/compose/README.md +++ b/compose/README.md @@ -8,7 +8,7 @@ To test the module in a MagicMirror instance: - 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', @@ -21,4 +21,4 @@ To test the module in a MagicMirror instance: }, ``` -- open MagicMirror ui at http://0.0.0.0:8080 +- open MagicMirror ui at [`http://0.0.0.0:8080`](http://0.0.0.0:8080) From 0a1931274d8709ae5179d8411884c88bef0174d5 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:33:29 +0200 Subject: [PATCH 08/10] add config --- compose/.gitignore | 3 +- compose/config/config.js.template | 61 +++++++++++++++++++++++++++++++ compose/docker-compose.yml | 1 + 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 compose/config/config.js.template diff --git a/compose/.gitignore b/compose/.gitignore index 3a62afd..ab85fb4 100644 --- a/compose/.gitignore +++ b/compose/.gitignore @@ -1,3 +1,4 @@ -config/ +config/** css/ modules/ +!config/config.js.template 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 index 91ec983..8ee2b95 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: TZ: Europe/Berlin MM_SHOW_CURSOR: "true" + env_file: ../.env ports: - 8080:8080 restart: unless-stopped From dced62fedafde7e1ae027959caf86963d6e86e66 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:37:54 +0200 Subject: [PATCH 09/10] rename test --- helper.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper.test.js b/helper.test.js index 6225efe..f8b935f 100644 --- a/helper.test.js +++ b/helper.test.js @@ -66,7 +66,7 @@ describe('helper', () => { }) describe('authentication', () => { - test('and data with refresh_token from config', () => { + test('with refresh_token from config', () => { // prepare moduleUnderTest.sendSocketNotification = jest.fn((type, payload) => { expect(type).toBe(moduleUnderTest.notifications.AUTH_RESPONSE) From f1a636f60ee3927cdd5f7798d95c6f8aa4e4f27b Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 15 Aug 2023 00:43:08 +0200 Subject: [PATCH 10/10] remove old files --- node_helper_impl.js | 165 --------------------------------------- node_helper_impl.test.js | 11 --- 2 files changed, 176 deletions(-) delete mode 100644 node_helper_impl.js delete mode 100644 node_helper_impl.test.js 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') - }) -})