iobroker.netatmo-crawler
Version:
Crawls information from public netatmo stations
659 lines (617 loc) • 27.1 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v1.26.0
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
const url = require('url');
const moment = require('moment');
const request = require('request');
// const { Adapter } = require('@iobroker/adapter-core');
let logger;
let myAdapter;
// Load your modules here, e.g.:
// const fs = require('fs');
class NetatmoCrawler extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options]
*/
constructor(options) {
super({
...options,
name: 'netatmo-crawler',
});
this.on('ready', this.onReady.bind(this));
this.on('unload', this.onUnload.bind(this));
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize your adapter here
logger = this.log;
// eslint-disable-next-line @typescript-eslint/no-this-alias
myAdapter = this;
// The adapters config (in the instance object everything under the attribute 'native') is accessible via
// this.config:
this.log.debug(`config stationUrls: ${this.config.stationUrls}`);
this.log.debug(`Going to save station information with: ${this.config.stationNameType}`);
const regex = /(https:\/\/weathermap\.netatmo\.com\/{1,}[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
const stationUrls = this.config.stationUrls.match(regex) || [];
if (!stationUrls.length) {
this.log.warn(`no valid station urls detected at ${this.config.stationUrls}`);
}
try {
let token = await this.getAuthorizationToken(this);
for (const [counter, stationUrl] of stationUrls.entries()) {
this.log.debug(`Working with stationUrl: ${stationUrl}`);
if (stationUrl) {
try {
await this.getStationData(counter + 1, stationUrl, token, this.config.stationNameType);
} catch (e) {
this.log.warn(`Could not work with station ${counter + 1} - Message: ${e}`);
}
}
}
this.log.debug('all done, exiting');
this.terminate
? this.terminate('Everything done. Going to terminate till next schedule', 0)
: process.exit(0);
} catch (e) {
// if (this.supportsFeature && this.supportsFeature('PLUGINS')) {
// const sentryInstance = this.getPluginInstance('sentry');
// if (sentryInstance) {
// sentryInstance.getSentryObject().captureException(e);
// }
// }
this.log.error(`Error while running Netatmo Crawler. Error Message:${e}`);
this.log.debug('all done, exiting');
this.terminate ? this.terminate(15) : process.exit(15);
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param {() => void} callback
*/
onUnload(callback) {
try {
// Here you must clear all timeouts or intervals that may still be active
// clearTimeout(timeout1);
// clearTimeout(timeout2);
// ...
// clearInterval(interval1);
callback();
} catch {
callback();
}
}
async getStationData(id, url, token, stationNameType) {
logger.debug(`Going to get information for station: ${id}`);
let stationid = this.getStationId(url);
let stationData = await this.getPublicData(stationid, token);
if (!stationData) {
logger.info('Got no station data. Trying again');
await this.sleep(1000);
stationData = await this.getPublicData(stationid, token);
}
if (stationData && stationData.measures) {
logger.debug(`Saving station data for station: ${id}`);
if (stationNameType === 'id') {
id = stationid;
}
await this.saveStationData(id, stationData);
const measureTypes = [
'temperature',
'rain',
'pressure',
'humidity',
'windangle',
'guststrength',
'windstrength',
];
for (const measure of measureTypes) {
if (this.hasMeasure(stationData.measures, measure)) {
logger.debug(`Saving data for station: ${id} and measure: ${measure}`);
let measureLastUpdated = this.getMeasureTimestamp(stationData.measures, measure);
if (measureLastUpdated) {
await this.saveTimestamp(id, measure, measureLastUpdated);
}
if (measure === 'windangle') {
await this.saveMeasure(
id,
measure,
this.getWindrichtungsName(this.getMeasureValue(stationData.measures, measure)),
);
} else if (measure === 'guststrength' || measure === 'windstrength') {
await this.saveMeasure(id, measure, this.getMeasureValue(stationData.measures, measure));
await this.saveMeasure(id, `${measure}2`, this.getMeasureValue(stationData.measures, measure));
} else {
await this.saveMeasure(id, measure, this.getMeasureValue(stationData.measures, measure));
}
if (measure === 'rain') {
let startOfDay = moment().startOf('day');
if (startOfDay.add(15, 'minutes').isBefore(moment())) {
let rainToday = await this.getRainToday(stationData.measures, stationid, token);
if (rainToday == null) {
await this.sleep(1000);
rainToday = await this.getRainToday(stationData.measures, stationid, token);
}
if (rainToday != null) {
await this.saveMeasure(id, 'rain_today', rainToday);
logger.debug(`Saved rain_today for station: ${id}`);
await this.saveMeasure(id, 'lastUpdated.rain_today', moment().valueOf());
}
} else {
logger.debug('Not getting rain today, because it is start of a new day');
}
let rainYesterday = await this.getRainYesterday(stationData.measures, stationid, token);
if (rainYesterday == null) {
await this.sleep(1000);
rainYesterday = await this.getRainYesterday(stationData.measures, stationid, token);
}
if (rainYesterday != null) {
await this.saveMeasure(id, 'rain_yesterday', rainYesterday);
logger.debug(`Saved rain_yesterday for station: ${id}`);
await this.saveMeasure(id, 'lastUpdated.rain_yesterday', moment().valueOf());
}
let rainLastHour = await this.getRainLastHour(stationData.measures, stationid, token);
if (rainLastHour == null) {
await this.sleep(1000);
rainLastHour = await this.getRainLastHour(stationData.measures, stationid, token);
}
if (rainLastHour != null) {
await this.saveMeasure(id, 'rain_lastHour', rainLastHour);
logger.debug(`Saved rain_lastHour for station: ${id}`);
await this.saveMeasure(id, 'lastUpdated.rain_lastHour', moment().valueOf());
}
}
}
}
} else {
throw `Did not get any values for station ${stationid}`;
}
}
async saveStationData(id, stationData) {
await this.saveMeasure(id, 'info.stationId', stationData['_id']);
await this.saveMeasure(id, 'info.city', stationData['place']['city']);
await this.saveMeasure(id, 'info.country', stationData['place']['country']);
await this.saveMeasure(id, 'info.street', stationData['place']['street']);
await this.saveMeasure(id, 'info.lastInfoRetrieved', moment().valueOf());
}
async saveMeasure(id, measureName, measureValue) {
if (measureValue !== null) {
const stateName = `stationData.${id}.${measureName}`;
let roundValue = false;
switch (measureName) {
case 'rain':
await this.createOwnState(stateName, 'mm', 'number', 'value.rain.hour');
roundValue = true;
break;
case 'rain_today':
await this.createOwnState(stateName, 'mm', 'number', 'value.rain.today');
roundValue = true;
break;
case 'rain_yesterday':
await this.createOwnState(stateName, 'mm', 'number', 'value');
roundValue = true;
break;
case 'rain_lastHour':
await this.createOwnState(stateName, 'mm', 'number', 'value.rain.hour');
roundValue = true;
break;
case 'pressure':
await this.createOwnState(stateName, 'mBar', 'number', 'value.pressure');
roundValue = true;
break;
case 'temperature':
await this.createOwnState(stateName, '°C', 'number', 'value.temperature');
roundValue = true;
break;
case 'humidity':
await this.createOwnState(stateName, '%', 'number', 'value.humidity');
roundValue = true;
break;
case 'windangle':
await this.createOwnState(stateName, null, 'string', 'weather.direction.wind ');
break;
case 'windstrength':
await this.createOwnState(stateName, 'km/h', 'number', 'value.speed.wind');
roundValue = true;
break;
case 'windstrength2':
await this.createOwnState(stateName, 'm/s', 'number', 'value.speed.wind');
measureValue = (measureValue * 1000) / 3600;
roundValue = true;
break;
case 'guststrength':
await this.createOwnState(stateName, 'km/h', 'number', 'value.speed.wind.gust');
roundValue = true;
break;
case 'guststrength2':
await this.createOwnState(stateName, 'm/s', 'number', 'value.speed.wind.gust');
measureValue = (measureValue * 1000) / 3600;
roundValue = true;
break;
case 'info.lastInfoRetrieved':
await this.createOwnState(stateName, null, 'number', 'date');
break;
default:
if (measureName.startsWith('lastUpdated')) {
await this.createOwnState(stateName, null, 'number', 'date');
} else {
await this.createOwnState(stateName, null, 'string', 'text');
}
break;
}
if (roundValue) {
measureValue = this.roundValue(measureValue);
}
await this.setStateAsync(stateName, measureValue, true);
}
}
async saveTimestamp(id, measureName, timestamp) {
if (timestamp !== null) {
const stateName = `stationData.${id}.lastUpdated.${measureName}`;
await this.createOwnState(stateName, null, 'number', 'date');
timestamp = Number.parseInt(timestamp) * 1000;
await this.setStateAsync(stateName, timestamp, true);
}
}
async createOwnState(stateName, unit, type, role) {
await this.setObjectNotExistsAsync(stateName, {
type: 'state',
common: {
name: stateName,
type: type,
role: role,
read: true,
write: false,
unit: unit,
},
native: {},
});
}
getStationId(u) {
const queryParams = url.parse(u, true).query;
const stationId = queryParams['stationid'];
return stationId;
}
async saveState(name, value) {
await this.createOwnState(name, null, 'string', 'text');
await this.setStateAsync(name, value, true);
}
getAuthorizationToken(adapter) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (res, rej) => {
const tokenState = 'common.authorisationToken';
logger.debug('Trying to get token from state');
let token = await adapter.getStateAsync(tokenState);
let foundToken = false;
if (token && null !== token.val) {
const now = new Date().getTime();
if (now < token.ts + 86400 * 1000) {
//logger.debug('Token is fresh, using it. Now:' + now + ' Token Timestamp: ' + token.ts + ' Token calculated: ' + (token.ts + 86400 * 1000));
foundToken = true;
res(token.val);
}
}
if (!foundToken) {
logger.debug('Found no token in state, going to get it from website');
request(
{
url: 'https://auth.netatmo.com/weathermap/token',
headers: {
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8,en-US;q=0.7',
'User-Agent': 'Mozilla/5.0',
},
rejectUnauthorized: false,
},
async function (error, response, body) {
if (error) {
rej(error);
}
//logger.debug('Body:' + body);
if (!body) {
await adapter.saveState(tokenState, null);
rej('No body for authorization token found.');
}
try {
logger.debug(`Body:${body}`);
const innerBody = JSON.parse(body);
if (innerBody && innerBody.body) {
const token = `Bearer ${innerBody.body}`;
logger.debug(`Token:${token}`);
await adapter.saveState(tokenState, token);
res(token);
} else {
rej('No authorization token found');
}
} catch (e) {
rej(`Could not load page to get authorization token: ${e}`);
}
},
);
}
});
}
getPublicData(stationId, token) {
return new Promise((res, rej) => {
logger.info(`Getting data for stationid:${stationId}`);
request.post(
{
url: 'https://app.netatmo.net/api/getpublicmeasure',
rejectUnauthorized: false,
headers: {
Authorization: token,
},
json: {
device_id: stationId,
},
},
async function (error, response, body) {
if (error) {
rej(error);
}
if (body && body.body) {
logger.debug(`Body:${JSON.stringify(body.body)}`);
//console.log('Body:' + JSON.stringify(responseBody, null, 4));
try {
res(body.body[0]);
} catch (e) {
logger.warn(`Could not get Data for station ${stationId}: ${e}`);
res('');
}
//res(body)
} else {
logger.info(`No body:${JSON.stringify(body)}`);
if (body && body.error && body.error.code === 2) {
logger.debug('Accesstoken is invalid. Going to reset state');
await myAdapter.saveState('common.authorisationToken', null);
}
res('');
}
},
);
});
}
hasMeasure(measures, measureName) {
let measureKey = Object.keys(measures).filter(key => {
return measures[key].type.indexOf(measureName) !== -1;
});
if (Array.isArray(measureKey) && measureKey.length > 0) {
return true;
}
return false;
}
getMeasureValue(measures, measureName) {
let measureKey = Object.keys(measures).filter(key => {
return measures[key].type.indexOf(measureName) !== -1;
});
if (Array.isArray(measureKey) && measureKey.length > 0) {
let measureIndex = measures[measureKey[0]].type.indexOf(measureName);
const measureValues = measures[measureKey[0]].res[Object.keys(measures[measureKey[0]].res)[0]];
const measureValue = measureValues[measureIndex];
return measureValue;
}
return null;
}
getMeasureTimestamp(measures, measureName) {
let measureKey = Object.keys(measures).filter(key => {
return measures[key].type.indexOf(measureName) !== -1;
});
if (Array.isArray(measureKey) && measureKey.length > 0) {
let res = measures[measureKey[0]].res;
let timeStamp = Object.keys(res)[0];
return timeStamp;
}
return null;
}
getRainToday(measures, stationId, token) {
return new Promise((res, rej) => {
//console.log('Getting data for stationid:' + stationId);
var start = moment().startOf('day').unix();
const moduleId = Object.keys(measures).filter(key => {
return measures[key].type.indexOf('rain') !== -1;
})[0];
const inputObj = {
device_id: stationId,
module_id: moduleId,
scale: '1day',
type: 'sum_rain',
real_time: true,
date_begin: start.toString(),
};
//console.log('InputObj:' + JSON.stringify(inputObj));
request.post(
{
url: 'https://app.netatmo.net/api/getmeasure',
rejectUnauthorized: false,
headers: {
Authorization: token,
},
json: inputObj,
},
async function (error, response, body) {
if (error) {
rej(error);
}
logger.debug(`Body Rain_Today:${JSON.stringify(body)}`);
if (body && body.body) {
if (!body.body[0] || !body.body[0].value) {
logger.debug(`No rain today value for Station ${stationId}`);
res('');
}
//console.log('Body:' + JSON.stringify(responseBody, null, 4));
try {
const rainToday = body.body[0].value[0][0];
logger.debug(`Rain Today for Station ${stationId} is: ${rainToday}`);
res(rainToday);
} catch (e) {
logger.info(`Could not get Rain Today for station ${stationId}: ${e}`);
res('');
}
//res(body)
} else {
logger.info(`No body in Rain_Today:${JSON.stringify(body)}`);
res('');
}
},
);
});
}
getRainYesterday(measures, stationId, token) {
return new Promise((res, rej) => {
//console.log('Getting data for stationid:' + stationId);
var start = moment().subtract(1, 'days').startOf('day').unix();
var end = moment().subtract(1, 'days').endOf('day').unix();
const moduleId = Object.keys(measures).filter(key => {
return measures[key].type.indexOf('rain') !== -1;
})[0];
const inputObj = {
device_id: stationId,
module_id: moduleId,
scale: '1day',
type: 'sum_rain',
real_time: true,
date_begin: start.toString(),
date_end: end.toString(),
};
//console.log('InputObj:' + JSON.stringify(inputObj));
request.post(
{
url: 'https://app.netatmo.net/api/getmeasure',
rejectUnauthorized: false,
headers: {
Authorization: token,
},
json: inputObj,
},
async function (error, response, body) {
if (error) {
rej(error);
}
if (body && body.body) {
logger.debug(`Body Rain_Yesterday:${JSON.stringify(body.body)}`);
//console.log('Body:' + JSON.stringify(responseBody, null, 4));
try {
const rainYesterday = body.body[0].value[0][0];
logger.debug(`Rain Yesterday for Station ${stationId} is: ${rainYesterday}`);
res(rainYesterday);
} catch (e) {
logger.info(`Could not get Rain Yesterday for station ${stationId}: ${e}`);
res('');
}
//res(body)
} else {
logger.info(`No body in Rain_Yesterday:${JSON.stringify(body)}`);
res('');
}
},
);
});
}
getRainLastHour(measures, stationId, token) {
return new Promise((res, rej) => {
//console.log('Getting data for stationid:' + stationId);
var start = moment().subtract(1, 'hour').unix();
var end = moment().unix();
const moduleId = Object.keys(measures).filter(key => {
return measures[key].type.indexOf('rain') !== -1;
})[0];
const inputObj = {
device_id: stationId,
module_id: moduleId,
scale: '1hour',
type: 'sum_rain',
real_time: true,
date_begin: start.toString(),
date_end: end.toString(),
};
//console.log('InputObj:' + JSON.stringify(inputObj));
request.post(
{
url: 'https://app.netatmo.net/api/getmeasure',
rejectUnauthorized: false,
headers: {
Authorization: token,
},
json: inputObj,
},
async function (error, response, body) {
if (error) {
rej(error);
}
if (body && body.body) {
logger.debug(`Body Rain_LastHour:${JSON.stringify(body.body)}`);
//console.log('Body:' + JSON.stringify(responseBody, null, 4));
try {
const rainLastHour = body.body[0].value[0][0];
logger.debug(`Rain Last Hour for Station ${stationId} is: ${rainLastHour}`);
res(rainLastHour);
} catch (e) {
logger.info(`Could not get Rain LastHour for station ${stationId}: ${e}`);
res('');
}
//res(body)
} else {
logger.info(`No body in Rain_LastHour:${JSON.stringify(body)}`);
res('');
}
},
);
});
}
getWindrichtungsName(value) {
if (value === -1) {
return 'calm';
}
if (value < 22.5 || value >= 337.5) {
return 'N';
}
if (value < 67.5 && value >= 22.5) {
return 'NE';
}
if (value < 125.5 && value >= 67.5) {
return 'E';
}
if (value < 157.5 && value >= 125.5) {
return 'SE';
}
if (value < 202.5 && value >= 157.5) {
return 'S';
}
if (value < 247.5 && value >= 202.5) {
return 'SW';
}
if (value < 292.5 && value >= 247.5) {
return 'W';
}
if (value < 337.5 && value >= 292.5) {
return 'NW';
}
return 'unknown';
}
sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
roundValue(val) {
const roundedVal = Math.round(val * 100) / 100;
logger.debug(`Rounded value ${val} to ${roundedVal}`);
return roundedVal;
}
}
// @ts-expect-error parent is a valid property on module
if (module.parent) {
// Export the constructor in compact mode
/**
* @param {Partial<utils.AdapterOptions>} [options]
*/
module.exports = options => new NetatmoCrawler(options);
} else {
// otherwise start the instance directly
new NetatmoCrawler();
}