iobroker.weatherunderground
Version:
1,018 lines (937 loc) • 137 kB
JavaScript
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
/**
*
* weatherunderground adapter
*
* Adapter loading the json forecast of weatherunderground
*
* note: you need an account and an api key to get the forecast. This is free for non excess usage: 500 requests/d
* see: http://www.wunderground.com/weather/api/d/pricing
*
* register for a key:
* http://www.wunderground.com/weather/api/d/questionnaire.html?plan=a&level=0&history=undefined
*
* see http://www.wunderground.com/weather/api/d/docs?d=data/hourly
* for reference of the possible values from hourly WU forecast
*
*/
'use strict';
const utils = require('@iobroker/adapter-core'); // Get common adapter utils
const axios = require('axios');
const crypto = require('crypto');
const adapterName = require('./package.json').name.split('.').pop();
let adapter;
const dictionary = require('./lib/words');
let lang = 'en';
let locale = 'en-GB';
let nonMetric = false;
const windDirections = ['N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'];
let officialApiKey;
let pwsStationKey;
let newWebKey;
let currentObservationUrl;
let forecastDailyUrl;
let forecastHourlyUrl;
let errorCounter = 0;
let forceTimeout = null;
let stopInProgress = false;
const requestHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows) Gecko/20100101 Firefox/68.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3'
};
function _(text) {
if (!text) {
return '';
}
if (dictionary[text]) {
let newText = dictionary[text][lang];
if (newText) {
return newText;
} else if (lang !== 'en') {
newText = dictionary[text].en;
if (newText) {
return newText;
}
}
}
return text;
}
function time2date(date) {
return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0');
//return date.getDate().toString().padStart(2, '0') + '.' + (date.getMonth() + 1).toString().padStart(2, '0') + '.' + date.getFullYear();
}
function sleep(ms) {
return new Promise(resolve => setTimeout(() => !stopInProgress && resolve(), ms));
}
function startAdapter(options) {
options = options || {};
Object.assign(options, {name: adapterName});
adapter = new utils.Adapter(options);
adapter.on('unload', callback => {
stopInProgress = true;
forceTimeout && clearTimeout(forceTimeout);
callback && callback();
})
adapter.on('ready', async () => {
officialApiKey = adapter.config.apikey;
if (officialApiKey && officialApiKey.length > 0 && officialApiKey.length !== 32) {
adapter.log.warn('API key invalid, please enter the new PWS owner API key or remove the key, ignoring it!');
officialApiKey = '';
}
if (!officialApiKey) {
try {
const instObj = await adapter.getForeignObjectAsync(`system.adapter.${adapter.namespace}`);
if (instObj && instObj.common && instObj.common.schedule && instObj.common.schedule === '12 * * * *') {
instObj.common.schedule = `${Math.floor(Math.random() * 60)} * * * *`;
adapter.log.info(`Default schedule found and adjusted to spread calls better over the full hour!`);
await adapter.setForeignObjectAsync(`system.adapter.${adapter.namespace}`, instObj);
adapter.terminate ? adapter.terminate(0) : process.exit(0);
return;
}
} catch (err) {
adapter.log.error(`Could not check or adjust the schedule: ${err.message}`);
}
const delay = Math.floor(Math.random() * 30000);
adapter.log.debug(`Delay execution by ${delay}ms to better spread API calls`);
await sleep(delay);
}
adapter.config.language = adapter.config.language || 'DL';
switch (adapter.config.language) {
case 'DL':
lang = 'de';
locale = 'de-DE';
break;
case 'EN':
lang = 'en';
locale = 'en-GB';
break;
case 'RU':
lang = 'ru';
locale = 'ru-RU';
break;
case 'NL':
lang = 'nl';
locale = 'nl-NL';
break;
}
if (!adapter.config.country) {
adapter.config.country = 'DE';
}
if (adapter.config.useLegacyApi === undefined) {
adapter.config.useLegacyApi = true;
}
adapter.config.useLegacyApi = false;
if (typeof adapter.config.forecast_periods_txt === 'undefined') {
adapter.log.info('forecast_periods_txt not defined. now enabled. check settings and save');
adapter.config.forecast_periods_txt = true;
}
if (typeof adapter.config.forecast_periods === 'undefined') {
adapter.log.info('forecast_periods not defined. now enabled. check settings and save');
adapter.config.forecast_periods = true;
}
if (typeof adapter.config.forecast_hourly === 'undefined') {
adapter.log.info('forecast_hourly not defined. now enabled. check settings and save');
adapter.config.forecast_hourly = true;
}
if (typeof adapter.config.current === 'undefined') {
adapter.log.info('current not defined. now enabled. check settings and save');
adapter.config.current = true;
}
if (typeof adapter.config.custom_icon_base_url === 'undefined') {
adapter.config.custom_icon_base_url = '';
} else {
adapter.config.custom_icon_base_url = adapter.config.custom_icon_base_url.trim();
if (adapter.config.custom_icon_base_url && adapter.config.custom_icon_base_url[adapter.config.custom_icon_base_url.length - 1] !== '/') {
adapter.config.custom_icon_base_url += '/';
}
}
if (typeof adapter.config.custom_icon_format === 'undefined') {
adapter.config.custom_icon_format = 'gif';
}
adapter.log.debug('on ready: ' + adapter.config.language + ' ' + adapter.config.forecast_periods_txt + ' ' + adapter.config.forecast_periods + ' ' + adapter.config.current + ' ' + adapter.config.forecast_hourly);
nonMetric = !!adapter.config.nonMetric;
await checkWeatherVariables();
adapter.getState('currentStationKey', (err, state) => {
if (!err && state && state.val) {
if (typeof state.val !== 'string') state.val = state.val.toString();
pwsStationKey = state.val;
adapter.log.debug('initialize PWS Station Key: ' + pwsStationKey);
}
adapter.getState('currentWebKey', (err, state) => {
if (!err && state && state.val) {
if (typeof state.val !== 'string') state.val = state.val.toString();
newWebKey = state.val;
adapter.log.debug('initialize Web Key: ' + newWebKey);
}
adapter.getState('currentObservationUrl', (err, state) => {
if (!err && state && state.val) {
if (typeof state.val !== 'string') state.val = state.val.toString();
currentObservationUrl = state.val;
adapter.log.debug('initialize Current Observation url: ' + currentObservationUrl);
}
adapter.getState('forecastDailyUrl', (err, state) => {
if (!err && state && state.val) {
if (typeof state.val !== 'string') state.val = state.val.toString();
forecastDailyUrl = state.val;
adapter.log.debug('initialize Daily Forecast Url: ' + forecastDailyUrl);
if (forecastDailyUrl.includes('/v1/')) {
adapter.log.debug(' Daily Forecast Url incompatible ... refetch');
forecastDailyUrl = '';
}
}
adapter.getState('forecastHourlyUrl', (err, state) => {
if (!err && state && state.val) {
if (typeof state.val !== 'string') state.val = state.val.toString();
forecastHourlyUrl = state.val;
adapter.log.debug('initialize Hourly Forecast Url: ' + forecastHourlyUrl);
if (forecastHourlyUrl.includes('/v1/')) {
adapter.log.debug(' Daily Forecast Url incompatible ... refetch');
forecastHourlyUrl = '';
}
}
adapter.getState('locationChecksum', (err, state) => {
const locationHash = crypto.createHash('md5').update(adapter.config.location + adapter.config.station).digest('hex');
let locationChange = true;
if (!err && state && state.val && locationHash === state.val) {
adapter.log.debug('location has not changed, reuse extracted URLs');
locationChange = false;
}
if (locationChange) {
adapter.log.debug('location change detected, extract URLs');
currentObservationUrl = null;
forecastDailyUrl = null;
forecastHourlyUrl = null;
adapter.setObjectNotExists('locationChecksum', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Helper state to detect location changes', def: ''},
native: {id: 'locationChecksum'}
}, () => adapter.setState('locationChecksum', {val: locationHash, ack: true}));
}
getKeysAndData(() => {
forceTimeout && clearTimeout(forceTimeout);
forceTimeout = setTimeout(() => adapter.stop(), 2000);
});
});
});
});
});
});
});
// force terminate after 1min
// don't know why it does not terminate by itself...
forceTimeout = setTimeout(() => {
forceTimeout = null;
stopInProgress = true;
adapter.log.warn('force terminate');
adapter.terminate ? adapter.terminate(0) : process.exit(0);
}, 60000);
});
return adapter;
}
function getKeysAndData(cb) {
if (errorCounter > 2) {
if (adapter.config.useLegacyApi) {
adapter.config.useLegacyApi = false;
errorCounter = 0;
} else {
return cb();
}
}
getApiKey(() => {
if (stopInProgress) {
return;
}
if (adapter.config.useLegacyApi) {
adapter.log.debug('Use Legacy API');
getLegacyWuData(cb);
} else {
adapter.log.debug('Use New API');
getNewWuDataCurrentObservations(data => getNewWuDataDailyForecast(data, data => getNewWuDataHourlyForecast(data, data => parseNewResult(data, cb))));
}
});
}
function handleIconUrl(original) {
if (!original) return original;
let iconSet = adapter.config.iconSet;
if (typeof original !== 'string') {
original = original.toString();
}
// old url https://icons.wxug.com
// new url https://www.wunderground.com/static
if (original.match(/^[0-9]{1,4}$/)) {
original = `https://www.wunderground.com/static/i/c/v4/${original}.svg`;
if (iconSet === 'i') {
iconSet = null;
}
}
if (iconSet) {
original = `https://www.wunderground.com/static/i/c/${encodeURIComponent(iconSet)}/${original.substring(original.lastIndexOf('/') + 1)}`;
}
else if (adapter.config.custom_icon_base_url) {
const pos = original.lastIndexOf('.');
if (original.substring(pos + 1) !== adapter.config.custom_icon_format) {
original = original.replace(/\.\w+$/, '.' + adapter.config.custom_icon_format);
}
original = adapter.config.custom_icon_base_url + original.substring(original.lastIndexOf('/') + 1);
}
return original;
}
function getApiKey(cb) {
getStationKey(() =>
getWebsiteKey( () => cb()));
}
function getStationKey(cb) {
if (pwsStationKey && pwsStationKey.length) {
return cb && cb();
}
let url = 'https://www.wunderground.com/dashboard/pws/IBERLIN1658';
if (adapter.config.station) {
adapter.config.station = adapter.config.station.trim();
if (adapter.config.station.startsWith('pws:')) {
adapter.config.station = adapter.config.station.substr(4).trim();
}
if (/[^A-Z0-9]/.test(adapter.config.station)) {
adapter.log.info(`Please check the configured station-id "${adapter.config.station}" if it do not work because usually station ids consist of capital letters and numbers only!`)
}
url = 'https://www.wunderground.com/dashboard/pws/' + encodeURIComponent(adapter.config.station);
}
else {
adapter.log.info('using fallback station ID to get key because no PWS station ID provided.');
}
adapter.log.debug('get PWS dashboard page: ' + url);
axios.get(url, {
headers: requestHeaders,
timeout: 15000,
validateStatus: status => status === 200
})
.then(response => {
const body = response.data;
const scriptFile = body.match(/<script src="(.*\/wui-pwsdashboard\/.*wui.pwsdashboard.min.js)"><\/script>/);
if (!scriptFile || !scriptFile[1]) {
const pwsApiKey = body.match(/WU_LEGACY_API_KEY&q;:&q;([^&]+)&q/);
if (!pwsApiKey || !pwsApiKey[1]) {
return cb && cb();
}
pwsStationKey = pwsApiKey[1];
adapter.log.debug('fetched new stationKey from WU webpage-0419: ' + pwsStationKey);
adapter.setObjectNotExists('currentStationKey', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Current Station API Key from webpage', def: ''},
native: {id: 'currentStationKey'}
}, () => {
adapter.setState('currentStationKey', {val: pwsStationKey, ack: true});
});
return cb && cb();
} else {
if (scriptFile[1].startsWith('//')) {
scriptFile[1] = 'https:' + scriptFile[1];
}
adapter.log.debug('get PWS dashboard script: ' + scriptFile[1]);
return axios.get(scriptFile[1], {
headers: requestHeaders,
timeout: 15000,
validateStatus: status => status === 200
});
}
})
.then(response => {
if (!response) {
// no request done
return;
}
const body = response.data;
if (stopInProgress) {
return;
}
// "https://api.wunderground.com/api/606f3f6977348613/conditions/forecast10day/hourly10day/astronomy10day/pwsidentity/units:" + units + "/v:2.0/q/pws:" + stationid + ".json?ID=" + stationid + "&callback=?"
const pwsApiKey = body.match(/https:\/\/api.wunderground.com\/api\/([^\/]+)\/conditions\//);
if (!pwsApiKey || !pwsApiKey[1]) {
return cb && cb();
}
pwsStationKey = pwsApiKey[1];
adapter.log.debug('fetched new stationKey from WU webpage: ' + pwsStationKey);
adapter.setObjectNotExists('currentStationKey', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Current Station API Key from webpage', def: ''},
native: {id: 'currentStationKey'}
}, () => {
adapter.setState('currentStationKey', {val: pwsStationKey, ack: true});
});
return cb && cb();
})
.catch(error => {
// ERROR
adapter.log.error(`Unable to get PWS dashboard script: ${error.response ? error.response.status : '--'}/${error.response && error.response.data ? JSON.stringify(error.response.data) : JSON.stringify(error)}`);
return cb && cb();
});
}
function getWebsiteKey(cb, tryQ) {
if (newWebKey && currentObservationUrl && forecastDailyUrl && forecastHourlyUrl) {
return cb && cb();
}
let url;
if (adapter.config.location.startsWith('pws:') || adapter.config.location.match(/^[A-Z]+[0-9]{1,4}$/) || adapter.config.location.match(/^[0-9]+\.[0-9]+ *, *[0-9]+\.[0-9]+$/)) { // Geocode
url = `https://www.wunderground.com/hourly/${tryQ ? 'q/' : ''}${encodeURIComponent(adapter.config.location)}`;
} else {
url = `https://www.wunderground.com/hourly/${encodeURIComponent(adapter.config.country)}/${tryQ ? 'q/' : ''}${encodeURIComponent(adapter.config.location)}`;
}
adapter.log.debug('get WU weather page: ' + url);
axios.get(url, {
headers: requestHeaders,
timeout: 15000,
validateStatus: status => status === 200
})
.then(response => {
if (stopInProgress) {
return;
}
let body = response.data;
if (body) {
body = body.replace(/&q;/g, '"').replace(/&a;/g, '&');
}
const data = body.match(/api\.weather\.com\/.*apiKey=([0-9a-zA-Z]{32}).*/);
if (!data || !data[1]) {
return cb && cb();
}
newWebKey = data[1];
adapter.log.debug('fetched new webkey from WU weather page: ' + newWebKey);
adapter.setObjectNotExists('currentWebKey', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Current Web Key from webpage', def: ''},
native: {id: 'currentWebKey'}
}, () => {
adapter.setState('currentWebKey', {val: newWebKey, ack: true});
});
const currentObservation = body.match(/"(https:\/\/api\.weather\.com\/[^"]+\/observations\/current[^"]+)"/);
if (currentObservation && currentObservation[1]) {
currentObservationUrl = currentObservation[1];
adapter.log.debug('fetched current observations Url from WU weather page: ' + currentObservationUrl);
adapter.setObjectNotExists('currentObservationUrl', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Current Observations Url', def: ''},
native: {id: 'currentObservationUrl'}
}, () => {
adapter.setState('currentObservationUrl', {val: currentObservationUrl, ack: true});
});
}
const forecastDaily = body.match(/"(https:\/\/api\.weather\.com\/[^"]+\/forecast\/daily\/[^"]+)"/);
//adapter.log.debug('body match forecast: ' + data);
if (forecastDaily && forecastDaily[1]) {
forecastDailyUrl = forecastDaily[1];
adapter.log.debug('fetched forecast 5 day Url from WU weather page: ' + forecastDailyUrl);
adapter.setObjectNotExists('forecastDailyUrl', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Daily Forecast Url', def: ''},
native: {id: 'forecastDailyUrl'}
}, () => {
adapter.setState('forecastDailyUrl', {val: forecastDailyUrl, ack: true});
});
}
const forecastHourly = body.match(/"(https:\/\/api\.weather\.com\/[^"]+\/forecast\/hourly\/[^"]+)"/);
if (forecastHourly && forecastHourly[1]) {
forecastHourlyUrl = forecastHourly[1];
adapter.log.debug('fetched hourly forecast Url from WU weather page: ' + forecastHourlyUrl);
adapter.setObjectNotExists('forecastHourlyUrl', {
type: 'state',
common: {type: 'string', role: 'text', name: 'Hourly Forecast Url', def: ''},
native: {id: 'forecastHourlyUrl'}
}, () => {
adapter.setState('forecastHourlyUrl', {val: forecastHourlyUrl, ack: true});
});
}
return cb && cb();
})
.catch(error => {
if (error.response && error.response.status === 404 && !tryQ) {
getWebsiteKey(cb, true);
} else if (error.response && error.response.status === 404) {
adapter.log.error('The given Location can not be found. Please check on https://wunderground.com or try geo coordinates (lat,lon) or nearby cities!');
return cb && cb();
} else {
// ERROR
adapter.log.error(`Unable to get PWS dashboard script: ${error.response ? error.response.status : '--'}/${error.response && error.response.data ? JSON.stringify(error.response.data) : JSON.stringify(error)}`);
return cb && cb();
}
});
}
async function parseLegacyResult(body, cb) {
let qpfMax = 0;
let popMax = 0;
let uviSum = 0;
adapter.log.debug(`Process legacy results: ${JSON.stringify(body)}`);
if (adapter.config.current) {
if (body.current_observation) {
try {
await adapter.setStateAsync('forecast.current.displayLocationFull', {
ack: true,
val: body.current_observation.display_location.full
});
await adapter.setStateAsync('forecast.current.displayLocationLatitude', {
ack: true,
val: parseFloat(body.current_observation.display_location.latitude)
});
await adapter.setStateAsync('forecast.current.displayLocationLongitude', {
ack: true,
val: parseFloat(body.current_observation.display_location.longitude)
});
await adapter.setStateAsync('forecast.current.displayLocationElevation', {
ack: true,
val: parseFloat(body.current_observation.display_location.elevation)
});
await adapter.setStateAsync('forecast.current.observationLocationFull', {
ack: true,
val: body.current_observation.observation_location.full
});
await adapter.setStateAsync('forecast.current.observationLocationLatitude', {
ack: true,
val: parseFloat(body.current_observation.observation_location.latitude)
});
await adapter.setStateAsync('forecast.current.observationLocationLongitude', {
ack: true,
val: parseFloat(body.current_observation.observation_location.longitude)
});
if (nonMetric) {
await adapter.setStateAsync('forecast.current.observationLocationElevation', {
ack: true,
val: (parseFloat(body.current_observation.observation_location.elevation))
}); // ft
} else {
await adapter.setStateAsync('forecast.current.observationLocationElevation', {
ack: true,
val: (Math.round(parseFloat(body.current_observation.observation_location.elevation) * 0.3048) * 100) / 100
}); // convert ft to m
}
await adapter.setStateAsync('forecast.current.observationLocationStationID', {
ack: true,
val: body.current_observation.station_id
});
await adapter.setStateAsync('forecast.current.localTimeRFC822', {
ack: true,
val: body.current_observation.local_time_rfc822
});
await adapter.setStateAsync('forecast.current.observationTimeRFC822', {
ack: true,
val: body.current_observation.observation_time_rfc822
}); // PDE
await adapter.setStateAsync('forecast.current.observationTime', {
ack: true,
val: new Date(parseInt(body.current_observation.epoch, 10) * 1000).toLocaleString()
}); // PDE
await adapter.setStateAsync('forecast.current.weather', {ack: true, val: body.current_observation.weather});
if (nonMetric) {
await adapter.setStateAsync('forecast.current.temp', {ack: true, val: parseFloat(body.current_observation.temp_f)});
} else {
await adapter.setStateAsync('forecast.current.temp', {ack: true, val: parseFloat(body.current_observation.temp_c)});
}
await adapter.setStateAsync('forecast.current.relativeHumidity', {
ack: true,
val: parseFloat(body.current_observation.relative_humidity.replace('%', ''))
});
await adapter.setStateAsync('forecast.current.windDegrees', {
ack: true,
val: parseFloat(body.current_observation.wind_degrees)
});
await adapter.setStateAsync('forecast.current.windDirection', {
ack: true,
val: windDirections[Math.floor((body.current_observation.wind_degrees + 11.25) / 22.5)]
});
if (nonMetric) {
await adapter.setStateAsync('forecast.current.wind', {
ack: true,
val: parseFloat(body.current_observation.wind_mph)
});
await adapter.setStateAsync('forecast.current.windGust', {
ack: true,
val: parseFloat(body.current_observation.wind_gust_mph)
});
} else {
await adapter.setStateAsync('forecast.current.wind', {
ack: true,
val: parseFloat(body.current_observation.wind_kph)
});
await adapter.setStateAsync('forecast.current.windGust', {
ack: true,
val: parseFloat(body.current_observation.wind_gust_kph)
});
}
await adapter.setStateAsync('forecast.current.pressure', {
ack: true,
val: parseFloat(body.current_observation.pressure_mb)
}); //PDE
if (nonMetric) {
await adapter.setStateAsync('forecast.current.dewPoint', {
ack: true,
val: body.current_observation.dewpoint_f === 'NA' ? null : parseFloat(body.current_observation.dewpoint_f)
});
await adapter.setStateAsync('forecast.current.windChill', {
ack: true,
val: body.current_observation.windchill_f === 'NA' ? null : parseFloat(body.current_observation.windchill_f)
});
await adapter.setStateAsync('forecast.current.feelsLike', {
ack: true,
val: body.current_observation.feelslike_f === 'NA' ? null : parseFloat(body.current_observation.feelslike_f)
});
await adapter.setStateAsync('forecast.current.visibility', {
ack: true,
val: parseFloat(body.current_observation.visibility_mi)
});
} else {
await adapter.setStateAsync('forecast.current.dewPoint', {
ack: true,
val: body.current_observation.dewpoint_c === 'NA' ? null : parseFloat(body.current_observation.dewpoint_c)
});
await adapter.setStateAsync('forecast.current.windChill', {
ack: true,
val: body.current_observation.windchill_c === 'NA' ? null : parseFloat(body.current_observation.windchill_c)
});
await adapter.setStateAsync('forecast.current.feelsLike', {
ack: true,
val: body.current_observation.feelslike_c === 'NA' ? null : parseFloat(body.current_observation.feelslike_c)
});
await adapter.setStateAsync('forecast.current.visibility', {
ack: true,
val: parseFloat(body.current_observation.visibility_km)
});
}
await adapter.setStateAsync('forecast.current.solarRadiation', {
ack: true,
val: body.current_observation.solarradiation
});
await adapter.setStateAsync('forecast.current.UV', {ack: true, val: parseFloat(body.current_observation.UV)});
if (nonMetric) {
if (!isNaN(parseInt(body.current_observation.precip_1hr_in, 10))) {
await adapter.setStateAsync('forecast.current.precipitationHour', {
ack: true,
val: parseInt(body.current_observation.precip_1hr_in, 10)
});
}
if (!isNaN(parseInt(body.current_observation.precip_today_in, 10))) {
await adapter.setStateAsync('forecast.current.precipitationDay', {
ack: true,
val: parseInt(body.current_observation.precip_today_in, 10)
});
}
} else {
if (!isNaN(parseInt(body.current_observation.precip_1hr_metric, 10))) {
await adapter.setStateAsync('forecast.current.precipitationHour', {
ack: true,
val: parseInt(body.current_observation.precip_1hr_metric, 10)
});
}
if (!isNaN(parseInt(body.current_observation.precip_today_metric, 10))) {
await adapter.setStateAsync('forecast.current.precipitationDay', {
ack: true,
val: parseInt(body.current_observation.precip_today_metric, 10)
});
}
}
await adapter.setStateAsync('forecast.current.iconURL', {
ack: true,
val: handleIconUrl(body.current_observation.icon_url)
});
await adapter.setStateAsync('forecast.current.forecastURL', {
ack: true,
val: body.current_observation.forecast_url
});
await adapter.setStateAsync('forecast.current.historyURL', {ack: true, val: body.current_observation.history_url});
adapter.log.debug('all current conditions values set');
} catch (error) {
adapter.log.error('Could not parse Conditions-Data: ' + error);
adapter.log.error('Reported WU-Error Type: ' + body.response.error.type);
}
} else {
adapter.log.error('No current observation data found in response');
}
}
//next 12 periods (day and night) -> text and icon forecast
if (adapter.config.forecast_periods_txt) {
if (body.forecast && body.forecast.txt_forecast && body.forecast.txt_forecast.forecastday) {
for (let i = 0; i < 12; i++) {
if (!body.forecast.txt_forecast.forecastday[i]) continue;
try {
const now = new Date();
now.setHours(now.getHours() + body.forecast.txt_forecast.forecastday[i].period * 12);
await adapter.setStateAsync('forecastPeriod.' + i + 'p.date', {
ack: true,
val: time2date(now)
});
let iconId = parseInt(body.forecast.txt_forecast.forecastday[i].icon, 10);
if (isNaN(iconId)) { // accept that for the case it is not only numbers to get feedback
iconId = body.forecast.txt_forecast.forecastday[i].icon;
}
await adapter.setStateAsync('forecastPeriod.' + i + 'p.icon', {
ack: true,
val: iconId
});
await adapter.setStateAsync('forecastPeriod.' + i + 'p.iconURL', {
ack: true,
val: handleIconUrl(body.forecast.txt_forecast.forecastday[i].icon_url)
});
await adapter.setStateAsync('forecastPeriod.' + i + 'p.title', {
ack: true,
val: body.forecast.txt_forecast.forecastday[i].title
});
if (nonMetric) {
await adapter.setStateAsync('forecastPeriod.' + i + 'p.state', {
ack: true,
val: body.forecast.txt_forecast.forecastday[i].fcttext
});
} else {
await adapter.setStateAsync('forecastPeriod.' + i + 'p.state', {
ack: true,
val: body.forecast.txt_forecast.forecastday[i].fcttext_metric
});
}
await adapter.setStateAsync('forecastPeriod.' + i + 'p.precipitationChance', {
ack: true,
val: body.forecast.txt_forecast.forecastday[i].pop
});
}
catch (error) {
adapter.log.error('exception in : body.txt_forecast' + error);
}
}
}
}
if (adapter.config.forecast_periods) {
//next 6 days
if (body.forecast && body.forecast.simpleforecast && body.forecast.simpleforecast.forecastday) {
for (let i = 0; i < 6; i++) {
if (!body.forecast.simpleforecast.forecastday[i]) continue;
try {
await adapter.setStateAsync('forecast.' + i + 'd.date', {
ack: true,
val: time2date(new Date(parseInt(body.forecast.simpleforecast.forecastday[i].date.epoch, 10) * 1000))
});
await adapter.setStateAsync('forecast.' + i + 'd.tempMax', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].high.fahrenheit) : parseFloat(body.forecast.simpleforecast.forecastday[i].high.celsius)
});
await adapter.setStateAsync('forecast.' + i + 'd.tempMin', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].low.fahrenheit) : parseFloat(body.forecast.simpleforecast.forecastday[i].low.celsius)
});
let iconId = parseInt(body.forecast.simpleforecast.forecastday[i].icon, 10);
if (isNaN(iconId)) { // accept that for the case it is not only numbers to get feedback
iconId = body.forecast.simpleforecast.forecastday[i].icon;
}
await adapter.setStateAsync('forecast.' + i + 'd.icon', {
ack: true,
val: iconId
});
await adapter.setStateAsync('forecast.' + i + 'd.state', {
ack: true,
val: _('state_' + body.forecast.simpleforecast.forecastday[i].icon)
});
await adapter.setStateAsync('forecast.' + i + 'd.iconURL', {
ack: true,
val: handleIconUrl(body.forecast.simpleforecast.forecastday[i].icon_url)
});
await adapter.setStateAsync('forecast.' + i + 'd.precipitationChance', {
ack: true,
val: body.forecast.simpleforecast.forecastday[i].pop
});
await adapter.setStateAsync('forecast.' + i + 'd.precipitationAllDay', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_allday.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_allday.mm)
});
await adapter.setStateAsync('forecast.' + i + 'd.precipitationDay', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_day.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_day.mm)
});
await adapter.setStateAsync('forecast.' + i + 'd.precipitationNight', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_night.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].qpf_night.mm)
});
await adapter.setStateAsync('forecast.' + i + 'd.snowAllDay', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].snow_allday.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].snow_allday.cm)
});
await adapter.setStateAsync('forecast.' + i + 'd.snowDay', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].snow_day.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].snow_day.cm)
});
await adapter.setStateAsync('forecast.' + i + 'd.snowNight', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].snow_night.in) : parseFloat(body.forecast.simpleforecast.forecastday[i].snow_night.cm)
});
await adapter.setStateAsync('forecast.' + i + 'd.windSpeedMax', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].maxwind.mph) : parseFloat(body.forecast.simpleforecast.forecastday[i].maxwind.kph)
});
await adapter.setStateAsync('forecast.' + i + 'd.windDirectionMax', {
ack: true,
val: body.forecast.simpleforecast.forecastday[i].maxwind.dir
});
await adapter.setStateAsync('forecast.' + i + 'd.windDegreesMax', {
ack: true,
val: parseFloat(body.forecast.simpleforecast.forecastday[i].maxwind.degrees)
});
await adapter.setStateAsync('forecast.' + i + 'd.windSpeed', {
ack: true,
val: nonMetric ? parseFloat(body.forecast.simpleforecast.forecastday[i].avewind.mph) : parseFloat(body.forecast.simpleforecast.forecastday[i].avewind.kph)
});
await adapter.setStateAsync('forecast.' + i + 'd.windDirection', {
ack: true,
val: body.forecast.simpleforecast.forecastday[i].avewind.dir
});
await adapter.setStateAsync('forecast.' + i + 'd.windDegrees', {
ack: true,
val: parseFloat(body.forecast.simpleforecast.forecastday[i].avewind.degrees)
});
await adapter.setStateAsync('forecast.' + i + 'd.humidity', {
ack: true,
val: parseFloat(body.forecast.simpleforecast.forecastday[i].avehumidity)
});
await adapter.setStateAsync('forecast.' + i + 'd.humidityMax', {
ack: true,
val: parseFloat(body.forecast.simpleforecast.forecastday[i].maxhumidity)
});
await adapter.setStateAsync('forecast.' + i + 'd.humidityMin', {
ack: true,
val: parseFloat(body.forecast.simpleforecast.forecastday[i].minhumidity)
});
}
catch (error) {
adapter.log.error('exception in : body.simpleforecast' + error);
}
}
}
}
// next 36 hours
if (adapter.config.forecast_hourly) {
if (body.hourly_forecast) {
const type = nonMetric ? 'english' : 'metric';
for (let i = 0; i < 36; i++) {
//if (!body.hourly_forecast[i]) continue;
try {
// see http://www.wunderground.com/weather/api/d/docs?d=resources/phrase-glossary for infos about properties and codes
await adapter.setStateAsync('forecastHourly.' + i + 'h.time', {
ack: true,
val: new Date(parseInt(body.hourly_forecast.validTimeUtc[i], 10) * 1000).toString()
});
await adapter.setStateAsync('forecastHourly.' + i + 'h.temp', {
ack: true,
val: parseFloat(body.hourly_forecast.temperature[i])
});
await adapter.setStateAsync('forecastHourly.' + i + 'h.fctcode', {
ack: true,
val: body.hourly_forecast.iconCode[i]
}); //forecast description number -> see link above
await adapter.setStateAsync('forecastHourly.' + i + 'h.sky', {ack: true, val: body.hourly_forecast.cloudCover[i]}); //?
await adapter.setStateAsync('forecastHourly.' + i + 'h.windSpeed', {
ack: true,
val: parseFloat(body.hourly_forecast.windSpeed[i])
}); // windspeed in kmh
await adapter.setStateAsync('forecastHourly.' + i + 'h.windDirection', {
ack: true,
val: parseFloat(body.hourly_forecast.windDirection[i])
}); //wind dir in degrees
await adapter.setStateAsync('forecastHourly.' + i + 'h.uv', {ack: true, val: parseFloat(body.hourly_forecast.uvIndex[i])}); //UV Index -> wikipedia
await adapter.setStateAsync('forecastHourly.' + i + 'h.humidity', {
ack: true,
val: parseFloat(body.hourly_forecast.relativeHumidity[i])
});
await adapter.setStateAsync('forecastHourly.' + i + 'h.heatIndex', {
ack: true,
val: parseFloat(body.hourly_forecast.temperatureHeatIndex[i])
}); // -> wikipedia
await adapter.setStateAsync('forecastHourly.' + i + 'h.feelsLike', {
ack: true,
val: parseFloat(body.hourly_forecast.temperatureFeelsLike[i])
}); // -> wikipedia
await adapter.setStateAsync('forecastHourly.' + i + 'h.precipitation', {
ack: true,
val: parseFloat(body.hourly_forecast.qpf[i])
}); // Quantitative precipitation forecast
await adapter.setStateAsync('forecastHourly.' + i + 'h.snow', {
ack: true,
val: parseFloat(body.hourly_forecast.qpfSnow[i])
});
await adapter.setStateAsync('forecastHourly.' + i + 'h.precipitationChance', {
ack: true,
val: parseFloat(body.hourly_forecast.precipChance[i])
}); // probability of Precipitation
await adapter.setStateAsync('forecastHourly.' + i + 'h.mslp', {
ack: true,
val: parseFloat(body.hourly_forecast.pressureMeanSeaLevel[i])
}); // mean sea level pressure
await adapter.setStateAsync('forecastHourly.' + i + 'h.visibility', {
ack: true,
val: parseFloat(body.hourly_forecast.visibility[i])
});
qpfMax += Number(body.hourly_forecast.qpf[i]);
uviSum += Number(body.hourly_forecast.uvIndex[i]);
if (Number(body.hourly_forecast.precipChance[i]) > popMax) {
popMax = Number(body.hourly_forecast.precipChance[i]);
}
// 6h
if (i === 5) {
await adapter.setStateAsync('forecastHourly.6h.sum.precipitation', {ack: true, val: qpfMax});
await adapter.setStateAsync('forecastHourly.6h.sum.precipitationChance', {ack: true, val: popMax});
await adapter.setStateAsync('forecastHourly.6h.sum.uv', {ack: true, val: uviSum / 6});
}
// 12h
if (i === 11) {
await adapter.setStateAsync('forecastHourly.12h.sum.precipitation', {ack: true, val: qpfMax});
await adapter.setStateAsync('forecastHourly.12h.sum.precipitationChance', {ack: true, val: popMax});
await adapter.setStateAsync('forecastHourly.12h.sum.uv', {ack: true, val: uviSum / 12});
}
// 24h
if (i === 23) {
await adapter.setStateAsync('forecastHourly.24h.sum.precipitation', {ack: true, val: qpfMax});
await adapter.setStateAsync('forecastHourly.24h.sum.precipitationChance', {ack: true, val: popMax});
await adapter.setStateAsync('forecastHourly.24h.sum.uv', {ack: true, val: uviSum / 24});
}
} catch (error) {
adapter.log.error('Could not parse Forecast-Data: ' + error);
adapter.log.error('Reported WU-Error Type: ' + body.response.error.type);
}
}
adapter.log.debug('all forecast values set');
}
else {
adapter.log.error('No forecast data found in response');
}
}
cb && cb();
}
async function parseNewResult(body, cb) {
let qpfMax = 0;
let popMax = 0;
let uviSum = 0;
adapter.log.debug(`Process new results: ${JSON.stringify(body)}`);
if (!body || !Object.keys(body).length) {
adapter.log.error('No data received!');
return cb && cb();
}
if (adapter.config.current) {
if (body.current_observation) {
if (nonMetric && body.current_observation.imperial) {
body.current_observation.metric = body.current_observation.imperial;
}
try {
await adapter.setStateAsync('forecast.current.displayLocationFull', {
ack: true,
val: body.current_observation.neighborhood
});
await adapter.setStateAsync('forecast.current.displayLocationLatitude', {
ack: true,
val: body.current_observation.lat
});
await adapter.setStateAsync('forecast.current.displayLocationLongitude', {
ack: true,
val: body.current_observation.lon
});
await adapter.setStateAsync('forecast.current.displayLocationElevation', {
ack: true,
val: body.current_observation.metric.elev
});
await adapter.setStateAsync('forecast.current.observationLocationFull', {
ack: true,
val: body.current_observation.neighborhood
});
await adapter.setStateAsync('forecast.current.observationLocationLatitude', {