Skip to content

Commit

Permalink
fix: netatmo api authentication (#182)
Browse files Browse the repository at this point in the history
* fix issue

* fix issue

* change test names

* add compose file

* ignore docker folders

* add compose description

* fix markdownlint findings

* add config

* rename test

* remove old files
  • Loading branch information
CFenner committed Aug 14, 2023
1 parent 8ed3806 commit 3a9e706
Show file tree
Hide file tree
Showing 15 changed files with 463 additions and 188 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

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Ignore all node modules.
node_modules
report
.env
.DS_Store
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
4 changes: 4 additions & 0 deletions compose/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
config/**
css/
modules/
!config/config.js.template
24 changes: 24 additions & 0 deletions compose/README.md
Original file line number Diff line number Diff line change
@@ -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 <branchName>` to load a specific branch
- 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',
header: 'Netatmo',
config: {
clientId: '',
clientSecret: '',
refresh_token: '',
},
},
```

- open MagicMirror ui at [`http://0.0.0.0:8080`](http://0.0.0.0:8080)
61 changes: 61 additions & 0 deletions compose/config/config.js.template
Original file line number Diff line number Diff line change
@@ -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;}
21 changes: 21 additions & 0 deletions compose/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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"
env_file: ../.env
ports:
- 8080:8080
restart: unless-stopped
command:
- npm
- run
- server
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
}
},
}
Loading

0 comments on commit 3a9e706

Please sign in to comment.