Астро таймер, солнечный калькулятор

Всем здравия !
Тестирую солнечный калькулятор по коду, который здесь и нашел,

он такой
/*
 (c) 2011-2015, Vladimir Agafonkin
 SunCalc is a JavaScript library for calculating sun/moon position and light phases.
 https://github.com/mourner/suncalc
*/

function getSunCalc() {
    // shortcuts for easier to read formulas
    var PI = Math.PI,
        sin = Math.sin,
        cos = Math.cos,
        tan = Math.tan,
        asin = Math.asin,
        atan = Math.atan2,
        acos = Math.acos,
        rad = PI / 180;

    // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas


    // date/time constants and conversions

    var dayMs = 1000 * 60 * 60 * 24,
        J1970 = 2440588,
        J2000 = 2451545;

    function toJulian(date) {
        return date.valueOf() / dayMs - 0.5 + J1970;
    }

    function fromJulian(j) {
        return new Date((j + 0.5 - J1970) * dayMs);
    }

    function toDays(date) {
        return toJulian(date) - J2000;
    }


    // general calculations for position

    var e = rad * 23.4397; // obliquity of the Earth

    function rightAscension(l, b) {
        return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l));
    }

    function declination(l, b) {
        return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l));
    }

    function azimuth(H, phi, dec) {
        return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi));
    }

    function altitude(H, phi, dec) {
        return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H));
    }

    function siderealTime(d, lw) {
        return rad * (280.16 + 360.9856235 * d) - lw;
    }

    function astroRefraction(h) {
        if (h < 0) // the following formula works for positive altitudes only.
            h = 0; // if h = -0.08901179 a div/0 would occur.

        // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
        // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
        return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
    }

    // general sun calculations

    function solarMeanAnomaly(d) {
        return rad * (357.5291 + 0.98560028 * d);
    }

    function eclipticLongitude(M) {

        var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
            P = rad * 102.9372; // perihelion of the Earth

        return M + C + P + PI;
    }

    function sunCoords(d) {

        var M = solarMeanAnomaly(d),
            L = eclipticLongitude(M);

        return {
            dec: declination(L, 0),
            ra: rightAscension(L, 0)
        };
    }


    var SunCalc = {};


    // calculates sun position for a given date and latitude/longitude

    SunCalc.getPosition = function (date, lat, lng) {

        var lw = rad * -lng,
            phi = rad * lat,
            d = toDays(date),

            c = sunCoords(d),
            H = siderealTime(d, lw) - c.ra;

        return {
            azimuth: azimuth(H, phi, c.dec),
            altitude: altitude(H, phi, c.dec)
        };
    };


    // sun times configuration (angle, morning name, evening name)

    var times = SunCalc.times = [
        [-0.833, 'sunrise', 'sunset'],
        [-0.3, 'sunriseEnd', 'sunsetStart'],
        [-6, 'dawn', 'dusk'],
        [-12, 'nauticalDawn', 'nauticalDusk'],
        [-18, 'nightEnd', 'night'],
        [6, 'goldenHourEnd', 'goldenHour']
    ];

    // adds a custom time to the times config

    SunCalc.addTime = function (angle, riseName, setName) {
        times.push([angle, riseName, setName]);
    };


    // calculations for sun times

    var J0 = 0.0009;

    function julianCycle(d, lw) {
        return Math.round(d - J0 - lw / (2 * PI));
    }

    function approxTransit(Ht, lw, n) {
        return J0 + (Ht + lw) / (2 * PI) + n;
    }

    function solarTransitJ(ds, M, L) {
        return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L);
    }

    function hourAngle(h, phi, d) {
        return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d)));
    }

    // returns set time for the given sun altitude
    function getSetJ(h, lw, phi, dec, n, M, L) {

        var w = hourAngle(h, phi, dec),
            a = approxTransit(w, lw, n);
        return solarTransitJ(a, M, L);
    }


    // calculates sun times for a given date and latitude/longitude

    SunCalc.getTimes = function (date, lat, lng) {

        var lw = rad * -lng,
            phi = rad * lat,

            d = toDays(date),
            n = julianCycle(d, lw),
            ds = approxTransit(0, lw, n),

            M = solarMeanAnomaly(ds),
            L = eclipticLongitude(M),
            dec = declination(L, 0),

            Jnoon = solarTransitJ(ds, M, L),

            i, len, time, Jset, Jrise;


        var result = {
            solarNoon: fromJulian(Jnoon),
            nadir: fromJulian(Jnoon - 0.5)
        };

        for (i = 0, len = times.length; i < len; i += 1) {
            time = times[i];

            Jset = getSetJ(time[0] * rad, lw, phi, dec, n, M, L);
            Jrise = Jnoon - (Jset - Jnoon);

            result[time[1]] = fromJulian(Jrise);
            result[time[2]] = fromJulian(Jset);
        }

        return result;
    };


    // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas

    function moonCoords(d) { // geocentric ecliptic coordinates of the moon

        var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
            M = rad * (134.963 + 13.064993 * d), // mean anomaly
            F = rad * (93.272 + 13.229350 * d),  // mean distance

            l = L + rad * 6.289 * sin(M), // longitude
            b = rad * 5.128 * sin(F),     // latitude
            dt = 385001 - 20905 * cos(M);  // distance to the moon in km

        return {
            ra: rightAscension(l, b),
            dec: declination(l, b),
            dist: dt
        };
    }

    SunCalc.getMoonPosition = function (date, lat, lng) {

        var lw = rad * -lng,
            phi = rad * lat,
            d = toDays(date),

            c = moonCoords(d),
            H = siderealTime(d, lw) - c.ra,
            h = altitude(H, phi, c.dec),
            // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
            pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));

        h = h + astroRefraction(h); // altitude correction for refraction

        return {
            azimuth: azimuth(H, phi, c.dec),
            altitude: h,
            distance: c.dist,
            parallacticAngle: pa
        };
    };


    // calculations for illumination parameters of the moon,
    // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
    // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.

    SunCalc.getMoonIllumination = function (date) {

        var d = toDays(date || new Date()),
            s = sunCoords(d),
            m = moonCoords(d),

            sdist = 149598000, // distance from Earth to Sun in km

            phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
            inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
            angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
                cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));

        return {
            fraction: (1 + cos(inc)) / 2,
            phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
            angle: angle
        };
    };


    function hoursLater(date, h) {
        return new Date(date.valueOf() + h * dayMs / 24);
    }

    // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article

    SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
        var t = new Date(date);
        if (inUTC) t.setUTCHours(0, 0, 0, 0);
        else t.setHours(0, 0, 0, 0);

        var hc = 0.133 * rad,
            h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
            h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;

        // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
        for (var i = 1; i <= 24; i += 2) {
            h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
            h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;

            a = (h0 + h2) / 2 - h1;
            b = (h2 - h0) / 2;
            xe = -b / (2 * a);
            ye = (a * xe + b) * xe + h1;
            d = b * b - 4 * a * h1;
            roots = 0;

            if (d >= 0) {
                dx = Math.sqrt(d) / (Math.abs(a) * 2);
                x1 = xe - dx;
                x2 = xe + dx;
                if (Math.abs(x1) <= 1) roots++;
                if (Math.abs(x2) <= 1) roots++;
                if (x1 < -1) x1 = x2;
            }

            if (roots === 1) {
                if (h0 < 0) rise = i + x1;
                else set = i + x1;

            } else if (roots === 2) {
                rise = i + (ye < 0 ? x2 : x1);
                set = i + (ye < 0 ? x1 : x2);
            }

            if (rise && set) break;

            h0 = h2;
        }

        var result = {};

        if (rise) result.rise = hoursLater(t, rise);
        if (set) result.set = hoursLater(t, set);

        if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;

        return result;
    };
    return SunCalc;
}

настроил правило:

оно такое
defineRule("night_lighting", {
    when: cron("@every 60s"),
    then: function() {
        function light_switch(dev_name,param_name,state) {
            if (dev[dev_name][param_name] != state)
            {
                dev[dev_name][param_name] = state;
            }
        }

        var lat = 56.0396;
        var lng = 92.9283;
        now = new Date();
        var SunCalc = getSunCalc();
        times=SunCalc.getTimes(now, lat, lng);
        var sunrise = times.sunrise;
        var sunset  = times.sunset;
        log.info("Дата\t\t", now);
        log.info("Восход\t\t", sunrise);
        log.info("Закат\t\t", sunset);

	var on_date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 07, 00)
        var off_date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59)
        var next_day_sunrise = new Date(sunrise);
		
		if ((now < sunrise) && (now > on_date)) {
            light_switch("wb-led","Channel 2",true);
			log.info("Освещение включено 1");
        } else if ((now > sunset) && (now < off_date)) {
            light_switch("wb-led","Channel 2",true);
			log.info("Освещение включено 2");
        } else if ((now > sunrise) && (now < sunset)) {
            light_switch("wb-led", "Channel 2", false);
			log.info("Освещение выключено 1");
        } else {
			light_switch("wb-led","Channel 2", false);
			log.info("Освещение выключено 2");
        }
    }
});

в общем то все хорошо, калькулятор работает, правило тоже работает (включает и выключает свет и по времени и по восходу / закату, НО…

согласно правила свет выкл. в 23-59, и не должно вкл-я до 07-00, но свет включается в 00-00 и горит, пока не обновятся даты восхода и заката следующего дня… (на скрине обвел)…

вопрос к тем, кто ковырял данный код, где что посмотреть и подправить ??

лампочки пока не вешал, тестирую на диммере…бывает через час, а бывает через два часа данные обновляются и свет тухнет до назначенного времени…

Смогу подключиться чуть позже, пока прошу попробовать ответить @AIbot

Проблема с включением света в полночь связана с тем, что данные о восходе и закате обновляются позже, чем происходит переключение на новый день. Это может быть исправлено, добавив проверку, чтобы данные обновлялись сразу после полуночи.

Возможное решение

  1. Обновление данных сразу после полуночи:

    • В вашем скрипте используйте проверку времени и обновляйте данные о восходе и закате сразу после полуночи.
  2. Изменение логики включения/выключения:

    • Убедитесь, что логика включения/выключения света учитывает задержку в обновлении данных.

Пример изменения кода

Добавьте в начало функции then проверку и обновление данных о восходе и закате, если текущее время близко к полуночи.

then: function() {
    now = new Date();
    if (now.getHours() === 0 && now.getMinutes() < 5) {
        times = SunCalc.getTimes(now, lat, lng);
    }
    // остальная логика
}

Попробуйте эти изменения и проверьте, решает ли это вашу проблему. Если нет, пожалуйста, дайте знать, и мы продолжим искать решение.

Я просто бездушная машина, поэтому могу ошибаться.

1 лайк

Добрый день!

Помог ли вам наш помощник? Удалось ли реализовать задуманное?

Нет, не помог… не люблю я эти ИИ… не знаю куда встроить этот код…
а так я попробовал его и так и этак… не получается…
проверить то могу только раз в сутки…
куда и как его правильно вставить относительно моего правила ?!

Добрый день,

Прошу подробнее описать задачу, которую вы решаете. Возможно, я смогу предложить более простой способ её реализации.

Помнится, экспериментировал с подобным.

Но я шел другим путем - заводил таймеры.

Грубо - включил свет - запусти таймер на выключение.
выключил - запусти таймер на включение

ну или более ветвистая логика.

В дело не применилось. боюсь и черновиков не отыщу.

image
Первые два значения возвращает библиотека SunCalc. Почему она для 2025-02-09 00:00:53 возвращает 2025-02-08, надо смотреть в библиотеке.

Добрый день, удалось ли решить вопрос?

Это не так. Геокалькурятор позволяет рассчитать положение солнца/луны на указанное вами в запросе время. И это не обязательно должно быть “текущее время”…

Для отладки этого скрипта я заводил переменную, в которой указывал “смещение” от текущего времени.

var delta_time = 0 ; // Отладочное смещение текущего времени в секундах (0 = нет смещения)
//var delta_time = ((9*60*60)+(41*60) + (50) )*1000 ;  // текущее время смещено на 9 часов 41 минуту  и 50 секунд

Т.е. для отладки вы можете сами задавать “как бы текущее время” и инициировать события заката/восходы хоть сто раз в сутки :slight_smile:

Далее, при формировании вызова процедуры расчета положения солнца и луны “готовлю” время , на которое мне интересно узнать эти данные (фрагмент кода):

//  ......... (обновляем данные, допустим раз в 10 секунд - не настолько это важные события, чтобы следить за ними чаще и лишний раз греть процессор :) .
    when: cron("@every 10s"),
    then: function() {
// ЛО - хх.ххххххх, хх.ххххххх
        var lat = хх.ххххххх;
        var lng = хх.ххххххх;
        now = new Date();                         // получаем текущее значение системного времени
        now.setTime(now.getTime() + delta_time);  // для отладки смещаем значение текущего времени на указанную дельту
      
        var SunCalc = getSunCalc();

        times=SunCalc.getTimes(now, lat, lng);
        var sunrise = times.sunrise;
        var sunset  = times.sunset;
    

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////        
// Еще раз вычисляем время восхода/захода на завтра (+24 часа к текущей дате)
        now_24h = new Date();                                               // Текущее системное время
        now_24h.setTime(now_24h.getTime() + delta_time + (24*60*60*1000));  // + 24 часа к текущей дате и дельте смещения
        var SunCalc_24h = getSunCalc();

        times_24h=SunCalc_24h.getTimes(now_24h, lat, lng);
        var sunrise_24h = times_24h.sunrise;
        var sunset_24h  = times_24h.sunset;

Дополнительно (я также как и вы столкнулся с необходимостью знать время восхода “завтра” для выключения ландшафтного и фасадного освещения) я сразу вычисляю время событий на завтра, ибо если “спросить” калькулятор , допустим в полдень, о времени восхода, то он рассчитает (покажет) время уже состоявшегося восхода.
В моей логике (как и в вашей) требуется знать время “будущего” события, а не уже состоявшегося.

Для удобства я завел себе переменную (и контрол в виртуальном устройстве) вида “солнце за горизонтом = да/нет” и уже исходя из этого анализирую какое ближайшее событие произойдет.
Т.е. если солнце сейчас за горизонтом (ночь), то меня интересует какое время ближайшего (в будущем) восхода. И на оборот - если сейчас день, то смотрю на ближайший будущий “закат” …

        if (((now > sunrise) && (now < sunset)) ) {
            dev["NightTime/NightTimeOn"] = false;

            if (sunset < now) {ToSunSet = sunset_24h - now} else {ToSunSet = sunset - now} ;            // До заката
            if (now < sunrise) {FromSunRise = now - sunrise_24h } else {FromSunRise = now - sunrise} ;  // После восхода

            dev["NightTime/ToSunSet"] = MyConverterTime(ToSunSet);
            dev["NightTime/FromSunRise"] = MyConverterTime(FromSunRise);

            dev["NightTime/ToSunRise_second"] =  0;                                                   // Солнце НАД горизонтом => Обнулим время "до Восхода осталось, сек"
            dev["NightTime/ToSunRise"] =  MyConverterTime( 0 );                                       // Солнце НАД горизонтом => Обнулим время "До Восхода осталось"
            
//          log.info("Восход");
        }

        if (((now < sunrise) || (now > sunset)) ) {
            dev["NightTime/NightTimeOn"] = true;

            if (sunrise < now) {ToSunRise = sunrise_24h - now} else {ToSunRise = sunrise - now} ;         // До восхода
            if (now > sunset) {FromSunSet = now_24h - sunset_24h } else {FromSunSet = now_24h - sunset} ; // После заката

            dev["NightTime/ToSunRise_second"] =  Math.round(ToSunRise / 1000);
            dev["NightTime/ToSunRise"] =  MyConverterTime( ToSunRise );
            dev["NightTime/FromSunSet"] = MyConverterTime( FromSunSet );

            dev["NightTime/FromSunRise"] = MyConverterTime(0);                                       // Солнце ЗА горизонтом => Обнулим время "После восхода прошло"
            dev["NightTime/ToSunSet"] = MyConverterTime(0);                                          // Солнце ЗА горизонтом => Обнулим "До заката осталось"
//            log.info("Закат");
        }

Я получаю в итоге время восхода и заката (абсолютное) и набор “времен” вида “До восхода осталось”, “До заката осталось”, “После заката прошло”, “После восхода прошло”
которые уже можно использовать в других алгоритмах…

И да, как и коллеги, отписавшиеся в этой теме выше, я тоже пришел к решению, что проще (нагляднее) использовать таймер(ы) (оставшееся время до события), чем сравнивать два времени (кто его знает, что он там на сравнивает :slight_smile: )…

Т.е. там где мне нужно применить “астрономию” я обращаюсь в своему виртуальному устройству для получения конкретных значений и уже далее их использую (например, выключить освещение с восходом + 20 минут, или включить подсветку ландшафта в тенистой аллее за 30 минут до заката, ибо в момент заката в этом месте уже темновато :slight_smile: ).

В целом сам скрипт калькулятора работает достаточно правильно - высчитываемые значения адекватны, но имеет свои особенности. Я не стал править код самого калькулятора, а просто обошел эти особенности указанным выше способом :slight_smile:

PS Здесь я привожу фрагменты своего действующего кода, без описания создания соответствующих виртуальных устройств (из контекста их и так “видно”) и своей функции конвертации “времени” в удобный для меня формат отображения - MyConverterTime(0)

PPS Код категорически не претендует на оптимальность, например Date() можно было бы вызвать и один раз, а далее работать только с математикой (+ дельта, + 24 часа и пр.)
Функции сравнения “времени событий” можно можно было бы оптимизировать и пр.
Такой код остался после моих “экспериментов и мучений с отладкой” и после того как все заработало так, как мне было нужно, оптимизировать его я больше не стал (работает - не трогай :slight_smile: ) - поэтому не судите строго…

2 лайка

Спасибо откликнувшимся ! Тему прошу не закрывать. Буду разбираться. Пока работа не позволяет дотянуться до контроллера.

Добрый день, удалось ли решить вопрос?