iobroker.followthesun
Version:
369 lines (331 loc) • 18.4 kB
JavaScript
;
/*
* Created with @iobroker/create-adapter v1.25.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');
// Load your modules here, e.g.:
const suncalc = require('suncalc');
const windrose = require('windrose');
const schedule = require('node-schedule');
const jsonExplorer = require('iobroker-jsonexplorer');
const stateAttr = require(__dirname + '/lib/stateAttr.js'); // Load attribute library
const { version } = require('./package.json');
//global variables
let latitude, longitude;
let azimuth, altitude;
let todaySolarNoonTime;
let todayNadirTime;
let polling = null;
let executioninterval = 0;
const dayToMs = 24 * 60 * 60 * 1000;
class Followthesun extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
constructor(options) {
super({
...options,
name: 'followthesun',
});
this.on('ready', this.onReady.bind(this));
//this.on('objectChange', this.onObjectChange.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
//this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
jsonExplorer.init(this, stateAttr);
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize adapter
jsonExplorer.sendVersionInfo(version);
//get adapter configuration
this.log.info('Started with JSON-Explorer version ' + jsonExplorer.version);
executioninterval = this.config.executioninterval;
if (isNaN(executioninterval) || executioninterval < 10) { executioninterval = 120; }
this.log.info(`Sun position calculation will be done every ${executioninterval} seconds`);
//subscribe relevant states changes
this.subscribeStates('altitude');
this.subscribeStates('azimuth');
//get Geodata from configuration
this.getForeignObject('system.config', async (err, obj) => {
if (this.config.overwrite) {
if (!this.config.longOR || !this.config.latOR) {
this.log.error(`Latitude or longitude not set in Adapter Settings!`);
this.terminate ? this.terminate(utils.EXIT_CODES.INVALID_CONFIG_OBJECT) : process.exit(0);
return;
}
this.log.info('Coordinates available in adapter settings - use this settings');
latitude = this.config.latOR;
longitude = this.config.longOR;
this.log.info(`LATITUDE from adapter-config: ${latitude}`);
this.log.info(`LONGITUDE from adapter-config: ${longitude}`);
} else {
if (err || !obj) {
this.log.error('Adapter could not read latitude/longitude in System Settings!');
} else {
latitude = parseFloat(obj.common.latitude);
longitude = parseFloat(obj.common.longitude);
console.log(`LATITUDE from config: ${latitude}`);
console.log(`LONGITUDE from config: ${longitude}`);
this.log.debug(`LATITUDE from config: ${latitude}`);
this.log.debug(`LONGITUDE from config: ${longitude}`);
if (!latitude || !longitude) {
this.log.error(`Latitude or longitude not set in System Settings!`);
this.terminate ? this.terminate(utils.EXIT_CODES.INVALID_CONFIG_OBJECT) : process.exit(0);
return;
}
}
}
//start calculation
try {
await this.CalcSunData();
await this.calcPosition();
} catch (error) {
this.log.error('Error in onReady: ' + error);
console.error('Error in onReady: ' + error);
this.sendSentry(error);
}
});
//Daily schedule somewhen from 00:00:20 to 00:00:40
let scheduleSeconds = Math.round(Math.random() * 20 + 20);
this.log.info(`Daily sun parameter calculation scheduled for 00:00:${scheduleSeconds}`);
schedule.scheduleJob('SunData', `${scheduleSeconds} 0 0 * * *`, async () => {
this.log.info(`Cronjob 'Sun parameter calculation' starts`);
this.CalcSunData();
});
}
async CalcSunData() {
try {
this.log.debug('Run CalcSunData');
let now = new Date();
let thisyear = now.getFullYear();
let nextyear = thisyear + 1;
let startDate = new Date('2000-03-20');
startDate.setHours(7); startDate.setMinutes(39); startDate.setSeconds(22);
let startDateInMs = startDate.getTime();
let spring = ((thisyear - 2000) * 365.24 + 1 / 24) * dayToMs + startDateInMs;
let summer = ((thisyear - 2000) * 365.24 + 92.76 + 2 / 24 + Math.floor((thisyear - 2000) / 12) * 0.01) * dayToMs + startDateInMs;
let autumn = ((thisyear - 2000) * 365.24 + 186.41 + 2 / 24 + Math.floor((thisyear - 2000) / 12) * 0.02) * dayToMs + startDateInMs;
let winter = ((thisyear - 2000) * 365.24 + 276.26 + 1 / 24 + Math.floor((thisyear - 2000) / 12) * 0.02) * dayToMs + startDateInMs;
let springdate_ty = new Date(spring);
let summerdate_ty = new Date(summer);
let autumndate_ty = new Date(autumn);
let winterdate_ty = new Date(winter);
spring = ((nextyear - 2000) * 365.24 + 1 / 24) * dayToMs + startDateInMs;
summer = ((nextyear - 2000) * 365.24 + 92.76 + 2 / 24 + Math.floor((nextyear - 2000) / 12) * 0.01) * dayToMs + startDateInMs;
autumn = ((nextyear - 2000) * 365.24 + 186.41 + 2 / 24 + Math.floor((nextyear - 2000) / 12) * 0.02) * dayToMs + startDateInMs;
winter = ((nextyear - 2000) * 365.24 + 276.26 + 1 / 24 + Math.floor((nextyear - 2000) / 12) * 0.02) * dayToMs + startDateInMs;
let springdate_ny = new Date(spring);
let summerdate_ny = new Date(summer);
let autumndate_ny = new Date(autumn);
let winterdate_ny = new Date(winter);
let days = [];
let sunData = [];
let altitudes = [];
let azimuths = [];
days['short term.today'] = [];
days['short term.today']['numberdays'] = 0;
days['short term.yesterday'] = [];
days['short term.yesterday']['numberdays'] = -1;
days['short term.tomorrow'] = [];
days['short term.tomorrow']['numberdays'] = +1;
days['short term.in one week'] = [];
days['short term.in one week']['numberdays'] = +7;
days['thisYear.start of spring'] = [];
days['thisYear.start of spring']['date'] = new Date(springdate_ty);
days['thisYear.start of summer'] = [];
days['thisYear.start of summer']['date'] = new Date(summerdate_ty);
days['thisYear.start of autumn'] = [];
days['thisYear.start of autumn']['date'] = new Date(autumndate_ty);
days['thisYear.start of winter'] = [];
days['thisYear.start of winter']['date'] = new Date(winterdate_ty);
days['nextYear.start of spring'] = [];
days['nextYear.start of spring']['date'] = new Date(springdate_ny);
days['nextYear.start of summer'] = [];
days['nextYear.start of summer']['date'] = new Date(summerdate_ny);
days['nextYear.start of autumn'] = [];
days['nextYear.start of autumn']['date'] = new Date(autumndate_ny);
days['nextYear.start of winter'] = [];
days['nextYear.start of winter']['date'] = new Date(winterdate_ny);
for (let i in days) {
if (days[i]['date'] == undefined) {
days[i]['date'] = new Date();
days[i]['date'].setDate(days[i]['date'].getDate() + days[i]['numberdays']);
days[i]['date'].setHours(12);
days[i]['date'].setMinutes(0);
days[i]['date'].setSeconds(0);
}
this.log.debug('Timestamp used: ' + i + ' ' + days[i]['date']);
sunData[i] = await suncalc.getTimes(days[i]['date'], latitude, longitude);
}
todaySolarNoonTime = sunData['short term.today'].solarNoon;
todayNadirTime = sunData['short term.today'].nadir;
for (let i in sunData) {
altitudes[i] = {};
altitudes[i].solarnoon = Math.round((await suncalc.getPosition(sunData[i].solarNoon, latitude, longitude).altitude * 180 / Math.PI) * 10) / 10;
altitudes[i].sunset = Math.round((await suncalc.getPosition(sunData[i].sunset, latitude, longitude).altitude * 180 / Math.PI) * 10) / 10;
altitudes[i].sunrise = Math.round((await suncalc.getPosition(sunData[i].sunrise, latitude, longitude).altitude * 180 / Math.PI) * 10) / 10;
altitudes[i].dawn = Math.round((await suncalc.getPosition(sunData[i].dawn, latitude, longitude).altitude * 180 / Math.PI) * 10) / 10;
altitudes[i].dusk = Math.round((await suncalc.getPosition(sunData[i].dusk, latitude, longitude).altitude * 180 / Math.PI) * 10) / 10;
azimuths[i] = {};
azimuths[i].solarnoon = Math.round((await suncalc.getPosition(sunData[i].solarNoon, latitude, longitude).azimuth * 180 / Math.PI + 180) * 10) / 10;
azimuths[i].sunset = Math.round((await suncalc.getPosition(sunData[i].sunset, latitude, longitude).azimuth * 180 / Math.PI + 180) * 10) / 10;
azimuths[i].sunrise = Math.round((await suncalc.getPosition(sunData[i].sunrise, latitude, longitude).azimuth * 180 / Math.PI + 180) * 10) / 10;
azimuths[i].dawn = Math.round((await suncalc.getPosition(sunData[i].dawn, latitude, longitude).azimuth * 180 / Math.PI + 180) * 10) / 10;
azimuths[i].dusk = Math.round((await suncalc.getPosition(sunData[i].dusk, latitude, longitude).azimuth * 180 / Math.PI + 180) * 10) / 10;
jsonExplorer.stateSetCreate(`${i}.solarnoon_time`, `solarnoon time`, sunData[i].solarNoon.getTime());
jsonExplorer.stateSetCreate(`${i}.solarnoon_altitude`, `solarnoon altitude`, altitudes[i].solarnoon);
jsonExplorer.stateSetCreate(`${i}.solarnoon_azimuth`, `solarnoon azimuth`, azimuths[i].solarnoon);
jsonExplorer.stateSetCreate(`${i}.sunset_time`, `sunset time`, sunData[i].sunset.getTime());
jsonExplorer.stateSetCreate(`${i}.sunset_altitude`, `sunset altitude`, altitudes[i].sunset);
jsonExplorer.stateSetCreate(`${i}.sunset_azimuth`, `sunset azimuth`, azimuths[i].sunset);
jsonExplorer.stateSetCreate(`${i}.sunrise_time`, `sunrise time`, sunData[i].sunrise.getTime());
jsonExplorer.stateSetCreate(`${i}.sunrise_altitude`, `sunrise altitude`, altitudes[i].sunrise);
jsonExplorer.stateSetCreate(`${i}.sunrise_azimuth`, `sunrise azimuth`, azimuths[i].sunrise);
jsonExplorer.stateSetCreate(`${i}.dawn_time`, `dawn time`, sunData[i].dawn.getTime());
jsonExplorer.stateSetCreate(`${i}.dawn_altitude`, `dawn altitude`, altitudes[i].dawn);
jsonExplorer.stateSetCreate(`${i}.dawn_azimuth`, `dawn azimuth`, azimuths[i].dawn);
jsonExplorer.stateSetCreate(`${i}.dusk_time`, `dusk time`, sunData[i].dusk.getTime());
jsonExplorer.stateSetCreate(`${i}.dusk_azimuth`, `dusk azimuth`, azimuths[i].dusk);
jsonExplorer.stateSetCreate(`${i}.dusk_altitude`, `dusk altitude`, altitudes[i].dusk);
}
} catch (error) {
let eMsg = 'Error in CalcSunData: ' + error;
this.log.error(eMsg);
console.error(eMsg);
this.sendSentry(error);
}
}
async calcPosition() {
try {
this.log.debug('Run calcPosition');
let now = new Date();
let sunpos = await suncalc.getPosition(now, latitude, longitude);
//store old values to compare in next calculation cycle
let altitude_old = altitude;
let azimuth_old = azimuth;
//calculate
altitude = Math.round((sunpos.altitude * 180 / Math.PI) * 10) / 10;
azimuth = Math.round((sunpos.azimuth * 180 / Math.PI + 180) * 10) / 10;
this.log.silly('Altitude: ' + altitude + ' Azimuth: ' + azimuth);
//compare, if there is any change
if (altitude != altitude_old || azimuth != azimuth_old) {
this.log.debug('Altitude (' + altitude_old + '|' + altitude + ') and/or azimuth (' + azimuth_old + '|' + azimuth + ') changed');
this.calcAdditionalInfo(altitude, azimuth);
} else {
this.log.debug('Altitude (' + altitude_old + '|' + altitude + ') and azimuth (' + azimuth_old + '|' + azimuth + ') did not change');
}
//Timmer
(function () { if (polling) { clearTimeout(polling); polling = null; } })();
polling = setTimeout(() => {
this.log.debug(`New calculation triggered by polling (every ${executioninterval} seconds)`);
this.calcPosition();
}, executioninterval * 1000);
} catch (error) {
let eMsg = 'Error in calcPosition: ' + error;
this.log.error(eMsg);
console.error(eMsg);
this.sendSentry(error);
}
}
/**
* @param {number} altitude - after last change
* @param {number} azimuth - after last change
*/
async calcAdditionalInfo(altitude, azimuth) {
let now = new Date();
let sunPositon = await windrose.getPoint(azimuth, { depth: 2 }).symbol;
jsonExplorer.stateSetCreate(`current.azimuth`, `current azimuth`, azimuth);
jsonExplorer.stateSetCreate(`current.altitude`, `current altitude`, altitude);
jsonExplorer.stateSetCreate(`current.compass_direction`, `compass direction`, sunPositon);
this.log.debug(`Sunposition is '${sunPositon}'`);
let NowInMinutes = now.getHours() * 60 + now.getMinutes() + now.getSeconds() / 60;
let SolarNoonInMinutes = todaySolarNoonTime.getHours() * 60 + todaySolarNoonTime.getMinutes() + todaySolarNoonTime.getSeconds() / 60;
let NadirInMinutes = todayNadirTime.getHours() * 60 + todayNadirTime.getMinutes() + todayNadirTime.getSeconds() / 60;
this.log.silly(`NowInMinutes: ${NowInMinutes}`);
this.log.silly(`SolarNoonInMinutes: ${SolarNoonInMinutes}`);
this.log.silly(`NadirInMinutes: ${NadirInMinutes}`);
if (NadirInMinutes < 720) { //Sun is in the lowest position after midnight
this.log.silly(`Sun is in the lowest position after midnight`);
if (NowInMinutes > NadirInMinutes && NowInMinutes < SolarNoonInMinutes) {
jsonExplorer.stateSetCreate(`current.movement`, `movement`, 'sunrise');
this.log.debug(`Movement is 'Sunrise'`);
} else {
jsonExplorer.stateSetCreate(`current.movement`, `movement`, 'sunset');
this.log.debug(`Movement is 'Sunset'`);
}
}
else { //Sun is in the lowest position before midnight
this.log.silly(`Sun is in the lowest position before midnight`);
if ((NowInMinutes > NadirInMinutes || NowInMinutes > 0) && (NowInMinutes < SolarNoonInMinutes)) {
jsonExplorer.stateSetCreate(`current.movement`, `movement`, 'sunrise');
this.log.debug(`Movement is 'Sunrise'`);
} else {
jsonExplorer.stateSetCreate(`current.movement`, `movement`, 'sunset');
this.log.debug(`Movement is 'Sunset'`);
}
}
jsonExplorer.stateSetCreate(`current.lastupdate`, `last update`, now.getTime());
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
* @param {() => void} callback
*/
onUnload(callback) {
try {
schedule.cancelJob('SunData');
if (polling) {
clearTimeout(polling);
polling = null;
}
this.log.info('cleaned everything up...');
callback();
} catch {
callback();
}
}
/**
* Is called if a subscribed state changes
* @param {string} id
* @param {ioBroker.State | null | undefined} state
*/
onStateChange(id, state) {
if (state) {
// The state was changed
this.log.debug(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
} else {
// The state was deleted
this.log.debug(`state ${id} deleted`);
}
}
/**
* @param {any} errorObject
*/
sendSentry(errorObject) {
try {
if (this.supportsFeature && this.supportsFeature('PLUGINS')) {
const sentryInstance = this.getPluginInstance('sentry');
if (sentryInstance) {
sentryInstance.getSentryObject().captureException(errorObject);
}
}
} catch (error) {
this.log.error(`Error in function sendSentry(): ${error}`);
}
}
}
// @ts-ignore 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 Followthesun(options);
} else {
// otherwise start the instance directly
new Followthesun();
}