From b3f27dfae07a93760017d3e617990dd90784bf85 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 6 Dec 2023 11:49:04 +0000 Subject: [PATCH] astronomy: Update docs and code. --- astronomy/README.md | 61 +++++++++++++++++++++++++++++++++---------- astronomy/sun_moon.py | 40 +++++++++++++++++++++------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/astronomy/README.md b/astronomy/README.md index df7799d..ff42ca8 100644 --- a/astronomy/README.md +++ b/astronomy/README.md @@ -7,6 +7,7 @@ 2. [The RiSet class](./README.md#2-the-riset-class) 2.1 [Constructor](./README.md#21-constructor) 2.2 [Methods](./README.md#22-methods) + 2.3 [Effect of local time](./README.md#23-effect-of-local-time) 3. [The moonphase function](./README.md#3-the-moonphase-function) 4. [Utility functions](./README.md#4-utility-functions) 5. [Demo script](./README.md#5-demo-script) @@ -94,7 +95,8 @@ rise or set event. Args (float): * `lat=LAT` Latitude in degrees (-ve is South). Defaults are my location. :) * `long=LONG` Longitude in degrees (-ve is West). -* `lto=0` Local time offset in hours to UTC (-ve is West). +* `lto=0` Local time offset in hours to UTC (-ve is West); the value is checked +to ensure `-12 < lto < 12`. See [section 2.3](./README.md#23-effect-of-local-time). ## 2.2 Methods @@ -114,6 +116,7 @@ moon, 0.5 is full. See [section 3](./README.md#3-the-moonphase-function) for observations about this. * `set_lto(t)` Set localtime offset in hours relative to UTC. Primarily intended for daylight saving time. Rise and set times are updated if the lto is changed. +The value is checked to ensure `-12 < lto < 12`. See [section 2.3](./README.md#23-effect-of-local-time). The return value of the rise and set method is determined by the `variant` arg. In all cases rise and set events are identified which occur in the current 24 @@ -132,6 +135,27 @@ r = RiSet() # UK near Manchester r = RiSet(lat=47.609722, long=-122.3306, lto=-8) # Seattle 47°36′35″N 122°19′59″W r = RiSet(lat=-33.87667, long=151.21, lto=11) # Sydney 33°52′04″S 151°12′36″E ``` +## 2.3 Effect of local time + +MicroPython has no concept of local time. A hardware platform has a clock; +depending on application this might be permanently set to local winter time, or +it might be adjusted twice per year for local daylight saving time. It is the +responsibility of the application to do this if it is considered necessary. + +Rise and set times are computed relative to UTC and then adjusted using the +`RiSet` instance's local time offset before being returned (see `.adjust()`). +This applies to all variants - note that the local platform epoch is on a fixed +date at 00:00:00 local time. + +If the machine clock has a fixed relationship to UTC, `RiSet` instances should +have a corresponding fixed local time offset: rise and set times will be +relative to that time. If the application implements daylight saving time, the +local time offsets should be adjusted accordingly. + +The constructor and the `set_day()` method set the instance's date relative to +the machine clock. They use only the date component of system time, hence they +may be run at any time of day. In continuously-running applications, `set_day()` +may be run each day just after midnight to keep a `RiSet` instance up to date. # 3. The moonphase function @@ -203,22 +227,31 @@ Code comments show times retrieved from `timeanddate.com`. # 6. Scheduling events A likely use case is to enable events to be timed relative to sunrise and set. -In simple cases this can be done with `asyncio`. This routine, called before -sunrise, will perform some action at dawn and quit: +In simple cases this can be done with `asyncio`. This coroutine will execute a +payload at sunrise every day. A similar coroutine might handle sunsets. ```python -from sched.sun_moon import RiSet +import uasyncio as asyncio import time -rs = RiSet() -async def wait_for_sunrise(): - tsecs = time.time() % 86400 # Time now in secs since midnight - rs.set_day() # Ensure today's date - twait = rs.sunrise() - tsecs # Time before Sunrise - if twait > 0: # Sunrise has not yet occurred - await asyncio.sleep(twait) - # Turn the lights off, or whatever +from sched.sun_moon import RiSet + +async def tomorrow(): # Wait until 1 minute past midnight + now = round(time.time()) + tw = 86400 + 60 - (now % 86400) # Time from now to one minute past next midnight + await asyncio.sleep(tw) + +async def do_sunrise(): + rs = RiSet() # May need args + while True: + if (now := round(time.time())) < rs.sunrise(1): # Sun has not yet risen + await asyncio.sleep(rs.sunrise(1) - now) # Wait for it to rise + # Sun has risen, execute payload + await tomorrow() + rs.set_day() # Update to new day ``` -The problem with the above is ensuring that `wait_for_sunrise` is called shortly -after midnight. A simple solution is to use the +This code assumes that `.sunrise()` will never return `None`. At polar latitudes +waiting for sunrise in winter would require changes. + +Code may be simplified by using the [schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md). This may be installed with ```bash diff --git a/astronomy/sun_moon.py b/astronomy/sun_moon.py index 6c2cba4..c23c361 100644 --- a/astronomy/sun_moon.py +++ b/astronomy/sun_moon.py @@ -23,6 +23,10 @@ LONG = -2.102811634540558 # (date(2000, 1, 1) - date(1970, 1, 1)).days = 10957 # Return time now in days relative to platform epoch. +# System time is set to local time, and MP has no concept of this. Hence +# time.time() returns secs since epoch 00:00:00 local time. If lto is local time +# offset to UTC, provided -12 < lto < 12, the effect of rounding ensures the +# right number of days for platform epoch at UTC. def now_days() -> int: secs_per_day = 86400 # 24 * 3600 t = time.time() @@ -77,7 +81,7 @@ def quad(ym, yz, yp): # hence the MJD of an integer day number will always be an integer # Re platform comparisons get_mjd returns the same value regardless of -# the platform's epoch: integer days since 00:00 on 17 November 1858. +# the platform's epoch: integer days since 00:00 UTC on 17 November 1858. def get_mjd(ndays: int = 0) -> int: secs_per_day = 86400 # 24 * 3600 days_from_epoch = now_days() + ndays # Days since platform epoch @@ -198,6 +202,8 @@ class RiSet: self.sglat = sin(radians(lat)) self.cglat = cos(radians(lat)) self.long = long + if not -12 < lto < 12: + raise ValueError("Invalid local time offset.") self.lto = round(lto * 3600) # Localtime offset in secs self.mjd = None # Current integer MJD # Times in integer secs from midnight on current day (in local time) @@ -237,23 +243,37 @@ class RiSet: def moonphase(self) -> float: return self._phase - # May need to temporarily adjust self.mjd - # def is_up(self, sun=True): - # t = ((time.time() % 86400) + self.lto) / 3600 - # return sin_alt(t, sun) > 0 - def set_lto(self, t): # Update the offset from UTC + if not -12 < t < 12: + raise ValueError("Invalid local time offset.") lto = round(t * 3600) # Localtime offset in secs if self.lto != lto: # changed self.lto = lto self.update(self.mjd) def is_up(self, sun: bool): # Return current state of sun or moon - t = time.time() + self.lto # UTC - t %= 86400 - t /= 3600 # UTC Hour of day self.set_day() # Ensure today's date - return self.sin_alt(t, sun) > 0 + now = round(time.time()) + self.lto # UTC + rt = self.sunrise(1) if sun else self.moonrise(1) + st = self.sunset(1) if sun else self.moonset(1) + if rt is None: + if st is None: + t = (now % 86400) / 3600 # Time as UTC hours (float) + return self.sin_alt(t, sun) > 0 + return st > now + if st is None: + return rt < now + print(rt, now, st) + return rt < now < st + + # This is in error by ~12 minutes: sin_alt() produces incorrect values + # unless t corresponds to an exact hour. Odd. + # def is_up(self, sun: bool): + # t = time.time() + self.lto # UTC + # t %= 86400 + # t /= 3600 # UTC Hour of day + # self.set_day() # Ensure today's date + # return self.sin_alt(t, sun) > 0 # ***** API end ***** # Re-calculate rise and set times