You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
16 KiB

/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
const xml2js = require('xml2js');
const http = require('http');
const utils = require(__dirname + '/lib/utils'); // Get common adapter utils
const dictionary = require(__dirname + '/lib/words');
const adapter = utils.Adapter({
name: 'yr', // adapter name
useFormatDate: true // read date format from config
});
adapter.on('ready', main);
function main() {
const tmp = adapter.config.location.split('/');
const city = decodeURI(tmp.pop());
adapter.config.language = adapter.config.language || 'en';
if (adapter.config.sendTranslations === undefined) adapter.config.sendTranslations = true;
if (adapter.config.sendTranslations === 'true') adapter.config.sendTranslations = true;
if (adapter.config.sendTranslations === 'false') adapter.config.sendTranslations = false;
adapter.getObject('forecast.day0.temperatureActual', (err, obj) => {
if (obj && obj.common && obj.common.unit) {
if (obj.common.unit === '°C' && adapter.config.nonMetric) {
obj.common.unit = '°F';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
} else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) {
obj.common.unit = '°C';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
}
}
});
for (let d = 0; d < 3; d++) {
adapter.getObject('forecast.day' + d + '.windSpeed', (err, obj) => {
if (obj && obj.common && obj.common.unit) {
if (obj.common.unit === 'km/h' && adapter.config.nonMetric) {
obj.common.unit = 'm/h';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
} else if (obj.common.unit !== 'km/h' && !adapter.config.nonMetric) {
obj.common.unit = 'km/h';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
}
}
});
adapter.getObject('forecast.day' + d + '.temperatureMin', (err, obj) => {
if (obj && obj.common && obj.common.unit) {
if (obj.common.unit === '°C' && adapter.config.nonMetric) {
obj.common.unit = '°F';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
} else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) {
obj.common.unit = '°C';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
}
}
});
adapter.getObject('forecast.day' + d + '.temperatureMax', (err, obj) => {
if (obj && obj.common && obj.common.unit) {
if (obj.common.unit === '°C' && adapter.config.nonMetric) {
obj.common.unit = '°F';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
} else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) {
obj.common.unit = '°C';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
}
}
});
adapter.getObject('forecast.day' + d + '.precipitation', (err, obj) => {
if (obj && obj.common && obj.common.unit) {
if (obj.common.unit === 'mm' && adapter.config.nonMetric) {
obj.common.unit = 'in';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
} else if (obj.common.unit !== 'mm' && !adapter.config.nonMetric) {
obj.common.unit = 'mm';
adapter.setObject(obj._id, obj, () => {
adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`);
});
}
}
});
}
adapter.getObject('forecast', (err, obj) => {
if (!obj || !obj.common || obj.common.name !== 'yr.no forecast ' + city) {
adapter.setObject('forecast', {
type: 'device',
role: 'forecast',
common: {
name: 'yr.no forecast ' + city
},
native: {
url: adapter.config.location,
country: decodeURI(tmp[0]),
state: decodeURI(tmp[1]),
city: city
}
});
}
});
if (adapter.config.location.indexOf('forecast.xml') === -1) {
if (adapter.config.location.indexOf('%') === -1) adapter.config.location = encodeURI(adapter.config.location);
adapter.setState('forecast.info.diagram', 'http://www.yr.no/place/' + adapter.config.location + '/avansert_meteogram.png', true);
const reqOptions = {
hostname: 'www.yr.no',
port: 80,
path: '/place/' + adapter.config.location + '/forecast.xml',
method: 'GET'
};
adapter.log.debug('get http://' + reqOptions.hostname + reqOptions.path);
const req = http.request(reqOptions, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
adapter.log.debug('received data from yr.no');
parseData(data.toString());
});
});
req.on('error', e => {
adapter.log.error(e.message);
parseData(null);
});
req.end();
} else {
parseData(require('fs').readFileSync(adapter.config.location).toString());
}
// Force terminate after 5min
setTimeout(() => {
adapter.log.error('force terminate');
process.exit(1);
}, 300000);
}
function _(text) {
if (!text) return '';
if (dictionary[text]) {
let newText = dictionary[text][adapter.config.language];
if (newText) {
return newText;
} else if (adapter.config.language !== 'en') {
newText = dictionary[text].en;
if (newText) {
return newText;
}
}
} else {
if (adapter.config.sendTranslations) {
const options = {
hostname: 'download.yunkong2.net',
port: 80,
path: '/yr.php?word=' + encodeURIComponent(text)
};
const req = http.request(options, res => {
console.log('STATUS: ' + res.statusCode);
adapter.log.info('Missing translation sent to yunkong2.net: "' + text + '"');
});
req.on('error', e => {
adapter.log.error('Cannot send to server missing translation for "' + text + '": ' + e.message);
});
req.end();
} else {
adapter.log.warn('Translate: "' + text + '": {"en": "' + text + '", "cn": "' + text + '"}, please send to developer');
}
}
return text;
}
function celsius2fahrenheit(degree, isConvert) {
if (isConvert) {
return degree * 9 / 5 + 32;
} else {
return degree;
}
}
function parseData(xml) {
if (!xml) {
setTimeout(() => process.exit(0), 5000);
return;
}
const options = {
explicitArray: false,
mergeAttrs: true
};
const parser = new xml2js.Parser(options);
parser.parseString(xml, (err, result) => {
if (err) {
adapter.log.error(err);
} else {
adapter.log.info('got weather data from yr.no');
const forecastArr = result.weatherdata.forecast.tabular.time;
let tableDay = '<table style="border-collapse: collapse; padding: 0; margin: 0"><tr class="yr-day">';
let tableHead = '</tr><tr class="yr-time">';
let tableMiddle = '</tr><tr class="yr-img">';
let tableBottom = '</tr><tr class="yr-temp">';
const dateObj = new Date();
const dayEnd = dateObj.getFullYear() + '-' + ('0' + (dateObj.getMonth() + 1)).slice(-2) + '-' + ('0' + dateObj.getDate()).slice(-2) + 'T24:00:00';
let daySwitch = false;
let day = -1; // Start from today
const days = [];
for (let i = 0; i < 12 && i < forecastArr.length; i++) {
const period = forecastArr[i];
if (!period.period || period.period === '0') day++;
// We want to process only today, tomorrow and the day after tomorrow
if (day === 3) break;
period.symbol.url = '/adapter/yr/icons/' + period.symbol.var + '.svg';
period.symbol.name = _(period.symbol.name);
period.windDirection.code = _(period.windDirection.code);
period.windDirection.name = _(period.windDirection.name);
if (i < 8) {
switch (i) {
case 0:
tableHead += '<td>' + _('Now') + '</td>';
break;
default:
if (period.from > dayEnd) {
if (!daySwitch) {
daySwitch = true;
tableDay += '<td colspan="' + i + '">' + _('Today') + '</td><td colspan="4">' + _('Tomorrow') + '</td>';
if (i < 3) tableDay += '<td colspan="' + (4 - i) + '">' + _('After tomorrow') + '</td>';
tableHead += '<td>' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '</td>';
} else {
tableHead += '<td>' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '</td>';
}
} else {
tableHead += '<td>' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '</td>';
}
}
tableMiddle += '<td><img style="position: relative;margin: 0;padding: 0;left: 0;top: 0;width: 38px;height: 38px;" src="' + period.symbol.url + '" alt="' + period.symbol.name + '" title="' + period.symbol.name + '"><br/>';
tableBottom += '<td><span class="">' + period.temperature.value + '°C</span></td>';
}
if (day === -1 && !i) day = 0;
if (!days[day]) {
days[day] = {
date: new Date(period.from),
icon: period.symbol.url,
state: period.symbol.name,
temperatureMin: celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric),
temperatureMax: celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric),
precipitation: adapter.config.nonMetric ? parseFloat(period.precipitation.value) / 25.4 : parseFloat(period.precipitation.value),
windDirection: period.windDirection.code,
windSpeed: adapter.config.nonMetric ? parseFloat(period.windSpeed.mps) : parseFloat(period.windSpeed.mps) * 3.6,
pressure: parseFloat(period.pressure.value),
count: 1
};
} else {
// Summarize
let t;
// Take icon for day always from 12:00 to 18:00 if possible
if (i === 2) {
days[day].icon = period.symbol.url;
days[day].state = period.symbol.name;
days[day].windDirection = period.windDirection.code;
}
t = celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric);
if (t < days[day].temperatureMin) {
days[day].temperatureMin = t;
} else
if (t > days[day].temperatureMax) {
days[day].temperatureMax = t;
}
days[day].precipitation += adapter.config.nonMetric ? parseFloat(period.precipitation.value) / 25.4 : parseFloat(period.precipitation.value);
days[day].windSpeed += adapter.config.nonMetric ? parseFloat(period.windSpeed.mps) : parseFloat(period.windSpeed.mps) * 3.6;
days[day].pressure += parseFloat(period.pressure.value);
days[day].count++;
}
// Set actual temperature
if (!day && !i) {
days[day].temperatureActual = celsius2fahrenheit(parseInt(period.temperature.value, 10), adapter.config.nonMetric);
}
}
const style = '<style type="text/css">tr.yr-day td {font-family: sans-serif; font-size: 9px; padding:0; margin: 0;}\ntr.yr-time td {text-align: center; font-family: sans-serif; font-size: 10px; padding:0; margin: 0;}\ntr.yr-temp td {text-align: center; font-family: sans-serif; font-size: 12px; padding: 0; margin: 0;}\ntr.yr-img td {text-align: center; padding: 0; margin: 0;}</style>';
const table = style + tableDay + tableHead + tableMiddle + tableBottom + '</tr></table>';
//console.log(JSON.stringify(result, null, " "));
for (day = 0; day < days.length; day++) {
// Take the average
if (days[day].count > 1) {
days[day].precipitation /= days[day].count;
days[day].windSpeed /= days[day].count;
days[day].pressure /= days[day].count;
}
days[day].temperatureMin = Math.round(days[day].temperatureMin);
days[day].temperatureMax = Math.round(days[day].temperatureMax);
days[day].precipitation = Math.round(days[day].precipitation);
days[day].windSpeed = Math.round(days[day].windSpeed * 10) / 10;
days[day].pressure = Math.round(days[day].pressure);
days[day].date = adapter.formatDate(days[day].date);
delete days[day].count;
for (const name in days[day]) {
if (days[day].hasOwnProperty(name)) {
adapter.setState('forecast.day' + day + '.' + name, {val: days[day][name], ack: true});
}
}
}
adapter.log.debug('data successfully parsed. setting states');
adapter.setState('forecast.info.html', {val: table, ack: true});
adapter.setState('forecast.info.object', {val: JSON.stringify(days), ack: true}, () => {
setTimeout(() => process.exit(0), 5000);
});
}
});
}