Skip to content

Commit

Permalink
fix issue
Browse files Browse the repository at this point in the history
  • Loading branch information
CFenner committed Aug 14, 2023
1 parent 7250c6f commit a683bd5
Show file tree
Hide file tree
Showing 10 changed files with 818 additions and 462 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Ignore all node modules.
node_modules
report
.env
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
}
```
Expand All @@ -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|
Expand Down
132 changes: 132 additions & 0 deletions helper.js
Original file line number Diff line number Diff line change
@@ -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
}
},
}
151 changes: 151 additions & 0 deletions helper.test.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
Loading

0 comments on commit a683bd5

Please sign in to comment.