solarmoonpy 0.1.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/PKG-INFO +16 -3
- solarmoonpy-1.0.1/README.md +15 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/pyproject.toml +1 -1
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy/__init__.py +3 -2
- solarmoonpy-1.0.1/solarmoonpy/moon.py +377 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy/sun.py +25 -15
- solarmoonpy-1.0.1/solarmoonpy/version.py +2 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy.egg-info/PKG-INFO +16 -3
- solarmoonpy-0.1.0/README.md +0 -2
- solarmoonpy-0.1.0/solarmoonpy/moon.py +0 -356
- solarmoonpy-0.1.0/solarmoonpy/version.py +0 -2
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/LICENSE +0 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/setup.cfg +0 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy/location.py +0 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy.egg-info/SOURCES.txt +0 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy.egg-info/dependency_links.txt +0 -0
- {solarmoonpy-0.1.0 → solarmoonpy-1.0.1}/solarmoonpy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarmoonpy
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Precise solar and lunar calculations for astronomical applications
|
|
5
5
|
Author-email: figorr <jdcuartero@yahoo.es>
|
|
6
6
|
License: Apache License
|
|
@@ -220,5 +220,18 @@ Description-Content-Type: text/markdown
|
|
|
220
220
|
License-File: LICENSE
|
|
221
221
|
Dynamic: license-file
|
|
222
222
|
|
|
223
|
-
# solarmoonpy
|
|
224
|
-
|
|
223
|
+
# ☀️🌙 solarmoonpy
|
|
224
|
+
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
225
|
+
|
|
226
|
+
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
227
|
+
|
|
228
|
+
This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
|
|
229
|
+
|
|
230
|
+
## 🚀 Features
|
|
231
|
+
|
|
232
|
+
- ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
|
|
233
|
+
- 🌙 Moon phase and position calculations.
|
|
234
|
+
- Integration with meteorological data from Meteocat.
|
|
235
|
+
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
236
|
+
- Lightweight, easy to use, and well-documented.
|
|
237
|
+
- Configurable for different geographic locations.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ☀️🌙 solarmoonpy
|
|
2
|
+
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
3
|
+
|
|
4
|
+
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
5
|
+
|
|
6
|
+
This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
|
|
7
|
+
|
|
8
|
+
## 🚀 Features
|
|
9
|
+
|
|
10
|
+
- ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
|
|
11
|
+
- 🌙 Moon phase and position calculations.
|
|
12
|
+
- Integration with meteorological data from Meteocat.
|
|
13
|
+
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
14
|
+
- Lightweight, easy to use, and well-documented.
|
|
15
|
+
- Configurable for different geographic locations.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solarmoonpy"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Precise solar and lunar calculations for astronomical applications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "figorr", email = "jdcuartero@yahoo.es" }]
|
|
@@ -9,11 +9,12 @@ https://github.com/figorr/solarmoonpy
|
|
|
9
9
|
|
|
10
10
|
# solarmoonpy/__init__.py
|
|
11
11
|
from .version import __version__
|
|
12
|
-
from .moon import moon_phase, moon_rise_set, illuminated_percentage, moon_distance, moon_angular_diameter
|
|
12
|
+
from .moon import moon_phase, moon_day, moon_rise_set, illuminated_percentage, moon_distance, moon_angular_diameter
|
|
13
13
|
from .location import Location, LocationInfo
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
|
-
"moon_phase",
|
|
16
|
+
"moon_phase",
|
|
17
|
+
"moon_day",
|
|
17
18
|
"moon_rise_set",
|
|
18
19
|
"illuminated_percentage",
|
|
19
20
|
"moon_distance",
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from datetime import date, datetime, timedelta, timezone
|
|
2
|
+
import math
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
from .sun import julianday, julianday_to_juliancentury, sun_apparent_long, sun_distance
|
|
5
|
+
|
|
6
|
+
_SYNODIC_MONTH = 29.530588853
|
|
7
|
+
AU = 149597870.7
|
|
8
|
+
JD_REF_PHASE = 2423436.0 # para moon_phase
|
|
9
|
+
JD_REF_LUNATION = 2423407.51818 # para lunation_number
|
|
10
|
+
|
|
11
|
+
def moon_phase(date_utc: date) -> float:
|
|
12
|
+
"""
|
|
13
|
+
Devuelve los días transcurridos desde la última Luna Nueva.
|
|
14
|
+
Basado en el algoritmo de Chapront-Touzé (usado por timeanddate/NASA).
|
|
15
|
+
"""
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
# Convertimos a datetime UTC (mediodía del día solicitado)
|
|
19
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, tzinfo=timezone.utc)
|
|
20
|
+
jd = julianday(dt)
|
|
21
|
+
|
|
22
|
+
# Fracción del ciclo (0.0 = luna nueva, 0.5 = llena, 1.0 = nueva)
|
|
23
|
+
phase = ((jd - JD_REF_PHASE) / _SYNODIC_MONTH) % 1.0
|
|
24
|
+
|
|
25
|
+
# Días desde la última luna nueva
|
|
26
|
+
return phase * _SYNODIC_MONTH
|
|
27
|
+
|
|
28
|
+
def moon_day(date_utc: date, tol_hours: float = 0.01) -> int:
|
|
29
|
+
"""
|
|
30
|
+
Calcula el día lunar 0–29 usando la última luna nueva exacta calculada por elongación.
|
|
31
|
+
Usa el fin del día UTC para alinear con convenciones de timeanddate/NASA.
|
|
32
|
+
tol_hours: precisión en horas (mantenido en 0.01 para consistencia).
|
|
33
|
+
"""
|
|
34
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 23, 59, tzinfo=timezone.utc)
|
|
35
|
+
last_new_moon = find_last_phase_exact(date_utc, target=0.0)
|
|
36
|
+
age_days = (dt - last_new_moon).total_seconds() / 86400.0
|
|
37
|
+
if age_days < 0:
|
|
38
|
+
day = 0
|
|
39
|
+
else:
|
|
40
|
+
day = int(math.floor(age_days))
|
|
41
|
+
if day > 29:
|
|
42
|
+
day = 29
|
|
43
|
+
return day
|
|
44
|
+
|
|
45
|
+
def illuminated_percentage(date_utc: date) -> float:
|
|
46
|
+
"""
|
|
47
|
+
Calcula el porcentaje de luna iluminada para una fecha UTC (versión precisa).
|
|
48
|
+
Usa posiciones eclípticas reales de Sol y Luna (Meeus) para la elongación y corrección por distancias.
|
|
49
|
+
Args:
|
|
50
|
+
date_utc (date): Fecha en UTC.
|
|
51
|
+
Returns:
|
|
52
|
+
float: Porcentaje de superficie lunar iluminada (0–100).
|
|
53
|
+
"""
|
|
54
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
|
|
55
|
+
jd = julianday(dt)
|
|
56
|
+
jc = julianday_to_juliancentury(jd)
|
|
57
|
+
lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
|
|
58
|
+
lambda_s = sun_apparent_long(jc)
|
|
59
|
+
r_sun = sun_distance(jc)
|
|
60
|
+
diff = _normalize_angle(lambda_m - lambda_s)
|
|
61
|
+
elong_rad = math.radians(diff)
|
|
62
|
+
beta_rad = math.radians(beta_m)
|
|
63
|
+
cos_E = math.cos(beta_rad) * math.cos(elong_rad)
|
|
64
|
+
d = r_sun * AU
|
|
65
|
+
s2 = d**2 + R**2 - 2 * d * R * cos_E
|
|
66
|
+
s = math.sqrt(s2)
|
|
67
|
+
cos_i = (s**2 + R**2 - d**2) / (2 * s * R)
|
|
68
|
+
illum_fraction = (1 + cos_i) / 2
|
|
69
|
+
return illum_fraction * 100.0
|
|
70
|
+
|
|
71
|
+
def _normalize_angle(angle: float) -> float:
|
|
72
|
+
"""Normaliza ángulo a 0-360 grados."""
|
|
73
|
+
return angle % 360.0
|
|
74
|
+
|
|
75
|
+
# Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
|
|
76
|
+
SIGMA_LR_TABLE = [
|
|
77
|
+
[0, 0, 1, 0, 6288774.0, -20905355.0], [2, 0, -1, 0, 1274027.0, -3699111.0],
|
|
78
|
+
[2, 0, 0, 0, 658314.0, -2955968.0], [0, 0, 2, 0, 213618.0, -569925.0],
|
|
79
|
+
[0, 1, 0, 0, -185116.0, 48888.0], [0, 0, 0, 2, -114332.0, -3149.0],
|
|
80
|
+
[2, 0, -2, 0, 58793.0, 246158.0], [2, -1, -1, 0, 57066.0, -152138.0],
|
|
81
|
+
[2, 0, 1, 0, 53322.0, -170733.0], [2, -1, 0, 0, 45758.0, -204586.0],
|
|
82
|
+
[0, 1, -1, 0, -40923.0, -129620.0], [1, 0, 0, 0, -34720.0, 108743.0],
|
|
83
|
+
[0, 1, 1, 0, -30383.0, 104755.0], [2, 0, 0, -2, 15327.0, 10321.0],
|
|
84
|
+
[0, 0, 1, 2, -12528.0, 0.0], [0, 0, 1, -2, 10980.0, 79661.0],
|
|
85
|
+
[4, 0, -1, 0, 10675.0, -34782.0], [0, 0, 3, 0, 10034.0, -23210.0],
|
|
86
|
+
[4, 0, -2, 0, 8548.0, -21636.0], [2, 1, -1, 0, -7888.0, 24208.0],
|
|
87
|
+
[2, 1, 0, 0, -6766.0, 30824.0], [1, 0, -1, 0, -5163.0, -8379.0],
|
|
88
|
+
[1, 1, 0, 0, 4987.0, -16675.0], [2, -1, 1, 0, 4036.0, -12831.0],
|
|
89
|
+
[2, 0, 2, 0, 3994.0, -10445.0], [4, 0, 0, 0, 3861.0, -11650.0],
|
|
90
|
+
[2, 0, -3, 0, 3665.0, 14403.0], [0, 1, -2, 0, -2689.0, -7003.0],
|
|
91
|
+
[2, 0, -1, 2, -2602.0, 0.0], [2, -1, -2, 0, 2390.0, 10056.0],
|
|
92
|
+
[1, 0, 1, 0, -2348.0, 6322.0], [2, -2, 0, 0, 2236.0, -9884.0],
|
|
93
|
+
[0, 1, 2, 0, -2120.0, 5751.0], [0, 2, 0, 0, -2069.0, 0.0],
|
|
94
|
+
[2, -2, -1, 0, 2048.0, -4950.0], [2, 0, 1, -2, -1773.0, 4130.0],
|
|
95
|
+
[2, 0, 0, 2, -1595.0, 0.0], [4, -1, -1, 0, 1215.0, -3958.0],
|
|
96
|
+
[0, 0, 2, 2, -1110.0, 0.0], [3, 0, -1, 0, -892.0, 3258.0],
|
|
97
|
+
[2, 1, 1, 0, -810.0, 2616.0], [4, -1, -2, 0, 759.0, -1897.0],
|
|
98
|
+
[0, 2, -1, 0, -713.0, -2117.0], [2, 2, -1, 0, -700.0, 2354.0],
|
|
99
|
+
[2, 1, -2, 0, 691.0, 0.0], [2, -1, 0, -2, 596.0, 0.0],
|
|
100
|
+
[4, 0, 1, 0, 549.0, -1423.0], [0, 0, 4, 0, 537.0, -1117.0],
|
|
101
|
+
[4, -1, 0, 0, 520.0, -1571.0], [1, 0, -2, 0, -487.0, -1739.0],
|
|
102
|
+
[2, 1, 0, -2, -399.0, 0.0], [0, 0, 2, -2, -381.0, -4421.0],
|
|
103
|
+
[1, 1, 1, 0, 351.0, 0.0], [3, 0, -2, 0, -340.0, 0.0],
|
|
104
|
+
[4, 0, -3, 0, 330.0, 0.0], [2, -1, 2, 0, 327.0, 0.0],
|
|
105
|
+
[0, 2, 1, 0, -323.0, 1165.0], [1, 1, -1, 0, 299.0, 0.0],
|
|
106
|
+
[2, 0, 3, 0, 294.0, 0.0], [2, 0, -1, -2, 0.0, 8752.0]
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
|
|
110
|
+
SIGMA_B_TABLE = [
|
|
111
|
+
[0, 0, 0, 1, 5128122.0], [0, 0, 1, 1, 280602.0], [0, 0, 1, -1, 277693.0],
|
|
112
|
+
[2, 0, 0, -1, 173237.0], [2, 0, -1, 1, 55413.0], [2, 0, -1, -1, 46271.0],
|
|
113
|
+
[2, 0, 0, 1, 32573.0], [0, 0, 2, 1, 17198.0], [2, 0, 1, -1, 9266.0],
|
|
114
|
+
[0, 0, 2, -1, 8822.0], [2, -1, 0, -1, 8216.0], [2, 0, -2, -1, 4324.0],
|
|
115
|
+
[2, 0, 1, 1, 4200.0], [2, 1, 0, -1, -3359.0], [2, -1, -1, 1, 2463.0],
|
|
116
|
+
[2, -1, 0, 1, 2211.0], [2, -1, -1, -1, 2065.0], [0, 1, -1, -1, -1870.0],
|
|
117
|
+
[4, 0, -1, -1, 1828.0], [0, 1, 0, 1, -1794.0], [0, 0, 0, 3, -1749.0],
|
|
118
|
+
[0, 1, -1, 1, -1565.0], [1, 0, 0, 1, -1491.0], [0, 1, 1, 1, -1475.0],
|
|
119
|
+
[0, 1, 1, -1, -1410.0], [0, 1, 0, -1, -1344.0], [1, 0, 0, -1, -1335.0],
|
|
120
|
+
[0, 0, 3, 1, 1107.0], [4, 0, 0, -1, 1021.0], [4, 0, -1, 1, 833.0],
|
|
121
|
+
[0, 0, 1, -3, 777.0], [4, 0, -2, 1, 671.0], [2, 0, 0, -3, 607.0],
|
|
122
|
+
[2, 0, 2, -1, 596.0], [2, -1, 1, -1, 491.0], [2, 0, -2, 1, -451.0],
|
|
123
|
+
[0, 0, 3, -1, 439.0], [2, 0, 2, 1, 422.0], [2, 0, -3, -1, 421.0],
|
|
124
|
+
[2, 1, -1, 1, -366.0], [2, 1, 0, 1, -351.0], [4, 0, 0, 1, 331.0],
|
|
125
|
+
[2, -1, 1, 1, 315.0], [2, -2, 0, -1, 302.0], [0, 0, 1, 3, -283.0],
|
|
126
|
+
[2, 1, 1, -1, -229.0], [1, 1, 0, -1, 223.0], [1, 1, 0, 1, 223.0],
|
|
127
|
+
[0, 1, -2, -1, -220.0], [2, 1, -1, -1, -220.0], [1, 0, 1, 1, -185.0],
|
|
128
|
+
[2, -1, -2, -1, 181.0], [0, 1, 2, 1, -177.0], [4, 0, -2, -1, 176.0],
|
|
129
|
+
[4, -1, -1, -1, 166.0], [1, 0, 1, -1, -164.0], [4, 0, 1, -1, 132.0],
|
|
130
|
+
[1, 0, -1, -1, -119.0], [4, -1, 0, -1, 115.0], [2, -2, 0, 1, 107.0]
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
def _periodic_terms(D, M, Mp, F, T):
|
|
134
|
+
"""Calcula sigma_l, sigma_r, sigma_b con tablas completas y factor E."""
|
|
135
|
+
E = 1 - 0.002516 * T - 0.0000074 * T**2
|
|
136
|
+
E2 = E * E
|
|
137
|
+
sigma_l = 0.0
|
|
138
|
+
sigma_r = 0.0
|
|
139
|
+
sigma_b = 0.0
|
|
140
|
+
|
|
141
|
+
for d_coef, m, mp, f, l, r in SIGMA_LR_TABLE:
|
|
142
|
+
factor = 1.0
|
|
143
|
+
if abs(m) == 1:
|
|
144
|
+
factor = E
|
|
145
|
+
elif abs(m) == 2:
|
|
146
|
+
factor = E2
|
|
147
|
+
arg = d_coef * D + m * M + mp * Mp + f * F
|
|
148
|
+
sigma_l += l * math.sin(math.radians(arg)) * factor
|
|
149
|
+
sigma_r += r * math.cos(math.radians(arg)) * factor
|
|
150
|
+
|
|
151
|
+
for d_coef, m, mp, f, b in SIGMA_B_TABLE:
|
|
152
|
+
factor = 1.0
|
|
153
|
+
if abs(m) == 1:
|
|
154
|
+
factor = E
|
|
155
|
+
elif abs(m) == 2:
|
|
156
|
+
factor = E2
|
|
157
|
+
arg = d_coef * D + m * M + mp * Mp + f * F
|
|
158
|
+
sigma_b += b * math.sin(math.radians(arg)) * factor
|
|
159
|
+
|
|
160
|
+
sigma_l /= 1000000.0 # a grados
|
|
161
|
+
sigma_r *= 0.001 # a km
|
|
162
|
+
sigma_b /= 1000000.0 # a grados
|
|
163
|
+
return sigma_l, sigma_r, sigma_b
|
|
164
|
+
|
|
165
|
+
def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
|
|
166
|
+
"""Calcula longitud eclíptica aparente, latitud, distancia y delta_psi de la Luna."""
|
|
167
|
+
T = (jd - 2451545.0) / 36525.0
|
|
168
|
+
Lp = _normalize_angle(218.3164477 + 481267.88123421 * T - 0.0015786 * T**2 + T**3 / 538841.0 - T**4 / 65194000.0)
|
|
169
|
+
D = _normalize_angle(297.8501921 + 445267.1114034 * T - 0.0018819 * T**2 + T**3 / 545868.0 - T**4 / 113065000.0)
|
|
170
|
+
M = _normalize_angle(357.5291092 + 35999.0502909 * T - 0.0001536 * T**2 + T**3 / 24490000.0)
|
|
171
|
+
Mp = _normalize_angle(134.9633964 + 477198.8675055 * T + 0.0087414 * T**2 + T**3 / 69699.0 - T**4 / 14712000.0)
|
|
172
|
+
F = _normalize_angle(93.2720950 + 483202.0175238 * T - 0.0036539 * T**2 - T**3 / 3526000.0 + T**4 / 863310000.0)
|
|
173
|
+
Omega = _normalize_angle(125.04452 - 1934.136261 * T + 0.0020708 * T**2 + T**3 / 450000.0)
|
|
174
|
+
|
|
175
|
+
sigma_l, sigma_r, sigma_b = _periodic_terms(D, M, Mp, F, T)
|
|
176
|
+
|
|
177
|
+
lambda_ = Lp + sigma_l + 0.003958 * math.sin(math.radians(119.75 + 131.849 * T)) + 0.000319 * math.sin(math.radians(53.09 + 479264.290 * T)) + 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T))
|
|
178
|
+
beta = sigma_b - 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T - 2 * F))
|
|
179
|
+
R = 385000.529 + sigma_r
|
|
180
|
+
delta_psi = (-17.20 * math.sin(math.radians(Omega)) - 1.32 * math.sin(math.radians(2 * (Lp - F))) + 0.23 * math.sin(math.radians(2 * Lp)) + 0.21 * math.sin(math.radians(2 * Omega))) / 3600.0
|
|
181
|
+
lambda_ += delta_psi
|
|
182
|
+
return lambda_, beta, R, delta_psi
|
|
183
|
+
|
|
184
|
+
def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
|
|
185
|
+
"""Calcula alt, h0 para un tiempo dado t (ra no usado)."""
|
|
186
|
+
jd = julianday(t.date()) + (t.hour + t.minute / 60 + t.second / 3600) / 24
|
|
187
|
+
T = (jd - 2451545.0) / 36525.0
|
|
188
|
+
lambda_, beta, R, delta_psi = _moon_ecliptic_position(jd)
|
|
189
|
+
|
|
190
|
+
par = math.asin(6378.14 / R)
|
|
191
|
+
eps = math.radians(23.439281 - 0.0000004 * T)
|
|
192
|
+
lambda_r = math.radians(lambda_)
|
|
193
|
+
beta_r = math.radians(beta)
|
|
194
|
+
ra = math.atan2(math.sin(lambda_r) * math.cos(eps) - math.tan(beta_r) * math.sin(eps), math.cos(lambda_r))
|
|
195
|
+
dec = math.asin(math.sin(beta_r) * math.cos(eps) + math.cos(beta_r) * math.sin(eps) * math.sin(lambda_r))
|
|
196
|
+
sd = 0.2725076 * par
|
|
197
|
+
ref = math.radians(0.5667)
|
|
198
|
+
h0 = par - sd - ref
|
|
199
|
+
gmst = (280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T**2 - T**3 / 38710000.0) % 360.0
|
|
200
|
+
lst = math.radians((gmst + lon) % 360.0)
|
|
201
|
+
ha = lst - ra
|
|
202
|
+
if ha < -math.pi: ha += 2 * math.pi
|
|
203
|
+
elif ha > math.pi: ha -= 2 * math.pi
|
|
204
|
+
alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
|
|
205
|
+
return alt, h0
|
|
206
|
+
|
|
207
|
+
def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
|
|
208
|
+
"""
|
|
209
|
+
Cálculo preciso de moonrise y moonset con series completas de Meeus (precisión ~1-2 min).
|
|
210
|
+
Devuelve tuplas UTC (datetime tz-aware) o None si no ocurre en ese día.
|
|
211
|
+
"""
|
|
212
|
+
lat_r = math.radians(lat)
|
|
213
|
+
rise = None
|
|
214
|
+
set_ = None
|
|
215
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
|
|
216
|
+
prev_t = None
|
|
217
|
+
prev_alt = None
|
|
218
|
+
|
|
219
|
+
step_grueso = 1.0
|
|
220
|
+
intervalos_rise = []
|
|
221
|
+
intervalos_set = []
|
|
222
|
+
for i in range(int(24 / step_grueso) + 1):
|
|
223
|
+
hour = i * step_grueso
|
|
224
|
+
t = dt + timedelta(hours=hour)
|
|
225
|
+
alt, h0 = _calculate_position_and_alt(t, lat_r, lon)
|
|
226
|
+
if prev_alt is not None:
|
|
227
|
+
den = alt - prev_alt
|
|
228
|
+
if abs(den) < 1e-12:
|
|
229
|
+
continue
|
|
230
|
+
if prev_alt < h0 and alt >= h0:
|
|
231
|
+
intervalos_rise.append((prev_t, t, prev_alt, alt))
|
|
232
|
+
elif prev_alt >= h0 and alt < h0:
|
|
233
|
+
intervalos_set.append((prev_t, t, prev_alt, alt))
|
|
234
|
+
prev_t = t
|
|
235
|
+
prev_alt = alt
|
|
236
|
+
|
|
237
|
+
def refine_event(start_t: datetime, end_t: datetime, start_alt: float, end_alt: float, is_rise: bool) -> datetime:
|
|
238
|
+
for _ in range(10):
|
|
239
|
+
mid_t = start_t + (end_t - start_t) / 2
|
|
240
|
+
mid_alt, mid_h0 = _calculate_position_and_alt(mid_t, lat_r, lon)
|
|
241
|
+
den = mid_alt - start_alt if is_rise else mid_alt - end_alt
|
|
242
|
+
if abs(den) < 1e-12:
|
|
243
|
+
return mid_t
|
|
244
|
+
if (is_rise and mid_alt < mid_h0) or (not is_rise and mid_alt >= mid_h0):
|
|
245
|
+
start_t, start_alt = mid_t, mid_alt
|
|
246
|
+
else:
|
|
247
|
+
end_t, end_alt = mid_t, mid_alt
|
|
248
|
+
return start_t + (end_t - start_t) / 2
|
|
249
|
+
|
|
250
|
+
if intervalos_rise and rise is None:
|
|
251
|
+
start_t, end_t, start_alt, end_alt = intervalos_rise[0]
|
|
252
|
+
rise = refine_event(start_t, end_t, start_alt, end_alt, True)
|
|
253
|
+
if intervalos_set and set_ is None:
|
|
254
|
+
start_t, end_t, start_alt, end_alt = intervalos_set[0]
|
|
255
|
+
set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
|
|
256
|
+
return rise, set_
|
|
257
|
+
|
|
258
|
+
def moon_distance(date_utc: date) -> float:
|
|
259
|
+
"""
|
|
260
|
+
Calcula la distancia Tierra-Luna en kilómetros para una fecha UTC.
|
|
261
|
+
Args:
|
|
262
|
+
date_utc (date): Fecha en UTC.
|
|
263
|
+
Returns:
|
|
264
|
+
float: Distancia en kilómetros.
|
|
265
|
+
"""
|
|
266
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
|
|
267
|
+
jd = julianday(dt)
|
|
268
|
+
_, _, R, _ = _moon_ecliptic_position(jd)
|
|
269
|
+
return R
|
|
270
|
+
|
|
271
|
+
def moon_angular_diameter(date_utc: date) -> float:
|
|
272
|
+
"""
|
|
273
|
+
Calcula el diámetro angular de la Luna en arcosegundos para una fecha UTC.
|
|
274
|
+
Args:
|
|
275
|
+
date_utc (date): Fecha en UTC.
|
|
276
|
+
Returns:
|
|
277
|
+
float: Diámetro angular en arcosegundos.
|
|
278
|
+
"""
|
|
279
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
|
|
280
|
+
jd = julianday(dt)
|
|
281
|
+
_, _, R, _ = _moon_ecliptic_position(jd)
|
|
282
|
+
par = math.asin(6378.14 / R)
|
|
283
|
+
sd = 0.2725076 * par
|
|
284
|
+
return 2 * math.degrees(sd) * 3600
|
|
285
|
+
|
|
286
|
+
def lunation_number(date_utc: date) -> int:
|
|
287
|
+
"""
|
|
288
|
+
Calcula el número de lunación según el sistema de TimeandDate/NASA.
|
|
289
|
+
"""
|
|
290
|
+
last_new_moon = find_last_phase_exact(date_utc, target=0.0)
|
|
291
|
+
jd_new_moon = julianday(last_new_moon)
|
|
292
|
+
|
|
293
|
+
lun = (jd_new_moon - JD_REF_LUNATION) / _SYNODIC_MONTH
|
|
294
|
+
return round(lun)
|
|
295
|
+
|
|
296
|
+
def find_last_phase_exact(date_utc: date, target: float = 0.0) -> datetime:
|
|
297
|
+
"""
|
|
298
|
+
Devuelve la fase lunar exacta ANTERIOR o coincidente a date_utc.
|
|
299
|
+
target: 0.0 para new_moon, 90.0 para first_quarter, 180.0 para full_moon, 270.0 para last_quarter.
|
|
300
|
+
Método:
|
|
301
|
+
1) Usa moon_phase(date_utc) para estimar la fecha/hora aproximada desde la última Luna Nueva, luego ajusta para target.
|
|
302
|
+
2) Construye un intervalo alrededor de esa estimación y asegura que contiene un cruce
|
|
303
|
+
(elongation - target cambia de signo).
|
|
304
|
+
3) Refina con bisección hasta ~1 segundo de precisión.
|
|
305
|
+
Este método evita saltos de ciclo porque la estimación inicial está basada en la elongación.
|
|
306
|
+
"""
|
|
307
|
+
# punto central (mediodía UTC) y estimación inicial usando moon_phase para new_moon
|
|
308
|
+
dt_mid = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, tzinfo=timezone.utc)
|
|
309
|
+
# phase_days es el número de días desde la última Luna Nueva (según moon_phase)
|
|
310
|
+
phase_days = moon_phase(date_utc)
|
|
311
|
+
approx_from_new = (target / 360.0) * _SYNODIC_MONTH # Días aproximados desde new_moon al target
|
|
312
|
+
approx_dt = dt_mid - timedelta(days=phase_days) + timedelta(days=approx_from_new)
|
|
313
|
+
|
|
314
|
+
def elongation_dt(t: datetime) -> float:
|
|
315
|
+
jd = julianday(t)
|
|
316
|
+
jc = julianday_to_juliancentury(jd)
|
|
317
|
+
lambda_m, _, _, _ = _moon_ecliptic_position(jd)
|
|
318
|
+
lambda_s = sun_apparent_long(jc)
|
|
319
|
+
# normalizamos a -180..+180 alrededor de target
|
|
320
|
+
diff = (lambda_m - lambda_s - target + 180.0) % 360.0 - 180.0
|
|
321
|
+
return diff
|
|
322
|
+
|
|
323
|
+
# bracket inicial ±2 días alrededor de la estimación
|
|
324
|
+
low = approx_dt - timedelta(days=2)
|
|
325
|
+
high = approx_dt + timedelta(days=2)
|
|
326
|
+
|
|
327
|
+
# Intentamos asegurar que low..high contiene un cambio de signo.
|
|
328
|
+
# Si no, expandimos gradualmente hasta un máximo razonable (p.ej. ±60 días).
|
|
329
|
+
max_expand_days = 60
|
|
330
|
+
step_expand_days = 2
|
|
331
|
+
el_low = elongation_dt(low)
|
|
332
|
+
el_high = elongation_dt(high)
|
|
333
|
+
|
|
334
|
+
expand_attempts = 0
|
|
335
|
+
while el_low * el_high > 0: # mismo signo -> no hay cruce dentro del intervalo
|
|
336
|
+
expand_attempts += 1
|
|
337
|
+
if (high - low).days >= max_expand_days:
|
|
338
|
+
raise RuntimeError(f"No se encontró fase con target {target} en el rango de búsqueda (±60 días).")
|
|
339
|
+
low -= timedelta(days=step_expand_days)
|
|
340
|
+
high += timedelta(days=step_expand_days)
|
|
341
|
+
el_low = elongation_dt(low)
|
|
342
|
+
el_high = elongation_dt(high)
|
|
343
|
+
|
|
344
|
+
# Si exactamente en un extremo es cero, devuelvo ese instante
|
|
345
|
+
if abs(el_low) < 1e-12:
|
|
346
|
+
return low
|
|
347
|
+
if abs(el_high) < 1e-12:
|
|
348
|
+
return high
|
|
349
|
+
|
|
350
|
+
# Bisección: refinamos hasta ~1 segundo de precisión
|
|
351
|
+
# (puedes relajar a 30-60 s si prefieres).
|
|
352
|
+
while (high - low).total_seconds() > 1:
|
|
353
|
+
mid = low + (high - low) / 2
|
|
354
|
+
el_mid = elongation_dt(mid)
|
|
355
|
+
# si mid es exactamente 0 (raro), lo devolvemos
|
|
356
|
+
if abs(el_mid) < 1e-12:
|
|
357
|
+
return mid
|
|
358
|
+
# si el signo de low y mid difiere, el cruce está en low..mid; si no, en mid..high
|
|
359
|
+
if el_low * el_mid <= 0:
|
|
360
|
+
high = mid
|
|
361
|
+
el_high = el_mid
|
|
362
|
+
else:
|
|
363
|
+
low = mid
|
|
364
|
+
el_low = el_mid
|
|
365
|
+
|
|
366
|
+
return low + (high - low) / 2
|
|
367
|
+
|
|
368
|
+
def moon_elongation(date_utc: date) -> float:
|
|
369
|
+
"""
|
|
370
|
+
Devuelve la elongación lunar (0-360°) para la fecha UTC al mediodía.
|
|
371
|
+
"""
|
|
372
|
+
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
|
|
373
|
+
jd = julianday(dt)
|
|
374
|
+
jc = julianday_to_juliancentury(jd)
|
|
375
|
+
lambda_m, _, _, _ = _moon_ecliptic_position(jd)
|
|
376
|
+
lambda_s = sun_apparent_long(jc)
|
|
377
|
+
return _normalize_angle(lambda_m - lambda_s)
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from math import acos, asin, atan2, cos, degrees, radians, sin, tan, sqrt
|
|
3
3
|
|
|
4
|
-
def julianday(
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
def julianday(date_or_dt: datetime.date | datetime.datetime) -> float:
|
|
5
|
+
"""
|
|
6
|
+
Calculate the Julian Day number for a date or datetime (UTC).
|
|
7
|
+
Handles both date (midnight UTC) and datetime (exact time).
|
|
8
|
+
"""
|
|
9
|
+
if isinstance(date_or_dt, datetime.datetime):
|
|
10
|
+
year = date_or_dt.year
|
|
11
|
+
month = date_or_dt.month
|
|
12
|
+
day = date_or_dt.day
|
|
13
|
+
hour = date_or_dt.hour
|
|
14
|
+
minute = date_or_dt.minute
|
|
15
|
+
second = date_or_dt.second
|
|
16
|
+
else:
|
|
17
|
+
year = date_or_dt.year
|
|
18
|
+
month = date_or_dt.month
|
|
19
|
+
day = date_or_dt.day
|
|
20
|
+
hour = minute = second = 0
|
|
9
21
|
|
|
10
22
|
if month <= 2:
|
|
11
23
|
year -= 1
|
|
@@ -14,7 +26,7 @@ def julianday(date: datetime.date) -> float:
|
|
|
14
26
|
a = year // 100
|
|
15
27
|
b = 2 - a + (a // 4)
|
|
16
28
|
jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + b - 1524.5
|
|
17
|
-
return jd
|
|
29
|
+
return jd + (hour + minute/60 + second/3600) / 24.0
|
|
18
30
|
|
|
19
31
|
def julianday_to_juliancentury(jd: float) -> float:
|
|
20
32
|
"""Convert a Julian Day number to a Julian Century."""
|
|
@@ -52,10 +64,14 @@ def sun_true_long(jc: float) -> float:
|
|
|
52
64
|
return l0 + c
|
|
53
65
|
|
|
54
66
|
def sun_apparent_long(jc: float) -> float:
|
|
55
|
-
"""Calculate the sun's apparent longitude."""
|
|
56
|
-
|
|
67
|
+
"""Calculate the sun's apparent longitude with nutation correction (Meeus Ch. 25)."""
|
|
68
|
+
l0 = geom_mean_long_sun(jc)
|
|
69
|
+
m = geom_mean_anomaly_sun(jc)
|
|
70
|
+
c = sun_eq_of_center(jc)
|
|
71
|
+
lambda_geo = l0 + c
|
|
57
72
|
omega = 125.04 - 1934.136 * jc
|
|
58
|
-
|
|
73
|
+
nutation_long = -0.0048 * sin(radians(2 * omega)) # Nutation correction
|
|
74
|
+
return (lambda_geo + nutation_long - 0.00569) % 360.0
|
|
59
75
|
|
|
60
76
|
def sun_distance(jc: float) -> float:
|
|
61
77
|
"""Calculate the distance to the Sun in astronomical units (AU)."""
|
|
@@ -108,10 +124,8 @@ def refraction_at_zenith(zenith: float) -> float:
|
|
|
108
124
|
"""Calculate the atmospheric refraction correction for the given zenith angle (in degrees)."""
|
|
109
125
|
if zenith < 0 or zenith > 90:
|
|
110
126
|
return 0.0
|
|
111
|
-
|
|
112
127
|
# Convert zenith to radians
|
|
113
128
|
zenith_rad = radians(zenith)
|
|
114
|
-
|
|
115
129
|
# Refraction formula (Bennett 1982, used in Astral)
|
|
116
130
|
tan_zenith = tan(zenith_rad)
|
|
117
131
|
if zenith > 85.0:
|
|
@@ -123,14 +137,12 @@ def refraction_at_zenith(zenith: float) -> float:
|
|
|
123
137
|
) / 3600.0
|
|
124
138
|
else:
|
|
125
139
|
refraction = (58.276 / tan_zenith) / 3600.0
|
|
126
|
-
|
|
127
140
|
return refraction
|
|
128
141
|
|
|
129
142
|
def adjust_to_horizon(elevation: float) -> float:
|
|
130
143
|
"""Calculate the extra degrees of depression due to the observer's elevation."""
|
|
131
144
|
if elevation <= 0:
|
|
132
145
|
return 0.0
|
|
133
|
-
|
|
134
146
|
r = 6356900 # Radius of the Earth in meters
|
|
135
147
|
a1 = r
|
|
136
148
|
h1 = r + elevation
|
|
@@ -213,11 +225,9 @@ def sunrise_sunset(
|
|
|
213
225
|
# Convert to local timezone
|
|
214
226
|
sunrise_local = sunrise_utc.astimezone(timezone)
|
|
215
227
|
sunset_local = sunset_utc.astimezone(timezone)
|
|
216
|
-
|
|
217
228
|
return sunrise_local, sunset_local
|
|
218
229
|
|
|
219
230
|
def noon(
|
|
220
|
-
latitude: float,
|
|
221
231
|
longitude: float,
|
|
222
232
|
date: datetime.date,
|
|
223
233
|
timezone: datetime.timezone = datetime.timezone.utc,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarmoonpy
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Precise solar and lunar calculations for astronomical applications
|
|
5
5
|
Author-email: figorr <jdcuartero@yahoo.es>
|
|
6
6
|
License: Apache License
|
|
@@ -220,5 +220,18 @@ Description-Content-Type: text/markdown
|
|
|
220
220
|
License-File: LICENSE
|
|
221
221
|
Dynamic: license-file
|
|
222
222
|
|
|
223
|
-
# solarmoonpy
|
|
224
|
-
|
|
223
|
+
# ☀️🌙 solarmoonpy
|
|
224
|
+
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
225
|
+
|
|
226
|
+
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
227
|
+
|
|
228
|
+
This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
|
|
229
|
+
|
|
230
|
+
## 🚀 Features
|
|
231
|
+
|
|
232
|
+
- ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
|
|
233
|
+
- 🌙 Moon phase and position calculations.
|
|
234
|
+
- Integration with meteorological data from Meteocat.
|
|
235
|
+
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
236
|
+
- Lightweight, easy to use, and well-documented.
|
|
237
|
+
- Configurable for different geographic locations.
|
solarmoonpy-0.1.0/README.md
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
from datetime import date, datetime, timedelta, timezone
|
|
2
|
-
import math
|
|
3
|
-
from typing import Optional, Tuple
|
|
4
|
-
from .sun import julianday, julianday_to_juliancentury, sun_apparent_long, sun_distance
|
|
5
|
-
|
|
6
|
-
_SYNODIC_MONTH = 29.530588853
|
|
7
|
-
AU = 149597870.7 # km por unidad astronómica (valor estándar)
|
|
8
|
-
|
|
9
|
-
def moon_phase(date_utc: date) -> float:
|
|
10
|
-
"""
|
|
11
|
-
Devuelve la fase de la luna como valor fraccionario (0 a ~29.53, 0=nueva, ~14.77=llena).
|
|
12
|
-
Usa una luna nueva reciente (21 Sep 2025) para mayor precisión y opcionalmente elongación eclíptica.
|
|
13
|
-
Args:
|
|
14
|
-
date_utc (date): Fecha en UTC.
|
|
15
|
-
Returns:
|
|
16
|
-
float: Días desde la última luna nueva (0 a ~29.53).
|
|
17
|
-
"""
|
|
18
|
-
# Luna nueva de referencia: 21 de septiembre de 2025, 19:54 UTC
|
|
19
|
-
known_new_moon = datetime(2025, 9, 21, 19, 54, tzinfo=timezone.utc)
|
|
20
|
-
dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
|
|
21
|
-
diff = dt - known_new_moon
|
|
22
|
-
days = diff.days + (diff.seconds / 86400.0)
|
|
23
|
-
|
|
24
|
-
# Cálculo aproximado con mes sinódico
|
|
25
|
-
lunations = days / _SYNODIC_MONTH
|
|
26
|
-
approx_phase = (lunations % 1) * _SYNODIC_MONTH
|
|
27
|
-
|
|
28
|
-
# Cálculo preciso usando elongación eclíptica
|
|
29
|
-
jd = julianday(dt.date()) + 0.5 # Mediodía UTC
|
|
30
|
-
jc = julianday_to_juliancentury(jd)
|
|
31
|
-
lambda_m, _, _, _ = _moon_ecliptic_position(jd)
|
|
32
|
-
lambda_s = sun_apparent_long(jc)
|
|
33
|
-
elong = _normalize_angle(lambda_m - lambda_s)
|
|
34
|
-
|
|
35
|
-
# Convertir elongación a fase (0°=nueva, 180°=llena, 360°=nueva)
|
|
36
|
-
precise_phase = (elong / 360.0) * _SYNODIC_MONTH
|
|
37
|
-
if precise_phase > _SYNODIC_MONTH:
|
|
38
|
-
precise_phase -= _SYNODIC_MONTH
|
|
39
|
-
|
|
40
|
-
# Promedio ponderado: favorece cálculo preciso, pero usa aproximado como respaldo
|
|
41
|
-
return round(0.9 * precise_phase + 0.1 * approx_phase, 6)
|
|
42
|
-
|
|
43
|
-
def illuminated_percentage(date_utc: date) -> float:
|
|
44
|
-
"""
|
|
45
|
-
Calcula el porcentaje de luna iluminada para una fecha UTC (versión precisa).
|
|
46
|
-
Usa posiciones eclípticas reales de Sol y Luna (Meeus) para la elongación y corrección por distancias.
|
|
47
|
-
Args:
|
|
48
|
-
date_utc (date): Fecha en UTC.
|
|
49
|
-
Returns:
|
|
50
|
-
float: Porcentaje de superficie lunar iluminada (0–100).
|
|
51
|
-
"""
|
|
52
|
-
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
|
|
53
|
-
jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
|
|
54
|
-
jc = julianday_to_juliancentury(jd)
|
|
55
|
-
lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
|
|
56
|
-
lambda_s = sun_apparent_long(jc)
|
|
57
|
-
r_sun = sun_distance(jc)
|
|
58
|
-
diff = _normalize_angle(lambda_m - lambda_s)
|
|
59
|
-
elong_rad = math.radians(diff)
|
|
60
|
-
beta_rad = math.radians(beta_m)
|
|
61
|
-
cos_E = math.cos(beta_rad) * math.cos(elong_rad)
|
|
62
|
-
d = r_sun * AU
|
|
63
|
-
s2 = d**2 + R**2 - 2 * d * R * cos_E
|
|
64
|
-
s = math.sqrt(s2)
|
|
65
|
-
cos_i = (s**2 + R**2 - d**2) / (2 * s * R)
|
|
66
|
-
illum_fraction = (1 + cos_i) / 2
|
|
67
|
-
return illum_fraction * 100.0
|
|
68
|
-
|
|
69
|
-
def _normalize_angle(angle: float) -> float:
|
|
70
|
-
"""Normaliza ángulo a 0-360 grados."""
|
|
71
|
-
return angle % 360.0
|
|
72
|
-
|
|
73
|
-
# Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
|
|
74
|
-
SIGMA_LR_TABLE = [
|
|
75
|
-
[0, 0, 1, 0, 6288774.0, -20905355.0],
|
|
76
|
-
[2, 0, -1, 0, 1274027.0, -3699111.0],
|
|
77
|
-
[2, 0, 0, 0, 658314.0, -2955968.0],
|
|
78
|
-
[0, 0, 2, 0, 213618.0, -569925.0],
|
|
79
|
-
[0, 1, 0, 0, -185116.0, 48888.0],
|
|
80
|
-
[0, 0, 0, 2, -114332.0, -3149.0],
|
|
81
|
-
[2, 0, -2, 0, 58793.0, 246158.0],
|
|
82
|
-
[2, -1, -1, 0, 57066.0, -152138.0],
|
|
83
|
-
[2, 0, 1, 0, 53322.0, -170733.0],
|
|
84
|
-
[2, -1, 0, 0, 45758.0, -204586.0],
|
|
85
|
-
[0, 1, -1, 0, -40923.0, -129620.0],
|
|
86
|
-
[1, 0, 0, 0, -34720.0, 108743.0],
|
|
87
|
-
[0, 1, 1, 0, -30383.0, 104755.0],
|
|
88
|
-
[2, 0, 0, -2, 15327.0, 10321.0],
|
|
89
|
-
[0, 0, 1, 2, -12528.0, 0.0],
|
|
90
|
-
[0, 0, 1, -2, 10980.0, 79661.0],
|
|
91
|
-
[4, 0, -1, 0, 10675.0, -34782.0],
|
|
92
|
-
[0, 0, 3, 0, 10034.0, -23210.0],
|
|
93
|
-
[4, 0, -2, 0, 8548.0, -21636.0],
|
|
94
|
-
[2, 1, -1, 0, -7888.0, 24208.0],
|
|
95
|
-
[2, 1, 0, 0, -6766.0, 30824.0],
|
|
96
|
-
[1, 0, -1, 0, -5163.0, -8379.0],
|
|
97
|
-
[1, 1, 0, 0, 4987.0, -16675.0],
|
|
98
|
-
[2, -1, 1, 0, 4036.0, -12831.0],
|
|
99
|
-
[2, 0, 2, 0, 3994.0, -10445.0],
|
|
100
|
-
[4, 0, 0, 0, 3861.0, -11650.0],
|
|
101
|
-
[2, 0, -3, 0, 3665.0, 14403.0],
|
|
102
|
-
[0, 1, -2, 0, -2689.0, -7003.0],
|
|
103
|
-
[2, 0, -1, 2, -2602.0, 0.0],
|
|
104
|
-
[2, -1, -2, 0, 2390.0, 10056.0],
|
|
105
|
-
[1, 0, 1, 0, -2348.0, 6322.0],
|
|
106
|
-
[2, -2, 0, 0, 2236.0, -9884.0],
|
|
107
|
-
[0, 1, 2, 0, -2120.0, 5751.0],
|
|
108
|
-
[0, 2, 0, 0, -2069.0, 0.0],
|
|
109
|
-
[2, -2, -1, 0, 2048.0, -4950.0],
|
|
110
|
-
[2, 0, 1, -2, -1773.0, 4130.0],
|
|
111
|
-
[2, 0, 0, 2, -1595.0, 0.0],
|
|
112
|
-
[4, -1, -1, 0, 1215.0, -3958.0],
|
|
113
|
-
[0, 0, 2, 2, -1110.0, 0.0],
|
|
114
|
-
[3, 0, -1, 0, -892.0, 3258.0],
|
|
115
|
-
[2, 1, 1, 0, -810.0, 2616.0],
|
|
116
|
-
[4, -1, -2, 0, 759.0, -1897.0],
|
|
117
|
-
[0, 2, -1, 0, -713.0, -2117.0],
|
|
118
|
-
[2, 2, -1, 0, -700.0, 2354.0],
|
|
119
|
-
[2, 1, -2, 0, 691.0, 0.0],
|
|
120
|
-
[2, -1, 0, -2, 596.0, 0.0],
|
|
121
|
-
[4, 0, 1, 0, 549.0, -1423.0],
|
|
122
|
-
[0, 0, 4, 0, 537.0, -1117.0],
|
|
123
|
-
[4, -1, 0, 0, 520.0, -1571.0],
|
|
124
|
-
[1, 0, -2, 0, -487.0, -1739.0],
|
|
125
|
-
[2, 1, 0, -2, -399.0, 0.0],
|
|
126
|
-
[0, 0, 2, -2, -381.0, -4421.0],
|
|
127
|
-
[1, 1, 1, 0, 351.0, 0.0],
|
|
128
|
-
[3, 0, -2, 0, -340.0, 0.0],
|
|
129
|
-
[4, 0, -3, 0, 330.0, 0.0],
|
|
130
|
-
[2, -1, 2, 0, 327.0, 0.0],
|
|
131
|
-
[0, 2, 1, 0, -323.0, 1165.0],
|
|
132
|
-
[1, 1, -1, 0, 299.0, 0.0],
|
|
133
|
-
[2, 0, 3, 0, 294.0, 0.0],
|
|
134
|
-
[2, 0, -1, -2, 0.0, 8752.0]
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
# Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
|
|
138
|
-
SIGMA_B_TABLE = [
|
|
139
|
-
[0, 0, 0, 1, 5128122.0],
|
|
140
|
-
[0, 0, 1, 1, 280602.0],
|
|
141
|
-
[0, 0, 1, -1, 277693.0],
|
|
142
|
-
[2, 0, 0, -1, 173237.0],
|
|
143
|
-
[2, 0, -1, 1, 55413.0],
|
|
144
|
-
[2, 0, -1, -1, 46271.0],
|
|
145
|
-
[2, 0, 0, 1, 32573.0],
|
|
146
|
-
[0, 0, 2, 1, 17198.0],
|
|
147
|
-
[2, 0, 1, -1, 9266.0],
|
|
148
|
-
[0, 0, 2, -1, 8822.0],
|
|
149
|
-
[2, -1, 0, -1, 8216.0],
|
|
150
|
-
[2, 0, -2, -1, 4324.0],
|
|
151
|
-
[2, 0, 1, 1, 4200.0],
|
|
152
|
-
[2, 1, 0, -1, -3359.0],
|
|
153
|
-
[2, -1, -1, 1, 2463.0],
|
|
154
|
-
[2, -1, 0, 1, 2211.0],
|
|
155
|
-
[2, -1, -1, -1, 2065.0],
|
|
156
|
-
[0, 1, -1, -1, -1870.0],
|
|
157
|
-
[4, 0, -1, -1, 1828.0],
|
|
158
|
-
[0, 1, 0, 1, -1794.0],
|
|
159
|
-
[0, 0, 0, 3, -1749.0],
|
|
160
|
-
[0, 1, -1, 1, -1565.0],
|
|
161
|
-
[1, 0, 0, 1, -1491.0],
|
|
162
|
-
[0, 1, 1, 1, -1475.0],
|
|
163
|
-
[0, 1, 1, -1, -1410.0],
|
|
164
|
-
[0, 1, 0, -1, -1344.0],
|
|
165
|
-
[1, 0, 0, -1, -1335.0],
|
|
166
|
-
[0, 0, 3, 1, 1107.0],
|
|
167
|
-
[4, 0, 0, -1, 1021.0],
|
|
168
|
-
[4, 0, -1, 1, 833.0],
|
|
169
|
-
[0, 0, 1, -3, 777.0],
|
|
170
|
-
[4, 0, -2, 1, 671.0],
|
|
171
|
-
[2, 0, 0, -3, 607.0],
|
|
172
|
-
[2, 0, 2, -1, 596.0],
|
|
173
|
-
[2, -1, 1, -1, 491.0],
|
|
174
|
-
[2, 0, -2, 1, -451.0],
|
|
175
|
-
[0, 0, 3, -1, 439.0],
|
|
176
|
-
[2, 0, 2, 1, 422.0],
|
|
177
|
-
[2, 0, -3, -1, 421.0],
|
|
178
|
-
[2, 1, -1, 1, -366.0],
|
|
179
|
-
[2, 1, 0, 1, -351.0],
|
|
180
|
-
[4, 0, 0, 1, 331.0],
|
|
181
|
-
[2, -1, 1, 1, 315.0],
|
|
182
|
-
[2, -2, 0, -1, 302.0],
|
|
183
|
-
[0, 0, 1, 3, -283.0],
|
|
184
|
-
[2, 1, 1, -1, -229.0],
|
|
185
|
-
[1, 1, 0, -1, 223.0],
|
|
186
|
-
[1, 1, 0, 1, 223.0],
|
|
187
|
-
[0, 1, -2, -1, -220.0],
|
|
188
|
-
[2, 1, -1, -1, -220.0],
|
|
189
|
-
[1, 0, 1, 1, -185.0],
|
|
190
|
-
[2, -1, -2, -1, 181.0],
|
|
191
|
-
[0, 1, 2, 1, -177.0],
|
|
192
|
-
[4, 0, -2, -1, 176.0],
|
|
193
|
-
[4, -1, -1, -1, 166.0],
|
|
194
|
-
[1, 0, 1, -1, -164.0],
|
|
195
|
-
[4, 0, 1, -1, 132.0],
|
|
196
|
-
[1, 0, -1, -1, -119.0],
|
|
197
|
-
[4, -1, 0, -1, 115.0],
|
|
198
|
-
[2, -2, 0, 1, 107.0]
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
def _periodic_terms(D, M, Mp, F, T):
|
|
202
|
-
"""Calcula sigma_l, sigma_r, sigma_b con tablas completas y factor E."""
|
|
203
|
-
E = 1 - 0.002516 * T - 0.0000074 * T**2
|
|
204
|
-
E2 = E * E
|
|
205
|
-
sigma_l = 0.0
|
|
206
|
-
sigma_r = 0.0
|
|
207
|
-
sigma_b = 0.0
|
|
208
|
-
|
|
209
|
-
for d_coef, m, mp, f, l, r in SIGMA_LR_TABLE:
|
|
210
|
-
factor = 1.0
|
|
211
|
-
if abs(m) == 1:
|
|
212
|
-
factor = E
|
|
213
|
-
elif abs(m) == 2:
|
|
214
|
-
factor = E2
|
|
215
|
-
arg = d_coef * D + m * M + mp * Mp + f * F
|
|
216
|
-
sigma_l += l * math.sin(math.radians(arg)) * factor
|
|
217
|
-
sigma_r += r * math.cos(math.radians(arg)) * factor
|
|
218
|
-
|
|
219
|
-
for d_coef, m, mp, f, b in SIGMA_B_TABLE:
|
|
220
|
-
factor = 1.0
|
|
221
|
-
if abs(m) == 1:
|
|
222
|
-
factor = E
|
|
223
|
-
elif abs(m) == 2:
|
|
224
|
-
factor = E2
|
|
225
|
-
arg = d_coef * D + m * M + mp * Mp + f * F
|
|
226
|
-
sigma_b += b * math.sin(math.radians(arg)) * factor
|
|
227
|
-
|
|
228
|
-
sigma_l /= 1000000.0 # a grados
|
|
229
|
-
sigma_r *= 0.001 # a km
|
|
230
|
-
sigma_b /= 1000000.0 # a grados
|
|
231
|
-
|
|
232
|
-
return sigma_l, sigma_r, sigma_b
|
|
233
|
-
|
|
234
|
-
def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
|
|
235
|
-
"""Calcula longitud eclíptica aparente, latitud, distancia y delta_psi de la Luna."""
|
|
236
|
-
T = (jd - 2451545.0) / 36525.0
|
|
237
|
-
Lp = _normalize_angle(218.3164477 + 481267.88123421 * T - 0.0015786 * T**2 + T**3 / 538841.0 - T**4 / 65194000.0)
|
|
238
|
-
D = _normalize_angle(297.8501921 + 445267.1114034 * T - 0.0018819 * T**2 + T**3 / 545868.0 - T**4 / 113065000.0)
|
|
239
|
-
M = _normalize_angle(357.5291092 + 35999.0502909 * T - 0.0001536 * T**2 + T**3 / 24490000.0)
|
|
240
|
-
Mp = _normalize_angle(134.9633964 + 477198.8675055 * T + 0.0087414 * T**2 + T**3 / 69699.0 - T**4 / 14712000.0)
|
|
241
|
-
F = _normalize_angle(93.2720950 + 483202.0175238 * T - 0.0036539 * T**2 - T**3 / 3526000.0 + T**4 / 863310000.0)
|
|
242
|
-
Omega = _normalize_angle(125.04452 - 1934.136261 * T + 0.0020708 * T**2 + T**3 / 450000.0)
|
|
243
|
-
|
|
244
|
-
sigma_l, sigma_r, sigma_b = _periodic_terms(D, M, Mp, F, T)
|
|
245
|
-
|
|
246
|
-
lambda_ = Lp + sigma_l + 0.003958 * math.sin(math.radians(119.75 + 131.849 * T)) + 0.000319 * math.sin(math.radians(53.09 + 479264.290 * T)) + 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T))
|
|
247
|
-
beta = sigma_b - 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T - 2 * F))
|
|
248
|
-
R = 385000.529 + sigma_r
|
|
249
|
-
delta_psi = (-17.20 * math.sin(math.radians(Omega)) - 1.32 * math.sin(math.radians(2 * (Lp - F))) + 0.23 * math.sin(math.radians(2 * Lp)) + 0.21 * math.sin(math.radians(2 * Omega))) / 3600.0
|
|
250
|
-
lambda_ += delta_psi
|
|
251
|
-
|
|
252
|
-
return lambda_, beta, R, delta_psi
|
|
253
|
-
|
|
254
|
-
def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
|
|
255
|
-
"""Calcula alt, h0 para un tiempo dado t (ra no usado)."""
|
|
256
|
-
jd = julianday(t.date()) + (t.hour + t.minute / 60 + t.second / 3600) / 24
|
|
257
|
-
T = (jd - 2451545.0) / 36525.0
|
|
258
|
-
lambda_, beta, R, delta_psi = _moon_ecliptic_position(jd)
|
|
259
|
-
|
|
260
|
-
par = math.asin(6378.14 / R)
|
|
261
|
-
eps = math.radians(23.439281 - 0.0000004 * T)
|
|
262
|
-
lambda_r = math.radians(lambda_)
|
|
263
|
-
beta_r = math.radians(beta)
|
|
264
|
-
ra = math.atan2(math.sin(lambda_r) * math.cos(eps) - math.tan(beta_r) * math.sin(eps), math.cos(lambda_r))
|
|
265
|
-
dec = math.asin(math.sin(beta_r) * math.cos(eps) + math.cos(beta_r) * math.sin(eps) * math.sin(lambda_r))
|
|
266
|
-
sd = 0.2725076 * par
|
|
267
|
-
ref = math.radians(0.5667)
|
|
268
|
-
h0 = par - sd - ref
|
|
269
|
-
gmst = (280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T**2 - T**3 / 38710000.0) % 360.0
|
|
270
|
-
lst = math.radians((gmst + lon) % 360.0)
|
|
271
|
-
ha = lst - ra
|
|
272
|
-
if ha < -math.pi: ha += 2 * math.pi
|
|
273
|
-
elif ha > math.pi: ha -= 2 * math.pi
|
|
274
|
-
alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
|
|
275
|
-
|
|
276
|
-
return alt, h0
|
|
277
|
-
|
|
278
|
-
def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
|
|
279
|
-
"""
|
|
280
|
-
Cálculo preciso de moonrise y moonset con series completas de Meeus (precisión ~1-2 min).
|
|
281
|
-
Devuelve tuplas UTC (datetime tz-aware) o None si no ocurre en ese día.
|
|
282
|
-
"""
|
|
283
|
-
lat_r = math.radians(lat)
|
|
284
|
-
rise = None
|
|
285
|
-
set_ = None
|
|
286
|
-
dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
|
|
287
|
-
prev_t = None
|
|
288
|
-
prev_alt = None
|
|
289
|
-
|
|
290
|
-
step_grueso = 1.0
|
|
291
|
-
intervalos_rise = []
|
|
292
|
-
intervalos_set = []
|
|
293
|
-
for i in range(int(24 / step_grueso) + 1):
|
|
294
|
-
hour = i * step_grueso
|
|
295
|
-
t = dt + timedelta(hours=hour)
|
|
296
|
-
alt, h0 = _calculate_position_and_alt(t, lat_r, lon)
|
|
297
|
-
if prev_alt is not None:
|
|
298
|
-
den = alt - prev_alt
|
|
299
|
-
if abs(den) < 1e-12:
|
|
300
|
-
continue
|
|
301
|
-
if prev_alt < h0 and alt >= h0:
|
|
302
|
-
intervalos_rise.append((prev_t, t, prev_alt, alt))
|
|
303
|
-
elif prev_alt >= h0 and alt < h0:
|
|
304
|
-
intervalos_set.append((prev_t, t, prev_alt, alt))
|
|
305
|
-
prev_t = t
|
|
306
|
-
prev_alt = alt
|
|
307
|
-
|
|
308
|
-
def refine_event(start_t: datetime, end_t: datetime, start_alt: float, end_alt: float, is_rise: bool) -> datetime:
|
|
309
|
-
for _ in range(10):
|
|
310
|
-
mid_t = start_t + (end_t - start_t) / 2
|
|
311
|
-
mid_alt, mid_h0 = _calculate_position_and_alt(mid_t, lat_r, lon)
|
|
312
|
-
den = mid_alt - start_alt if is_rise else mid_alt - end_alt
|
|
313
|
-
if abs(den) < 1e-12:
|
|
314
|
-
return mid_t
|
|
315
|
-
if (is_rise and mid_alt < mid_h0) or (not is_rise and mid_alt >= mid_h0):
|
|
316
|
-
start_t, start_alt = mid_t, mid_alt
|
|
317
|
-
else:
|
|
318
|
-
end_t, end_alt = mid_t, mid_alt
|
|
319
|
-
return start_t + (end_t - start_t) / 2
|
|
320
|
-
|
|
321
|
-
if intervalos_rise and rise is None:
|
|
322
|
-
start_t, end_t, start_alt, end_alt = intervalos_rise[0]
|
|
323
|
-
rise = refine_event(start_t, end_t, start_alt, end_alt, True)
|
|
324
|
-
if intervalos_set and set_ is None:
|
|
325
|
-
start_t, end_t, start_alt, end_alt = intervalos_set[0]
|
|
326
|
-
set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
|
|
327
|
-
|
|
328
|
-
return rise, set_
|
|
329
|
-
|
|
330
|
-
def moon_distance(date_utc: date) -> float:
|
|
331
|
-
"""
|
|
332
|
-
Calcula la distancia Tierra-Luna en kilómetros para una fecha UTC.
|
|
333
|
-
Args:
|
|
334
|
-
date_utc (date): Fecha en UTC.
|
|
335
|
-
Returns:
|
|
336
|
-
float: Distancia en kilómetros.
|
|
337
|
-
"""
|
|
338
|
-
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
|
|
339
|
-
jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
|
|
340
|
-
_, _, R, _ = _moon_ecliptic_position(jd)
|
|
341
|
-
return R
|
|
342
|
-
|
|
343
|
-
def moon_angular_diameter(date_utc: date) -> float:
|
|
344
|
-
"""
|
|
345
|
-
Calcula el diámetro angular de la Luna en arcosegundos para una fecha UTC.
|
|
346
|
-
Args:
|
|
347
|
-
date_utc (date): Fecha en UTC.
|
|
348
|
-
Returns:
|
|
349
|
-
float: Diámetro angular en arcosegundos.
|
|
350
|
-
"""
|
|
351
|
-
dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
|
|
352
|
-
jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
|
|
353
|
-
_, _, R, _ = _moon_ecliptic_position(jd)
|
|
354
|
-
par = math.asin(6378.14 / R) # Paralaje horizontal
|
|
355
|
-
sd = 0.2725076 * par # Semi-diámetro
|
|
356
|
-
return 2 * math.degrees(sd) * 3600 # Diámetro angular en arcosegundos
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|