UNPKG

iobroker.weatherunderground

Version:
1,018 lines (937 loc) 137 kB
/* 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', {