netatmo-node
v1.0.0
Published
Read a Netatmo Weather Station (indoor CO₂, temperature, humidity, outdoor, rain, wind) through Netatmo's official cloud API — a small, modern, Promise-first TypeScript client with zero runtime dependencies and automatic OAuth2 refresh-token rotation.
Maintainers
Readme
netatmo-node
A small, modern, Promise-first TypeScript client for the Netatmo Weather Station — read indoor CO₂, temperature, humidity, pressure, noise, plus outdoor/rain/wind modules — through Netatmo's official cloud API. Zero runtime dependencies.
Netatmo has no local LAN protocol: the station uploads to Netatmo's cloud and
you read it back over HTTPS with an OAuth2 bearer token. This library handles
the token lifecycle for you, including the part everyone gets bitten by —
Netatmo rotates the refresh token on every refresh, so the old one stops
working. netatmo-node hands you the new token via onTokenRefresh; persist it
and your auth never silently dies.
Install
npm install netatmo-nodeRequires Node 18+ (uses the global fetch).
Getting credentials
- Create an app at dev.netatmo.com → note the client id and client secret.
- Generate a token with the
read_stationscope (the dev portal has a token generator), which gives you a refresh token.
Quick start
import { writeFileSync, readFileSync } from 'node:fs'
import { Netatmo, co2Level } from 'netatmo-node'
const n = new Netatmo({
clientId: process.env.NETATMO_CLIENT_ID!,
clientSecret: process.env.NETATMO_CLIENT_SECRET!,
refreshToken: readFileSync('.token', 'utf8').trim(),
// Persist the rotated refresh token or auth dies on the next boot.
onTokenRefresh: (t) => writeFileSync('.token', t.refreshToken),
})
for (const m of await n.getCo2Readings())
console.log(`${m.name}: ${m.co2} ppm (${co2Level(m.co2)})`)
// Office: 1483 ppm (bad)API
new Netatmo(options)| Option | Type | Notes |
| --- | --- | --- |
| clientId | string | from dev.netatmo.com |
| clientSecret | string | from dev.netatmo.com |
| refreshToken | string | long-lived, rotates on each refresh |
| accessToken? | string | optional, skips one refresh on boot |
| accessTokenExpiresAt? | number | unix ms; pair with accessToken |
| onTokenRefresh? | (t) => void \| Promise | persist t.refreshToken |
| timeoutMs? | number | default 15000 |
| maxRetries? | number | default 3 (5xx / network / one re-auth) |
Methods
getStationsData(deviceId?)→Station[]— every station, each with its base unit + modules normalized intoModuleReadings.getCo2Readings()→ModuleReading[]— every module that reports CO₂ (the indoor base + indoor add-ons). The one for "is the office stuffy?".findModule(name)→ModuleReading | undefined— fuzzy, case-insensitive lookup by module name (e.g.findModule('office')).getMeasure({ deviceId, moduleId?, scale, type, dateBegin?, dateEnd?, limit? })→ time-series rows for graphs/trends.currentRefreshToken— the latest (possibly rotated) refresh token.
co2Level(ppm) helper
| Band | ppm | Meaning |
| --- | --- | --- |
| good | < 800 | fresh air |
| fair | 800–999 | fine |
| poor | 1000–1399 | stuffy; ventilate soon |
| bad | ≥ 1400 | headaches/drowsiness; open a window now |
CLI
A tiny CLI (scripts/netatmo.mjs) reads creds from .secrets/netatmo.env and
persists the rotated refresh token back to that file after every run:
node scripts/netatmo.mjs co2 # all CO₂ modules + comfort label
node scripts/netatmo.mjs module office # one module's latest reading
node scripts/netatmo.mjs stations # everything, JSON
node scripts/netatmo.mjs history office CO2 1hourLicense
MIT © Noel Portugal
