solarmoonpy 1.0.1__tar.gz → 1.0.9__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-1.0.1 → solarmoonpy-1.0.9}/PKG-INFO +5 -2
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/README.md +4 -1
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/pyproject.toml +1 -1
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy/location.py +196 -8
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy/moon.py +47 -1
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy/sun.py +192 -1
- solarmoonpy-1.0.9/solarmoonpy/version.py +2 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy.egg-info/PKG-INFO +5 -2
- solarmoonpy-1.0.1/solarmoonpy/version.py +0 -2
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/LICENSE +0 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/setup.cfg +0 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy/__init__.py +0 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy.egg-info/SOURCES.txt +0 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy.egg-info/dependency_links.txt +0 -0
- {solarmoonpy-1.0.1 → solarmoonpy-1.0.9}/solarmoonpy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarmoonpy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Precise solar and lunar calculations for astronomical applications
|
|
5
5
|
Author-email: figorr <jdcuartero@yahoo.es>
|
|
6
6
|
License: Apache License
|
|
@@ -221,6 +221,9 @@ License-File: LICENSE
|
|
|
221
221
|
Dynamic: license-file
|
|
222
222
|
|
|
223
223
|
# ☀️🌙 solarmoonpy
|
|
224
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
225
|
+

|
|
226
|
+
|
|
224
227
|
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
225
228
|
|
|
226
229
|
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
@@ -229,7 +232,7 @@ This project enables you to obtain information such as sunrise, sunset, moon pha
|
|
|
229
232
|
|
|
230
233
|
## 🚀 Features
|
|
231
234
|
|
|
232
|
-
- ☀️ Solar position calculations (sunrise, sunset,
|
|
235
|
+
- ☀️ Solar position calculations (sunrise, sunset, noon, etc.).
|
|
233
236
|
- 🌙 Moon phase and position calculations.
|
|
234
237
|
- Integration with meteorological data from Meteocat.
|
|
235
238
|
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# ☀️🌙 solarmoonpy
|
|
2
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
3
|
+

|
|
4
|
+
|
|
2
5
|
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
3
6
|
|
|
4
7
|
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
@@ -7,7 +10,7 @@ This project enables you to obtain information such as sunrise, sunset, moon pha
|
|
|
7
10
|
|
|
8
11
|
## 🚀 Features
|
|
9
12
|
|
|
10
|
-
- ☀️ Solar position calculations (sunrise, sunset,
|
|
13
|
+
- ☀️ Solar position calculations (sunrise, sunset, noon, etc.).
|
|
11
14
|
- 🌙 Moon phase and position calculations.
|
|
12
15
|
- Integration with meteorological data from Meteocat.
|
|
13
16
|
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solarmoonpy"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.9"
|
|
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" }]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from datetime import date, datetime, timezone
|
|
3
3
|
from zoneinfo import ZoneInfo
|
|
4
|
-
from typing import Optional, Union
|
|
4
|
+
from typing import Optional, Union, Literal, Dict
|
|
5
5
|
|
|
6
|
-
from .sun import sunrise_sunset, noon # Importar funciones de sun.py
|
|
6
|
+
from .sun import sunrise_sunset, noon, dawn, dusk, midnight, sun_position # Importar funciones de sun.py
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
9
|
class LocationInfo:
|
|
@@ -253,20 +253,117 @@ class Location:
|
|
|
253
253
|
date = datetime.now(self.tzinfo if local else timezone.utc).date()
|
|
254
254
|
|
|
255
255
|
return noon(
|
|
256
|
-
latitude=self.latitude,
|
|
257
256
|
longitude=self.longitude,
|
|
258
257
|
date=date,
|
|
259
258
|
timezone=self.tzinfo if local else timezone.utc
|
|
260
259
|
)
|
|
260
|
+
|
|
261
|
+
def dawn(
|
|
262
|
+
self,
|
|
263
|
+
date: Optional[date] = None,
|
|
264
|
+
local: bool = True,
|
|
265
|
+
twilight_type: Literal["civil", "nautical", "astronomical"] = "civil",
|
|
266
|
+
elevation: Optional[float] = None
|
|
267
|
+
) -> Optional[datetime]:
|
|
268
|
+
"""🌅 Calcula amanecer (inicio crepúsculo)."""
|
|
269
|
+
if local and self.timezone is None:
|
|
270
|
+
raise ValueError("Se solicitó hora local pero no se definió una zona horaria.")
|
|
261
271
|
|
|
262
|
-
|
|
272
|
+
date = date or datetime.now(self.tzinfo if local else timezone.utc).date()
|
|
273
|
+
elevation = elevation if elevation is not None else self.elevation
|
|
274
|
+
|
|
275
|
+
return dawn(
|
|
276
|
+
latitude=self.latitude,
|
|
277
|
+
longitude=self.longitude,
|
|
278
|
+
date=date,
|
|
279
|
+
twilight_type=twilight_type,
|
|
280
|
+
elevation=elevation,
|
|
281
|
+
timezone=self.tzinfo if local else timezone.utc,
|
|
282
|
+
with_refraction=True
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def dusk(
|
|
263
286
|
self,
|
|
264
287
|
date: Optional[date] = None,
|
|
265
288
|
local: bool = True,
|
|
289
|
+
twilight_type: Literal["civil", "nautical", "astronomical"] = "civil",
|
|
290
|
+
elevation: Optional[float] = None
|
|
291
|
+
) -> Optional[datetime]:
|
|
292
|
+
"""🌙 Calcula anochecer (fin crepúsculo)."""
|
|
293
|
+
if local and self.timezone is None:
|
|
294
|
+
raise ValueError("Se solicitó hora local pero no se definió una zona horaria.")
|
|
295
|
+
|
|
296
|
+
date = date or datetime.now(self.tzinfo if local else timezone.utc).date()
|
|
297
|
+
elevation = elevation if elevation is not None else self.elevation
|
|
298
|
+
|
|
299
|
+
return dusk(
|
|
300
|
+
latitude=self.latitude,
|
|
301
|
+
longitude=self.longitude,
|
|
302
|
+
date=date,
|
|
303
|
+
twilight_type=twilight_type,
|
|
304
|
+
elevation=elevation,
|
|
305
|
+
timezone=self.tzinfo if local else timezone.utc,
|
|
306
|
+
with_refraction=True
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def midnight(
|
|
310
|
+
self,
|
|
311
|
+
date: Optional[date] = None,
|
|
312
|
+
local: bool = True
|
|
313
|
+
) -> datetime:
|
|
314
|
+
"""🌑 Calcula medianoche solar."""
|
|
315
|
+
if local and self.timezone is None:
|
|
316
|
+
raise ValueError("Se solicitó hora local pero no se definió una zona horaria.")
|
|
317
|
+
|
|
318
|
+
date = date or datetime.now(self.tzinfo if local else timezone.utc).date()
|
|
319
|
+
|
|
320
|
+
return midnight(
|
|
321
|
+
longitude=self.longitude,
|
|
322
|
+
date=date,
|
|
323
|
+
timezone=self.tzinfo if local else timezone.utc
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def sun_position(
|
|
327
|
+
self,
|
|
328
|
+
dt: Optional[datetime] = None,
|
|
329
|
+
local: bool = True,
|
|
266
330
|
elevation: Optional[float] = None
|
|
267
331
|
) -> dict:
|
|
268
332
|
"""
|
|
269
|
-
|
|
333
|
+
Calcula la posición actual del Sol.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
dt: Momento exacto. Si None, usa ahora.
|
|
337
|
+
local: True para usar zona horaria local.
|
|
338
|
+
elevation: Elevación del observador.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
dict: elevation, azimuth, above_horizon, rising
|
|
342
|
+
"""
|
|
343
|
+
if local and self.timezone is None:
|
|
344
|
+
raise ValueError("Se solicitó hora local pero no se definió una zona horaria.")
|
|
345
|
+
|
|
346
|
+
tz = self.tzinfo if local else timezone.utc
|
|
347
|
+
dt = dt or datetime.now(tz)
|
|
348
|
+
elevation = elevation if elevation is not None else self.elevation
|
|
349
|
+
|
|
350
|
+
return sun_position(
|
|
351
|
+
latitude=self.latitude,
|
|
352
|
+
longitude=self.longitude,
|
|
353
|
+
dt=dt,
|
|
354
|
+
elevation=elevation,
|
|
355
|
+
timezone=tz,
|
|
356
|
+
with_refraction=True
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def sun_events(
|
|
360
|
+
self,
|
|
361
|
+
date: Optional[date] = None,
|
|
362
|
+
local: bool = True,
|
|
363
|
+
elevation: Optional[float] = None,
|
|
364
|
+
) -> Dict:
|
|
365
|
+
"""
|
|
366
|
+
Devuelve un diccionario con los eventos solares para una fecha dada, incluyendo todos los tipos de crepúsculo.
|
|
270
367
|
|
|
271
368
|
Args:
|
|
272
369
|
date: Fecha para la cual calcular los eventos solares. Si es None, usa la fecha actual.
|
|
@@ -275,16 +372,25 @@ class Location:
|
|
|
275
372
|
|
|
276
373
|
Returns:
|
|
277
374
|
dict: {
|
|
375
|
+
"dawn_civil": datetime | None,
|
|
376
|
+
"dawn_nautical": datetime | None,
|
|
377
|
+
"dawn_astronomical": datetime | None,
|
|
278
378
|
"sunrise": datetime,
|
|
279
379
|
"noon": datetime,
|
|
280
|
-
"sunset": datetime
|
|
380
|
+
"sunset": datetime,
|
|
381
|
+
"dusk_civil": datetime | None,
|
|
382
|
+
"dusk_nautical": datetime | None,
|
|
383
|
+
"dusk_astronomical": datetime | None,
|
|
384
|
+
"midnight": datetime,
|
|
385
|
+
"daylight_duration": float | None
|
|
281
386
|
}
|
|
282
387
|
"""
|
|
283
388
|
if local and self.timezone is None:
|
|
284
389
|
raise ValueError("Se solicitó hora local pero no se definió una zona horaria.")
|
|
285
390
|
|
|
286
391
|
tz = self.tzinfo if local else timezone.utc
|
|
287
|
-
|
|
392
|
+
now = datetime.now(tz)
|
|
393
|
+
date = date or now.date()
|
|
288
394
|
elevation = elevation if elevation is not None else self.elevation
|
|
289
395
|
|
|
290
396
|
# Calcular amanecer y atardecer
|
|
@@ -299,14 +405,96 @@ class Location:
|
|
|
299
405
|
|
|
300
406
|
# Calcular mediodía solar
|
|
301
407
|
noon_time = noon(
|
|
408
|
+
longitude=self.longitude,
|
|
409
|
+
date=date,
|
|
410
|
+
timezone=tz,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Calcular medianoche solar
|
|
414
|
+
midnight_time = midnight(
|
|
415
|
+
longitude=self.longitude,
|
|
416
|
+
date=date,
|
|
417
|
+
timezone=tz,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Calcular duración del día (en horas)
|
|
421
|
+
daylight_duration = None
|
|
422
|
+
if sunrise and sunset:
|
|
423
|
+
daylight_duration = (sunset - sunrise).total_seconds() / 3600
|
|
424
|
+
|
|
425
|
+
# Calcular dawn y dusk para cada tipo de crepúsculo
|
|
426
|
+
dawn_civil = dawn(
|
|
427
|
+
latitude=self.latitude,
|
|
428
|
+
longitude=self.longitude,
|
|
429
|
+
date=date,
|
|
430
|
+
twilight_type="civil",
|
|
431
|
+
elevation=elevation,
|
|
432
|
+
timezone=tz,
|
|
433
|
+
with_refraction=True,
|
|
434
|
+
)
|
|
435
|
+
dusk_civil = dusk(
|
|
302
436
|
latitude=self.latitude,
|
|
303
437
|
longitude=self.longitude,
|
|
304
438
|
date=date,
|
|
439
|
+
twilight_type="civil",
|
|
440
|
+
elevation=elevation,
|
|
305
441
|
timezone=tz,
|
|
442
|
+
with_refraction=True,
|
|
306
443
|
)
|
|
444
|
+
dawn_nautical = dawn(
|
|
445
|
+
latitude=self.latitude,
|
|
446
|
+
longitude=self.longitude,
|
|
447
|
+
date=date,
|
|
448
|
+
twilight_type="nautical",
|
|
449
|
+
elevation=elevation,
|
|
450
|
+
timezone=tz,
|
|
451
|
+
with_refraction=True,
|
|
452
|
+
)
|
|
453
|
+
dusk_nautical = dusk(
|
|
454
|
+
latitude=self.latitude,
|
|
455
|
+
longitude=self.longitude,
|
|
456
|
+
date=date,
|
|
457
|
+
twilight_type="nautical",
|
|
458
|
+
elevation=elevation,
|
|
459
|
+
timezone=tz,
|
|
460
|
+
with_refraction=True,
|
|
461
|
+
)
|
|
462
|
+
dawn_astronomical = dawn(
|
|
463
|
+
latitude=self.latitude,
|
|
464
|
+
longitude=self.longitude,
|
|
465
|
+
date=date,
|
|
466
|
+
twilight_type="astronomical",
|
|
467
|
+
elevation=elevation,
|
|
468
|
+
timezone=tz,
|
|
469
|
+
with_refraction=True,
|
|
470
|
+
)
|
|
471
|
+
dusk_astronomical = dusk(
|
|
472
|
+
latitude=self.latitude,
|
|
473
|
+
longitude=self.longitude,
|
|
474
|
+
date=date,
|
|
475
|
+
twilight_type="astronomical",
|
|
476
|
+
elevation=elevation,
|
|
477
|
+
timezone=tz,
|
|
478
|
+
with_refraction=True,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Posición actual del Sol
|
|
482
|
+
pos = self.sun_position(dt=now, local=local, elevation=elevation)
|
|
307
483
|
|
|
308
484
|
return {
|
|
485
|
+
"dawn_civil": dawn_civil,
|
|
486
|
+
"dawn_nautical": dawn_nautical,
|
|
487
|
+
"dawn_astronomical": dawn_astronomical,
|
|
309
488
|
"sunrise": sunrise,
|
|
310
489
|
"noon": noon_time,
|
|
311
490
|
"sunset": sunset,
|
|
312
|
-
|
|
491
|
+
"dusk_civil": dusk_civil,
|
|
492
|
+
"dusk_nautical": dusk_nautical,
|
|
493
|
+
"dusk_astronomical": dusk_astronomical,
|
|
494
|
+
"midnight": midnight_time,
|
|
495
|
+
"daylight_duration": daylight_duration,
|
|
496
|
+
"sun_elevation": pos["elevation"],
|
|
497
|
+
"sun_azimuth": pos["azimuth"],
|
|
498
|
+
"sun_horizon_position": pos["horizon_position"],
|
|
499
|
+
"sun_rising": pos["rising"],
|
|
500
|
+
}
|
|
@@ -374,4 +374,50 @@ def moon_elongation(date_utc: date) -> float:
|
|
|
374
374
|
jc = julianday_to_juliancentury(jd)
|
|
375
375
|
lambda_m, _, _, _ = _moon_ecliptic_position(jd)
|
|
376
376
|
lambda_s = sun_apparent_long(jc)
|
|
377
|
-
return _normalize_angle(lambda_m - lambda_s)
|
|
377
|
+
return _normalize_angle(lambda_m - lambda_s)
|
|
378
|
+
|
|
379
|
+
def get_moon_phase_name(d: date) -> str:
|
|
380
|
+
"""Determina el nombre de la fase lunar para la fecha dada."""
|
|
381
|
+
percentage = illuminated_percentage(d)
|
|
382
|
+
elongation = moon_elongation(d)
|
|
383
|
+
|
|
384
|
+
# Determinar nombre intermedio basado en elongación y porcentaje iluminado
|
|
385
|
+
is_waxing = elongation < 180.0 # <180° creciente, >=180° menguante
|
|
386
|
+
if percentage < 50.0:
|
|
387
|
+
phase_name = "waxing_crescent" if is_waxing else "waning_crescent"
|
|
388
|
+
else:
|
|
389
|
+
phase_name = "waxing_gibbous" if is_waxing else "waning_gibbous"
|
|
390
|
+
|
|
391
|
+
# Verificar fases primarias exactas y override si corresponde
|
|
392
|
+
last_new = find_last_phase_exact(d, 0.0)
|
|
393
|
+
if last_new and last_new.date() == d:
|
|
394
|
+
return "new_moon"
|
|
395
|
+
|
|
396
|
+
last_first = find_last_phase_exact(d, 90.0)
|
|
397
|
+
if last_first and last_first.date() == d:
|
|
398
|
+
return "first_quarter"
|
|
399
|
+
|
|
400
|
+
last_full = find_last_phase_exact(d, 180.0)
|
|
401
|
+
if last_full and last_full.date() == d:
|
|
402
|
+
return "full_moon"
|
|
403
|
+
|
|
404
|
+
last_last = find_last_phase_exact(d, 270.0)
|
|
405
|
+
if last_last and last_last.date() == d:
|
|
406
|
+
return "last_quarter"
|
|
407
|
+
|
|
408
|
+
return phase_name
|
|
409
|
+
|
|
410
|
+
def get_lunation_duration(date_utc: date) -> str:
|
|
411
|
+
"""Calcula la duración exacta de la lunación actual (desde la última luna nueva hasta la siguiente)."""
|
|
412
|
+
last_new = find_last_phase_exact(date_utc, 0.0)
|
|
413
|
+
# Buscar la siguiente luna nueva: empezar ~25 días después (buffer seguro)
|
|
414
|
+
search_date = (last_new + timedelta(days=25)).date()
|
|
415
|
+
next_new = find_last_phase_exact(search_date + timedelta(days=10), 0.0) # Buffer para asegurar el cruce
|
|
416
|
+
if next_new <= last_new:
|
|
417
|
+
next_new = find_last_phase_exact(search_date + timedelta(days=35), 0.0) # Seguridad extra si falla
|
|
418
|
+
duration = next_new - last_new
|
|
419
|
+
days = duration.days
|
|
420
|
+
secs = duration.seconds
|
|
421
|
+
hours = secs // 3600
|
|
422
|
+
minutes = (secs % 3600) // 60
|
|
423
|
+
return f"{days}d {hours}h {minutes}m"
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from math import acos, asin, atan2, cos, degrees, radians, sin, tan, sqrt
|
|
3
|
+
from typing import Optional, Tuple, Literal
|
|
4
|
+
|
|
5
|
+
# Constantes estándar para diferentes eventos solares
|
|
6
|
+
SUN_APPARENT_RADIUS = 959.63 / 3600.0 # ≈ 0.2666° (radio aparente del Sol)
|
|
7
|
+
|
|
8
|
+
# Zenith angles estándar (90° = horizonte)
|
|
9
|
+
ZENITH_SUNRISE_SUNSET = 90.0 + SUN_APPARENT_RADIUS
|
|
10
|
+
ZENITH_CIVIL_DAWN_DUSK = 96.0
|
|
11
|
+
ZENITH_NAUTICAL_DAWN_DUSK = 102.0
|
|
12
|
+
ZENITH_ASTRONOMICAL_DAWN_DUSK = 108.0
|
|
3
13
|
|
|
4
14
|
def julianday(date_or_dt: datetime.date | datetime.datetime) -> float:
|
|
5
15
|
"""
|
|
@@ -239,4 +249,185 @@ def noon(
|
|
|
239
249
|
time_utc = (720.0 - (4 * longitude) - eqtime) # minutos
|
|
240
250
|
noon_utc = datetime.datetime(date.year, date.month, date.day, tzinfo=datetime.timezone.utc) \
|
|
241
251
|
+ datetime.timedelta(minutes=time_utc)
|
|
242
|
-
return noon_utc.astimezone(timezone)
|
|
252
|
+
return noon_utc.astimezone(timezone)
|
|
253
|
+
|
|
254
|
+
def dawn(
|
|
255
|
+
latitude: float, longitude: float, date: datetime.date,
|
|
256
|
+
twilight_type: Literal["civil", "nautical", "astronomical"] = "civil",
|
|
257
|
+
elevation: float = 0.0,
|
|
258
|
+
timezone: datetime.timezone = datetime.timezone.utc,
|
|
259
|
+
with_refraction: bool = True
|
|
260
|
+
) -> Optional[datetime.datetime]:
|
|
261
|
+
"""🌅 Calcula amanecer (inicio crepúsculo)."""
|
|
262
|
+
zeniths = {"civil": 96.0, "nautical": 102.0, "astronomical": 108.0}
|
|
263
|
+
zenith = zeniths[twilight_type]
|
|
264
|
+
|
|
265
|
+
# Reutiliza la misma lógica que sunrise_sunset pero para un zenith específico
|
|
266
|
+
adjustment_for_elevation = adjust_to_horizon(elevation)
|
|
267
|
+
adjusted_zenith = zenith + adjustment_for_elevation
|
|
268
|
+
if with_refraction:
|
|
269
|
+
adjusted_zenith += refraction_at_zenith(adjusted_zenith)
|
|
270
|
+
|
|
271
|
+
latitude = max(min(latitude, 89.8), -89.8)
|
|
272
|
+
jd = julianday(date)
|
|
273
|
+
jc = julianday_to_juliancentury(jd)
|
|
274
|
+
declination = sun_declination(jc)
|
|
275
|
+
eqtime = eq_of_time(jc)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
ha_rising = hour_angle(latitude, declination, adjusted_zenith, "rising")
|
|
279
|
+
except ValueError:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
delta = -longitude - degrees(ha_rising)
|
|
283
|
+
time_utc_minutes = 720.0 + (delta * 4.0) - eqtime
|
|
284
|
+
if time_utc_minutes < -720.0:
|
|
285
|
+
time_utc_minutes += 1440
|
|
286
|
+
|
|
287
|
+
base_dt = datetime.datetime(date.year, date.month, date.day, tzinfo=datetime.timezone.utc)
|
|
288
|
+
result_utc = base_dt + datetime.timedelta(minutes=time_utc_minutes)
|
|
289
|
+
return result_utc.astimezone(timezone)
|
|
290
|
+
|
|
291
|
+
def dusk(
|
|
292
|
+
latitude: float, longitude: float, date: datetime.date,
|
|
293
|
+
twilight_type: Literal["civil", "nautical", "astronomical"] = "civil",
|
|
294
|
+
elevation: float = 0.0,
|
|
295
|
+
timezone: datetime.timezone = datetime.timezone.utc,
|
|
296
|
+
with_refraction: bool = True
|
|
297
|
+
) -> Optional[datetime.datetime]:
|
|
298
|
+
"""🌙 Calcula anochecer (fin crepúsculo)."""
|
|
299
|
+
zeniths = {"civil": 96.0, "nautical": 102.0, "astronomical": 108.0}
|
|
300
|
+
zenith = zeniths[twilight_type]
|
|
301
|
+
|
|
302
|
+
adjustment_for_elevation = adjust_to_horizon(elevation)
|
|
303
|
+
adjusted_zenith = zenith + adjustment_for_elevation
|
|
304
|
+
if with_refraction:
|
|
305
|
+
adjusted_zenith += refraction_at_zenith(adjusted_zenith)
|
|
306
|
+
|
|
307
|
+
latitude = max(min(latitude, 89.8), -89.8)
|
|
308
|
+
jd = julianday(date)
|
|
309
|
+
jc = julianday_to_juliancentury(jd)
|
|
310
|
+
declination = sun_declination(jc)
|
|
311
|
+
eqtime = eq_of_time(jc)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
ha_setting = hour_angle(latitude, declination, adjusted_zenith, "setting")
|
|
315
|
+
except ValueError:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
delta = -longitude - degrees(ha_setting)
|
|
319
|
+
time_utc_minutes = 720.0 + (delta * 4.0) - eqtime
|
|
320
|
+
if time_utc_minutes < -720.0:
|
|
321
|
+
time_utc_minutes += 1440
|
|
322
|
+
|
|
323
|
+
base_dt = datetime.datetime(date.year, date.month, date.day, tzinfo=datetime.timezone.utc)
|
|
324
|
+
result_utc = base_dt + datetime.timedelta(minutes=time_utc_minutes)
|
|
325
|
+
return result_utc.astimezone(timezone)
|
|
326
|
+
|
|
327
|
+
def midnight(
|
|
328
|
+
longitude: float,
|
|
329
|
+
date: datetime.date,
|
|
330
|
+
timezone: datetime.timezone = datetime.timezone.utc
|
|
331
|
+
) -> datetime.datetime:
|
|
332
|
+
"""🌑 Medianoche solar (12h después del mediodía solar)."""
|
|
333
|
+
noon_time = noon(longitude, date, timezone)
|
|
334
|
+
return noon_time + datetime.timedelta(hours=12)
|
|
335
|
+
|
|
336
|
+
def sun_position(
|
|
337
|
+
latitude: float,
|
|
338
|
+
longitude: float,
|
|
339
|
+
dt: datetime.datetime,
|
|
340
|
+
elevation: float = 0.0,
|
|
341
|
+
timezone: datetime.timezone = datetime.timezone.utc,
|
|
342
|
+
with_refraction: bool = True
|
|
343
|
+
) -> dict:
|
|
344
|
+
"""
|
|
345
|
+
Calcula la posición del Sol con alta precisión.
|
|
346
|
+
"""
|
|
347
|
+
if dt.tzinfo is None:
|
|
348
|
+
dt = dt.replace(tzinfo=timezone)
|
|
349
|
+
dt_utc = dt.astimezone(datetime.timezone.utc)
|
|
350
|
+
|
|
351
|
+
jd = julianday(dt_utc)
|
|
352
|
+
jc = julianday_to_juliancentury(jd)
|
|
353
|
+
declination = sun_declination(jc)
|
|
354
|
+
eqtime = eq_of_time(jc)
|
|
355
|
+
|
|
356
|
+
# Tiempo solar verdadero en minutos desde medianoche
|
|
357
|
+
true_solar_time = (
|
|
358
|
+
dt_utc.hour * 60 + dt_utc.minute + dt_utc.second / 60.0
|
|
359
|
+
+ eqtime + 4.0 * longitude
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Ángulo horario
|
|
363
|
+
hour_angle = (true_solar_time / 4.0) - 180.0
|
|
364
|
+
if hour_angle < -180:
|
|
365
|
+
hour_angle += 360
|
|
366
|
+
elif hour_angle > 180:
|
|
367
|
+
hour_angle -= 360
|
|
368
|
+
|
|
369
|
+
# Convertir a radianes
|
|
370
|
+
ha_rad = radians(hour_angle)
|
|
371
|
+
lat_rad = radians(latitude)
|
|
372
|
+
dec_rad = radians(declination)
|
|
373
|
+
|
|
374
|
+
# Elevación geométrica
|
|
375
|
+
sin_elev = sin(lat_rad) * sin(dec_rad) + cos(lat_rad) * cos(dec_rad) * cos(ha_rad)
|
|
376
|
+
elevation_geom = degrees(asin(max(min(sin_elev, 1.0), -1.0)))
|
|
377
|
+
|
|
378
|
+
# Corrección por altura del observador
|
|
379
|
+
elevation_adj = elevation_geom + adjust_to_horizon(elevation)
|
|
380
|
+
|
|
381
|
+
# Refracción atmosférica
|
|
382
|
+
if with_refraction and -10 <= elevation_adj <= 90:
|
|
383
|
+
refraction = refraction_at_zenith(90.0 - elevation_adj)
|
|
384
|
+
elevation_final = elevation_adj + refraction
|
|
385
|
+
else:
|
|
386
|
+
elevation_final = elevation_adj
|
|
387
|
+
|
|
388
|
+
# Azimut (0° = Norte)
|
|
389
|
+
sin_elev_final = sin(radians(elevation_final))
|
|
390
|
+
cos_elev_final = cos(radians(elevation_final))
|
|
391
|
+
cos_lat = cos(lat_rad)
|
|
392
|
+
sin_lat = sin(lat_rad)
|
|
393
|
+
sin_dec = sin(dec_rad)
|
|
394
|
+
|
|
395
|
+
if abs(cos_elev_final * cos_lat) < 1e-12:
|
|
396
|
+
azimuth = 180.0 if ha_rad < 0 else 0.0
|
|
397
|
+
else:
|
|
398
|
+
cos_az = (sin_dec - sin_elev_final * sin_lat) / (cos_elev_final * cos_lat)
|
|
399
|
+
cos_az = max(min(cos_az, 1.0), -1.0)
|
|
400
|
+
azimuth = degrees(acos(cos_az))
|
|
401
|
+
if hour_angle > 0: # PM
|
|
402
|
+
azimuth = 360.0 - azimuth
|
|
403
|
+
|
|
404
|
+
horizon_position: Literal["above_horizon", "below_horizon"] = (
|
|
405
|
+
"above_horizon" if elevation_final > 0 else "below_horizon"
|
|
406
|
+
)
|
|
407
|
+
rising = elevation_final > 0 and hour_angle < 0
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
"elevation": round(elevation_final, 2),
|
|
411
|
+
"azimuth": round(azimuth, 2),
|
|
412
|
+
"horizon_position": horizon_position,
|
|
413
|
+
"rising": rising
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# 🎁 FUNCIÓN BONUS: Todo junto (opcional)
|
|
417
|
+
def all_sun_events(
|
|
418
|
+
latitude: float, longitude: float, date: datetime.date,
|
|
419
|
+
twilight_type: Literal["civil", "nautical", "astronomical"] = "civil",
|
|
420
|
+
elevation: float = 0.0,
|
|
421
|
+
timezone: datetime.timezone = datetime.timezone.utc,
|
|
422
|
+
with_refraction: bool = True
|
|
423
|
+
) -> dict:
|
|
424
|
+
"""🔥 Calcula TODOS los eventos solares del día."""
|
|
425
|
+
sr, ss = sunrise_sunset(latitude, longitude, date, elevation, timezone, with_refraction)
|
|
426
|
+
return {
|
|
427
|
+
"dawn": dawn(latitude, longitude, date, twilight_type, elevation, timezone, with_refraction),
|
|
428
|
+
"sunrise": sr,
|
|
429
|
+
"noon": noon(longitude, date, timezone),
|
|
430
|
+
"sunset": ss,
|
|
431
|
+
"dusk": dusk(latitude, longitude, date, twilight_type, elevation, timezone, with_refraction),
|
|
432
|
+
"midnight": midnight(longitude, date, timezone)
|
|
433
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarmoonpy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Precise solar and lunar calculations for astronomical applications
|
|
5
5
|
Author-email: figorr <jdcuartero@yahoo.es>
|
|
6
6
|
License: Apache License
|
|
@@ -221,6 +221,9 @@ License-File: LICENSE
|
|
|
221
221
|
Dynamic: license-file
|
|
222
222
|
|
|
223
223
|
# ☀️🌙 solarmoonpy
|
|
224
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
225
|
+

|
|
226
|
+
|
|
224
227
|
Solar and moon calculations for Meteocat Home Assistant integration.
|
|
225
228
|
|
|
226
229
|
**solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
|
|
@@ -229,7 +232,7 @@ This project enables you to obtain information such as sunrise, sunset, moon pha
|
|
|
229
232
|
|
|
230
233
|
## 🚀 Features
|
|
231
234
|
|
|
232
|
-
- ☀️ Solar position calculations (sunrise, sunset,
|
|
235
|
+
- ☀️ Solar position calculations (sunrise, sunset, noon, etc.).
|
|
233
236
|
- 🌙 Moon phase and position calculations.
|
|
234
237
|
- Integration with meteorological data from Meteocat.
|
|
235
238
|
- Compatible with Home Assistant for automations based on solar and lunar events.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|