Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.1.0: Token read/write // refact code #382

Merged
merged 13 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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