Новая версия …
Оптимизирована по производительности
Убран параметр расстояние до Солнца (я не вижу практического применения ему и никто не попросил оставить)
Фазы разделены на три параметра … и вообще оптимизировано под различные панели и отображение информации.
Добавлена “виртуальная” фаза “Инициализация” (-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);