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

fix: netatmo api authentication #182

Merged
merged 10 commits into from
Aug 14, 2023
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: 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

Check warning on line 23 in compose/config/config.js.template

View check run for this annotation

In Solidarity / Inclusive Language

Match Found

Please consider an alternative to `Whitelist`. Possibilities include: `include list`, `allow list`
Raw output
/white[_-]*list/gi
// 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
Loading