Skip to content

Commit

Permalink
fix: token read/write (#382)
Browse files Browse the repository at this point in the history
Co-authored-by: Christopher Fenner <[email protected]>
  • Loading branch information
bugsounet and CFenner committed Jun 22, 2024
1 parent bd1f69b commit 74a848b
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 278 deletions.
3 changes: 0 additions & 3 deletions .env.template

This file was deleted.

2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"schedule:automergeMonthly",
"schedule:automergeMonthly"
],
"packageRules": [
{
Expand Down
19 changes: 0 additions & 19 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,6 @@ jobs:
run: npm clean-install
- name: Validate CSS Sources
run: npm run validate:css
unit:
runs-on: ubuntu-latest
name: 'Unit Tests'
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Use Node.js ${{ env.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.node-version }}
cache: "npm"
- name: Install Dependencies
run: npm clean-install
- name: Execute Unit Tests
run: npm run test
env:
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }}
package-lock-is-up-to-date:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
report
.env*
.DS_Store
/token.json
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ cd ~/MagicMirror/modules && git clone https://github.com/CFenner/MMM-Netatmo net

:warning: Note that the checkout folder is named `netatmo` and not `MMM-Netatmo` as the repository.

Navigate into the module folder and install missing dependencies:

```shell
cd netatmo && npm ci --production --ignore-scripts
```
Since v2.1.0: **No special dependencies and no others commands are now needed!**

### Connection to Netatmo Service API

Expand Down
207 changes: 163 additions & 44 deletions helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
* Module: MMM-Netatmo
*
* By Christopher Fenner https://github.com/CFenner
* Review @bugsounet https://github.com/bugsounet
* MIT Licensed.
*/
const fs = require('fs')
const path = require('path')
const URLSearchParams = require('@ungap/url-search-params')
const moment = require('moment')

module.exports = {
notifications: {
Expand All @@ -15,117 +16,235 @@ module.exports = {
DATA: 'NETATMO_DATA',
DATA_RESPONSE: 'NETATMO_DATA_RESPONSE',
},
start: function () {

start () {
console.log('Netatmo helper started ...')
this.token = null
},
authenticate: async function (config) {
const self = this
self.config = config

async authenticate () {
const params = new URLSearchParams()
params.append('grant_type', 'refresh_token')
params.append('refresh_token', self.refresh_token || self.config.refresh_token)
params.append('client_id', self.config.clientId)
params.append('client_secret', self.config.clientSecret)
params.append('refresh_token', this.refreshToken)
params.append('client_id', this.clientId)
params.append('client_secret', this.clientSecret)

try {
const result = await fetch('https://' + self.config.apiBase + self.config.authEndpoint, {
const result = await fetch(`https://${this.config.apiBase}${this.config.authEndpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
}).then(response => response.json())
}).then((response) => response.json())

if (result.error) {
throw new Error(result.error + ': ' + result.error_description)
}
throw new Error(`${result.error} : ${result.error_description}`)
};

this.token = result.access_token
this.token_expires_in = result.expires_in
this.refreshToken = result.refresh_token
console.log('Netatmo: Authenticated')

// write in token file and provides for token refresh
this.writeToken(result)
if (result.expires_in) {
const expireAt = moment(Date.now() + (result.expires_in * 1000)).format('LLLL')
console.log(`Netatmo: New Token Expire ${expireAt}`)
setTimeout(() => this.authenticateRefresh(result.refresh_token), (result.expires_in - 60) * 1000)
};

console.log('UPDATING TOKEN ' + result.access_token)
self.token = result.access_token
self.token_expires_in = result.expires_in
self.refresh_token = result.refresh_token
// we got a new token, save it to main file to allow it to request the datas
self.sendSocketNotification(self.notifications.AUTH_RESPONSE, {
// inform module AUTH ok
this.sendSocketNotification(this.notifications.AUTH_RESPONSE, {
status: 'OK',
})
} catch (error) {
console.log('error:', error)
self.sendSocketNotification(self.notifications.AUTH_RESPONSE, {
console.error('Netatmo:', error)
this.sendSocketNotification(this.notifications.AUTH_RESPONSE, {
payloadReturn: error,
status: 'NOTOK',
message: error,
})
}
};
},
loadData: async function (config) {
const self = this
self.config = config

if (self.config.mockData === true) {
self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
async loadData () {
if (this.config.mockData === true) {
this.sendSocketNotification(this.notifications.DATA_RESPONSE, {
payloadReturn: this.mockData(),
status: 'OK',
})
return
}
if (self.token === null || self.token === undefined) {
self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
};

if (!this.token) {
this.sendSocketNotification(this.notifications.DATA_RESPONSE, {
payloadReturn: 400,
status: 'INVALID_TOKEN',
message: 'token not set',
})
return
}
};

try {
let result = await fetch('https://' + self.config.apiBase + self.config.dataEndpoint, {
let result = await fetch(`https://${this.config.apiBase}${this.config.dataEndpoint}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${self.token}`,
Authorization: `Bearer ${this.token}`,
},
})

if (result.status === 403) {
console.log('status code:', result.status, '\n', result.statusText)
self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
console.warn(`Netatmo: status code: ${result.status} (${result.statusText})`)
this.sendSocketNotification(this.notifications.DATA_RESPONSE, {
payloadReturn: result.statusText,
status: 'INVALID_TOKEN',
message: result,
})
return
}
};

result = await result.json()

if (result.error) {
throw new Error(result.error.message)
}
};

self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
this.sendSocketNotification(this.notifications.DATA_RESPONSE, {
payloadReturn: result.body.devices,
status: 'OK',
})
} catch (error) {
console.log('error:', error)
self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
console.error('Netatmo:', error)
this.sendSocketNotification(this.notifications.DATA_RESPONSE, {
payloadReturn: error,
status: 'NOTOK',
message: error,
})
}
};
},
mockData: function () {

mockData () {
const sample = fs.readFileSync(path.join(__dirname, 'sample', 'sample.json'), 'utf8')
return JSON.parse(sample)
},
socketNotificationReceived: function (notification, payload) {

socketNotificationReceived (notification, payload) {
switch (notification) {
case 'INIT':
this.Init(payload)
break
case this.notifications.AUTH:
this.authenticate(payload)
this.authenticate()
break
case this.notifications.DATA:
this.loadData(payload)
this.loadData()
break
}
},

Init (config) {
this.config = config
if (!this.config.clientId) {
console.error('Netatmo: clientId not set in config.')
return
}
this.clientId = this.config.clientId

if (!this.config.clientSecret) {
console.error('Netatmo: clientSecret not set in config.')
return
}
this.clientSecret = this.config.clientSecret

const refreshToken = this.readToken()
this.refreshToken = refreshToken || this.config.refresh_token

if (!this.refreshToken) {
console.error('Netatmo: refresh_token not set in config.')
return
}

console.log('Netatmo: Initialized')
this.authenticate()
},

/* from MMM-NetatmoThermostat
* @bugsounet
*/
readToken () {
const file = path.resolve(__dirname, './token.json')
// check presence of token.json
if (fs.existsSync(file)) {
console.log('Netatmo: using token.json file')
const tokenFile = JSON.parse(fs.readFileSync(file))
const refreshToken = tokenFile.refresh_token
if (!refreshToken) {
console.error('Netatmo: Token not found in token.json file')
console.log('Netatmo: using refresh_token from config')
return null
}
return refreshToken
}
// Token file not used
console.log('Netatmo: using refresh_token from config')
return null
},

writeToken (token) {
try {
const file = path.resolve(__dirname, './token.json')
fs.writeFileSync(file, JSON.stringify(token))
console.log('Netatmo: token.json was written successfully')
return token
} catch (error) {
;
console.error('Netatmo: writeToken error', error.message)
return null
};
},

// Refresh Token
async authenticateRefresh (refreshToken) {
console.log('Netatmo: Refresh Token')
const params = new URLSearchParams()
params.append('grant_type', 'refresh_token')
params.append('refresh_token', refreshToken)
params.append('client_id', this.clientId)
params.append('client_secret', this.clientSecret)

try {
const result = await fetch(`https://${this.config.apiBase}${this.config.authEndpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
}).then((response) => response.json())

if (result.error) {
throw new Error(result.error)
};

this.writeToken(result)
this.token = result.access_token
console.log('Netatmo: TOKEN Updated')

if (result.expires_in) {
const expireAt = moment(Date.now() + (result.expires_in * 1000)).format('LLLL')
console.log(`Netatmo: New Token Expire ${expireAt}`)
setTimeout(() => this.authenticateRefresh(result.refresh_token), (result.expires_in - 60) * 1000)
};

this.sendSocketNotification(this.notifications.AUTH_RESPONSE, {
status: 'OK',
})
} catch (error) {
console.error('Netatmo:', error)
this.sendSocketNotification(this.notifications.AUTH_RESPONSE, {
payloadReturn: error,
status: 'NOTOK',
message: error,
})
console.log('Netatmo: Retry login in 60 sec')
setTimeout(() => this.authenticate(this.config), 60 * 1000)
}
},
}
Loading

0 comments on commit 74a848b

Please sign in to comment.