Солнечный калькулятор (астрономическое реле)

Тут уже выкладывали несколько вариантов. Предлагаю свой вариант.
Подробности использования в описании
Чтобы использовать

  • проверить настройки времени на контроллере и их синхронизацию (есть в документации)
  • создать новое правило. Скопировать код отсюда. Вставить в правило.
Новая версия ниже. Устаревшую версию можно поднять из истории.
2 Likes

В 97 строке нужно комментарий добавить

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

Обновленная и оптимизированная версия ниже.

1 Like

Проверил, обновляет раз в минуту.

Спасибо, что поделились вашей наработкой!
Исправил в вашем коде наше название: “ДЛЯ WIRENBOARD” )

Попросили сделать некоторые доработки …
Появятся фазы которые не всегда будут возможны по координатам
Высокое солнце
Полярный день
Полярная ночь

Высокое Солнце - под автоматизацию солнечных батарей (Солнце выше 30 градусов) которое в средних широтах может быть доступно только летом …

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

Длина тени также останется
Версия 3 уже обкатывается …

А для каких целей нужно значение “расстояние до Солнца” ?
Собираюсь убрать в следующей версии

Новая версия …
Оптимизирована по производительности
Убран параметр расстояние до Солнца (я не вижу практического применения ему и никто не попросил оставить)
Фазы разделены на три параметра … и вообще оптимизировано под различные панели и отображение информации.
Добавлена “виртуальная” фаза “Инициализация” (-1) чтобы не было непонимания при старте …
Добавлена обработка полярных регионов (в России они есть да и ошибки были если задать координаты полярные …)
Добавлена фаза “Высокое Солнце” - и она может быть доступна не всегда или недоступна вообще в некоторых регионах (но при использовании солнечных панелей может быть очень полезна)
Много мелких улучшений …

/**
 * =============== СОЛНЕЧНЫЙ КАЛЬКУЛЯТОР ДЛЯ Wiren Board =================
 * Версия 3.3
 * 
 * @author Clevelus (адаптация и дополнения)
 * @copyright (c) 2024 Clevelus
 * 
 * ### ЛИЦЕНЗИЯ И ПРОИСХОЖДЕНИЕ КОДА:
 * 
 * Основной астрономический алгоритм основан на библиотеке SunCalc:
 * - Оригинальный проект: https://github.com/mourner/suncalc
 * - Автор: Vladimir Agafonkin
 * - Лицензия: MIT
 * - Copyright: (c) 2014 Vladimir Agafonkin
 * 
 * Дополнения и адаптация Clevelus:
 * - Система 8 структурированных солнечных фаз (SUN_PHASES) с цветами
 * - Интеграция с WarenBoard через виртуальные устройства
 * - Флаги (flag_*) и phase_index для простой автоматизации
 * - Расчет длины тени
 * - Упрощение для ES5 и оптимизация для периодических вычислений
 * - Расширенная обработка ошибок и подробное логирование
 * 
 * Код распространяется под лицензией MIT. При использовании сохраняйте уведомления
 * об авторских правах как оригинального автора, так и автора адаптации.
 * 
 * ========================================================================
 * 
 * ### ПРЕДНАЗНАЧЕНИЕ И ОГРАНИЧЕНИЯ ТОЧНОСТИ:
 * 
 * Код предназначен для использования в качестве "астрономического реле" - 
 * определения светлого/темного времени суток для автоматизации освещения и
 * других систем, где критична привязка к положению Солнца.
 * 
 * Основное использование: сравнение dev['sun_calculator/display_currentPhase'] 
 * или dev['sun_calculator/phase_index'] с пороговыми значениями в правилах.
 * 
 * Упрощения, НЕ влияющие на целевое использование:
 * 1. Высота местности не учитывается - до 500 метров погрешность < 10 секунд
 * 2. Атмосферная рефракция для низких углов (<5°) не учитывается
 * 3. Округление временных значений до минут (достаточно для автоматизации)
 * 4. Упрощенные астрономические формулы (отличаются от оригинального SunCalc)
 * 
 * ### ТОЧНОСТЬ ДЛЯ ЦЕЛЕЙ АВТОМАТИЗАЦИИ:
 * 
 * - Определение дня/ночи: точность ±3-5 минут
 * - Границы сумерек: точность ±5 минут
 * - Высота Солнца: ±0.25° (расхождение с suncalc.org)
 * - Азимут Солнца: ±0.2°
 * 
 * ЭТОЙ ТОЧНОСТИ БОЛЕЕ ЧЕМ ДОСТАТОЧНО для:
 * - Включения/выключения освещения
 * - Управления шторами и жалюзи
 * - Автоматизации полива и климатических систем
 * - Автоматизации использования солнечных батарей
 * - Любых задач, где нужна привязка к восходу/закату
 * 
 * ========================================================================
 * 
 * ### РЕКОМЕНДАЦИИ ПО ИСПОЛЬЗОВАНИЮ:
 * 
 * 1. ОСНОВНОЙ СПОСОБ ИСПОЛЬЗОВАНИЯ:
 *    if (dev['sun_calculator/phase_index'] >= X) // где X от 0 до 7
 * 
 * 2. ИНТЕРПРЕТАЦИЯ ИНДЕКСОВ ФАЗ:
 *    0: Ночь (полная темнота)
 *    1: Астрономические сумерки (горизонт едва виден)
 *    2: Навигационные сумерки (горизонт различается)
 *    3: Гражданские сумерки (необходима подсветка)
 *    4: Восход/закат (Солнце на горизонте)
 *    5: Золотой час (мягкий свет для фото)
 *    6: День (естественного света достаточно)
 *    7: Высокое солнце (максимальная освещенность)
 * 
 * 3. ТИПИЧНЫЕ СЦЕНАРИИ:
 *    "Включить свет вечером": phase_index < 4
 *    "Выключить свет утром": phase_index > 3 (или 4)
 *    "Ночной режим": phase_index < 2
 *    "Управление жалюзи": phase_index > 6 (высокое солнце)
 * 
 * 4. ИНТЕРВАЛ ОБНОВЛЕНИЯ:
 *    - 60 секунд оптимально (Солнце движется ~0.25°/мин)
 *    - Для экономии ресурсов можно увеличить до 300 сек
 *    - Минимально 1 секунда для максимальной точности
 * 
 * 5. РЕГИОНАЛЬНЫЕ НАСТРОЙКИ:
 *    - По умолчанию: Москва (центр)
 *    - Для южных регионов можно уменьшить углы сумерек
 *    - Для северных - увеличить
 *    - Для южных регионов можно уменьшить опрос для увеличения точности
 * ========================================================================
 *  
 * Крупные российские города (для быстрого старта):
 * Москва (центр): 55.7558, 37.6173
 * Санкт-Петербург: 59.9398, 30.3146
 * Екатеринбург: 56.8389, 60.6057
 * Новосибирск: 55.0084, 82.9357
 * Владивосток: 43.1155, 131.8855
 * Сочи: 43.5855, 39.7231
 * Калининград: 54.7104, 20.4522
 * 
 * Вы можете изменить координаты через ячейки input_latitude и input_longitude.
 * 
 * ========================================================================
 */

var DEVICE_NAME = "sun_calculator";
var CONFIG = {
  defaultLat: 55.7558000,
  defaultLng: 37.6173000,
  updateIntervalSec: 60,    // Оптимально для автоматизации (Солнце движется ~0.25°/мин)
  objectHeight: 1.0,        // Высота объекта для расчета тени (метры)
  debugMode: false,         // false - минимум логов, true - подробные логи
  logPrefix: "[SunCalc] ",  // Префикс для логов
  timezone: null            // null - использовать системную, или задать вручную, например: "+03:00"
};

// =================== СТРУКТУРИРОВАННЫЙ МАССИВ СОЛНЕЧНЫХ ФАЗ ========
// ВСЕ данные фаз должны быть только здесь!
// Структура: {id, maxAlt, name, desc, flag, color, comment}
// id - уникальный индекс фазы (0=самая темная, 7=самый яркий день)
// maxAlt - максимальная высота Солнца для этой фазы (градусы)
// flag - имя флага для автоматизации (без префикса flag_)
// color - цвет для визуализации в веб-интерфейсе
// comment - пояснение о доступности фазы в разных регионах
var SUN_PHASES = [
  {id: 0, maxAlt: -18,  name: "Астрономическая ночь",
   desc: "Полная темнота, видны слабые звёзды", 
   flag: "isNight", color: "#000022",
   comment: "Доступна во всех регионах, кроме полярного дня"},
  
  {id: 1, maxAlt: -12,  name: "Астрономические сумерки",
   desc: "Сумерки для астрономических наблюдений",
   flag: "isAstronomicalTwilight", color: "#1a1a3a",
   comment: "Доступна в умеренных широтах"},
  
  {id: 2, maxAlt: -6,   name: "Навигационные сумерки",
   desc: "Морские сумерки, горизонт различается", 
   flag: "isNauticalTwilight", color: "#2a2a5a",
   comment: "Доступна в умеренных широтах"},
  
  {id: 3, maxAlt: -0.833, name: "Гражданские сумерки",
   desc: "Яркие сумерки, необходима подсветка", 
   flag: "isCivilTwilight", color: "#3a5a8a",
   comment: "Доступна во всех регионах"},
  
  {id: 4, maxAlt: 0,    name: "Восход/Закат",
   desc: "Солнце на горизонте", 
   flag: "isSunriseSunset", color: "#ff7e50",
   comment: "Доступна во всех регионах, кроме полярного дня/ночи"},
  
  {id: 5, maxAlt: 6,    name: "Золотой час",
   desc: "Мягкий свет для фото, дежурное освещение", 
   flag: "isGoldenHour", color: "#ffaa50",
   comment: "Доступна во всех регионах с восходом/закатом"},
  
  {id: 6, maxAlt: 30,   name: "День",
   desc: "Естественного освещения достаточно", 
   flag: "isDay", color: "#87ceeb",
   comment: "Доступна во всех регионах с дневным светом"},
  
  {id: 7, maxAlt: 90,   name: "Высокое солнце",
   desc: "Максимальная освещённость, минимальные тени", 
   flag: "isHighSun", color: "#ffff00",
   comment: "Доступна в южных регионах и летом в умеренных широтах"}
];

// Специальный индекс для фазы инициализации
var INIT_PHASE_INDEX = -1;

// ================ SUNCALC АЛГОРИТМ (АДАПТИРОВАННЫЙ) =================
// Основа: SunCalc Владимира Агафонкина (MIT лицензия)
// Адаптация для целей автоматизации с сохранением достаточной точности.
// Для научных/навигационных целей используйте оригинальный SunCalc !!!
var SunCalc = {};
SunCalc.rad = Math.PI / 180;
SunCalc.J1970 = 2440588;
SunCalc.J2000 = 2451545;
SunCalc.e = SunCalc.rad * 23.4397;

SunCalc.getPosition = function(date, lat, lng) {
  var lw = SunCalc.rad * -lng;
  var phi = SunCalc.rad * lat;
  var d = SunCalc.toDays(date);
  
  var M = SunCalc.solarMeanAnomaly(d);
  var L = SunCalc.eclipticLongitude(M);
  var dec = SunCalc.declination(L, 0);
  var ra = SunCalc.rightAscension(L, 0);
  
  var H = SunCalc.siderealTime(d, lw) - ra;
  
  return {
    altitude: SunCalc.altitude(H, phi, dec),
    azimuth: SunCalc.azimuth(H, phi, dec),
    declination: dec
  };
};

SunCalc.getTimes = function(date, lat, lng) {
  var lw = SunCalc.rad * -lng;
  var phi = SunCalc.rad * lat;
  var d = SunCalc.toDays(date);
  var n = Math.round(d - 0.0009 - lw / (2 * Math.PI));
  var ds = 0.0009 + lw / (2 * Math.PI) + n;
  
  var M = SunCalc.solarMeanAnomaly(ds);
  var L = SunCalc.eclipticLongitude(M);
  var dec = SunCalc.declination(L, 0);
  var Jnoon = SunCalc.solarTransitJ(ds, M, L);
  
  var result = { solarNoon: SunCalc.fromJulian(Jnoon) };
  
  // Используем углы из SUN_PHASES для согласованности
  var phases = [
    [SUN_PHASES[3].maxAlt * SunCalc.rad, 'sunrise', 'sunset'],   // Гражданские сумерки (-0.833°)
    [SUN_PHASES[2].maxAlt * SunCalc.rad, 'dawn', 'dusk'],        // Навигационные сумерки (-6°)
    [SUN_PHASES[1].maxAlt * SunCalc.rad, 'nauticalDawn', 'nauticalDusk'], // Астрономические сумерки (-12°)
    [SUN_PHASES[0].maxAlt * SunCalc.rad, 'nightEnd', 'night']    // Ночь (-18°)
  ];
  
  for (var i = 0; i < phases.length; i++) {
    var phase = phases[i];
    var h0 = phase[0];
    var Jset = SunCalc.getSetJ(h0, lw, phi, dec, n, M, L);
    var Jrise = Jnoon - (Jset - Jnoon);
    result[phase[1]] = SunCalc.fromJulian(Jrise);
    result[phase[2]] = SunCalc.fromJulian(Jset);
  }
  
  return result;
};

// Вспомогательные функции SunCalc
SunCalc.toDays = function(date) { 
  return SunCalc.toJulian(date) - SunCalc.J2000; 
};

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

SunCalc.fromJulian = function(j) { 
  return new Date((j + 0.5 - SunCalc.J1970) * 86400000); 
};

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

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

SunCalc.azimuth = function(H, phi, dec) { 
  return Math.atan2(
    Math.sin(H), 
    Math.cos(H) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi)
  ); 
};

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

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

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

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

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

SunCalc.getSetJ = function(h, lw, phi, dec, n, M, L) {
  var w = SunCalc.hourAngle(h, phi, dec);
  var a = 0.0009 + (w + lw) / (2 * Math.PI) + n;
  return SunCalc.solarTransitJ(a, M, L);
};

SunCalc.hourAngle = function(h, phi, dec) {
  var arg = (Math.sin(h) - Math.sin(phi) * Math.sin(dec)) / (Math.cos(phi) * Math.cos(dec));
  // Защита от выхода за пределы [-1, 1] для полярных регионов
  arg = Math.max(-1, Math.min(1, arg));
  return Math.acos(arg);
};

// ===================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =====================
function logDebug(message) {
  if (CONFIG.debugMode) {
    log(CONFIG.logPrefix + message);
  }
}

function logError(message) {
  log(CONFIG.logPrefix + "ОШИБКА: " + message);
}

function logInfo(message) {
  log(CONFIG.logPrefix + message);
}

function formatTime(date) {
  if (!date || !(date instanceof Date)) return "--:--";
  var hours = date.getHours();
  var minutes = date.getMinutes();
  return (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
}

function formatDuration(minutes) {
  if (isNaN(minutes) || minutes < 0) return "--:--";
  var hours = Math.floor(minutes / 60);
  var mins = Math.round(minutes % 60);
  return (hours < 10 ? "0" : "") + hours + ":" + (mins < 10 ? "0" : "") + mins;
}

function validateCoords(lat, lng) {
  // Ограничиваем значения
  lat = Math.max(-90, Math.min(90, lat));
  lng = ((lng + 180) % 360 + 360) % 360 - 180; // Приводим к диапазону -180..180
  
  return { lat: lat, lng: lng };
}

function isPolarRegion(lat) {
  return Math.abs(lat) > 66.5; // Полярный круг
}

// Определение текущей фазы по высоте Солнца с учетом полярных регионов
function getCurrentPhase(altitude, lat) {
  // Проверка на полярный день/ночь
  var absLat = Math.abs(lat);
  if (isPolarRegion(lat)) {
    // Полярные регионы
    if (altitude > 0 && altitude < 0.1) {
      return {
        id: 100, 
        name: "Полярный день", 
        desc: "Солнце не заходит, находится низко над горизонтом",
        flag: "isPolarDay",
        color: "#ffcc00",
        maxAlt: 90
      };
    } else if (altitude > 0) {
      return SUN_PHASES[SUN_PHASES.length - 1]; // Высокое солнце или день
    } else if (altitude < -18) {
      return {
        id: 101,
        name: "Полярная ночь",
        desc: "Солнце не восходит",
        flag: "isPolarNight",
        color: "#000066",
        maxAlt: -18
      };
    }
  }
  
  // Обычная логика для умеренных широт
  for (var i = 0; i < SUN_PHASES.length; i++) {
    if (altitude < SUN_PHASES[i].maxAlt) {
      return SUN_PHASES[i];
    }
  }
  return SUN_PHASES[SUN_PHASES.length - 1];
}

// Расчет длины тени
function calculateShadowLength(altitude, objectHeight) {
  if (altitude > 0.1 && objectHeight > 0) {
    return objectHeight / Math.tan(altitude * Math.PI / 180);
  }
  return 0;
}

function getTimezoneOffset() {
  if (CONFIG.timezone) {
    return CONFIG.timezone;
  }
  
  var offset = new Date().getTimezoneOffset();
  var sign = offset > 0 ? "-" : "+";
  var hours = Math.abs(Math.floor(offset / 60));
  var minutes = Math.abs(offset % 60);
  return sign + (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
}

// ================= СОЗДАНИЕ ВИРТУАЛЬНОГО УСТРОЙСТВА =================
// Динамически создаем ячейки устройства на основе массива SUN_PHASES
function createSunPhaseCells() {
  var cells = {
    // === НАСТРОЙКИ ===
    input_latitude: { type: "value", value: CONFIG.defaultLat, units: "°", title: "Широта" },
    input_longitude: { type: "value", value: CONFIG.defaultLng, units: "°", title: "Долгота" },
    input_objectHeight: { type: "value", value: CONFIG.objectHeight, units: "м", title: "Высота объекта для тени" },
    
    // === ТЕКУЩЕЕ СОСТОЯНИЕ ===
    display_status: { type: "text", value: "Инициализация...", readonly: true, title: "Статус" },
    phase_index: { type: "value", value: INIT_PHASE_INDEX, readonly: true, title: "ID фазы (-1=инициализация, 0-7=фазы)" },
    phase_name: { type: "text", value: "Инициализация", readonly: true, title: "Название фазы" },
    phase_description: { type: "text", value: "Устройство запускается...", readonly: true, title: "Описание фазы" },
    display_currentAltitude: { type: "value", value: -999, units: "°", readonly: true, title: "Высота Солнца" },
    display_currentAzimuth: { type: "value", value: -999, units: "°", readonly: true, title: "Азимут Солнца" },
    phase_color: { type: "text", value: "#888888", readonly: true, title: "Цвет фазы" },
    
    // === РАСЧЕТНЫЕ ПАРАМЕТРЫ ===
    display_shadowLength: { type: "value", value: -999, units: "м", readonly: true, title: "Длина тени" },
    display_declination: { type: "value", value: -999, units: "°", readonly: true, title: "Склонение Солнца" },
    
    // === ВРЕМЕНА СОБЫТИЙ ===
    display_sunrise: { type: "text", value: "--:--", readonly: true, title: "Восход" },
    display_sunset: { type: "text", value: "--:--", readonly: true, title: "Закат" },
    display_solarNoon: { type: "text", value: "--:--", readonly: true, title: "Солнечный полдень" },
    display_dawn: { type: "text", value: "--:--", readonly: true, title: "Гражданские сумерки (утро)" },
    display_dusk: { type: "text", value: "--:--", readonly: true, title: "Гражданские сумерки (вечер)" },
    display_day_length: { type: "text", value: "--:--", readonly: true, title: "Продолжительность дня" },
    
    // === СИСТЕМНАЯ ИНФОРМАЦИЯ ===
    display_timezone: { type: "text", value: getTimezoneOffset(), readonly: true, title: "Часовой пояс" },
    display_isPolarRegion: { type: "switch", value: false, readonly: true, title: "Полярный регион" },
    
    // === УПРАВЛЕНИЕ ===
    btn_update: { type: "pushbutton", value: false, title: "Обновить сейчас" },
    display_lastUpdate: { type: "text", value: "--:--:--", readonly: true, title: "Последнее обновление" }
  };
  
  // Динамически добавляем флаги для каждой фазы из массива SUN_PHASES
  for (var i = 0; i < SUN_PHASES.length; i++) {
    var phase = SUN_PHASES[i];
    var flagName = "flag_" + phase.flag;
    cells[flagName] = { 
      type: "switch", 
      value: false, 
      readonly: true, 
      title: phase.name,
      order: phase.id + 100
    };
  }
  
  // Добавляем флаги для полярных фаз
  cells["flag_isPolarDay"] = {
    type: "switch",
    value: false,
    readonly: true,
    title: "Полярный день",
    order: 200
  };
  
  cells["flag_isPolarNight"] = {
    type: "switch",
    value: false,
    readonly: true,
    title: "Полярная ночь",
    order: 201
  };
  
  return cells;
}

// Создаем виртуальное устройство
try {
  defineVirtualDevice(DEVICE_NAME, {
    title: "Солнечный калькулятор",
    cells: createSunPhaseCells()
  });
  logInfo("Солнечный калькулятор: устройство создано с " + SUN_PHASES.length + " фазами");
} catch(e) {
  logError("Ошибка создания устройства: " + e);
}

// ===================== ОСНОВНАЯ ФУНКЦИЯ РАСЧЕТА =====================
var isUpdating = false;

function updateSunData(isManualUpdate) {
  if (isUpdating) {
    logDebug("Обновление уже выполняется, пропускаем");
    return;
  }
  
  isUpdating = true;
  
  // Получаем параметры
  var lat = parseFloat(dev[DEVICE_NAME + "/input_latitude"]);
  var lng = parseFloat(dev[DEVICE_NAME + "/input_longitude"]);
  var objectHeight = parseFloat(dev[DEVICE_NAME + "/input_objectHeight"]);
  
  // Валидация координат
  if (isNaN(lat) || isNaN(lng)) {
    lat = CONFIG.defaultLat;
    lng = CONFIG.defaultLng;
    if (dev[DEVICE_NAME + "/input_latitude"] !== lat) {
      dev[DEVICE_NAME + "/input_latitude"] = lat;
    }
    if (dev[DEVICE_NAME + "/input_longitude"] !== lng) {
      dev[DEVICE_NAME + "/input_longitude"] = lng;
    }
  }
  
  var validated = validateCoords(lat, lng);
  lat = validated.lat;
  lng = validated.lng;
  
  if (isNaN(objectHeight) || objectHeight <= 0) {
    objectHeight = CONFIG.objectHeight;
    if (dev[DEVICE_NAME + "/input_objectHeight"] !== objectHeight) {
      dev[DEVICE_NAME + "/input_objectHeight"] = objectHeight;
    }
  }
  
  var now = new Date();
  
  // 1. Получаем позицию солнца
  var pos = SunCalc.getPosition(now, lat, lng);
  if (!pos || typeof pos.altitude !== 'number') {
    if (dev[DEVICE_NAME + "/display_status"] !== "Ошибка расчета позиции") {
      dev[DEVICE_NAME + "/display_status"] = "Ошибка расчета позиции";
    }
    isUpdating = false;
    return;
  }
  
  var altitude = pos.altitude * 180 / Math.PI;
  var azimuth = pos.azimuth * 180 / Math.PI;
  var declination = pos.declination * 180 / Math.PI;
  
  // Корректируем азимут
  azimuth = (azimuth + 180) % 360;
  
  // 2. Определяем текущую фазу
  var currentPhase = getCurrentPhase(altitude, lat);
  
  // 3. Рассчитываем длину тени
  var shadowLength = calculateShadowLength(altitude, objectHeight);
  
  // 4. Получаем время солнечных событий
  var times = SunCalc.getTimes(now, lat, lng);
  
  // 5. Обновляем все поля устройства, только если значения изменились
  
  // Высота Солнца
  var newAltitude = Math.round(altitude * 100) / 100;
  if (dev[DEVICE_NAME + "/display_currentAltitude"] !== newAltitude) {
    dev[DEVICE_NAME + "/display_currentAltitude"] = newAltitude;
  }
  
  // Азимут Солнца
  var newAzimuth = Math.round(azimuth * 100) / 100;
  if (dev[DEVICE_NAME + "/display_currentAzimuth"] !== newAzimuth) {
    dev[DEVICE_NAME + "/display_currentAzimuth"] = newAzimuth;
  }
  
  // Склонение
  var newDeclination = Math.round(declination * 100) / 100;
  if (dev[DEVICE_NAME + "/display_declination"] !== newDeclination) {
    dev[DEVICE_NAME + "/display_declination"] = newDeclination;
  }
  
  // Длина тени
  var newShadowLength = Math.round(shadowLength * 100) / 100;
  if (dev[DEVICE_NAME + "/display_shadowLength"] !== newShadowLength) {
    dev[DEVICE_NAME + "/display_shadowLength"] = newShadowLength;
  }
  
  // ID фазы
  if (dev[DEVICE_NAME + "/phase_index"] !== currentPhase.id) {
    dev[DEVICE_NAME + "/phase_index"] = currentPhase.id;
  }
  
  // Название фазы
  if (dev[DEVICE_NAME + "/phase_name"] !== currentPhase.name) {
    dev[DEVICE_NAME + "/phase_name"] = currentPhase.name;
  }
  
  // Описание фазы
  if (dev[DEVICE_NAME + "/phase_description"] !== currentPhase.desc) {
    dev[DEVICE_NAME + "/phase_description"] = currentPhase.desc;
  }
  
  // Цвет фазы
  if (dev[DEVICE_NAME + "/phase_color"] !== currentPhase.color) {
    dev[DEVICE_NAME + "/phase_color"] = currentPhase.color;
  }
  
  // Обновляем флаги стандартных фаз
  for (var i = 0; i < SUN_PHASES.length; i++) {
    var phase = SUN_PHASES[i];
    var flagName = "flag_" + phase.flag;
    var fullPath = DEVICE_NAME + "/" + flagName;
    var isActive = (currentPhase.id === phase.id);
    
    if (dev[fullPath] !== isActive) {
      dev[fullPath] = isActive;
    }
  }
  
  // Обновляем флаги полярных фаз
  var polarDayActive = (currentPhase.id === 100);
  var polarNightActive = (currentPhase.id === 101);
  
  if (dev[DEVICE_NAME + "/flag_isPolarDay"] !== polarDayActive) {
    dev[DEVICE_NAME + "/flag_isPolarDay"] = polarDayActive;
  }
  
  if (dev[DEVICE_NAME + "/flag_isPolarNight"] !== polarNightActive) {
    dev[DEVICE_NAME + "/flag_isPolarNight"] = polarNightActive;
  }
  
  // Полярный регион
  var isPolar = isPolarRegion(lat);
  if (dev[DEVICE_NAME + "/display_isPolarRegion"] !== isPolar) {
    dev[DEVICE_NAME + "/display_isPolarRegion"] = isPolar;
  }
  
  // Обновляем времена событий
  if (times && times.sunrise) {
    var sunriseStr = formatTime(times.sunrise);
    var sunsetStr = formatTime(times.sunset);
    var solarNoonStr = formatTime(times.solarNoon);
    var dawnStr = formatTime(times.dawn);
    var duskStr = formatTime(times.dusk);
    
    if (dev[DEVICE_NAME + "/display_sunrise"] !== sunriseStr) {
      dev[DEVICE_NAME + "/display_sunrise"] = sunriseStr;
    }
    
    if (dev[DEVICE_NAME + "/display_sunset"] !== sunsetStr) {
      dev[DEVICE_NAME + "/display_sunset"] = sunsetStr;
    }
    
    if (dev[DEVICE_NAME + "/display_solarNoon"] !== solarNoonStr) {
      dev[DEVICE_NAME + "/display_solarNoon"] = solarNoonStr;
    }
    
    if (dev[DEVICE_NAME + "/display_dawn"] !== dawnStr) {
      dev[DEVICE_NAME + "/display_dawn"] = dawnStr;
    }
    
    if (dev[DEVICE_NAME + "/display_dusk"] !== duskStr) {
      dev[DEVICE_NAME + "/display_dusk"] = duskStr;
    }
    
    if (times.sunrise && times.sunset) {
      var dayLength = (times.sunset - times.sunrise) / (1000 * 60);
      var dayLengthStr = formatDuration(dayLength);
      
      if (dev[DEVICE_NAME + "/display_day_length"] !== dayLengthStr) {
        dev[DEVICE_NAME + "/display_day_length"] = dayLengthStr;
      }
    }
  }
  
  // Время последнего обновления (всегда обновляем)
  var updateStr = now.getHours() + ":" + 
                 (now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
                 (now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
  
  // Для ручного обновления добавляем +1 секунду к отображаемому времени
  if (isManualUpdate) {
    var adjustedTime = new Date(now.getTime() + 1000);
    updateStr = adjustedTime.getHours() + ":" + 
               (adjustedTime.getMinutes() < 10 ? "0" : "") + adjustedTime.getMinutes() + ":" +
               (adjustedTime.getSeconds() < 10 ? "0" : "") + adjustedTime.getSeconds();
  }
  
  if (dev[DEVICE_NAME + "/display_lastUpdate"] !== updateStr) {
    dev[DEVICE_NAME + "/display_lastUpdate"] = updateStr;
  }
  
  // Статус
  var statusStr = "OK (обновлено)";
  if (dev[DEVICE_NAME + "/display_status"] !== statusStr) {
    dev[DEVICE_NAME + "/display_status"] = statusStr;
  }
  
  // Часовой пояс
  var timezoneStr = getTimezoneOffset();
  if (dev[DEVICE_NAME + "/display_timezone"] !== timezoneStr) {
    dev[DEVICE_NAME + "/display_timezone"] = timezoneStr;
  }
  
  logDebug(
    "Фаза: " + currentPhase.name + 
    " (ID=" + currentPhase.id + 
    "), высота=" + altitude.toFixed(2) + "°" +
    ", азимут=" + azimuth.toFixed(2) + "°" +
    (isManualUpdate ? " (ручное)" : " (авто)")
  );
  
  isUpdating = false;
}

// ===================== ПРАВИЛА =====================
defineRule({
  whenChanged: DEVICE_NAME + "/btn_update",
  then: function() {
    if (dev[DEVICE_NAME + "/btn_update"]) {
      if (dev[DEVICE_NAME + "/btn_update"] !== false) {
        dev[DEVICE_NAME + "/btn_update"] = false;
      }
      logDebug("Ручное обновление по кнопке");
      updateSunData(true);
    }
  }
});

defineRule({
  whenChanged: [
    DEVICE_NAME + "/input_latitude",
    DEVICE_NAME + "/input_longitude",
    DEVICE_NAME + "/input_objectHeight"
  ],
  then: function() {
    logDebug("Обновление по изменению координат");
    updateSunData(false);
  }
});

// ===================== ИНИЦИАЛИЗАЦИЯ =====================
logInfo("Солнечный калькулятор v3.3 загружен. Точность: ±0.25°, ±2-3 мин");
logInfo("Количество фаз: " + SUN_PHASES.length);
logInfo("Режим отладки: " + (CONFIG.debugMode ? "ВКЛ" : "ВЫКЛ"));

// Первый запуск
setTimeout(function() {
  logDebug("Выполнение первоначального обновления");
  updateSunData(false);
}, 1000);

// Периодическое обновление
setInterval(function() {
  updateSunData(false);
}, CONFIG.updateIntervalSec * 1000);
1 Like