Astral port/alternative

Discussion about programs, libraries and tools that work with MicroPython. Mostly these are provided by a third party.
Target audience: All users and developers of MicroPython.
Post Reply
User avatar
patvdleer
Posts: 46
Joined: Mon Jun 13, 2016 11:52 am
Location: Maastricht, NL, Europe
Contact:

Astral port/alternative

Post by patvdleer » Tue Jun 21, 2016 9:58 am

Is there a port or an alternative for the Astral package? Given it's dependencies I'm guessing a port is out of the question.

My goal is to port some code which I wrote for a Raspberry Pi to an ESP8266 so it can check sunrise/sunset.

The use for this is for my aquarium, I recently bought LED lighting which can be dimmed via the remote control or via a programmer I can set in the LED controller. This would mean the LED's turn on at the same time everyday so not accounting for seasons. I plotted the remote control so I do have the codes which are send via IR and would like to "emulate" the remote control via ESP8266 so I can mount in in front of the LED controller.
NodeMCU v0.9 / V1 / V2 / V3
WeMos D1 Mini
WeMos Lolin32 v1.0.0
WeMos Lolin D32 Pro V2

User avatar
pythoncoder
Posts: 4940
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Astral port/alternative

Post by pythoncoder » Thu Jun 23, 2016 3:39 pm

I have some C code for calculating sun rise and set times which I can provide if you wish. It calculates to an accuracy on the order of +- a minute, which is probably better than you need. One option is to port it to Python to run on the target (which should be easy). Alternatively adapt it to create a lookup table: for your purposes I'd have thought that a lookup by week or even by month would be adequate and would simplify the code to run on the target. The LUT could either be stored in a file or hard coded.
Peter Hinch

User avatar
patvdleer
Posts: 46
Joined: Mon Jun 13, 2016 11:52 am
Location: Maastricht, NL, Europe
Contact:

Re: Astral port/alternative

Post by patvdleer » Thu Jun 23, 2016 8:18 pm

That would be awesome! Does is support twilight phases? and (even more important) would you like to share it?
NodeMCU v0.9 / V1 / V2 / V3
WeMos D1 Mini
WeMos Lolin32 v1.0.0
WeMos Lolin D32 Pro V2

User avatar
pythoncoder
Posts: 4940
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Astral port/alternative

Post by pythoncoder » Fri Jun 24, 2016 7:29 am

@patvdleer I've posted it here http://hinch.me.uk/lunarmath.c

It doesn't support twilight phases. There is a lot of code concerned with lunar calculations which you can obviously eliminate, and you'll need to adjust the latitude and longitude for your location. If you decide to port it to Python, my experience of porting mathematical C code to Python is that it's pretty straightforward, because the syntax of the actual maths is so similar. I'd start out by eliminating the code you won't use. Then use search and replace to get rid of the semicolons and curly braces. Then search out and fix int, float, double declarations, #defines and function definitions plus oddities like the ternary operator. The potential pig is indentation: when eliminating semicolons and curly braces check that the indentation is correct. This is the one thing which has caught me out with subtle bugs.

The C code uses doubles whereas MicroPython on ESP8266 supports only floats, but for sunrise and sunset I doubt the loss of precision will be an issue.

The definitive text is "Astronomy on the Personal Computer" by Montenbruck and others. My edition came with a CD containing C++ source for sun rise, set and twilight events - my code was adapted from this for a platform which supported only C. Unfortunately the licence terms disallow the distribution of the original source.

Good luck, and let us know how you get on.
Peter Hinch

User avatar
patvdleer
Posts: 46
Joined: Mon Jun 13, 2016 11:52 am
Location: Maastricht, NL, Europe
Contact:

Re: Astral port/alternative

Post by patvdleer » Fri Jun 24, 2016 8:55 am

Awesome! thank you so much!

As soon as I have the time I'll port and publish it and later on try to add the twilight phases. The lunar phases might come in handy to set the lights when it's a full moon.
NodeMCU v0.9 / V1 / V2 / V3
WeMos D1 Mini
WeMos Lolin32 v1.0.0
WeMos Lolin D32 Pro V2

User avatar
pythoncoder
Posts: 4940
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Astral port/alternative

Post by pythoncoder » Fri Jun 24, 2016 3:53 pm

Looking at my code again it was originally ported from Javascript. The original source website has disappeared into /dev/null but I've put a copy here http://hinch.me.uk/riset.html. You can run the code to check the results of your port. Also if you examine the Javascript you'll see code for civil, nautical and astronomical twilight. Alas I stripped these out from my C port as I had no need for them but it should be easy to reinstate them. The html has some useful notes on algorithms and testing. My port has run for six years in an astronomical clock and continues to give accurate results.

For completeness I've also uploaded lunartick.h which is #included in lunarmath.c

Good luck!
Peter Hinch

Divergentti
Posts: 67
Joined: Fri Sep 04, 2020 9:27 am
Location: Hanko, Finland
Contact:

Re: Astral port/alternative

Post by Divergentti » Sat Feb 20, 2021 1:11 pm

Perhaps somewhere is better code available for MPY, but I ported Suntime class for Micropython. https://github.com/divergentti/airquali ... Suntime.py and this seems to be quite ok.

Compared to Google, sun rise drifts a few minutes, sun set is OK.

Original: https://github.com/SatAgro/suntime

Usage:


from Suntime import Sun
sun = Sun(LATITUDE, LONGITUDE, 2)

print(sun.get_sunrise_time())
print(sun.get_sunset_time())

Code: Select all

import math
from utime import localtime

class SunTimeException(Exception):

    def __init__(self, message):
        super(SunTimeException, self).__init__(message)


class Sun:
    """
    Original: https://github.com/SatAgro/suntime/blob/master/suntime/suntime.py

    Ported to Micropython 20.02.2021: Divergentti / Jari Hiltunen
    Replaced: date utils. Method call use hour difference to UTC as timezone! This will be added to return.
    """

    def __init__(self, lat, lon, tzone):
        self._lat = lat
        self._lon = lon
        self._tzone = tzone

    def get_sunrise_time(self, date=None):
        """
        Calculate the sunrise time for given date.
        :param date: Reference date. Today if not provided.
        :return: UTC sunrise datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date
        """

        date = localtime() if date is None else date
        sr = self._calc_sun_time(date, True)
        if sr is None:
            raise SunTimeException('The sun never rises on this location (on the specified date)')
        else:
            return sr

    def get_sunset_time(self, date=None):
        """
        Calculate the sunset time for given date.
        :param date: Reference date. Today if not provided.
        :return: UTC sunset datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date.
        """
        date = localtime() if date is None else date
        ss = self._calc_sun_time(date, False)
        if ss is None:
            raise SunTimeException('The sun never sets on this location (on the specified date)')
        else:
            return ss

    def _calc_sun_time(self, date, isrisetime=True, zenith=90.8):
        """
        Calculate sunrise or sunset date.
        :param date: Reference date (localtime())
        :param isRiseTime: True if you want to calculate sunrise time.
        :param zenith: Sun reference zenith
        :return: UTC sunset or sunrise datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date
        """
        # isRiseTime == False, returns sunsetTime
        day = date[2]
        month = date[1]
        year = date[0]
        to_rad = math.pi / 180.0

        # 1. first calculate the day of the year
        n1 = math.floor(275 * month / 9)
        n2 = math.floor((month + 9) / 12)
        n3 = (1 + math.floor((year - 4 * math.floor(year / 4) + 2) / 3))
        n = n1 - (n2 * n3) + day - 30

        # 2. convert the longitude to hour value and calculate an approximate time
        lnghour = self._lon / 15

        if isrisetime:
            t = n + ((6 - lnghour) / 24)
        else:  # sunset
            t = n + ((18 - lnghour) / 24)

        # 3. calculate the Sun's mean anomaly
        m = (0.9856 * t) - 3.289

        # 4. calculate the Sun's true longitude
        ll = m + (1.916 * math.sin(to_rad * m)) + (0.020 * math.sin(to_rad * 2 * m)) + 282.634
        ll = self._force_range(ll, 360)  # NOTE: L adjusted into the range [0,360)

        # 5a. calculate the Sun's right ascension

        ra = (1 / to_rad) * math.atan(0.91764 * math.tan(to_rad * ll))
        ra = self._force_range(ra, 360)  # NOTE: RA adjusted into the range [0,360)

        # 5b. right ascension value needs to be in the same quadrant as L
        lquadrant = (math.floor(ll / 90)) * 90
        raquadrant = (math.floor(ra / 90)) * 90
        ra = ra + (lquadrant - raquadrant)

        # 5c. right ascension value needs to be converted into hours
        ra = ra / 15

        # 6. calculate the Sun's declination
        sindec = 0.39782 * math.sin(to_rad * ll)
        cosdec = math.cos(math.asin(sindec))

        # 7a. calculate the Sun's local hour angle
        cosh = (math.cos(to_rad * zenith) - (sindec * math.sin(to_rad * self._lat))) / (
                    cosdec * math.cos(to_rad * self._lat))

        if cosh > 1:
            return None  # The sun never rises on this location (on the specified date)
        if cosh < -1:
            return None  # The sun never sets on this location (on the specified date)

        # 7b. finish calculating H and convert into hours

        if isrisetime:
            h = 360 - (1 / to_rad) * math.acos(cosh)
        else:  # setting
            h = (1 / to_rad) * math.acos(cosh)

        h = h / 15

        # 8. calculate local mean time of rising/setting
        t = h + ra - (0.06571 * t) - 6.622

        # 9. adjust back to UTC
        ut = t - lnghour
        ut = self._force_range(ut, 24)  # UTC time in decimal format (e.g. 23.23)

        # 10. Return
        hr = self._force_range(int(ut), 24)
        minutes = round((ut - int(ut)) * 60, 0)
        if minutes == 60:
            hr += 1
            minutes = 0
        return year, month, day, hr + self._tzone, int(minutes)

    @staticmethod
    def _force_range(v, maximum):
        # force v to be >= 0 and < max
        if v < 0:
            return v + maximum
        elif v >= maximum:
            return v - maximum
        return v

User avatar
pythoncoder
Posts: 4940
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Astral port/alternative

Post by pythoncoder » Sun Feb 21, 2021 9:28 am

The day of the year calculation interests me. It works, but its logic eludes me. This is perhaps easier to follow:

Code: Select all

t = time.mktime((year, month, day, 0, 0, 0, 0, 0))
t0 = time.mktime((year, 1, 1,  0, 0, 0, 0, 0))  # Jan 1st
day_of_year = 1 + (t - t0)//(24*3600)  # Jan 1st is day 1
Peter Hinch

Post Reply