2023-11-29 12:13:28 +00:00
|
|
|
|
# Astronomical calculations in MicroPython
|
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
1. [Overview](./README.md#1-overview)
|
|
|
|
|
1.1 [Applications](./README.md#11-applications)
|
|
|
|
|
1.2 [Licensing and acknowledgements](./README.md#12-licensing-and-acknowledgements)
|
|
|
|
|
1.3 [Installation](./README.md#13-installation)
|
2023-12-14 11:50:29 +00:00
|
|
|
|
1.4 [Definitions](./README.md#14-definitions) Local and universal time.
|
2023-12-04 17:20:08 +00:00
|
|
|
|
2. [The RiSet class](./README.md#2-the-riset-class)
|
|
|
|
|
2.1 [Constructor](./README.md#21-constructor)
|
|
|
|
|
2.2 [Methods](./README.md#22-methods)
|
2023-12-06 11:49:04 +00:00
|
|
|
|
2.3 [Effect of local time](./README.md#23-effect-of-local-time)
|
2023-12-06 17:07:48 +00:00
|
|
|
|
2.4 [Continuously running applications](./README.md#24-continuously-running-applications)
|
2023-12-25 10:04:33 +00:00
|
|
|
|
3. [Utility functions](./README.md#3-utility-functions)
|
|
|
|
|
4. [Demo script](./README.md#4-demo-script)
|
|
|
|
|
5. [Scheduling events](./README.md#5-scheduling-events)
|
|
|
|
|
6. [The moonphase module](./README.md#6-the-moonphase-module)
|
|
|
|
|
6.1 [Constructor](./README.md#61-constructor)
|
|
|
|
|
6.2 [Methods](./README.md#62-methods)
|
|
|
|
|
6.3 [Usage examples](./README.md#63-usage-examples)
|
|
|
|
|
6.4 [DST](./README.md#64-dst) Daylight savings time.
|
2023-12-04 17:20:08 +00:00
|
|
|
|
7. [Performance and accuracy](./README.md#7-performance-and-accuracy)
|
2023-12-25 10:04:33 +00:00
|
|
|
|
7.1 [RiSet class](./README.md#71-riset-class)
|
|
|
|
|
7.2 [moonphase class](./README.md#72-moonphase-class)
|
2023-12-04 17:20:08 +00:00
|
|
|
|
|
|
|
|
|
# 1. Overview
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
The `sun_moon` module enables sun and moon rise and set times to be determined
|
|
|
|
|
at any geographical location. Times are in seconds from midnight and refer to
|
|
|
|
|
any event in a 24 hour period starting at midnight. The midnight datum is
|
|
|
|
|
defined in local time. The start is a day specified as the current day plus an
|
|
|
|
|
offset in days. It can also compute Civil, Nautical or Astronomical twilight
|
|
|
|
|
times.
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
The `moonphase` module enables the moon phase to be determined for any date, and
|
|
|
|
|
the dates and times of lunar quarters to be calculated.
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
|
|
|
|
Caveat. I am not an astronomer. If there are errors in the fundamental
|
|
|
|
|
algorithms I am unlikely to be able to offer an opinion, still less a fix.
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
Moon phase options have been removed from `sun_moon` because accuracy was poor.
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
## 1.1 Applications
|
2023-12-04 12:01:27 +00:00
|
|
|
|
|
|
|
|
|
There are two application areas. Firstly timing of events relative to sun or
|
|
|
|
|
moon rise and set times, discussed later in this doc. Secondly constructing
|
|
|
|
|
lunar clocks such as this one - the "lunartick":
|
|
|
|
|
![Image](./lunartick.jpg)
|
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
## 1.2 Licensing and acknowledgements
|
2023-11-30 15:44:14 +00:00
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
#### sun_moon.py
|
|
|
|
|
|
2023-12-15 08:54:45 +00:00
|
|
|
|
Some code was ported from C/C++ as presented in "Astronomy on the Personal
|
2023-11-30 15:44:14 +00:00
|
|
|
|
Computer" by Montenbruck and Pfleger, with mathematical improvements contributed
|
2023-12-15 08:54:45 +00:00
|
|
|
|
by Marcus Mendenhall and Raul Kompaß. I (Peter Hinch) performed the port and
|
|
|
|
|
enabled support for timezones. Raul Kompaß substantially improved the accuracy
|
|
|
|
|
when run on hardware with 32-bit floating point.
|
|
|
|
|
|
|
|
|
|
The sourcecode exists in the book and also on an accompanying CD-R. The file
|
|
|
|
|
`CDR_license.txt` contains a copy of the license file on the disk, which
|
|
|
|
|
contains source, executable code, and databases. This module only references the
|
|
|
|
|
source. I have not spotted any restrictions on use in the book. I am not a
|
|
|
|
|
lawyer; I have no idea of the legal status of code based on sourcecode in a
|
|
|
|
|
published work.
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
#### moonphase.py
|
|
|
|
|
|
|
|
|
|
This was derived from unrestricted public sources and is released under the MIT
|
|
|
|
|
licence.
|
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
## 1.3 Installation
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
|
|
|
|
Installation copies files from the `astronomy` directory to a directory
|
2024-01-03 14:40:05 +00:00
|
|
|
|
`\lib\sched` on the target. This directory eases optional use with the
|
2023-12-01 16:41:40 +00:00
|
|
|
|
[schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md).
|
2024-01-03 14:40:05 +00:00
|
|
|
|
Installation may be done with the official
|
2023-12-01 16:41:40 +00:00
|
|
|
|
[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html):
|
|
|
|
|
```bash
|
|
|
|
|
$ mpremote mip install "github:peterhinch/micropython-samples/astronomy"
|
|
|
|
|
```
|
|
|
|
|
On networked platforms it may alternatively be installed with
|
|
|
|
|
[mip](https://docs.micropython.org/en/latest/reference/packages.html).
|
|
|
|
|
```py
|
|
|
|
|
>>> mip.install("github:peterhinch/micropython-samples/astronomy")
|
|
|
|
|
```
|
|
|
|
|
Currently these tools install to `/lib` on the built-in Flash memory. To install
|
|
|
|
|
to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
Clone the repo and move to `micropython-samples` on the PC, run `rshell` and
|
|
|
|
|
issue:
|
2023-12-01 16:41:40 +00:00
|
|
|
|
```
|
|
|
|
|
> rsync astronomy /sd/sched
|
|
|
|
|
```
|
|
|
|
|
`mip` installs the following files in the `sched` directory.
|
|
|
|
|
* `sun_moon.py`
|
2023-12-25 10:27:40 +00:00
|
|
|
|
* `sun_moon_test.py` A test/demo script for the above.
|
|
|
|
|
* `moonphase.py` Determine lunar quarters and phase.
|
2023-12-01 16:41:40 +00:00
|
|
|
|
After installation the `RiSet` class may be accessed with
|
|
|
|
|
```python
|
|
|
|
|
from sched.sun_moon import RiSet
|
|
|
|
|
```
|
2023-12-11 10:39:39 +00:00
|
|
|
|
## 1.4 Definitions
|
|
|
|
|
|
|
|
|
|
Time is a slippery concept when applied to global locations. This document uses
|
|
|
|
|
the following conventions:
|
|
|
|
|
* `UTC` The international time standard based on the Greenwich meridian.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
* `LT (Local time)` Time on a clock at the device's location. May include
|
2023-12-11 10:39:39 +00:00
|
|
|
|
daylight saving time (`DST`).
|
|
|
|
|
* `MT (Machine time)` Time defined by the platform's hardware clock.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
* `LTO (Local time offset)` A `RiSet` instance contains a user supplied `LTO`
|
|
|
|
|
intended for timezone support. The class computes rise and set times in UTC,
|
|
|
|
|
using `LTO` to compute results using `RESULT = UTC + LTO`. For output in `LT`
|
|
|
|
|
there are two options: periodically adjust `LTO` to handle DST or (better)
|
|
|
|
|
provide a `dst` function so that conversion is automatic.
|
2023-11-30 15:44:14 +00:00
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
# 2. The RiSet class
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
2023-12-11 10:39:39 +00:00
|
|
|
|
This holds the local geographic coordinates and the local time offset (`LTO`).
|
|
|
|
|
An instance is initialised to the current date (defined by `MT`) and can provide
|
|
|
|
|
the times of rise and set events occurring within a 24 hour window starting at
|
|
|
|
|
00:00:00 `LT`. A `RiSet` instance's date may be changed allowing rise and set
|
|
|
|
|
times to be retrieved for other 24 hour windows. Continuously running
|
|
|
|
|
applications should detect machine time (`MT`) date rollover and cause `RiSet`
|
|
|
|
|
instances to re-calculate rise and set times for the new day. This is done by
|
|
|
|
|
issuing `.set_day()`.
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
|
|
|
|
Rise and set times may be retrieved in various formats including seconds from
|
|
|
|
|
local midnight: this may be used to enable the timing of actions relative to a
|
|
|
|
|
rise or set event.
|
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
## 2.1 Constructor
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
|
|
|
|
Args (float):
|
2023-11-30 15:44:14 +00:00
|
|
|
|
* `lat=LAT` Latitude in degrees (-ve is South). Defaults are my location. :)
|
2023-11-29 12:13:28 +00:00
|
|
|
|
* `long=LONG` Longitude in degrees (-ve is West).
|
2023-12-06 11:49:04 +00:00
|
|
|
|
* `lto=0` Local time offset in hours to UTC (-ve is West); the value is checked
|
2023-12-15 08:54:45 +00:00
|
|
|
|
to ensure `-15 < lto < 15`. See [section 2.3](./README.md#23-effect-of-local-time).
|
2023-12-11 10:39:39 +00:00
|
|
|
|
The constructor sets the object's date to the system date as defined by machine
|
|
|
|
|
time (`MT`).
|
2023-12-15 08:54:45 +00:00
|
|
|
|
* `tl=None` If provided, set an offset in degrees for the definition of twilight
|
|
|
|
|
(6 is Civil, 12 is Nautical, 18 is Astronomical). By default twilight times are
|
|
|
|
|
not computed, saving some processor time. Offsets are positive numbers
|
|
|
|
|
representing degrees below the horizon where twilight is deemed to start and end.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
* `dst=lambda x: x` This is an optional user defined function for Daylight
|
|
|
|
|
Saving Time (DST). The assumption is that machine time is not changed, typically
|
|
|
|
|
permanently in winter time. A `dst` function handles seasonal changes. The
|
|
|
|
|
default assumes no DST is applicable. For how to write a DST function for a
|
|
|
|
|
given country see [section 6.4](./README.md#64-dst).
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
2023-12-16 19:11:46 +00:00
|
|
|
|
By default when an application instantiates `RiSet` for the first time the
|
|
|
|
|
constructor prints the system date and time. This can be inhibited by setting
|
2024-01-03 14:40:05 +00:00
|
|
|
|
the class variable `verbose` to `False`. The purpose is to alert the user to a
|
|
|
|
|
common source of error where machine time is not set.
|
2023-12-16 19:11:46 +00:00
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
## 2.2 Methods
|
|
|
|
|
|
|
|
|
|
* `set_day(day: int = 0)` `day` is an offset in days from the current system
|
2023-12-11 10:39:39 +00:00
|
|
|
|
date. If the passed day differs from that stored in the instance, rise and set
|
|
|
|
|
times are updated - otherwise return is "immediate". Returns the `RiSet`
|
|
|
|
|
instance.
|
2023-11-29 12:13:28 +00:00
|
|
|
|
* `sunrise(variant: int = 0)` See below for details and the `variant` arg.
|
|
|
|
|
* `sunset(variant: int = 0)`
|
|
|
|
|
* `moonrise(variant: int = 0)`
|
|
|
|
|
* `moonset(variant: int = 0)`
|
2023-12-17 08:47:51 +00:00
|
|
|
|
* `tstart(variant: int = 0)` Twilight start, Sun about to rise.
|
|
|
|
|
* `tend(variant: int = 0)` Twilight end, (Sun has set).
|
2023-12-11 10:39:39 +00:00
|
|
|
|
* `is_up(sun: bool)-> bool` Returns `True` if the selected object is above the
|
|
|
|
|
horizon.
|
|
|
|
|
* `has_risen(sun: bool)->bool` Returns `True` if the selected object has risen.
|
|
|
|
|
* `has_set(sun: bool)->bool` Returns `True` if the selected object has set.
|
|
|
|
|
* `set_lto(t)` Set local time offset `LTO` in hours relative to UTC. Primarily
|
2024-01-03 14:40:05 +00:00
|
|
|
|
intended for timezone support, but this function can be used to support DST. The
|
|
|
|
|
value is checked to ensure `-15.0 < lto < 15.0`. See
|
|
|
|
|
[section 2.3](./README.md#23-effect-of-local-time).
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
hour period. Note that a given event may be absent in the period: this can occur
|
|
|
|
|
with the moon at most locations, and with the sun in polar regions.
|
|
|
|
|
|
|
|
|
|
Variants:
|
2023-12-11 10:39:39 +00:00
|
|
|
|
* 0 Return integer seconds since midnight `LT` (or `None` if no event).
|
2023-11-29 12:13:28 +00:00
|
|
|
|
* 1 Return integer seconds since since epoch of the MicroPython platform
|
2024-01-03 14:40:05 +00:00
|
|
|
|
(or `None`). This allows comparisons with machine time (`MT`) as per
|
|
|
|
|
`time.time()`.
|
2023-12-11 10:39:39 +00:00
|
|
|
|
* 2 Return text of form hh:mm:ss (or --:--:--) being local time (`LT`).
|
2023-11-29 12:13:28 +00:00
|
|
|
|
|
2024-07-12 08:26:29 +00:00
|
|
|
|
If bit 2 of `variant` is set and a `dst` constructor arg has been passed, the
|
|
|
|
|
`dst` function will be applied to the result. This caters for the case where the
|
|
|
|
|
machine clock runs winter time and a dst-adjusted result is required. It should
|
|
|
|
|
be noted that applying dst adjustment to a time close to midnight can result in
|
|
|
|
|
rollover, i.e. a time > 24:00. Handling this case is the responsibility of the
|
|
|
|
|
application.
|
|
|
|
|
|
2023-11-30 15:44:14 +00:00
|
|
|
|
Example constructor invocations:
|
|
|
|
|
```python
|
|
|
|
|
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
|
|
|
|
|
```
|
2023-12-06 11:49:04 +00:00
|
|
|
|
## 2.3 Effect of local time
|
|
|
|
|
|
2024-01-03 14:40:05 +00:00
|
|
|
|
MicroPython has no concept of timezones. The hardware platform has a clock
|
2023-12-11 10:39:39 +00:00
|
|
|
|
which reports machine time (`MT`): this might be set to local winter time or
|
|
|
|
|
summer time. The `RiSet` instances' `LTO` should be set to represent the
|
|
|
|
|
difference between `MT` and `UTC`. In continuously running applications it is
|
|
|
|
|
best to avoid changing the hardware clock (`MT`) for reasons discussed below.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
Daylight savings time may be implemented in one of two ways:
|
|
|
|
|
* By changing the `RiSet` instances' `LTO` accordingly.
|
|
|
|
|
* Or by providing a `dst` function as discussed in
|
|
|
|
|
[section 6.4](./README.md#64-dst). This is the preferred solution as DST is then
|
|
|
|
|
handled automatically.
|
2023-12-06 11:49:04 +00:00
|
|
|
|
|
|
|
|
|
Rise and set times are computed relative to UTC and then adjusted using the
|
2023-12-11 10:39:39 +00:00
|
|
|
|
`RiSet` instance's `LTO` before being returned (see `.adjust()`). This means
|
|
|
|
|
that the accuracy of the hardware clock is not critical: only the date portion
|
|
|
|
|
is used in determining rise and set times.
|
2023-12-06 11:49:04 +00:00
|
|
|
|
|
2023-12-11 10:39:39 +00:00
|
|
|
|
The `.has_risen()`, `.has_set()` and `.is_up()` methods do use machine time
|
|
|
|
|
(`MT`) and rely on `MT == UTC + LTO`: if `MT` has drifted, precision will be
|
2024-01-03 14:40:05 +00:00
|
|
|
|
lost at times close to rise and set events.
|
2023-12-06 11:49:04 +00:00
|
|
|
|
|
|
|
|
|
The constructor and the `set_day()` method set the instance's date relative to
|
2023-12-11 10:39:39 +00:00
|
|
|
|
`MT`. They use only the date component of `MT`, hence they may be run at any
|
|
|
|
|
time of day and are not reliant on `MT` accuracy.
|
2023-11-30 15:44:14 +00:00
|
|
|
|
|
2023-12-06 17:07:48 +00:00
|
|
|
|
## 2.4 Continuously running applications
|
|
|
|
|
|
|
|
|
|
Where an application runs continuously there is usually a need for `RiSet`
|
|
|
|
|
instances to track the current date. One approach is this:
|
|
|
|
|
```python
|
2023-12-11 10:39:39 +00:00
|
|
|
|
async def tomorrow(offs):
|
2023-12-06 17:07:48 +00:00
|
|
|
|
now = round(time.time())
|
2023-12-11 10:39:39 +00:00
|
|
|
|
tw = 86400 + 60 - (now % 86400) # Time from now to one minute past next midnight
|
2023-12-06 17:07:48 +00:00
|
|
|
|
await asyncio.sleep(tw)
|
|
|
|
|
|
2023-12-11 10:39:39 +00:00
|
|
|
|
rs = RiSet() # May need args
|
|
|
|
|
|
|
|
|
|
async def keep_updated(rs): # Keep a RiSet instance updated
|
2023-12-06 17:07:48 +00:00
|
|
|
|
while True:
|
2023-12-11 10:39:39 +00:00
|
|
|
|
await tomorrow() # Wait until 1 minute past midnight
|
2023-12-06 17:07:48 +00:00
|
|
|
|
rs.set_day() # Update to new day
|
|
|
|
|
```
|
|
|
|
|
It is important that, at the time when `.set_day()` is called, the system time
|
|
|
|
|
has a date which is correct. Most hardware uses crystal controlled clocks so
|
2023-12-11 10:39:39 +00:00
|
|
|
|
drift is minimal. However with long run times it will accumulate. Care must be
|
|
|
|
|
taken if periodically synchronising system time to a time source: the resultant
|
|
|
|
|
sudden jumps in system time can cause havoc with `uasyncio` timing. If
|
|
|
|
|
synchronisation is required it is best done frequently to minimise the size of
|
|
|
|
|
jumps.
|
|
|
|
|
|
|
|
|
|
For this reason changing system time to accommodate daylight saving time is a
|
2024-01-03 14:40:05 +00:00
|
|
|
|
bad idea. It is usually best to run winter time all year round and to use the
|
|
|
|
|
`dst` constructor arg to handle time changes.
|
2023-12-06 17:07:48 +00:00
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
# 3. Utility functions
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
|
|
|
|
`now_days() -> int` Returns the current time as days since the platform epoch.
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
# 4. Demo script
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
|
|
|
|
This produces output for the fixed date 4th Dec 2023 at three geographical
|
|
|
|
|
locations. It can therefore be run on platforms where the system time is wrong.
|
|
|
|
|
To run issue:
|
|
|
|
|
```python
|
|
|
|
|
import sched.sun_moon_test
|
|
|
|
|
```
|
|
|
|
|
Expected output:
|
|
|
|
|
```python
|
|
|
|
|
>>> import sched.sun_moon_test
|
|
|
|
|
4th Dec 2023: Seattle UTC-8
|
|
|
|
|
Sun rise 07:40:09 set 16:18:15
|
|
|
|
|
Moon rise 23:38:11 set 12:53:40
|
|
|
|
|
|
|
|
|
|
4th Dec 2023: Sydney UTC+11
|
|
|
|
|
Sun rise 05:36:24 set 19:53:21
|
|
|
|
|
Moon rise 00:45:55 set 11:27:14
|
|
|
|
|
|
|
|
|
|
From 4th Dec 2023: UK, UTC
|
|
|
|
|
Day: 0
|
|
|
|
|
Sun rise 08:04:34 set 15:52:13
|
|
|
|
|
Moon rise 23:03:15 set 13:01:04
|
|
|
|
|
Day: 1
|
|
|
|
|
Sun rise 08:05:54 set 15:51:42
|
|
|
|
|
Moon rise --:--:-- set 13:10:35
|
|
|
|
|
Day: 2
|
|
|
|
|
Sun rise 08:07:13 set 15:51:13
|
|
|
|
|
Moon rise 00:14:40 set 13:18:59
|
|
|
|
|
Day: 3
|
|
|
|
|
Sun rise 08:08:28 set 15:50:49
|
|
|
|
|
Moon rise 01:27:12 set 13:27:08
|
|
|
|
|
Day: 4
|
|
|
|
|
Sun rise 08:09:42 set 15:50:28
|
|
|
|
|
Moon rise 02:40:34 set 13:35:56
|
|
|
|
|
Day: 5
|
|
|
|
|
Sun rise 08:10:53 set 15:50:10
|
|
|
|
|
Moon rise 03:56:44 set 13:46:27
|
|
|
|
|
Day: 6
|
|
|
|
|
Sun rise 08:12:01 set 15:49:56
|
|
|
|
|
Moon rise 05:18:32 set 14:00:11
|
2023-12-04 17:20:08 +00:00
|
|
|
|
Maximum error 0. Expect 0 on 64-bit platform, 30s on 32-bit
|
2023-12-01 16:41:40 +00:00
|
|
|
|
>>>
|
|
|
|
|
```
|
|
|
|
|
Code comments show times retrieved from `timeanddate.com`.
|
|
|
|
|
|
2024-01-03 14:40:05 +00:00
|
|
|
|
The script includes some commented out code at the end. This tests `is_up`,
|
|
|
|
|
`has_risen` and `has_set` over 365 days. It is commented out to reduce printed
|
|
|
|
|
output.
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
# 5. Scheduling events
|
2023-12-01 16:41:40 +00:00
|
|
|
|
|
|
|
|
|
A likely use case is to enable events to be timed relative to sunrise and set.
|
2023-12-06 17:07:48 +00:00
|
|
|
|
In simple cases this can be done with `asyncio`. This will execute a payload at
|
|
|
|
|
sunrise, and another at sunset, every day.
|
2023-12-01 16:41:40 +00:00
|
|
|
|
```python
|
2023-12-06 11:49:04 +00:00
|
|
|
|
import uasyncio as asyncio
|
2023-12-01 16:41:40 +00:00
|
|
|
|
import time
|
2023-12-06 11:49:04 +00:00
|
|
|
|
from sched.sun_moon import RiSet
|
|
|
|
|
|
2023-12-06 17:07:48 +00:00
|
|
|
|
async def tomorrow(offs): # Offset compensates for possible clock drift
|
2023-12-06 11:49:04 +00:00
|
|
|
|
now = round(time.time())
|
2023-12-06 17:07:48 +00:00
|
|
|
|
tw = 86400 + 60 * offs - (now % 86400) # Time from now to one minute past next midnight
|
2023-12-06 11:49:04 +00:00
|
|
|
|
await asyncio.sleep(tw)
|
|
|
|
|
|
|
|
|
|
async def do_sunrise():
|
|
|
|
|
rs = RiSet() # May need args
|
|
|
|
|
while True:
|
2023-12-06 17:07:48 +00:00
|
|
|
|
if (delay := rs.sunrise(1) - round(time.time())) > 0: # Sun has not yet risen
|
|
|
|
|
await asyncio.sleep(delay) # Wait for it to rise
|
|
|
|
|
# Sun has risen, execute payload e.g. turn off light
|
|
|
|
|
await tomorrow(1) # Wait until 1 minute past midnight
|
2023-12-06 11:49:04 +00:00
|
|
|
|
rs.set_day() # Update to new day
|
2023-12-06 17:07:48 +00:00
|
|
|
|
|
|
|
|
|
async def do_sunset():
|
|
|
|
|
rs = RiSet() # May need args
|
|
|
|
|
while True:
|
|
|
|
|
if (delay := rs.sunset(1) - round(time.time())) > 0: # Sun has not yet set
|
|
|
|
|
await asyncio.sleep(delay) # Wait for it to set
|
|
|
|
|
# Sun has risen, execute payload e.g. turn on light
|
|
|
|
|
await tomorrow(1) # Wait until 1 minute past midnight
|
|
|
|
|
rs.set_day() # Update to new day
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
sr = asyncio.create_task(do_sunrise())
|
|
|
|
|
ss = asyncio.create_task(do_sunset())
|
|
|
|
|
ayncio.gather(sr, ss)
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(main())
|
|
|
|
|
finally:
|
|
|
|
|
_ = asyncio.new_event_loop()
|
2023-12-01 16:41:40 +00:00
|
|
|
|
```
|
2023-12-06 11:49:04 +00:00
|
|
|
|
This code assumes that `.sunrise()` will never return `None`. At polar latitudes
|
2023-12-06 17:07:48 +00:00
|
|
|
|
waiting for sunrise in winter would require changes - and patience.
|
2023-12-06 11:49:04 +00:00
|
|
|
|
|
|
|
|
|
Code may be simplified by using the
|
2023-12-01 16:41:40 +00:00
|
|
|
|
[schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md).
|
2023-12-04 17:20:08 +00:00
|
|
|
|
This may be installed with
|
|
|
|
|
```bash
|
|
|
|
|
$ mpremote mip install "github:peterhinch/micropython-async/v3/as_drivers/sched"
|
|
|
|
|
```
|
|
|
|
|
The following is a minimal example:
|
2023-12-01 16:41:40 +00:00
|
|
|
|
```python
|
|
|
|
|
import uasyncio as asyncio
|
|
|
|
|
from sched.sched import schedule
|
|
|
|
|
from sched.sun_moon import RiSet
|
|
|
|
|
|
2023-12-06 17:07:48 +00:00
|
|
|
|
async def turn_off_lights(rs): # Runs every day at 00:01:00
|
2023-12-01 16:41:40 +00:00
|
|
|
|
rs.set_day() # Re-calculate for new daylight
|
2023-12-08 18:15:37 +00:00
|
|
|
|
await asyncio.sleep(rs.sunrise() - 60)
|
2023-12-01 16:41:40 +00:00
|
|
|
|
# Actually turn them off
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
rs = RiSet() # May need args for your location
|
|
|
|
|
await schedule(turn_off_lights, rs, hrs=0, mins=1) # Never terminates
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(main())
|
|
|
|
|
finally:
|
|
|
|
|
_ = asyncio.new_event_loop()
|
|
|
|
|
```
|
|
|
|
|
This approach lends itself to additional triggers and events:
|
|
|
|
|
```python
|
|
|
|
|
import uasyncio as asyncio
|
|
|
|
|
from sched.sched import schedule, Sequence
|
|
|
|
|
from sched.sun_moon import RiSet
|
|
|
|
|
|
|
|
|
|
async def turn_off_lights(t):
|
2023-12-08 18:15:37 +00:00
|
|
|
|
await asyncio.sleep(t)
|
2023-12-01 16:41:40 +00:00
|
|
|
|
# Actually turn them off
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
rs = RiSet() # May need args for your location
|
|
|
|
|
seq = Sequence() # A Sequence comprises one or more schedule instances
|
|
|
|
|
asyncio.create_task(schedule(seq, "off", hrs=0, mins=1))
|
|
|
|
|
# Can schedule other events here
|
|
|
|
|
async for args in seq:
|
|
|
|
|
if args[0] == "off": # Triggered at 00:01 hrs (there might be other triggers)
|
|
|
|
|
rs.set_day() # Re-calculate for new day
|
|
|
|
|
asyncio.create_task(turn_off_lights(rs.sunrise() - 60))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(main())
|
|
|
|
|
finally:
|
|
|
|
|
_ = asyncio.new_event_loop()
|
|
|
|
|
```
|
2023-12-25 10:04:33 +00:00
|
|
|
|
# 6. The moonphase module
|
|
|
|
|
|
|
|
|
|
This contains a single class `MoonPhase`. The term "machine time" below refers
|
|
|
|
|
to the time reported by the MicroPython `time` module. The "local time offset"
|
|
|
|
|
(LTO) passed to the constructor specifies the difference between machine time
|
|
|
|
|
and UTC based on system longitude. "Daylight saving time" (DST) allows reported
|
|
|
|
|
times to be offset to compensate for DST. Internally phases are calculated in
|
|
|
|
|
UTC, but where times are output they are adjusted for LTO and DST.
|
|
|
|
|
|
|
|
|
|
It is recommended that the machine clock is not adjusted for DST because large
|
|
|
|
|
changes can play havoc with program timing as described above. To accommodate
|
|
|
|
|
DST, a `dst` function can be provided to the constructor. The module uses this
|
|
|
|
|
to adjust reported times.
|
|
|
|
|
|
|
|
|
|
A `MoonPhase` instance has a time `datum`, which defaults to the instantiation
|
|
|
|
|
time. Phases are calculated with respect to this datum. It may be changed using
|
|
|
|
|
`.set_day` to enable future and past phases to be determined or to enable long
|
|
|
|
|
running applications to track time.
|
|
|
|
|
|
2023-12-25 10:27:40 +00:00
|
|
|
|
The module is imported as follows:
|
|
|
|
|
```python
|
|
|
|
|
from sched.moonphase import MoonPhase
|
|
|
|
|
```
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
## 6.1 Constructor
|
|
|
|
|
|
|
|
|
|
* `lto:float=0, dst = lambda x: x` Local time offset in hours to UTC (-ve is
|
|
|
|
|
West); the value is checked to ensure `-15 < lto < 15`. `dst` is an optional
|
|
|
|
|
user defined function for Daylight Saving Time (DST). See
|
|
|
|
|
[section 6.4](./README.md#64-dst)
|
|
|
|
|
|
|
|
|
|
## 6.2 Methods
|
|
|
|
|
|
|
|
|
|
* `quarter(q: int, text: bool = True)` Return the time of a given quarter. Five
|
|
|
|
|
quarters are calculated around the instance datum. By default the time
|
|
|
|
|
is last midnight machine time with an optional offset in days `doff` added. The
|
|
|
|
|
`quarter` arg specifies the quarter with 0 and 4 being new moons and quarter 2
|
|
|
|
|
being full. The `text` arg determines how the value is returned: as text or as
|
|
|
|
|
`int` is secs from the machine epoch. Results are adjusted for DST if a `dst`
|
|
|
|
|
function is provided to the constructor.
|
|
|
|
|
* `phase() -> float)` Returns moon phase where 0.0 <= phase < 1.0 with 0.5 being
|
|
|
|
|
full moon. The phase is that pertaining to the datum.
|
2023-12-25 10:27:40 +00:00
|
|
|
|
* `nextphase(text: bool = True)` This is a generator function. Each iteration
|
2023-12-25 10:04:33 +00:00
|
|
|
|
of the generator returns three values: the phase number, the lunation number and
|
|
|
|
|
the datetime of the phase. The `text` arg is as per `.quarter()`, defining the
|
|
|
|
|
format of the datetime.
|
|
|
|
|
* `set_day(doff: float = 0)` Set the `MoonPhase` datum time to machine time plus
|
|
|
|
|
an offset in days: this may include a fractional part if `.phase()` is required
|
|
|
|
|
to produce a time-precise value. The five quarters are calculated for the
|
|
|
|
|
lunation including the midnight at the start of the specified day.
|
|
|
|
|
* `set_lto(t:float)` Redefine the local time offset, `t` being in hours as
|
|
|
|
|
per the constructor arg.
|
2024-01-03 14:40:05 +00:00
|
|
|
|
* `datum(text: bool = True)` Returns the current datum in secs since local epoch
|
|
|
|
|
or in human-readable text form.
|
2023-12-25 10:04:33 +00:00
|
|
|
|
|
|
|
|
|
## 6.3 Usage examples
|
|
|
|
|
|
|
|
|
|
```python
|
2023-12-25 10:27:40 +00:00
|
|
|
|
from sched.moonphase import MoonPhase
|
2023-12-25 10:04:33 +00:00
|
|
|
|
mp = MoonPhase() # datum is midnight last night
|
|
|
|
|
print(f"Full moon, current lunation {mp.quarter(2)}")
|
|
|
|
|
mp.set_day(0.5) # Adjust datum to noon today machine time
|
|
|
|
|
print(f"Phase at Noon {mp.phase()}")
|
|
|
|
|
mp.set_day(182) # Set datum ahead 6 months
|
|
|
|
|
print(f"Lunation 1st new moon: {mp.quarter(0)}, 2nd new moon: {mp.quarter(4)}")
|
|
|
|
|
mp.set_day(0) # Reset datum to today
|
|
|
|
|
n = mp.nextphase() # Instantiate generator
|
|
|
|
|
for _ in range(8):
|
|
|
|
|
print(next(n))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 6.4 DST
|
|
|
|
|
|
|
|
|
|
Daylight saving time depends on country and geographic location, and there is no
|
|
|
|
|
built-in MicroPython support. The moonphase module supports DST via an optional
|
|
|
|
|
user supplied function. DST does not affect the calculation of quarters or phase
|
|
|
|
|
which is based on the machine clock. If the machine clock runs at a fixed offset
|
|
|
|
|
to UTC (which is recommended), a DST function can be used to enable reported
|
|
|
|
|
results to reflect local time.
|
|
|
|
|
|
2023-12-25 10:27:40 +00:00
|
|
|
|
A DST function takes as input a datetime measured in seconds since the machine
|
|
|
|
|
epoch (as returned by `time.time()`) and returns that number adjusted for local
|
|
|
|
|
time. The following example is for UK time, which adds one hour at 2:00 on the
|
|
|
|
|
last Sunday in March, reverting to winter time at 2:00 on the last Sunday in
|
|
|
|
|
October.
|
2023-12-25 10:04:33 +00:00
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def uk_dst(secs_epoch: int): # Change in March (3) and Oct (10)
|
|
|
|
|
t = time.gmtime(secs_epoch)
|
|
|
|
|
month = t[1]
|
|
|
|
|
mday = t[2]
|
|
|
|
|
wday = t[6]
|
|
|
|
|
winter = secs_epoch
|
|
|
|
|
summer = secs_epoch + 3600 # +1hr
|
|
|
|
|
if month in (1, 2, 11, 12): # Simple cases: depend only on month
|
|
|
|
|
return winter
|
|
|
|
|
if not month in (3, 10):
|
|
|
|
|
return summer # +1 hr
|
|
|
|
|
# We are in March or October. Find the day in month of last Sunday.
|
|
|
|
|
ld = (wday + 31 - mday) % 7 # weekday of 31st.
|
2023-12-25 10:27:40 +00:00
|
|
|
|
lsun = 31 - (1 + ld) % 7 # Monthday of last Sunday
|
|
|
|
|
thresh = time.mktime((t[0], month, lsun, 2, 0, 0, 6, 0)) # 2am last Sunday in month
|
2023-12-25 10:04:33 +00:00
|
|
|
|
return summer if ((secs_epoch >= thresh) ^ (month == 10)) else winter
|
|
|
|
|
```
|
2023-12-04 17:20:08 +00:00
|
|
|
|
|
|
|
|
|
# 7. Performance and accuracy
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
## 7.1 RiSet class
|
|
|
|
|
|
2023-12-04 17:20:08 +00:00
|
|
|
|
A recalculation is triggered whenever the 24 hour local time window is changed,
|
|
|
|
|
such as calling `.set_day()` where the stored date changes. Normally two days of
|
|
|
|
|
data are calculated, except where the local time is UTC where only one day is
|
2023-12-15 08:54:45 +00:00
|
|
|
|
required. The time to derive one day's data on RP2040 was 707μs (no twilight
|
|
|
|
|
calculation, standard clock).
|
2023-12-04 17:20:08 +00:00
|
|
|
|
|
|
|
|
|
The accuracy of rise and set times was checked against online sources for
|
|
|
|
|
several geographic locations. The online data had 1 minute resolution and the
|
|
|
|
|
checked values corresponded with data computed on a platform with 64 bit
|
|
|
|
|
floating point unit. The loss of precision from using a 32 bit FPU was no more
|
2023-12-15 08:54:45 +00:00
|
|
|
|
than 3s.
|
|
|
|
|
|
2023-12-25 10:04:33 +00:00
|
|
|
|
## 7.2 MoonPhase class
|
2023-12-04 17:20:08 +00:00
|
|
|
|
|
2024-01-03 14:40:05 +00:00
|
|
|
|
This uses Python's arbitrary precision integers to overcome the limitations of
|
|
|
|
|
32-bit floating point units. Results on 32 bit platforms match those on 64-bits
|
|
|
|
|
to within ~1 minute. Results match those on `timeanddate.com` within ~3 minutes.
|