Автономный расчет восхода и заката

Добрый день.

Писал не сам, а мучал ИИ, но вроде получилось очень да же не плохо

Основные возможности

  • :white_check_mark: Точный расчет солнечных времен по координатам (на основе алгоритма SunCalc)
  • :white_check_mark: Автоматическая адаптация к сезонным изменениям длины дня
  • :white_check_mark: Поддержка часовых поясов - легко перенастраивается при переезде
  • :white_check_mark: Приоритет ручного управления - выключатели имеют высший приоритет
  • :white_check_mark: Визуальный мониторинг - все параметры доступны через виртуальное устройство
  • :white_check_mark: Надежность - не зависит от интернета и внешних сервисов

Виртуальное устройство sun_times :

  • Время восхода (формат HH:MM)
  • Время заката (формат HH:MM)
  • Время последнего обновления
  • Кнопка ручного обновления

Особенности реализации

Точность расчетов

Скрипт использует проверенный астрономический алгоритм, учитывающий:

  • Эксцентриситет орбиты Земли
  • Наклон земной оси (23.44°)
  • Атмосферную рефракцию (-0.833°)
  • Географические координаты

Надежность

  • Резервный алгоритм при ошибках расчета
  • Защита от некорректных данных
  • Подробное логирование для диагностики
  • Автовосстановление при сбоях

Производительность

  • Расчет выполняется 1 раз в сутки
  • Время хранится в виртуальном устройстве
  • Быстрое сравнение времени в минутах
// Настройки
var LATITUDE = 55.26535;     // Ваши координаты
var LONGITUDE = 37.74975;    // Ваши координаты  
var TIMEZONE_OFFSET = 3;     // Часовой пояс (MSK = UTC+3)

// Обновлённое виртуальное устройство с кнопкой
defineVirtualDevice("sun_times", {
  title: "Время восхода и заката",
  cells: {
    sunrise: {
      title: "Время восхода",
      type: "text",
      value: "06:00",
      readonly: true
    },
    sunset: {
      title: "Время заката", 
      type: "text",
      value: "18:00",
      readonly: true
    },
    last_updated: {
      title: "Последнее обновление",
      type: "text",
      value: "не обновлялось",
      readonly: true
    },
    update_button: {
      title: "Обновить сейчас",
      type: "pushbutton"
    }
  }
});

// Функция для преобразования времени в минуты (для сравнения)
function timeToMinutes(timeStr) {
    try {
        var parts = timeStr.split(':');
        if (parts.length !== 2) {
            throw new Error("Неверный формат времени: " + timeStr);
        }
        var hours = parseInt(parts[0], 10);
        var minutes = parseInt(parts[1], 10);
        
        if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
            throw new Error("Некорректное время: " + timeStr);
        }
        
        return hours * 60 + minutes;
    } catch (e) {
        log("Ошибка в timeToMinutes: " + e.toString());
        return 6 * 60; // Возвращаем 06:00 по умолчанию
    }
}

// Функция для красивого форматирования даты и времени (без padStart)
function formatDateTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            return "неверная дата";
        }
        
        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        var day = date.getDate();
        var hours = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();
        
        // Ручное добавление ведущих нулей (без padStart)
        month = month < 10 ? "0" + month : month;
        day = day < 10 ? "0" + day : day;
        hours = hours < 10 ? "0" + hours : hours;
        minutes = minutes < 10 ? "0" + minutes : minutes;
        seconds = seconds < 10 ? "0" + seconds : seconds;
        
        return year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
    } catch (e) {
        log("Ошибка в formatDateTime: " + e.toString());
        return "ошибка формата";
    }
}

// Функция для форматирования времени в HH:MM (без padStart)
function formatTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            log("formatTime: date не является Date объектом");
            return "06:00";
        }
        
        var hours = date.getHours();
        var minutes = date.getMinutes();
        
        // Ручное добавление ведущих нулей
        var hoursStr = hours < 10 ? "0" + hours : "" + hours;
        var minutesStr = minutes < 10 ? "0" + minutes : "" + minutes;
        
        return hoursStr + ":" + minutesStr;
    } catch (e) {
        log("Ошибка в formatTime: " + e.toString());
        return "06:00";
    }
}

// Функция для получения локального времени
function getLocalTime() {
    var now = new Date();
    return new Date(now.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000);
}

// Точная функция расчета солнечных времен на основе SunCalc
function calculateSunTimes(date, lat, lng, timezoneOffset) {
    log("Точный расчет солнечных времен");
    
    try {
        // Константы
        var rad = Math.PI / 180;
        var dayMs = 1000 * 60 * 60 * 24;
        var J1970 = 2440588;
        var 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; 
        }

        function rightAscension(l, b) {
            var e = rad * 23.4397;
            return Math.atan2(Math.sin(l) * Math.cos(e) - Math.tan(b) * Math.sin(e), Math.cos(l));
        }

        function declination(l, b) {
            var e = rad * 23.4397;
            return Math.asin(Math.sin(b) * Math.cos(e) + Math.cos(b) * Math.sin(e) * Math.sin(l));
        }

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

        function eclipticLongitude(M) {
            var C = rad * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M));
            var P = rad * 102.9372;
            return M + C + P + Math.PI;
        }

        function sunCoords(d) {
            var M = solarMeanAnomaly(d);
            var L = eclipticLongitude(M);
            return {
                dec: declination(L, 0),
                ra: rightAscension(L, 0)
            };
        }

        var J0 = 0.0009;

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

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

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

        function hourAngle(h, phi, d) {
            var cosH = (Math.sin(h) - Math.sin(phi) * Math.sin(d)) / (Math.cos(phi) * Math.cos(d));
            if (cosH > 1) return 0;
            if (cosH < -1) return Math.PI;
            return Math.acos(cosH);
        }

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

        var times = [
            [-0.833, 'sunrise', 'sunset']
        ];

        var lw = rad * -lng;
        var phi = rad * lat;
        var d = toDays(date);
        var n = julianCycle(d, lw);
        var ds = approxTransit(0, lw, n);
        var M = solarMeanAnomaly(ds);
        var L = eclipticLongitude(M);
        var dec = declination(L, 0);
        var Jnoon = solarTransitJ(ds, M, L);

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

        for (var i = 0; i < times.length; i++) {
            var time = times[i];
            var h0 = time[0] * rad;
            var Jset = getSetJ(h0, lw, phi, dec, n, M, L);
            var Jrise = Jnoon - (Jset - Jnoon);
            result[time[1]] = fromJulian(Jrise);
            result[time[2]] = fromJulian(Jset);
        }

        // Корректируем на часовой пояс
        var offsetMs = timezoneOffset * 60 * 60 * 1000;
        var sunriseLocal = new Date(result.sunrise.getTime() + offsetMs);
        var sunsetLocal = new Date(result.sunset.getTime() + offsetMs);

        log("Точный расчет:");
        log("Восход: " + formatDateTime(sunriseLocal));
        log("Закат: " + formatDateTime(sunsetLocal));

        return {
            sunrise: sunriseLocal,
            sunset: sunsetLocal
        };
    } catch (e) {
        log("Ошибка в точном calculateSunTimes: " + e.toString());
        // Возвращаем значения по умолчанию при ошибке
        var defaultTime = new Date(date);
        var sunrise = new Date(defaultTime);
        var sunset = new Date(defaultTime);
        sunrise.setHours(6, 0, 0, 0);
        sunset.setHours(18, 0, 0, 0);
        return {
            sunrise: sunrise,
            sunset: sunset
        };
    }
}

// Функция для обновления времени восхода и заката
function updateSunTimes() {
    log("Начало updateSunTimes");
    
    try {
        var localTime = getLocalTime();
        log("Локальное время: " + formatDateTime(localTime));
        
        var times = calculateSunTimes(new Date(), LATITUDE, LONGITUDE, TIMEZONE_OFFSET);
        log("Времена получены");
        
        if (!times || !times.sunrise || !times.sunset) {
            throw new Error("calculateSunTimes не вернул корректные данные");
        }
        
        var sunriseStr = formatTime(times.sunrise);
        var sunsetStr = formatTime(times.sunset);
        var updateTimeStr = formatDateTime(localTime);
        
        log("Форматированные времена: " + sunriseStr + " / " + sunsetStr);
        
        dev["sun_times/sunrise"] = sunriseStr;
        dev["sun_times/sunset"] = sunsetStr;
        dev["sun_times/last_updated"] = updateTimeStr;
        
        log("Успешно обновлено время восхода/заката: " + sunriseStr + " / " + sunsetStr);
        
    } catch (e) {
        log("Ошибка в updateSunTimes: " + e.toString());
        var localTime = getLocalTime();
        dev["sun_times/sunrise"] = "06:00";
        dev["sun_times/sunset"] = "18:00";
        dev["sun_times/last_updated"] = "Ошибка: " + formatDateTime(localTime);
    }
}

// Правила обновления времени восхода и заката
defineRule("update_sun_times_on_start", {
    asSoonAs: function() {
        return true;
    },
    then: function() {
        log("Первоначальное обновление времени восхода/заката");
        updateSunTimes();
    }
});

defineRule("update_sun_times_daily", {
    when: cron("0 0 * * *"), // Каждый день в 00:00
    then: function() {
        log("Ежедневное обновление времени восхода/заката");
        updateSunTimes();
    }
});

// Ручное обновление по кнопке в виртуальном устройстве
defineRule("manual_sun_times_update", {
    when: function() {
        return dev["sun_times/update_button"];
    },
    then: function() {
        log("Ручное обновление времени восхода/заката");
        updateSunTimes();
    }
});

Мне собственно нужно было получить ночное время, вот пример функции

// Функция определения ночного времени с правильным сравнением
function isNight() {
    try {
        var localTime = getLocalTime();
        var currentTime = formatTime(localTime);
        var currentMinutes = timeToMinutes(currentTime);
        
        var sunrise = dev["sun_times/sunrise"] || "06:00";
        var sunset = dev["sun_times/sunset"] || "18:00";
        
        var sunriseMinutes = timeToMinutes(sunrise);
        var sunsetMinutes = timeToMinutes(sunset);
        
        // Правильное сравнение времени в минутах
        var isNightTime;
        if (sunriseMinutes < sunsetMinutes) {
            // Нормальный случай: восход до заката
            isNightTime = currentMinutes < sunriseMinutes || currentMinutes >= sunsetMinutes;
        } else {
            // Экзотический случай: восход после заката (полярная ночь/день)
            isNightTime = currentMinutes < sunriseMinutes && currentMinutes >= sunsetMinutes;
        }
        
        log("Текущее время: " + currentTime + " (" + currentMinutes + " мин)" + 
            ", Восход: " + sunrise + " (" + sunriseMinutes + " мин)" + 
            ", Закат: " + sunset + " (" + sunsetMinutes + " мин)" + 
            ", Ночь: " + isNightTime);
        
        return isNightTime;
        
    } catch (e) {
        log("Ошибка в isNight(): " + e.toString());
        // Резервный вариант
        var hours = getLocalTime().getHours();
        return hours < 6 || hours >= 18;
    }
}

Всем удачи)

3 лайка

На днях тоже подобное реализовывал :grinning:

1 лайк

Спасибо, что поделились своими наработками!
Для коллекции оставлю ссылку на тему с обсуждением реализации правил с использованием SunCalc от Vladimir Agafonkin.

1 лайк

Добрый день. Подскажите пожалуйста готовый код включение света при закате солнца и выключение при восходе на примере выходов реле (wb-mr6c_166/K1 и wb-mr6c_97/K2) они должны включаться и отключаться одновременно. А то я в этом не понимаю, а уже все перечитал и готовое правило так и не нашел.

Добрый день.

У меня в результате получился такой скрипт

/*************************************************************
 * motion_sensors.js
 *
 * Логика:
 * - вычисление восхода/заката и признака "ночь"
 * - крыльцо:
 *      реле   wb-mrm2-mini_41/K1
 *      кнопка wb-mrm2-mini_41/Input 1   (отдельно не отслеживаем, т.к. следим за самим реле)
 *      датчик wb-mrm2-mini_36/Input 2   (нет движения = true, движение = false)
 * - веранда:
 *      реле   wb-mrm2-mini_39/K1
 *      датчик wb-mrm2-mini_39/Input 1   (нет движения = true, движение = false)
 * - ворота:
 *      wb-mr6c_143/K1 = калитка
 *      wb-mr6c_143/K2 = полное открытие
 *      оба работают как импульс и как триггер света крыльца
 *
 * Принцип:
 * - если свет включили вручную (кнопка / HA / WB), датчики и ворота игнорируются
 * - если свет выключен, датчик/ворота включают свет на 10 минут
 * - повторный триггер продлевает таймер
 * - автоматика работает только ночью
 *************************************************************/


// =========================================================
// 1. ВРЕМЯ ВОСХОДА / ЗАКАТА
// =========================================================

// Координаты можно взять на яндекс картах 

// Настройки
var LATITUDE = 55.440362;     // Ваши координаты
var LONGITUDE = 38.107036;    // Ваши координаты
var TIMEZONE_OFFSET = 3;     // Часовой пояс (MSK = UTC+3)

// Обновлённое виртуальное устройство с кнопкой
defineVirtualDevice("sun_times", {
  title: "Время восхода и заката",
  cells: {
    sunrise: {
      title: "Время восхода",
      type: "text",
      value: "06:00",
      readonly: true
    },
    sunset: {
      title: "Время заката",
      type: "text",
      value: "18:00",
      readonly: true
    },
    last_updated: {
      title: "Последнее обновление",
      type: "text",
      value: "не обновлялось",
      readonly: true
    },
    update_button: {
      title: "Обновить сейчас",
      type: "pushbutton"
    },
    isNight: {
      type: "switch",
      value: false,
      readonly: true
    }
  }
});

// Функция для преобразования времени в минуты (для сравнения)
function timeToMinutes(timeStr) {
    try {
        var parts = timeStr.split(':');
        if (parts.length !== 2) {
            throw new Error("Неверный формат времени: " + timeStr);
        }

        var hours = parseInt(parts[0], 10);
        var minutes = parseInt(parts[1], 10);

        if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
            throw new Error("Некорректное время: " + timeStr);
        }

        return hours * 60 + minutes;
    } catch (e) {
        log("Ошибка в timeToMinutes: " + e.toString());
        return 6 * 60; // Возвращаем 06:00 по умолчанию
    }
}

// Функция для красивого форматирования даты и времени (без padStart)
function formatDateTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            return "неверная дата";
        }

        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        var day = date.getDate();
        var hours = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();

        month = month < 10 ? "0" + month : month;
        day = day < 10 ? "0" + day : day;
        hours = hours < 10 ? "0" + hours : hours;
        minutes = minutes < 10 ? "0" + minutes : minutes;
        seconds = seconds < 10 ? "0" + seconds : seconds;

        return year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
    } catch (e) {
        log("Ошибка в formatDateTime: " + e.toString());
        return "ошибка формата";
    }
}

// Функция для форматирования времени в HH:MM (без padStart)
function formatTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            log("formatTime: date не является Date объектом");
            return "06:00";
        }

        var hours = date.getHours();
        var minutes = date.getMinutes();

        var hoursStr = hours < 10 ? "0" + hours : "" + hours;
        var minutesStr = minutes < 10 ? "0" + minutes : "" + minutes;

        return hoursStr + ":" + minutesStr;
    } catch (e) {
        log("Ошибка в formatTime: " + e.toString());
        return "06:00";
    }
}

// Функция для получения локального времени
function getLocalTime() {
    var now = new Date();
    return new Date(now.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000);
}

// Функция определения ночного времени с правильным сравнением
function isNight() {
    try {
        var localTime = getLocalTime();
        var currentTime = formatTime(localTime);
        var currentMinutes = timeToMinutes(currentTime);

        var sunrise = dev["sun_times/sunrise"] || "06:00";
        var sunset = dev["sun_times/sunset"] || "18:00";

        var sunriseMinutes = timeToMinutes(sunrise);
        var sunsetMinutes = timeToMinutes(sunset);

        var isNightTime;
        if (sunriseMinutes < sunsetMinutes) {
            isNightTime = currentMinutes < sunriseMinutes || currentMinutes >= sunsetMinutes;
        } else {
            isNightTime = currentMinutes < sunriseMinutes && currentMinutes >= sunsetMinutes;
        }

        // log("Текущее время: " + currentTime + " (" + currentMinutes + " мин)" +
        //     ", Восход: " + sunrise + " (" + sunriseMinutes + " мин)" +
        //     ", Закат: " + sunset + " (" + sunsetMinutes + " мин)" +
        //     ", Ночь: " + isNightTime);

        return isNightTime;

    } catch (e) {
        log("Ошибка в isNight(): " + e.toString());
        var hours = getLocalTime().getHours();
        return hours < 6 || hours >= 18;
    }
}

// Точная функция расчета солнечных времен на основе SunCalc
function calculateSunTimes(date, lat, lng, timezoneOffset) {
    log("Точный расчет солнечных времен");

    try {
        var rad = Math.PI / 180;
        var dayMs = 1000 * 60 * 60 * 24;
        var J1970 = 2440588;
        var 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;
        }

        function rightAscension(l, b) {
            var e = rad * 23.4397;
            return Math.atan2(Math.sin(l) * Math.cos(e) - Math.tan(b) * Math.sin(e), Math.cos(l));
        }

        function declination(l, b) {
            var e = rad * 23.4397;
            return Math.asin(Math.sin(b) * Math.cos(e) + Math.cos(b) * Math.sin(e) * Math.sin(l));
        }

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

        function eclipticLongitude(M) {
            var C = rad * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M));
            var P = rad * 102.9372;
            return M + C + P + Math.PI;
        }

        function sunCoords(d) {
            var M = solarMeanAnomaly(d);
            var L = eclipticLongitude(M);
            return {
                dec: declination(L, 0),
                ra: rightAscension(L, 0)
            };
        }

        var J0 = 0.0009;

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

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

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

        function hourAngle(h, phi, d) {
            var cosH = (Math.sin(h) - Math.sin(phi) * Math.sin(d)) / (Math.cos(phi) * Math.cos(d));
            if (cosH > 1) return 0;
            if (cosH < -1) return Math.PI;
            return Math.acos(cosH);
        }

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

        var times = [
            [-0.833, 'sunrise', 'sunset']
        ];

        var lw = rad * -lng;
        var phi = rad * lat;
        var d = toDays(date);
        var n = julianCycle(d, lw);
        var ds = approxTransit(0, lw, n);
        var M = solarMeanAnomaly(ds);
        var L = eclipticLongitude(M);
        var dec = declination(L, 0);
        var Jnoon = solarTransitJ(ds, M, L);

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

        for (var i = 0; i < times.length; i++) {
            var time = times[i];
            var h0 = time[0] * rad;
            var Jset = getSetJ(h0, lw, phi, dec, n, M, L);
            var Jrise = Jnoon - (Jset - Jnoon);
            result[time[1]] = fromJulian(Jrise);
            result[time[2]] = fromJulian(Jset);
        }

        var offsetMs = timezoneOffset * 60 * 60 * 1000;
        var sunriseLocal = new Date(result.sunrise.getTime() + offsetMs);
        var sunsetLocal = new Date(result.sunset.getTime() + offsetMs);

        log("Точный расчет:");
        log("Восход: " + formatDateTime(sunriseLocal));
        log("Закат: " + formatDateTime(sunsetLocal));

        return {
            sunrise: sunriseLocal,
            sunset: sunsetLocal
        };
    } catch (e) {
        log("Ошибка в точном calculateSunTimes: " + e.toString());

        var defaultTime = new Date(date);
        var sunrise = new Date(defaultTime);
        var sunset = new Date(defaultTime);
        sunrise.setHours(6, 0, 0, 0);
        sunset.setHours(18, 0, 0, 0);

        return {
            sunrise: sunrise,
            sunset: sunset
        };
    }
}

// Функция для обновления времени восхода и заката
function updateSunTimes() {
    log("Начало updateSunTimes");

    try {
        var localTime = getLocalTime();
        log("Локальное время: " + formatDateTime(localTime));

        var times = calculateSunTimes(new Date(), LATITUDE, LONGITUDE, TIMEZONE_OFFSET);
        log("Времена получены");

        if (!times || !times.sunrise || !times.sunset) {
            throw new Error("calculateSunTimes не вернул корректные данные");
        }

        var sunriseStr = formatTime(times.sunrise);
        var sunsetStr = formatTime(times.sunset);
        var updateTimeStr = formatDateTime(localTime);

        log("Форматированные времена: " + sunriseStr + " / " + sunsetStr);

        dev["sun_times/sunrise"] = sunriseStr;
        dev["sun_times/sunset"] = sunsetStr;
        dev["sun_times/last_updated"] = updateTimeStr;

        log("Успешно обновлено время восхода/заката: " + sunriseStr + " / " + sunsetStr);

    } catch (e) {
        log("Ошибка в updateSunTimes: " + e.toString());
        var localTime2 = getLocalTime();
        dev["sun_times/sunrise"] = "06:00";
        dev["sun_times/sunset"] = "18:00";
        dev["sun_times/last_updated"] = "Ошибка: " + formatDateTime(localTime2);
    }
}

// Правила обновления времени восхода и заката
defineRule("update_sun_times_on_start", {
    asSoonAs: function() {
        return true;
    },
    then: function() {
        log("Первоначальное обновление времени восхода/заката");
        updateSunTimes();
    }
});

defineRule("update_sun_times_daily", {
    when: cron("0 0 * * *"), // Каждый день в 00:00 UTC контроллера
    then: function() {
        log("Ежедневное обновление времени восхода/заката");
        updateSunTimes();
    }
});

// Ручное обновление по кнопке в виртуальном устройстве
defineRule("manual_sun_times_update", {
    when: function() {
        return dev["sun_times/update_button"];
    },
    then: function() {
        log("Ручное обновление времени восхода/заката");
        updateSunTimes();
    }
});

defineRule("update_isNight", {
    when: cron("0 */1 * * *"), // Каждую минуту
    then: function() {
        // log("Ежеминутное обновление isNight");
        dev["sun_times/isNight"] = isNight();
    }
});


// =========================================================
// 2. ЛОГИКА УЛИЧНОГО ОСВЕЩЕНИЯ
// =========================================================

// Настройки автоматики света
var OUTDOOR_LIGHT_TIMEOUT_MS = 10 * 60 * 1000;   // 10 минут
var INTERNAL_CHANGE_PROTECT_MS = 1200;           // защита от собственных переключений
var MOTION_DEBOUNCE_MS = 3000;                   // антидребезг датчиков/ворот
var GATE_PULSE_MS = 1000;                        // длина импульса на ворота

var OUTDOOR_LIGHTS = {
    porch: {
        name: "Крыльцо",
        relay: "wb-mrm2-mini_41/K1",
        motion: "wb-mrm2-mini_36/Input 2"   // нет движения = true, движение = false
    },
    veranda: {
        name: "Веранда",
        relay: "wb-mrm2-mini_39/K1",        
        motion: "wb-mrm2-mini_39/Input 1"   // нет движения = true, движение = false
    }
};

var outdoorLightState = {
    porch: {
        manualMode: false,
        timerId: null,
        internalUntil: 0,
        lastTriggerTs: 0
    },
    veranda: {
        manualMode: false,
        timerId: null,
        internalUntil: 0,
        lastTriggerTs: 0
    }
};

function nowMs() {
    return new Date().getTime();
}

function isNightNow() {
    try {
        return !!dev["sun_times/isNight"];
    } catch (e) {
        log("Ошибка чтения sun_times/isNight: " + e.toString());
        return false;
    }
}

function markInternalChange(zoneKey, ms) {
    outdoorLightState[zoneKey].internalUntil = nowMs() + (ms || INTERNAL_CHANGE_PROTECT_MS);
}

function isInternalChange(zoneKey) {
    return nowMs() < outdoorLightState[zoneKey].internalUntil;
}

function clearAutoOffTimer(zoneKey) {
    if (outdoorLightState[zoneKey].timerId !== null) {
        clearTimeout(outdoorLightState[zoneKey].timerId);
        outdoorLightState[zoneKey].timerId = null;
    }
}

function setRelay(zoneKey, value) {
    markInternalChange(zoneKey, INTERNAL_CHANGE_PROTECT_MS);
    dev[OUTDOOR_LIGHTS[zoneKey].relay] = value;
}

function isDebounced(zoneKey) {
    var now = nowMs();
    if ((now - outdoorLightState[zoneKey].lastTriggerTs) < MOTION_DEBOUNCE_MS) {
        return true;
    }
    outdoorLightState[zoneKey].lastTriggerTs = now;
    return false;
}

function startOrRefreshAutoOff(zoneKey, sourceName) {
    clearAutoOffTimer(zoneKey);

    outdoorLightState[zoneKey].timerId = setTimeout(function () {
        outdoorLightState[zoneKey].timerId = null;

        if (outdoorLightState[zoneKey].manualMode) {
            log(OUTDOOR_LIGHTS[zoneKey].name + ": таймер истёк, но активен ручной режим");
            return;
        }

        log(OUTDOOR_LIGHTS[zoneKey].name + ": авто-выключение по таймеру");
        setRelay(zoneKey, false);

    }, OUTDOOR_LIGHT_TIMEOUT_MS);

    log(OUTDOOR_LIGHTS[zoneKey].name + ": таймер авто-выключения запущен/обновлён от " + sourceName);
}

function autoTurnOnByTrigger(zoneKey, triggerName) {
    if (!isNightNow()) {
        log(OUTDOOR_LIGHTS[zoneKey].name + ": триггер '" + triggerName + "' проигнорирован — сейчас не ночь");
        return;
    }

    if (outdoorLightState[zoneKey].manualMode) {
        log(OUTDOOR_LIGHTS[zoneKey].name + ": триггер '" + triggerName + "' проигнорирован — ручной режим");
        return;
    }

    log(OUTDOOR_LIGHTS[zoneKey].name + ": авто-включение от " + triggerName);
    setRelay(zoneKey, true);
    startOrRefreshAutoOff(zoneKey, triggerName);
}

function handleManualRelayChange(zoneKey, newValue) {
    if (isInternalChange(zoneKey)) {
        log(OUTDOOR_LIGHTS[zoneKey].name + ": внутреннее изменение реле, ручной режим не меняем");
        return;
    }

    if (newValue === true) {
        outdoorLightState[zoneKey].manualMode = true;
        clearAutoOffTimer(zoneKey);
        log(OUTDOOR_LIGHTS[zoneKey].name + ": свет включён вручную, активирован ручной режим");
    } else {
        outdoorLightState[zoneKey].manualMode = false;
        clearAutoOffTimer(zoneKey);
        log(OUTDOOR_LIGHTS[zoneKey].name + ": свет выключен вручную, ручной режим снят");
    }
}

// Инициализация
defineRule("outdoor_lights_init", {
    asSoonAs: function () {
        return true;
    },
    then: function () {
        log("Инициализация логики уличного освещения");

        clearAutoOffTimer("porch");
        clearAutoOffTimer("veranda");

        outdoorLightState.porch.manualMode = false;
        outdoorLightState.porch.timerId = null;
        outdoorLightState.porch.internalUntil = 0;
        outdoorLightState.porch.lastTriggerTs = 0;

        outdoorLightState.veranda.manualMode = false;
        outdoorLightState.veranda.timerId = null;
        outdoorLightState.veranda.internalUntil = 0;
        outdoorLightState.veranda.lastTriggerTs = 0;
    }
});

// Ручное управление:
// любое внешнее изменение реле считаем ручным:
// - кнопка
// - HA
// - интерфейс WB

defineRule("porch_light_manual_watch", {
    whenChanged: OUTDOOR_LIGHTS.porch.relay,
    then: function (newValue) {
        handleManualRelayChange("porch", newValue);
    }
});

defineRule("veranda_light_manual_watch", {
    whenChanged: OUTDOOR_LIGHTS.veranda.relay,
    then: function (newValue) {
        handleManualRelayChange("veranda", newValue);
    }
});

// Датчик крыльца
defineRule("porch_motion_trigger", {
    whenChanged: OUTDOOR_LIGHTS.porch.motion,
    then: function (newValue) {
        if (newValue === false) {
            if (isDebounced("porch")) {
                log("Крыльцо: триггер датчика движения проигнорирован антидребезгом");
                return;
            }

            autoTurnOnByTrigger("porch", "датчика движения");
        }
    }
});

// Датчик веранды
defineRule("veranda_motion_trigger", {
    whenChanged: OUTDOOR_LIGHTS.veranda.motion,
    then: function (newValue) {
        if (newValue === false) {
            if (isDebounced("veranda")) {
                log("Веранда: триггер датчика движения проигнорирован антидребезгом");
                return;
            }

            autoTurnOnByTrigger("veranda", "датчика движения");
        }
    }
});

// Ворота: формирование импульса
defineRule("vorota_pulse", {
    whenChanged: ["wb-mr6c_143/K1", "wb-mr6c_143/K2"],
    then: function (newValue, devName, cellName) {
        if (newValue === true) {
            log("Импульс на ворота: " + cellName);

            setTimeout(function () {
                dev[devName + "/" + cellName] = false;
            }, GATE_PULSE_MS);
        }
    }
});

// Ворота как триггер света крыльца
defineRule("porch_gate_light_trigger", {
    whenChanged: ["wb-mr6c_143/K1", "wb-mr6c_143/K2"],
    then: function (newValue, devName, cellName) {
        if (newValue === true) {
            if (isDebounced("porch")) {
                log("Крыльцо: триггер от ворот " + cellName + " проигнорирован антидребезгом");
                return;
            }

            autoTurnOnByTrigger("porch", "ворот (" + cellName + ")");
        }
    }
});

Так в “Сценариях” есть астрономический таймер. Зачем свой писать?
Да и управление светом там есть.
Можно это все просто на сценариях сделать.

Аааа.. Вам готовое решение нужно… хм… у меня логика кнопок и датчиков… с немного другой логикой…

Наверное так будет работать

// =========================================================
// 1. ВРЕМЯ ВОСХОДА / ЗАКАТА
// =========================================================

// 55.265350, 37.749750

// Настройки
var LATITUDE = 55.26535;     // Ваши координаты
var LONGITUDE = 37.74975;    // Ваши координаты
var TIMEZONE_OFFSET = 3;     // Часовой пояс (MSK = UTC+3)

// Обновлённое виртуальное устройство с кнопкой
defineVirtualDevice("sun_times", {
  title: "Время восхода и заката",
  cells: {
    sunrise: {
      title: "Время восхода",
      type: "text",
      value: "06:00",
      readonly: true
    },
    sunset: {
      title: "Время заката",
      type: "text",
      value: "18:00",
      readonly: true
    },
    last_updated: {
      title: "Последнее обновление",
      type: "text",
      value: "не обновлялось",
      readonly: true
    },
    update_button: {
      title: "Обновить сейчас",
      type: "pushbutton"
    },
    isNight: {
      type: "switch",
      value: false,
      readonly: true
    }
  }
});

// Функция для преобразования времени в минуты (для сравнения)
function timeToMinutes(timeStr) {
    try {
        var parts = timeStr.split(':');
        if (parts.length !== 2) {
            throw new Error("Неверный формат времени: " + timeStr);
        }

        var hours = parseInt(parts[0], 10);
        var minutes = parseInt(parts[1], 10);

        if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
            throw new Error("Некорректное время: " + timeStr);
        }

        return hours * 60 + minutes;
    } catch (e) {
        log("Ошибка в timeToMinutes: " + e.toString());
        return 6 * 60; // Возвращаем 06:00 по умолчанию
    }
}

// Функция для красивого форматирования даты и времени (без padStart)
function formatDateTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            return "неверная дата";
        }

        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        var day = date.getDate();
        var hours = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();

        month = month < 10 ? "0" + month : month;
        day = day < 10 ? "0" + day : day;
        hours = hours < 10 ? "0" + hours : hours;
        minutes = minutes < 10 ? "0" + minutes : minutes;
        seconds = seconds < 10 ? "0" + seconds : seconds;

        return year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
    } catch (e) {
        log("Ошибка в formatDateTime: " + e.toString());
        return "ошибка формата";
    }
}

// Функция для форматирования времени в HH:MM (без padStart)
function formatTime(date) {
    try {
        if (!(date instanceof Date) || isNaN(date.getTime())) {
            log("formatTime: date не является Date объектом");
            return "06:00";
        }

        var hours = date.getHours();
        var minutes = date.getMinutes();

        var hoursStr = hours < 10 ? "0" + hours : "" + hours;
        var minutesStr = minutes < 10 ? "0" + minutes : "" + minutes;

        return hoursStr + ":" + minutesStr;
    } catch (e) {
        log("Ошибка в formatTime: " + e.toString());
        return "06:00";
    }
}

// Функция для получения локального времени
function getLocalTime() {
    var now = new Date();
    return new Date(now.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000);
}

// Функция определения ночного времени с правильным сравнением
function isNight() {
    try {
        var localTime = getLocalTime();
        var currentTime = formatTime(localTime);
        var currentMinutes = timeToMinutes(currentTime);

        var sunrise = dev["sun_times/sunrise"] || "06:00";
        var sunset = dev["sun_times/sunset"] || "18:00";

        var sunriseMinutes = timeToMinutes(sunrise);
        var sunsetMinutes = timeToMinutes(sunset);

        var isNightTime;
        if (sunriseMinutes < sunsetMinutes) {
            isNightTime = currentMinutes < sunriseMinutes || currentMinutes >= sunsetMinutes;
        } else {
            isNightTime = currentMinutes < sunriseMinutes && currentMinutes >= sunsetMinutes;
        }

        // log("Текущее время: " + currentTime + " (" + currentMinutes + " мин)" +
        //     ", Восход: " + sunrise + " (" + sunriseMinutes + " мин)" +
        //     ", Закат: " + sunset + " (" + sunsetMinutes + " мин)" +
        //     ", Ночь: " + isNightTime);

        return isNightTime;

    } catch (e) {
        log("Ошибка в isNight(): " + e.toString());
        var hours = getLocalTime().getHours();
        return hours < 6 || hours >= 18;
    }
}

// Точная функция расчета солнечных времен на основе SunCalc
function calculateSunTimes(date, lat, lng, timezoneOffset) {
    log("Точный расчет солнечных времен");

    try {
        var rad = Math.PI / 180;
        var dayMs = 1000 * 60 * 60 * 24;
        var J1970 = 2440588;
        var 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;
        }

        function rightAscension(l, b) {
            var e = rad * 23.4397;
            return Math.atan2(Math.sin(l) * Math.cos(e) - Math.tan(b) * Math.sin(e), Math.cos(l));
        }

        function declination(l, b) {
            var e = rad * 23.4397;
            return Math.asin(Math.sin(b) * Math.cos(e) + Math.cos(b) * Math.sin(e) * Math.sin(l));
        }

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

        function eclipticLongitude(M) {
            var C = rad * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M));
            var P = rad * 102.9372;
            return M + C + P + Math.PI;
        }

        function sunCoords(d) {
            var M = solarMeanAnomaly(d);
            var L = eclipticLongitude(M);
            return {
                dec: declination(L, 0),
                ra: rightAscension(L, 0)
            };
        }

        var J0 = 0.0009;

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

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

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

        function hourAngle(h, phi, d) {
            var cosH = (Math.sin(h) - Math.sin(phi) * Math.sin(d)) / (Math.cos(phi) * Math.cos(d));
            if (cosH > 1) return 0;
            if (cosH < -1) return Math.PI;
            return Math.acos(cosH);
        }

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

        var times = [
            [-0.833, 'sunrise', 'sunset']
        ];

        var lw = rad * -lng;
        var phi = rad * lat;
        var d = toDays(date);
        var n = julianCycle(d, lw);
        var ds = approxTransit(0, lw, n);
        var M = solarMeanAnomaly(ds);
        var L = eclipticLongitude(M);
        var dec = declination(L, 0);
        var Jnoon = solarTransitJ(ds, M, L);

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

        for (var i = 0; i < times.length; i++) {
            var time = times[i];
            var h0 = time[0] * rad;
            var Jset = getSetJ(h0, lw, phi, dec, n, M, L);
            var Jrise = Jnoon - (Jset - Jnoon);
            result[time[1]] = fromJulian(Jrise);
            result[time[2]] = fromJulian(Jset);
        }

        var offsetMs = timezoneOffset * 60 * 60 * 1000;
        var sunriseLocal = new Date(result.sunrise.getTime() + offsetMs);
        var sunsetLocal = new Date(result.sunset.getTime() + offsetMs);

        log("Точный расчет:");
        log("Восход: " + formatDateTime(sunriseLocal));
        log("Закат: " + formatDateTime(sunsetLocal));

        return {
            sunrise: sunriseLocal,
            sunset: sunsetLocal
        };
    } catch (e) {
        log("Ошибка в точном calculateSunTimes: " + e.toString());

        var defaultTime = new Date(date);
        var sunrise = new Date(defaultTime);
        var sunset = new Date(defaultTime);
        sunrise.setHours(6, 0, 0, 0);
        sunset.setHours(18, 0, 0, 0);

        return {
            sunrise: sunrise,
            sunset: sunset
        };
    }
}

// Функция для обновления времени восхода и заката
function updateSunTimes() {
    log("Начало updateSunTimes");

    try {
        var localTime = getLocalTime();
        log("Локальное время: " + formatDateTime(localTime));

        var times = calculateSunTimes(new Date(), LATITUDE, LONGITUDE, TIMEZONE_OFFSET);
        log("Времена получены");

        if (!times || !times.sunrise || !times.sunset) {
            throw new Error("calculateSunTimes не вернул корректные данные");
        }

        var sunriseStr = formatTime(times.sunrise);
        var sunsetStr = formatTime(times.sunset);
        var updateTimeStr = formatDateTime(localTime);

        log("Форматированные времена: " + sunriseStr + " / " + sunsetStr);

        dev["sun_times/sunrise"] = sunriseStr;
        dev["sun_times/sunset"] = sunsetStr;
        dev["sun_times/last_updated"] = updateTimeStr;

        log("Успешно обновлено время восхода/заката: " + sunriseStr + " / " + sunsetStr);

    } catch (e) {
        log("Ошибка в updateSunTimes: " + e.toString());
        var localTime2 = getLocalTime();
        dev["sun_times/sunrise"] = "06:00";
        dev["sun_times/sunset"] = "18:00";
        dev["sun_times/last_updated"] = "Ошибка: " + formatDateTime(localTime2);
    }
}

// Правила обновления времени восхода и заката
defineRule("update_sun_times_on_start", {
    asSoonAs: function() {
        return true;
    },
    then: function() {
        log("Первоначальное обновление времени восхода/заката");
        updateSunTimes();
    }
});

defineRule("update_sun_times_daily", {
    when: cron("0 0 * * *"), // Каждый день в 00:00 UTC контроллера
    then: function() {
        log("Ежедневное обновление времени восхода/заката");
        updateSunTimes();
    }
});

// Ручное обновление по кнопке в виртуальном устройстве
defineRule("manual_sun_times_update", {
    when: function() {
        return dev["sun_times/update_button"];
    },
    then: function() {
        log("Ручное обновление времени восхода/заката");
        updateSunTimes();
    }
});

defineRule("update_isNight", {
    when: cron("0 */1 * * *"), // Каждую минуту
    then: function() {
        // log("Ежеминутное обновление isNight");
        dev["sun_times/isNight"] = isNight();
    }
});

// включаем свет есть ночь и выключаем если не ночь))
defineRule("night_light_trigger", {
    whenChanged: ["sun_times/isNight"],
    then: function (newValue, devName, cellName) {
        if (newValue === true) {
           dev["wb-mr6c_166/K1"] = true;
           dev["wb-mr6c_97/K2"] = true;
        }else{
           dev["wb-mr6c_166/K1"] = false;
           dev["wb-mr6c_97/K2"] = false;
        }
    }
});

Наверное… Не пользовался “Сценариями” у меня такое впечатление что они наверное недавно появились…