########################################################################################
#
# 95_Astro.pm
#
# Collection of various routines for astronomical data
# Prof. Dr. Peter A. Henning
#
# Equations from "Practical Astronomy with your Calculator" by Peter Duffett-Smith
# Program skeleton (with some errors) by Arnold Barmettler 
# http://lexikon.astronomie.info/java/sunmoon/
#
#  $Id: 95_Astro.pm 25198 2021-11-07 15:42:54Z phenning $
#
########################################################################################
#
#  This programm is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  The GNU General Public License can be found at
#  http://www.gnu.org/copyleft/gpl.html.
#  A copy is found in the textfile GPL.txt and important notices to the license
#  from the author is found in LICENSE.txt distributed with these scripts.
#
#  This script is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
########################################################################################

package FHEM::Astro;
use strict;
use warnings; 
use POSIX;
use utf8;

use Encode;
use FHEM::Meta;
use GPUtils qw(GP_Import GP_Export);
use Math::Trig;
use Time::HiRes qw(gettimeofday);
use Time::Local;
use UConv;
#use Data::Dumper;

my $DEG = pi/180.0;
my $RAD = 180./pi;

my $deltaT   = 65;  # Correction time in s

my %Astro;
my %Date;

#-- These we may set on request
my %sets = (
    "update" => "noArg",
);

#-- These we may get on request
my %gets = (
    "json"    => undef,
    "text"    => undef,
    "version" => "noArg",
);

#-- These we may configure
my %attrs = (
    "altitude"    => undef,
    "disable"     => "1,0",
    "horizon"     => undef,
    "interval"    => undef,
    "language"    => "EN,DE,ES,FR,IT,NL,PL",
    "latitude"    => undef,
    "lc_numeric"  => "en_EN.UTF-8,de_DE.UTF-8,es_ES.UTF-8,fr_FR.UTF-8,it_IT.UTF-8,nl_NL.UTF-8,pl_PL.UTF-8",
    "lc_time"     => "en_EN.UTF-8,de_DE.UTF-8,es_ES.UTF-8,fr_FR.UTF-8,it_IT.UTF-8,nl_NL.UTF-8,pl_PL.UTF-8",
    "longitude"   => undef,
    "recomputeAt" => "multiple-strict,MoonRise,MoonSet,MoonTransit,NewDay,SunRise,SunSet,SunTransit,AstroTwilightEvening,AstroTwilightMorning,CivilTwilightEvening,CivilTwilightMorning,CustomTwilightEvening,CustomTwilightMorning",
    "timezone"    => undef,
);

my $json;
my $tt;

#-- Export variables to other programs
our %transtable = (
    EN => {
        "coord"             => "Coordinates",
        "position"          => "Position",
        "longitude"         => "Longitude",
        "latitude"          => "Latitude",
        "altitude"          => "Height a.s.l.",
        "lonecl"            => "Ecliptical longitude",
        "latecl"            => "Ecliptical latitude",
        "ra"                => "Right ascension",
        "dec"               => "Declination",
        "az"                => "Azimuth",
        "alt"               => "Horizontal altitude",
        "age"               => "Age",
        "rise"              => "Rise",
        "set"               => "Set",
        "transit"           => "Transit",
        "distance"          => "Distance",
        "diameter"          => "Diameter",
        "toobs"             => "to observer",
        "toce"              => "to Earth center",
        "progress"          => "Progress",
        "twilightcivil"     => "Civil twilight",
        "twilightnautic"    => "Nautical twilight",
        "twilightastro"     => "Astronomical twilight",
        "twilightcustom"    => "Custom twilight",
        "sign"              => "Zodiac sign",
        "dst"               => "daylight saving time",
        "nt"                => "standard time",
        "switchtodst"       => "Time change ahead to daylight saving time",
        "switchtont"        => "Time change back to standard time",
        "hoursofsunlight"   => "Hours of sunlight",
        "hoursofnight"      => "Hours of night",
        "hoursofvisibility" => "Visibility",

        #--
        "time"          => "Time",
        "date"          => "Date",
        "jdate"         => "Julian date",
        "dayofyear"     => "day of year",
        "days"          => "days",
        "timezone"      => "Time Zone",
        "lmst"          => "Local Sidereal Time",

        #--
        "season" => "Astronomical Season",
        "spring" => "Spring",
        "summer" => "Summer",
        "fall"   => "Fall",
        "winter" => "Winter",

        #--
        "aries"       => "Ram",
        "taurus"      => "Bull",
        "gemini"      => "Twins",
        "cancer"      => "Crab",
        "leo"         => "Lion",
        "virgo"       => "Maiden",
        "libra"       => "Scales",
        "scorpio"     => "Scorpion",
        "sagittarius" => "Archer",
        "capricorn"   => "Goat",
        "aquarius"    => "Water Bearer",
        "pisces"      => "Fish",

        #--
        "sun" => "Sun",

        #--
        "moon"           => "Moon",
        "phase"          => "Phase",
        "newmoon"        => "New Moon",
        "waxingcrescent" => "Waxing Crescent",
        "firstquarter"   => "First Quarter",
        "waxingmoon"     => "Waxing Moon",
        "fullmoon"       => "Full Moon",
        "waningmoon"     => "Waning Moon",
        "lastquarter"    => "Last Quarter",
        "waningcrescent" => "Waning Crescent",
    },

    DE => {
        "coord"             => "Koordinaten",
        "position"          => "Position",
        "longitude"         => "Länge",
        "latitude"          => "Breite",
        "altitude"          => "Höhe ü. NHN",
        "lonecl"            => "Eklipt. Länge",
        "latecl"            => "Eklipt. Breite",
        "ra"                => "Rektaszension",
        "dec"               => "Deklination",
        "az"                => "Azimut",
        "alt"               => "Horizontwinkel",
        "age"               => "Alter",
        "phase"             => "Phase",
        "rise"              => "Aufgang",
        "set"               => "Untergang",
        "transit"           => "Kulmination",
        "distance"          => "Entfernung",
        "diameter"          => "Durchmesser",
        "toobs"             => "z. Beobachter",
        "toce"              => "z. Erdmittelpunkt",
        "progress"          => "Fortschritt",
        "twilightcivil"     => "Bürgerliche Dämmerung",
        "twilightnautic"    => "Nautische Dämmerung",
        "twilightastro"     => "Astronomische Dämmerung",
        "twilightcustom"    => "Konfigurierte Dämmerung",
        "sign"              => "Tierkreiszeichen",
        "dst"               => "Sommerzeit",
        "nt"                => "Normalzeit",
        "switchtodst"       => "Umstellung vorwärts auf Sommerzeit",
        "switchtont"        => "Umstellung zurück auf Normalzeit",
        "hoursofsunlight"   => "Tagesstunden",
        "hoursofnight"      => "Nachtstunden",
        "hoursofvisibility" => "Sichtbarkeit",

        #--
        "time"          => "Zeit",
        "date"          => "Datum",
        "jdate"         => "Julianisches Datum",
        "dayofyear"     => "Tag d. Jahres",
        "days"          => "Tage",
        "timezone"      => "Zeitzone",
        "lmst"          => "Lokale Sternzeit",

        #--
        "season" => "Astronomische Jahreszeit",
        "spring" => "Frühling",
        "summer" => "Sommer",
        "fall"   => "Herbst",
        "winter" => "Winter",

        #--
        "aries"       => "Widder",
        "taurus"      => "Stier",
        "gemini"      => "Zwillinge",
        "cancer"      => "Krebs",
        "leo"         => "Löwe",
        "virgo"       => "Jungfrau",
        "libra"       => "Waage",
        "scorpio"     => "Skorpion",
        "sagittarius" => "Schütze",
        "capricorn"   => "Steinbock",
        "aquarius"    => "Wassermann",
        "pisces"      => "Fische",

        #--
        "sun" => "Sonne",

        #--
        "moon"           => "Mond",
        "phase"          => "Phase",
        "newmoon"        => "Neumond",
        "waxingcrescent" => "Zunehmende Sichel",
        "firstquarter"   => "Erstes Viertel",
        "waxingmoon"     => "Zunehmender Mond",
        "fullmoon"       => "Vollmond",
        "waningmoon"     => "Abnehmender Mond",
        "lastquarter"    => "Letztes Viertel",
        "waningcrescent" => "Abnehmende Sichel",
    },

    ES => {
        "coord"             => "Coordenadas",
        "position"          => "Posición",
        "longitude"         => "Longitud",
        "latitude"          => "Latitud",
        "altitude"          => "Altura sobre el mar",
        "lonecl"            => "Longitud eclíptica",
        "latecl"            => "Latitud eclíptica",
        "ra"                => "Ascensión recta",
        "dec"               => "Declinación",
        "az"                => "Azimut",
        "alt"               => "Ángulo horizonte",
        "age"               => "Años",
        "rise"              => "Salida",
        "set"               => "Puesta",
        "transit"           => "Culminación",
        "distance"          => "Distancia",
        "diameter"          => "Diámetro",
        "toobs"             => "al observar",
        "toce"              => "al centro de la tierra",
        "progress"          => "Progreso",
        "twilightcivil"     => "Crepúsculo civil",
        "twilightnautic"    => "Crepúsculo náutico",
        "twilightastro"     => "Crepúsculo astronómico",
        "twilightcustom"    => "Crepúsculo personalizado",
        "sign"              => "Signo del zodiaco",
        "dst"               => "horario de verano",
        "nt"                => "hora estándar",
        "switchtodst"       => "Conversión adelante a la hora de verano",
        "switchtont"        => "Conversión hacia atrás a la hora estándar",
        "hoursofsunlight"   => "Horas de luz solar",
        "hoursofnight"      => "Horas de la noche",
        "hoursofvisibility" => "Visibilidad",

        #--
        "time"          => "Tiempo",
        "date"          => "Fecha",
        "jdate"         => "Fecha de Julian",
        "dayofyear"     => "Día del año",
        "days"          => "Días",
        "timezone"      => "Zona horaria",
        "lmst"          => "Hora sideral local",

        #--
        "season" => "Temporada Astronomica",
        "spring" => "Primavera",
        "summer" => "Verano",
        "fall"   => "Otoño",
        "winter" => "Invierno",

        #--
        "aries"       => "Aries",
        "taurus"      => "Tauro",
        "gemini"      => "Geminis",
        "cancer"      => "Cáncer",
        "leo"         => "León",
        "virgo"       => "Virgo",
        "libra"       => "Libra",
        "scorpio"     => "Escorpión",
        "sagittarius" => "Sagitario",
        "capricorn"   => "Capricornio",
        "aquarius"    => "Acuario",
        "pisces"      => "Piscis",

        #--
        "sun" => "Sol",

        #--
        "moon"           => "Luna",
        "phase"          => "Fase",
        "newmoon"        => "Luna nueva",
        "waxingcrescent" => "Luna creciente",
        "firstquarter"   => "Primer cuarto",
        "waxingmoon"     => "Luna creciente",
        "fullmoon"       => "Luna llena",
        "waningmoon"     => "Luna menguante",
        "lastquarter"    => "Último cuarto",
        "waningcrescent" => "Creciente menguante",
    },

    FR => {
        "coord"             => "Coordonnées",
        "position"          => "Position",
        "longitude"         => "Longitude",
        "latitude"          => "Latitude",
        "altitude"          => "Hauteur au dessus de la mer",
        "lonecl"            => "Longitude écliptique",
        "latecl"            => "Latitude écliptique",
        "ra"                => "Ascension droite",
        "dec"               => "Déclinaison",
        "az"                => "Azimut",
        "alt"               => "Angle horizon",
        "age"               => "Âge",
        "rise"              => "Lever",
        "set"               => "Coucher",
        "transit"           => "Culmination",
        "distance"          => "Distance",
        "diameter"          => "Diamètre",
        "toobs"             => "à l'observateur",
        "toce"              => "au centre de la terre",
        "progress"          => "Progrès",
        "twilightcivil"     => "Crépuscule civil",
        "twilightnautic"    => "Crépuscule nautique",
        "twilightastro"     => "Crépuscule astronomique",
        "twilightcustom"    => "Crépuscule personnalisé",
        "sign"              => "Signe du zodiaque",
        "dst"               => "heure d'été",
        "nt"                => "heure normale",
        "switchtodst"       => "Conversion en avant à l'heure d'été",
        "switchtont"        => "Conversion de retour à l'heure normale",
        "hoursofsunlight"   => "Heures de soleil",
        "hoursofnight"      => "Heures de la nuit",
        "hoursofvisibility" => "Visibilité",

        #--
        "time"          => "Temps",
        "date"          => "Date",
        "jdate"         => "Date de Julien",
        "dayofyear"     => "jour de l'année",
        "days"          => "jours",
        "timezone"      => "Fuseau horaire",
        "lmst"          => "Heure sidérale locale",

        #--
        "season" => "Saison Astronomique",
        "spring" => "Printemps",
        "summer" => "Été",
        "fall"   => "Automne",
        "winter" => "Hiver",

        #--
        "aries"       => "bélier",
        "taurus"      => "Taureau",
        "gemini"      => "Gémeaux",
        "cancer"      => "Cancer",
        "leo"         => "Lion",
        "virgo"       => "Jeune fille",
        "libra"       => "Balance",
        "scorpio"     => "Scorpion",
        "sagittarius" => "Sagittaire",
        "capricorn"   => "Capricorne",
        "aquarius"    => "Verseau",
        "pisces"      => "Poissons",

        #--
        "sun" => "Soleil",

        #--
        "moon"           => "Lune",
        "phase"          => "Phase",
        "newmoon"        => "Nouvelle lune",
        "waxingcrescent" => "Croissant croissant",
        "firstquarter"   => "Premier quart",
        "waxingmoon"     => "Lune croissante",
        "fullmoon"       => "Pleine lune",
        "waningmoon"     => "Lune décroissante",
        "lastquarter"    => "Le dernier quart",
        "waningcrescent" => "Croissant décroissant",
    },

    IT => {
        "coord"             => "Coordinate",
        "position"          => "Posizione",
        "longitude"         => "Longitudine",
        "latitude"          => "Latitudine",
        "altitude"          => "Altezza sopra il mare",
        "lonecl"            => "Longitudine ellittica",
        "latecl"            => "Latitudine eclittica",
        "ra"                => "Giusta ascensione",
        "dec"               => "Declinazione",
        "az"                => "Azimut",
        "alt"               => "Angolo di orizzonte",
        "age"               => "Età",
        "rise"              => "Crescente",
        "set"               => "Affondamento",
        "transit"           => "Culmine",
        "distance"          => "Distanza",
        "diameter"          => "Diametro",
        "toobs"             => "verso l'osservatore",
        "toce"              => "verso centro della terra",
        "progress"          => "Progresso",
        "twilightcivil"     => "Crepuscolo civile",
        "twilightnautic"    => "Crepuscolo nautico",
        "twilightastro"     => "Crepuscolo astronomico",
        "twilightcustom"    => "Crepuscolo personalizzato",
        "sign"              => "Segno zodiacale",
        "dst"               => "ora legale",
        "nt"                => "ora standard",
        "switchtodst"       => "Conversione in avanti a ora legale",
        "switchtont"        => "Conversione a ritroso in ora standard",
        "hoursofsunlight"   => "Ore di luce solare",
        "hoursofnight"      => "Ore della notte",
        "hoursofvisibility" => "Visibilità",

        #--
        "time"          => "Tempo",
        "date"          => "Data",
        "jdate"         => "Data giuliana",
        "dayofyear"     => "giorno dell'anno",
        "days"          => "giorni",
        "timezone"      => "Fuso orario",
        "lmst"          => "Tempo siderale locale",

        #--
        "season" => "Stagione Astronomica",
        "spring" => "Stagione primaverile",
        "summer" => "Estate",
        "fall"   => "Autunno",
        "winter" => "Inverno",

        #--
        "aries"       => "Ariete",
        "taurus"      => "Toro",
        "gemini"      => "Gemelli",
        "cancer"      => "Cancro",
        "leo"         => "Leone",
        "virgo"       => "Vergine",
        "libra"       => "Libra",
        "scorpio"     => "Scorpione",
        "sagittarius" => "Arciere",
        "capricorn"   => "Capricorno",
        "aquarius"    => "Acquario",
        "pisces"      => "Pesci",

        #--
        "sun" => "Sole",

        #--
        "moon"           => "Luna",
        "phase"          => "Fase",
        "newmoon"        => "Nuova luna",
        "waxingcrescent" => "Luna crescente",
        "firstquarter"   => "Primo quarto",
        "waxingmoon"     => "Luna crescente",
        "fullmoon"       => "Luna piena",
        "waningmoon"     => "Luna calante",
        "lastquarter"    => "Ultimo quarto",
        "waningcrescent" => "Pericolo crescente",
    },

    NL => {
        "coord"             => "Coördinaten",
        "position"          => "Positie",
        "longitude"         => "Lengtegraad",
        "latitude"          => "Breedtegraad",
        "altitude"          => "Hoogte b. Zee",
        "lonecl"            => "Eclipticale Lengtegraad",
        "latecl"            => "Eclipticale Breedtegraad",
        "ra"                => "Juiste klimming",
        "dec"               => "Declinatie",
        "az"                => "Azimuth",
        "alt"               => "Horizon Angle",
        "age"               => "Leeftijd",
        "rise"              => "Opkomst",
        "set"               => "Ondergang",
        "transit"           => "Culminatie",
        "distance"          => "Afstand",
        "diameter"          => "Diameter",
        "toobs"             => "voor de Waarnemer",
        "toce"              => "naar het Middelpunt van de Aarde",
        "progress"          => "Vooruitgang",
        "twilightcivil"     => "Burgerlijke Schemering",
        "twilightnautic"    => "Nautische Schemering",
        "twilightastro"     => "Astronomische Schemering",
        "twilightcustom"    => "Aangepaste Schemering",
        "sign"              => "Sterrenbeeld",
        "dst"               => "Zomertijd",
        "nt"                => "Standaardtijd",
        "switchtodst"       => "Voorwaartse omschakeling naar de zomertijd",
        "switchtont"        => "Omzetting terug naar de normale tijd",
        "hoursofsunlight"   => "Dagen Uur",
        "hoursofnight"      => "Uren van de Nacht",
        "hoursofvisibility" => "Zichtbaarheid",

        #--
        "time"          => "Tijd",
        "date"          => "Datum",
        "jdate"         => "Juliaanse Datum",
        "dayofyear"     => "Dag van het Jaar",
        "days"          => "Dagen",
        "timezone"      => "Tijdzone",
        "lmst"          => "Lokale Sterrentijd",

        #--
        "season" => "Astronomisch Seizoen",
        "spring" => "De lente",
        "summer" => "Zomer",
        "fall"   => "Herfst",
        "winter" => "Winter",

        #--
        "aries"       => "Ram",
        "taurus"      => "Stier",
        "gemini"      => "Tweelingen",
        "cancer"      => "Kanker",
        "leo"         => "Leeuw",
        "virgo"       => "Maagd",
        "libra"       => "Weegschaal",
        "scorpio"     => "Schorpioen",
        "sagittarius" => "Boogschutter",
        "capricorn"   => "Steenbok",
        "aquarius"    => "Waterman",
        "pisces"      => "Vis",

        #--
        "sun" => "Zon",

        #--
        "moon"           => "Maan",
        "phase"          => "Fase",
        "newmoon"        => "Nieuwe Maan",
        "waxingcrescent" => "Wassende halve Maan",
        "firstquarter"   => "Eerste Kwartier",
        "waxingmoon"     => "Wassende Maan",
        "fullmoon"       => "Volle Maan",
        "waningmoon"     => "Afnemende Maan",
        "lastquarter"    => "Het laatste Kwartier",
        "waningcrescent" => "Afnemende halve Maan",
    },

    PL => {
        "coord"             => "Współrzędne",
        "position"          => "Pozycja",
        "longitude"         => "Długość",
        "latitude"          => "Szerokość",
        "altitude"          => "Wysokość nad morzem",
        "lonecl"            => "Długość ekliptyczna",
        "latecl"            => "Szerokość ekliptyczna",
        "ra"                => "Rektascencja",
        "dec"               => "Deklinacja",
        "az"                => "Azymut",
        "alt"               => "Kąt horyzont",
        "age"               => "Wiek",
        "rise"              => "Wschód",
        "set"               => "Zachód",
        "transit"           => "Kulminacja",
        "distance"          => "Dystans",
        "diameter"          => "Średnica",
        "toobs"             => "w kierunku obserwatora",
        "toce"              => "w kierunku środka ziemi",
        "progress"          => "Postęp",
        "twilightcivil"     => "Zmierzch cywilny",
        "twilightnautic"    => "Zmierzch morski",
        "twilightastro"     => "Zmierzch astronomiczny",
        "twilightcustom"    => "Zmierzch niestandardowy",
        "sign"              => "Znak zodiaku",
        "dst"               => "czas letni",
        "nt"                => "standardowy czas",
        "switchtodst"       => "Przeliczenie napastnicy na czas letni",
        "switchtont"        => "Przeliczanie wstecz do czasu normalnego",
        "hoursofsunlight"   => "Godziny światła słonecznego",
        "hoursofnight"      => "Godziny nocy",
        "hoursofvisibility" => "Widoczność",

        #--
        "time"          => "Czas",
        "date"          => "Data",
        "jdate"         => "Juliańska data",
        "dayofyear"     => "dzień roku",
        "days"          => "dni",
        "timezone"      => "Strefa czasowa",
        "lmst"          => "Lokalny czas gwiazdowy",

        #--
        "season" => "Sezon Astronomiczny",
        "spring" => "Wiosna",
        "summer" => "Lato",
        "fall"   => "Jesień",
        "winter" => "Zima",

        #--
        "aries"       => "Baran",
        "taurus"      => "Byk",
        "gemini"      => "Bliźnięta",
        "cancer"      => "Rak",
        "leo"         => "Lew",
        "virgo"       => "Panna",
        "libra"       => "Libra",
        "scorpio"     => "Skorpion",
        "sagittarius" => "Strzelec",
        "capricorn"   => "Koziorożec",
        "aquarius"    => "Wodnik",
        "pisces"      => "Ryby",

        #--
        "sun" => "Słońce",

        #--
        "moon"           => "Księżyc",
        "phase"          => "Faza",
        "newmoon"        => "Nów",
        "waxingcrescent" => "Półksiężyc woskowy",
        "firstquarter"   => "Pierwszym kwartale",
        "waxingmoon"     => "Księżyc przybywający",
        "fullmoon"       => "Pełnia księżyca",
        "waningmoon"     => "Zmniejszający się księżyc",
        "lastquarter"    => "Ostatni kwartał",
        "waningcrescent" => "Zwiększający się księżyc",
    }
);

our @zodiac = ("aries","taurus","gemini","cancer","leo","virgo",
    "libra","scorpio","sagittarius","capricorn","aquarius","pisces");

our @phases = ("newmoon","waxingcrescent", "firstquarter", "waxingmoon", 
    "fullmoon", "waningmoon", "lastquarter", "waningcrescent");

our %seasons = (
    N => [ "winter", "spring", "summer", "fall" ],
    S => [ "summer", "fall", "winter", "spring" ]
);

#-- Run before package compilation
BEGIN {

    # Import from main context
    GP_Import(
        qw(
          attr
          AttrVal
          data
          Debug
          defs
          deviceEvents
          FmtDateTime
          FW_pO
          FW_RET
          FW_RETTYPE
          FW_webArgs
          GetType
          init_done
          InternalTimer
          IsDisabled
          Log
          Log3
          maxNum
          minNum
          modules
          readingFnAttributes
          readingsBeginUpdate
          readingsBulkUpdateIfChanged
          readingsEndUpdate
          readingsSingleUpdate
          RemoveInternalTimer
          time_str2num
          toJSON
          )
    );

    # Export to main context
    GP_Export(
        qw(
          Get
          Initialize
          )
    );

    # Export to main context with different name
    no strict qw/refs/;
    *{'main::asunrise_rel'} = *{ 'FHEM::Astro::SUNRISE_EL_sunrise_rel' };
    *{'main::asunset_rel'}  = *{ 'FHEM::Astro::SUNRISE_EL_sunset_rel' };
    *{'main::asunrise_abs'} = *{ 'FHEM::Astro::SUNRISE_EL_sunrise_abs' };
    *{'main::asunset_abs'}  = *{ 'FHEM::Astro::SUNRISE_EL_sunset_abs' };
    *{'main::asunrise'} = *{ 'FHEM::Astro::SUNRISE_EL_sunrise' };
    *{'main::asunset'}  = *{ 'FHEM::Astro::SUNRISE_EL_sunset' };
    *{'main::aisday'}   = *{ 'FHEM::Astro::SUNRISE_EL_isday' };
    *{'main::asunrise_abs_dat'} = *{ 'FHEM::Astro::SUNRISE_EL_sunrise_abs_dat' };
    *{'main::asunset_abs_dat'}  = *{ 'FHEM::Astro::SUNRISE_EL_sunset_abs_dat' };
    use strict qw/refs/;
}

_LoadOptionalPackages();

sub SunRise($$$$$$$$);
sub MoonRise($$$$$$$);
sub SetTime(;$$$);
sub Compute($;$);

########################################################################################################
#
# Initialize 
# 
#  Parameter hash = hash of device addressed 
#
########################################################################################################

sub Initialize ($) {
  my ($hash) = @_;
		
  $hash->{DefFn}       = "FHEM::Astro::Define";
  $hash->{SetFn}       = "FHEM::Astro::Set";  
  $hash->{GetFn}       = "FHEM::Astro::Get";
  $hash->{UndefFn}     = "FHEM::Astro::Undef";   
  $hash->{AttrFn}      = "FHEM::Astro::Attr";    
  $hash->{NotifyFn}    = "FHEM::Astro::Notify";
  $hash->{AttrList}    = join (" ", map {
                                    defined($attrs{$_}) ? "$_:$attrs{$_}" : $_
                                  } sort keys %attrs
                         )
                         ." "
                         .$readingFnAttributes;

  $hash->{parseParams} = 1;
  $hash->{NotifyOrderPrefix} = '45-';    # we are a data provider

  $data{FWEXT}{"/Astro_moonwidget"}{FUNC} = "FHEM::Astro::Moonwidget";
  $data{FWEXT}{"/Astro_moonwidget"}{FORKABLE} = 0;		

  return FHEM::Meta::InitMod( __FILE__, $hash );
}

########################################################################################################
#
# Define - Implements DefFn function
# 
#  Parameter hash = hash of device addressed, a = array of arguments, h = hash of parameters
#
########################################################################################################

sub Define ($@) {
 my ($hash,$a,$h) = @_;
 my $name = shift @$a;
 my $type = shift @$a;
 my $global = shift @$a;

 return $@ unless ( FHEM::Meta::SetInternals($hash) );
 use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' );
 return 'Astro may not be run on Windows systems' if $^O eq 'MSWin32';

 if ($global) {
   return "$type global device $modules{$type}{global}{NAME} is already defined"
     if ( defined( $modules{$type}{global} )
       && $modules{$type}{global}{NAME} ne $name );
   $modules{$type}{global} = $hash;
   $hash->{SCOPE} = 'global';
 }

 $hash->{NOTIFYDEV} = "global";
 $hash->{INTERVAL} = 3600;
 readingsSingleUpdate( $hash, "state", "Initialized", $init_done ); 
 
 $modules{Astro}{defptr}{$name} = $hash;

 # for the very first definition, set some default attributes
 if ( $init_done && !defined( $hash->{OLDDEF} ) ) {
   $attr{$name}{icon}        = 'telescope';
   $attr{$name}{recomputeAt} = 'NewDay,SunRise,SunSet,AstroTwilightEvening,AstroTwilightMorning,CivilTwilightEvening,CivilTwilightMorning,CustomTwilightEvening,CustomTwilightMorning';
   $hash->{RECOMPUTEAT} = $attr{$name}{recomputeAt};
 }

 return undef;
}

########################################################################################################
#
# Undef - Implements Undef function
# 
#  Parameter hash = hash of device addressed, arg = array of arguments
#
########################################################################################################

sub Undef ($$) {
  my ($hash,$arg) = @_;
  my $name = $hash->{NAME};
  my $type = $hash->{TYPE};
  
  RemoveInternalTimer($hash);

  delete $modules{$type}{defptr}{$name};

  if ( defined( $modules{$type}{global} )
      && $modules{$type}{global}{NAME} eq $name )
  {
      delete $modules{$type}{global};
      Debug "Yes";
  }
  
  return undef;
}

########################################################################################################
#
# Notify - Implements Notify function
# 
#  Parameter hash = hash of device addressed, dev = hash of device that triggered notification
#
########################################################################################################

sub Notify ($$) {
  my ($hash,$dev) = @_;
  my $name    = $hash->{NAME};
  my $TYPE    = $hash->{TYPE};
  my $devName = $dev->{NAME};
  my $devType = GetType($devName);

  return "" if ( IsDisabled($name) );

  if ( $devName eq "global" ) {
    my $events = deviceEvents( $dev, 1 );
    return "" unless ($events);

    foreach my $event ( @{$events} ) {
      next unless ( defined($event) );
      next if ( $event =~ m/^[A-Za-z\d_-]+:/ );

      if ( $event =~ m/^INITIALIZED|REREADCFG$/ ) {
        if ( ( defined( $hash->{INTERVAL} ) && $hash->{INTERVAL} > 0 )
            || defined( $hash->{RECOMPUTEAT} ) )
        {
            RemoveInternalTimer($hash);
            InternalTimer(gettimeofday()+5,"FHEM::Astro::Update",$hash,0);
        }
      }
      elsif ( $event =~
          m/^(DEFINED|MODIFIED)\s+([A-Za-z\d_-]+)$/ &&
          $2 eq $name )
      {
        if ( ( defined( $hash->{INTERVAL} ) && $hash->{INTERVAL} > 0 )
            || defined( $hash->{RECOMPUTEAT} ) )
        {
          RemoveInternalTimer($hash);
          InternalTimer(gettimeofday()+1,"FHEM::Astro::Update",$hash,0);
        }
      }

      # only process attribute events
      next
        unless ( $event =~
m/^((?:DELETE)?ATTR)\s+([A-Za-z\d._]+)\s+([A-Za-z\d_\.\-\/]+)(?:\s+(.*)\s*)?$/
        );

      my $cmd  = $1;
      my $d    = $2;
      my $attr = $3;
      my $val  = $4;
      my $type = GetType($d);

      # filter attributes to be processed
      next
        unless ( $attr eq "altitude"
          || $attr eq "language"
          || $attr eq "latitude"
          || $attr eq "lc_numeric"
          || $attr eq "lc_time"
          || $attr eq "longitude"
          || $attr eq "timezone" );

      # when global attributes were changed
      if ( $d eq "global" ) {
        RemoveInternalTimer($hash);
        InternalTimer( gettimeofday() + 1,
            "FHEM::Astro::Update", $hash, 0 );
      }
    }
  }

  return undef;
}

########################################################################################################
#
# Attr - Implements Attr function
# 
#  Parameter do = action, name = name of device, key = attribute name, value = attribute value
#
########################################################################################################

sub Attr(@) {
  my ($do,$name,$key,$value) = @_;
  
  my $hash = $defs{$name};
  my $ret;
  
  if ( $do eq "set") {
    ARGUMENT_HANDLER: {
      #-- altitude modified at runtime
      $key eq "altitude" and do {
        #-- check value
        return "[Astro] $do $name attribute $key must be a float number >= 0 meters"
          unless($value =~ m/^(\d+(?:\.\d+)?)$/ && $1 >= 0.);
      };
      #-- disable modified at runtime
      $key eq "disable" and do {
        #-- check value
        return "[Astro] $do $name attribute $key can only be 1 or 0"
          unless($value =~ m/^(1|0)$/);
        readingsSingleUpdate($hash,"state",$value?"inactive":"Initialized",$init_done);
      };
      #-- horizon modified at runtime
      $key eq "horizon" and do {
        #-- check value
        return "[Astro] $do $name attribute $key must be a float number >= -45 and <= 45 degrees"
          unless($value =~ m/^(-?\d+(?:\.\d+)?)(?::(-?\d+(?:\.\d+)?))?$/ && $1 >= -45. && $1 <= 45. && (!$2 || $2 >= -45. && $2 <= 45.));
      };
      #-- interval modified at runtime
      $key eq "interval" and do {
        #-- check value
        return "[Astro] $do $name attribute $key must be >= 0 seconds"
          unless($value =~ m/^\d+$/);
        #-- update timer
        $hash->{INTERVAL} = $value;
      };
      #-- latitude modified at runtime
      $key eq "latitude" and do {
        #-- check value
        return "[Astro] $do $name attribute $key must be float number >= -90 and <= 90 degrees"
          unless($value =~ m/^(-?\d+(?:\.\d+)?)$/ && $1 >= -90. && $1 <= 90.);
      };
      #-- longitude modified at runtime
      $key eq "longitude" and do {
        #-- check value
        return "[Astro] $do $name attribute $key must be float number >= -180 and <= 180 degrees"
          unless($value =~ m/^(-?\d+(?:\.\d+)?)$/ && $1 >= -180. && $1 <= 180.);
      };
      #-- recomputeAt modified at runtime
      $key eq "recomputeAt" and do {
        my @skel = split(',', $attrs{recomputeAt});
        shift @skel;
        #-- check value 1/2
        return "[Astro] $do $name attribute $key must be one or many of ".join(',', @skel)
          if(!$value || $value eq "");
        #-- check value 2/2
        my @vals = split(',', $value);
        foreach my $val (@vals) {
          return "[Astro] $do $name attribute value $val is invalid, must be one or many of ".join(',', @skel)
            unless(grep( m/^$val$/, @skel ));          
        }
        $hash->{RECOMPUTEAT} = join(',', @vals);
      };
    }
  }

  elsif ( $do eq "del") {
    readingsSingleUpdate($hash,"state","Initialized",$init_done)
      if ($key eq "disable");
    $hash->{INTERVAL} = 3600
      if ($key eq "interval");
    delete $hash->{RECOMPUTEAT}
      if ($key eq "recomputeAt");
  }

  if (   $init_done
      && exists( $attrs{$key} )
      && ( $hash->{INTERVAL} > 0 || $hash->{RECOMPUTEAT} || $hash->{NEXTUPDATE} )
    )
  {
      RemoveInternalTimer($hash);
      InternalTimer( gettimeofday() + 2, "FHEM::Astro::Update", $hash, 0 );
  }

  return $ret;
}

sub _mod($$) { my ($a,$b)=@_;if( $a =~ /\d*\.\d*/){return($a-floor($a/$b)*$b)}else{return undef}; }
sub _mod2Pi($) { my ($x)=@_;$x = _mod($x, 2.*pi);return($x); }
sub _round($$) { my ($x,$n)=@_; return int(10**$n*$x+0.5)/10**$n};

sub _tzoffset($) {
    my ($t)   = @_;
    my $utc   = mktime(gmtime($t));
    #-- the following does not properly calculate dst
    my $local = mktime(localtime($t));
    #-- this is the correction
    my $isdst = (localtime($t))[8];
    #-- correction
    if($isdst == 1){
      $local+=3600;
    }
    return (($local - $utc)/36);
}

########################################################################################################
#
# _LoadOptionalPackages - Load Perl packages that may not be installed
# 
########################################################################################################

sub _LoadOptionalPackages {

    # JSON preference order
    local $ENV{PERL_JSON_BACKEND} =
      'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP'
      unless ( defined( $ENV{PERL_JSON_BACKEND} ) );

    # try to use JSON::MaybeXS wrapper
    #   for chance of better performance + open code
    eval {
        require JSON::MaybeXS;
        $json = JSON::MaybeXS->new;
        1;
    };
    if ($@) {
        $@ = undef;

        # try to use JSON wrapper
        #   for chance of better performance
        eval {
            require JSON;
            $json = JSON->new;
            1;
        };

        if ($@) {
            $@ = undef;

            # In rare cases, Cpanel::JSON::XS may
            #   be installed but JSON|JSON::MaybeXS not ...
            eval {
                require Cpanel::JSON::XS;
                $json = Cpanel::JSON::XS->new;
                1;
            };

            if ($@) {
                $@ = undef;

                # In rare cases, JSON::XS may
                #   be installed but JSON not ...
                eval {
                    require JSON::XS;
                    $json = JSON::XS->new;
                    1;
                };

                if ($@) {
                    $@ = undef;

                    # Fallback to built-in JSON which SHOULD
                    #   be available since 5.014 ...
                    eval {
                        require JSON::PP;
                        $json = JSON::PP->new;
                        1;
                    };

                    if ($@) {
                        $@ = undef;

                        # Last chance may be a backport
                        eval {
                            require JSON::backportPP;
                            $json = JSON::backportPP->new;
                            1;
                        };
                        $@ = undef if ($@);
                    }
                }
            }
        }
    }

    if ($@) {
      $@ = undef;
    } else {
      $json->allow_nonref;
      $json->shrink;
      $json->utf8;
    }

    # only available for Perl versions after 2018-06-09
    eval {
        import Time::Local 'timelocal_modern';
        1;
    };
    eval {
        import Time::Local 'timegm_modern';
        1;
    };
}

########################################################################################################
#
# subroutines for 99_SUNRISE_EL.pm compatibility layer
#
########################################################################################################

sub SUNRISE_EL_sr($$$$$$) {
    my ( $rise, $seconds, $isrel, $daycheck, $min, $max ) = @_;
    SUNRISE_EL_sr_alt(
        time(), $rise,         $isrel,   $daycheck,
        1,      $main::defaultaltit, $seconds, $min,
        $max
    );
}

sub SUNRISE_EL_sr_alt($$$$$$$$$) {
  my $nt=shift;
  my $rise=shift;
  my $isrel=shift;
  my $daycheck=shift;
  my $nextDay=shift;
  my $altit = defined($_[0]) ? $_[0] : "";
  my $hasalt = 0;
  if(exists $main::alti{uc($altit)}) {
      $hasalt = 1;
      $altit=$main::alti{uc($altit)};
      shift;
  } elsif($altit =~ /HORIZON=(([\-\+]*[0-9\.]+)(?::([\-\+]*[0-9\.]+))?)/i) {
      $hasalt = 1;
      $altit=$1;
      shift;
  } else {
      $altit=-6; #default
  }
  my($seconds, $min, $max)=@_;
  my $needrise = ($rise || $daycheck) ? 1 : 0;
  my $needset = (!$rise || $daycheck) ? 1 : 0;
  $seconds = 0 if(!$seconds);

  my $hash = defined( $modules{Astro}{global} ) ? $modules{Astro}{global} : ();
  my $name = defined( $hash->{NAME} ) ? $hash->{NAME} : '';

   ############################
   # If set in global, use longitude/latitude
   # from global, otherwise set Frankfurt/Germany as
   # default
   my $long = AttrVal( $name, "longitude", AttrVal( "global", "longitude", 10.0 ) );
   my $lat  = AttrVal( $name, "latitude",  AttrVal( "global", "latitude",  50.0 ) );
   $altit  = AttrVal( $name, "horizon",  AttrVal( "global", "horizon",  -6. ) ) unless($hasalt);
   Log3 $name ne '' ? $name : undef, $name ne '' ? 4 : 5,
        "[FHEM::Astro::SUNRISE_EL] "
      . ( $name ne '' ? "$name: " : '' )
      . "Compute sunrise/sunset for latitude $lat , longitude $long , horizon $altit at "
      . FmtDateTime($nt);

  #-- readjust timezone
  my $tz =
    AttrVal( $name, "timezone", AttrVal( "global", "timezone", undef ) );
  local $ENV{TZ} = $tz if ($tz);
  eval { tzset() if ( exists &{'tzset'} ) };

  #my $nt = time;
  my @lt = localtime($nt);
  my $gmtoff = main::_calctz($nt,@lt); # in hour

  my ($rt,$st) = _SUNRISE_EL_sr_alt($lat,$long,$altit,$needrise,$needset,$nt,$gmtoff);
  my $sst = ($rise ? $rt : $st) + ($seconds/3600);

  my $nh = $lt[2] + $lt[1]/60 + $lt[0]/3600;    # Current hour since midnight
  if($daycheck) {
    if(defined($min) && defined($max)) { #Forum #43742
      $min = main::hms2h($min); $max = main::hms2h($max);
      if($min < $max) {
        $rt = $min if($rt < $min);
        $st = $max if($st > $max);
      } else {
        $rt = $max if($rt > $max);
        $st = $min if($st < $min);
      }
    }
    return 1 if($rt <= $nh && $nh <= $st);
    return 0;
  }

  $sst = main::hms2h($min) if(defined($min) && (main::hms2h($min) > $sst));
  $sst = main::hms2h($max) if(defined($max) && (main::hms2h($max) < $sst));

  my $diff = 0;
  if (($data{AT_RECOMPUTE} ||                     # compute it for tommorow
    int(($nh-$sst)*3600) >= 0) && $nextDay)  {    # if called a subsec earlier
    $nt += 86400;
    @lt = localtime($nt);
    my $ngmtoff = main::_calctz($nt,@lt); # in hour
    $diff = 24;

    ($rt,$st) = _SUNRISE_EL_sr_alt($lat,$long,$altit,$needrise,$needset,$nt,$ngmtoff);
    $sst = ($rise ? $rt : $st) + ($seconds/3600);

    $sst = main::hms2h($min) if(defined($min) && (main::hms2h($min) > $sst));
    $sst = main::hms2h($max) if(defined($max) && (main::hms2h($max) < $sst));
  }

  $sst += $diff if($isrel);
  $sst -= $nh if($isrel == 1);

  delete local $ENV{TZ};
  tzset() if ( exists &{'tzset'} );

  return main::h2hms_fmt($sst);
}

sub _SUNRISE_EL_sr_alt($$$$$$$) {
    my ( $lat, $long, $altit, $needrise, $needset, $nt, $offset ) = @_;
    my $hash =
      defined( $modules{Astro}{global} ) ? $modules{Astro}{global} : ();
    my $name = defined( $hash->{NAME} ) ? $hash->{NAME} : '';

    my $horM = 0.0;
    my $horE = 0.0;
    if ( $altit =~ m/^([^:]+)(?::(.+))?$/ ) {
        $horM = $1;
        $horE = defined($2) ? $2 : $1;
    }

    my $tz =
      AttrVal( $name, "timezone", AttrVal( "global", "timezone", undef ) );
    SetTime( $nt, $tz );
    my $JD0 = Date2JD( $Date{day}, $Date{month}, $Date{year} );

    my (
        $suntransit,            $sunrise,
        $sunset,                $CivilTwilightMorning,
        $CivilTwilightEvening,  $NauticTwilightMorning,
        $NauticTwilightEvening, $AstroTwilightMorning,
        $AstroTwilightEvening,  $CustomTwilightMorning,
        $CustomTwilightEvening
      )
      = SunRise(
        $JD0, $deltaT,
        $long * $DEG,
        $lat * $DEG,
        $Date{zonedelta}, $horM, $horE, 0
      );

    return ( $needrise ? $CustomTwilightMorning : undef ),
      ( $needset       ? $CustomTwilightEvening : undef );
}

sub SUNRISE_EL_sunrise_rel(@) { return SUNRISE_EL_sr_alt(time(),1,1,0,1,shift,shift,shift,shift); }
sub SUNRISE_EL_sunset_rel (@) { return SUNRISE_EL_sr_alt(time(),0,1,0,1,shift,shift,shift,shift); }
sub SUNRISE_EL_sunrise_abs(@) { return SUNRISE_EL_sr_alt(time(),1,0,0,0,shift,shift,shift,shift); }
sub SUNRISE_EL_sunset_abs (@) { return SUNRISE_EL_sr_alt(time(),0,0,0,0,shift,shift,shift,shift); }
sub SUNRISE_EL_sunrise    (@) { return SUNRISE_EL_sr_alt(time(),1,2,0,1,shift,shift,shift,shift); }
sub SUNRISE_EL_sunset     (@) { return SUNRISE_EL_sr_alt(time(),0,2,0,1,shift,shift,shift,shift); }
sub SUNRISE_EL_isday      (@) { return SUNRISE_EL_sr_alt(time(),1,0,1,1,shift,shift,shift,shift); }

sub SUNRISE_EL_sunrise_abs_dat(@) {
  return SUNRISE_EL_sr_alt(main::sr_noon(shift),1,0,0,0,shift,shift,shift,shift);
}
sub SUNRISE_EL_sunset_abs_dat (@) {
  return SUNRISE_EL_sr_alt(main::sr_noon(shift),0,0,0,0,shift,shift,shift,shift);
}

########################################################################################################
#
# wrapper for Perl versions before 2018-06-09
#
########################################################################################################  

sub _timelocal_modern {
    return timelocal_modern(@_)
      if ( exists &{'timelocal_modern'} );

    my @r = @_;
    $r[5] -= 1900;
    return timelocal(@r);
}

########################################################################################################
#
# time fragments into minutes, seconds
#
########################################################################################################  
  
sub HHMM($){
  my ($hh) = @_;
  return("---")
    if (!defined($hh) || $hh !~ /^-?\d+/ || $hh==0) ;
  
  my $h = floor($hh);
  my $m = ($hh-$h)*60.;
  return sprintf("%02d:%02d",$h,$m);
}

sub HHMMSS($){
  my ($hh) = @_;
  return("---")
    if (!defined($hh) || $hh !~ /^-?\d+/ || $hh==0) ;
  
  my $m = ($hh-floor($hh))*60.;
  my $s = ($m-floor($m))*60;
  my $h = floor($hh);
  return sprintf("%02d:%02d:%02d",$h,$m,$s);
}

########################################################################################################
#
# Date2JD - Calculate Julian date, adopted from Perl CPAN module Astro::Coord::ECI::Utils
#
########################################################################################################

sub Date2JD {
    my @args = @_;
    unshift @args, 0 while @args < 6.;
    my ( $sec, $min, $hr, $day, $mon, $yr ) = @args;
    return undef if ( ( $yr < -4712. ) or ( $mon < 1. || $mon > 12. ) );
    if ( $mon < 3. ) {
        --$yr;
        $mon += 12.;
    }

    my $JD_GREGORIAN = 2299160.5;
    my $A            = floor( $yr / 100 );
    my $B            = 2 - $A + floor( $A / 4 );
    my $jd =
      floor( 365.25 *  ( $yr + 4716 ) ) +
      floor( 30.6001 * ( $mon + 1 ) ) +
      $day + $B - 1524.5 +
      ( ( ( $sec || 0 ) / 60 + ( $min || 0 ) ) / 60 + ( $hr || 0 ) ) / 24;
    $jd < $JD_GREGORIAN
      and $jd =
      floor( 365.25 *  ( $yr + 4716 ) ) +
      floor( 30.6001 * ( $mon + 1 ) ) +
      $day - 1524.5 +
      ( ( ( $sec || 0 ) / 60 + ( $min || 0 ) ) / 60 + ( $hr || 0 ) ) / 24;
    return $jd;
}

########################################################################################################
#
# GMST - Julian Date to Greenwich Mean Sidereal Time
#
########################################################################################################

sub GMST($){
  my ($JD) = @_;
  my $UT   = ($JD-0.5) - int($JD-0.5);
  $UT      = $UT*24.;              # UT in hours
  $JD      = floor($JD-0.5)+0.5;   # JD at 0 hours UT
  my $T    = ($JD-2451545.0)/36525.0;
  my $T0   = 6.697374558 + $T*(2400.051336 + $T*0.000025862);
  
  return( _mod($T0+$UT*1.002737909,24.));
}

########################################################################################################
#
# GMST2UT - Convert Greenweek mean sidereal time to UT
#
########################################################################################################

sub GMST2UT($$){
  my ($JD, $gmst) = @_;
  $JD             = floor($JD-0.5)+0.5;   # JD at 0 hours UT
  my $T           = ($JD-2451545.0)/36525.0;
  my $T0          = _mod(6.697374558 + $T*(2400.051336 + $T*0.000025862), 24.);
  my $UT          = 0.9972695663*(($gmst-$T0));
  return($UT);
}

########################################################################################################
#
# GMST2LMST - Local Mean Sidereal Time, geographical longitude in radians, 
#                   East is positive
#
########################################################################################################

sub GMST2LMST($$){
  my ($gmst, $lon) = @_;
  my $lmst = _mod($gmst+$RAD*$lon/15, 24.);
  return( $lmst );
}

########################################################################################################
#
# Ecl2Equ - Transform ecliptical coordinates (lon/lat) to equatorial coordinates (RA/dec)
#
########################################################################################################

sub Ecl2Equ($$$){
  my ($lon, $lat, $TDT) = @_;
  my $T = ($TDT-2451545.0)/36525.; # Epoch 2000 January 1.5
  my $eps = (23.+(26+21.45/60.)/60. + $T*(-46.815 +$T*(-0.0006 + $T*0.00181) )/3600. )*$DEG;
  my $coseps = cos($eps);
  my $sineps = sin($eps);
  my $sinlon = sin($lon);
  my $ra  = _mod2Pi(atan2( ($sinlon*$coseps-tan($lat)*$sineps), cos($lon) ));  
  my $dec = asin( sin($lat)*$coseps + cos($lat)*$sineps*$sinlon );
 
  return ($ra,$dec);
}

########################################################################################################
#
# Equ2Altaz - Transform equatorial coordinates (RA/Dec) to horizontal coordinates 
#                   (azimuth/altitude). Refraction is ignored
#
########################################################################################################

sub Equ2Altaz($$$$$){
  my ($ra, $dec, $TDT, $lat, $lmst)=@_;
  my $cosdec = cos($dec);
  my $sindec = sin($dec);
  my $lha    = $lmst - $ra;
  my $coslha = cos($lha);
  my $sinlha = sin($lha);
  my $coslat = cos($lat);
  my $sinlat = sin($lat);
  
  my $N      = -$cosdec * $sinlha;
  my $D      = $sindec * $coslat - $cosdec * $coslha * $sinlat;
  my $az     = _mod2Pi( atan2($N, $D) );
  my $alt    = asin( $sindec * $sinlat + $cosdec * $coslha * $coslat );

  return ($az,$alt);
}

########################################################################################################
#
# GeoEqu2TopoEqu - Transform geocentric equatorial coordinates (RA/Dec) to 
#                        topocentric equatorial coordinates
#
########################################################################################################

sub GeoEqu2TopoEqu($$$$$$$){
  my ($ra, $dec, $distance, $lon, $lat, $radius, $lmst) = @_;

  my $cosdec = cos($dec);
  my $sindec = sin($dec);
  my $coslst = cos($lmst);
  my $sinlst = sin($lmst);
  my $coslat = cos($lat); # we should use geocentric latitude, not geodetic latitude
  my $sinlat = sin($lat);
  my $rho    = $radius; # observer-geocenter in km
  
  my $x = $distance*$cosdec*cos($ra) - $rho*$coslat*$coslst;
  my $y = $distance*$cosdec*sin($ra) - $rho*$coslat*$sinlst;
  my $z = $distance*$sindec - $rho*$sinlat;

  my $distanceTopocentric = sqrt($x*$x + $y*$y + $z*$z);
  my $decTopocentric = asin($z/$distanceTopocentric);
  my $raTopocentric = _mod2Pi( atan2($y, $x) );

  return ( ($distanceTopocentric,$decTopocentric,$raTopocentric) );
}

########################################################################################################
#
# EquPolar2Cart - Calculate cartesian from polar coordinates
#
########################################################################################################

sub EquPolar2Cart($$$){
  my ($lon,$lat,$distance) = @_;
  my $rcd = cos($lat)*$distance;
  my $x   = $rcd*cos($lon);
  my $y   = $rcd*sin($lon);
  my $z   = sin($lat)*$distance;
  return( ($x,$y,$z) );
}

########################################################################################################
#
# Observer2EquCart - Calculate observers cartesian equatorial coordinates (x,y,z in celestial frame) 
#                    from geodetic coordinates (longitude, latitude, height above WGS84 ellipsoid)
#                    Currently only used to calculate distance of a body from the observer
#
########################################################################################################

sub Observer2EquCart($$$$){
  my ($lon, $lat, $height, $gmst ) = @_;

  my $flat   = 298.257223563;        # WGS84 flatening of earth
  my $aearth = 6378.137;             # GRS80/WGS84 semi major axis of earth ellipsoid

  #-- Calculate geocentric latitude from geodetic latitude
  my $co = cos ($lat);
  my $si = sin ($lat);
  $si    = $si * $si;
  my $fl = 1.0 - 1.0 / $flat;
  $fl    = $fl * $fl;
  my $u  = 1.0 / sqrt ($co * $co + $fl * $si);
  my $a  = $aearth * $u + $height;
  my $b  = $aearth * $fl * $u + $height;
  my $radius = sqrt ($a * $a * $co *$co + $b *$b * $si); # geocentric distance from earth center
  my $y  = acos ($a * $co / $radius); # geocentric latitude, rad
  my $x  = $lon; # longitude stays the same
  my $z;
  if ($lat < 0.0) { $y = -$y; } # adjust sign
  
  #-- convert from geocentric polar to geocentric cartesian, with regard to Greenwich
  ($x,$y,$z) = EquPolar2Cart( $x, $y, $radius ); 
  
  #-- rotate around earth's polar axis to align coordinate system from Greenwich to vernal equinox
  my $rotangle = $gmst/24*2*pi; # sideral time gmst given in hours. Convert to radians
  my $x2 = $x*cos($rotangle) - $y*sin($rotangle);
  my $y2 = $x*sin($rotangle) + $y*cos($rotangle);
  
  return( ($x2,$y2,$z,$radius) );
}

########################################################################################################
#
# SunPosition - Calculate coordinates for Sun
# Coordinates are accurate to about 10s (right ascension) 
# and a few minutes of arc (declination)
# 
########################################################################################################

sub SunPosition($$$){
  my ($TDT, $observerlat, $lmst)=@_;
  
  my $D  = $TDT-2447891.5;
  my $eg = 279.403303*$DEG;
  my $wg = 282.768422*$DEG;
  my $e  = 0.016713;
  my $a  = 149598500; # km
  #-- mean angular diameter of sun
  my $diameter0 = 0.533128*$DEG; 
  
  my $MSun = 360*$DEG/365.242191*$D+$eg-$wg;
  my $nu   = $MSun + 360.*$DEG/pi*$e*sin($MSun);
  
  my %sunCoor;
  
  $sunCoor{lon}  =  _mod2Pi($nu+$wg);
  $sunCoor{lat}  = 0;
  $sunCoor{anomalyMean} = $MSun;
  
  my $distance  = (1-$e*$e)/(1+$e*cos($nu));   # distance in astronomical units
  $sunCoor{diameter} = $diameter0/$distance;        # angular diameter
  $sunCoor{distance} = $distance*$a;                # distance in km
  $sunCoor{parallax} = 6378.137/$sunCoor{distance};          # horizontal parallax

  ($sunCoor{ra},$sunCoor{dec}) = Ecl2Equ($sunCoor{lon}, $sunCoor{lat}, $TDT);
  
  #-- calculate horizontal coordinates of sun, if geographic positions is given
  if (defined($observerlat) && defined($lmst) ) {
    ($sunCoor{az},$sunCoor{alt}) = Equ2Altaz($sunCoor{ra}, $sunCoor{dec}, $TDT, $observerlat, $lmst);
  }
  $sunCoor{sign}    = floor($sunCoor{lon}*$RAD/30.);
  $sunCoor{sig}     = $zodiac[$sunCoor{sign}];
  
  return ( \%sunCoor );
}

########################################################################################################
#
# MoonPosition - Calculate data and coordinates for the Moon
#                      Coordinates are accurate to about 1/5 degree (in ecliptic coordinates)
# 
########################################################################################################

sub MoonPosition($$$$$$$){
  my ($sunlon, $sunanomalyMean, $TDT, $observerlon, $observerlat, $observerradius, $lmst) = @_;
  
  my $D = $TDT-2447891.5;
  
  #-- Mean Moon orbit elements as of 1990.0
  my $l0 = 318.351648*$DEG;
  my $P0 =  36.340410*$DEG;
  my $N0 = 318.510107*$DEG;
  my $i  = 5.145396*$DEG;
  my $e  = 0.054900;
  my $a  = 384401; # km
  my $diameter0 = 0.5181*$DEG; # angular diameter of Moon at a distance
  my $parallax0 = 0.9507*$DEG; # parallax at distance a
  
  my $l  = 13.1763966*$DEG*$D+$l0;
  my $MMoon = $l-0.1114041*$DEG*$D-$P0; # Moon's mean anomaly M
  my $N  = $N0-0.0529539*$DEG*$D;          # Moon's mean ascending node longitude
  my $C  = $l-$sunlon;
  my $Ev = 1.2739*$DEG*sin(2*$C-$MMoon);
  my $Ae = 0.1858*$DEG*sin($sunanomalyMean);
  my $A3 = 0.37*$DEG*sin($sunanomalyMean);
  my $MMoon2 = $MMoon+$Ev-$Ae-$A3;  # corrected Moon anomaly
  my $Ec = 6.2886*$DEG*sin($MMoon2);  # equation of centre
  my $A4 = 0.214*$DEG*sin(2*$MMoon2);
  my $l2 = $l+$Ev+$Ec-$Ae+$A4; # corrected Moon's longitude
  my $V  = 0.6583*$DEG*sin(2*($l2-$sunlon));
  my $l3 = $l2+$V; # true orbital longitude;

  my $N2 = $N-0.16*$DEG*sin($sunanomalyMean);
   
  my %moonCoor;
  $moonCoor{lon}      = _mod2Pi( $N2 + atan2( sin($l3-$N2)*cos($i), cos($l3-$N2) ) );
  $moonCoor{lat}      = asin( sin($l3-$N2)*sin($i) );
  $moonCoor{orbitLon} = $l3;
  
  ($moonCoor{ra},$moonCoor{dec}) = Ecl2Equ($moonCoor{lon},$moonCoor{lat},$TDT);
  #-- relative distance to semi mayor axis of lunar oribt
  my $distance = (1-$e*$e) / (1+$e*cos($MMoon2+$Ec) );
  $moonCoor{diameter} = $diameter0/$distance; # angular diameter in radians
  $moonCoor{parallax} = $parallax0/$distance; # horizontal parallax in radians
  $moonCoor{distance} = $distance*$a;         # distance in km

  #-- Calculate horizontal coordinates of moon, if geographic positions is given

  #-- backup geocentric coordinates
  $moonCoor{raGeocentric}       = $moonCoor{ra}; 
  $moonCoor{decGeocentric}      = $moonCoor{dec};
  $moonCoor{distanceGeocentric} = $moonCoor{distance};

  if (defined($observerlat) && defined($observerlon) && defined($lmst) ) {
    #-- transform geocentric coordinates into topocentric (==observer based) coordinates
	my  ($distanceTopocentric,$decTopocentric,$raTopocentric) = 
	  GeoEqu2TopoEqu($moonCoor{ra}, $moonCoor{dec}, $moonCoor{distance}, $observerlon, $observerlat, $observerradius, $lmst);
	#-- now ra and dec are topocentric
	$moonCoor{ra}  = $raTopocentric;
	$moonCoor{dec} = $decTopocentric;
    ($moonCoor{az},$moonCoor{alt})= Equ2Altaz($moonCoor{ra}, $moonCoor{dec}, $TDT, $observerlat, $lmst); 
  }
  
  #-- Age of Moon in radians since New Moon (0) - Full Moon (pi)
  $moonCoor{age}    = _mod2Pi($l3-$sunlon);   
  $moonCoor{phasen} = 0.5*(1-cos($moonCoor{age})); # Moon phase numerical, 0-1
  
  my $mainPhase = 1./29.53*360*$DEG; # show 'Newmoon, 'Quarter' for +/-1 day around the actual event
  my $p = _mod($moonCoor{age}, 90.*$DEG);
  if ($p < $mainPhase || $p > 90*$DEG-$mainPhase){
    $p = 2*floor($moonCoor{age} / (90.*$DEG)+0.5);
  }else{
    $p = 2*floor($moonCoor{age} / (90.*$DEG))+1;
  }
  $p = $p % 8;
  $moonCoor{phases} = $phases[$p]; 
  $moonCoor{phasei} = $p;
  $moonCoor{sign}   = floor($moonCoor{lon}*$RAD/30);
  $moonCoor{sig}    = $zodiac[$moonCoor{sign}];

  return ( \%moonCoor );
}

########################################################################################################
#
# Refraction - Input true altitude in radians, Output: increase in altitude in degrees
# 
########################################################################################################

sub Refraction($){
  my ($alt) = @_;
  my $altdeg = $alt*$RAD;
  if ($altdeg<-2 || $altdeg>=90){
    return(0);
  }
   
  my $pressure    = 1015;
  my $temperature = 10;
  if ($altdeg>15){
    return( 0.00452*$pressure/( (273+$temperature)*tan($alt)) );
  }
  
  my $y = $alt;
  my $D = 0.0;
  my $P = ($pressure-80.)/930.;
  my $Q = 0.0048*($temperature-10.);
  my $y0 = $y;
  my $D0 = $D;
  my $N;

  for (my $i=0; $i<3; $i++) {
	$N = $y+(7.31/($y+4.4));
    $N = 1./tan($N*$DEG);
	$D = $N*$P/(60.+$Q*($N+39.));
	$N = $y-$y0;
	$y0 = $D-$D0-$N;
	if (($N != 0.) && ($y0 != 0.)) { 
	  $N = $y-$N*($alt+$D-$y)/$y0; 
	} else { 
	  $N = $alt+$D; 
	}
	$y0 = $y;
	$D0 = $D;
	$y  = $N;
  }
  return( $D ); 
}

########################################################################################################
#
# GMSTRiseSet - returns Greenwich sidereal time (hours) of time of rise 
# and set of object with coordinates ra/dec
# at geographic position lon/lat (all values in radians)
# Correction for refraction and semi-diameter/parallax of body is taken care of in function RiseSet
# h is used to calculate the twilights. It gives the required elevation of the disk center of the sun
# 
########################################################################################################

sub GMSTRiseSet($$$$$){
  my ($ra, $dec, $lon, $lat, $h) = @_;
  
  $h = (defined($h)) ? $h : 0.0; # set default value
  #Log 1,"-------------------> Called GMSTRiseSet with $ra $dec $lon $lat $h";

  # my $tagbogen = acos(-tan(lat)*tan(coor.dec)); // simple formula if twilight is not required
  my $tagbarg  = (sin($h) - sin($lat)*sin($dec)) / (cos($lat)*cos($dec));
  if( ($tagbarg > 1.000000) || ($tagbarg < -1.000000) ){
    Log 5,"[FHEM::Astro::GMSTRiseSet] Parameters $ra $dec $lon $lat $h give complex angle";
    return( ("---","---","---") );
  };
  my $tagbogen = acos($tagbarg);

  my $transit =     $RAD/15*(          +$ra-$lon);
  my $rise    = 24.+$RAD/15*(-$tagbogen+$ra-$lon); # calculate GMST of rise of object
  my $set     =     $RAD/15*(+$tagbogen+$ra-$lon); # calculate GMST of set of object

  #--Using the modulo function mod, the day number goes missing. This may get a problem for the moon
  $transit = _mod($transit, 24);
  $rise    = _mod($rise, 24);
  $set     = _mod($set, 24);
  
  return( ($transit, $rise, $set) );
}

########################################################################################################
#
# InterpolateGMST - Find GMST of rise/set of object from the two calculated 
# (start)points (day 1 and 2) and at midnight UT(0)
# 
########################################################################################################

sub InterpolateGMST($$$$){
  my ($gmst0, $gmst1, $gmst2, $timefactor) = @_;
  return( ($timefactor*24.07*$gmst1- $gmst0*($gmst2-$gmst1)) / ($timefactor*24.07+$gmst1-$gmst2) );
}

########################################################################################################
#
# RiseSet
#    // JD is the Julian Date of 0h UTC time (midnight)
# 
########################################################################################################

sub RiseSet($$$$$$$$$$$){
  my ($jd0UT, $diameter, $parallax, $ra1, $dec1, $ra2, $dec2, $lon, $lat, $timeinterval, $altip) = @_;
 
  #--altitude of sun center: semi-diameter, horizontal parallax and (standard) refraction of 34'
  #  true height of sun center for sunrise and set calculation. Is kept 0 for twilight (ie. altitude given):
  my $alt      = (!defined($altip)) ? 0.5*$diameter-$parallax+34./60*$DEG : 0.; 
  my $altitude = (!defined($altip)) ? 0. : $altip; 

  my ($transit1, $rise1, $set1) = GMSTRiseSet($ra1, $dec1, $lon, $lat, $altitude);
  my ($transit2, $rise2, $set2) = GMSTRiseSet($ra2, $dec2, $lon, $lat, $altitude);
  
  #-- complex angle
  if( ($transit1 eq "---") || ($transit2 eq "---") ){
    return( ("---","---","---") );
  }
  
  #-- unwrap GMST in case we move across 24h -> 0h
  $transit2 += 24
    if ($transit1 > $transit2 && abs($transit1-$transit2)>18);
  $rise2 += 24
    if ($rise1 > $rise2    && abs($rise1-$rise2)>18);
  $set2 += 24
    if ($set1 > $set2    && abs($set1-$set2)>18);
    
  my $T0 = GMST($jd0UT);
  # my $T02 = T0-zone*1.002738; // Greenwich sidereal time at 0h time zone (zone: hours)
  #-- Greenwich sidereal time for 0h at selected longitude
  my $T02 = $T0-$lon*$RAD/15*1.002738;
  $T02 +=24 if ($T02 < 0);

  if ($transit1 < $T02) { 
    $transit1 += 24; 
    $transit2 += 24; 
  }
  if ($rise1    < $T02) { 
    $rise1    += 24; 
    $rise2    += 24; 
  }
  if ($set1     < $T02) { 
    $set1     += 24; 
    $set2     += 24; 
  }
  
  #-- Refraction and Parallax correction
  my $decMean = 0.5*($dec1+$dec2);
  my $psi = acos(sin($lat)/cos($decMean));
  my $y   = asin(sin($alt)/sin($psi));
  my $dt  = 240*$RAD*$y/cos($decMean)/3600; # time correction due to refraction, parallax

  my $transit = GMST2UT( $jd0UT, InterpolateGMST( $T0, $transit1, $transit2, $timeinterval) );
  my $rise    = GMST2UT( $jd0UT, InterpolateGMST( $T0, $rise1,    $rise2,    $timeinterval) - $dt );
  my $set     = GMST2UT( $jd0UT, InterpolateGMST( $T0, $set1,     $set2,     $timeinterval) + $dt );
  
  return( ($transit,$rise,$set) ); 
}

########################################################################################################
#
# SunRise - Find (local) time of sunrise and sunset, and twilights
#                 JD is the Julian Date of 0h local time (midnight)
#                 Accurate to about 1-2 minutes
#                 recursive: 1 - calculate rise/set in UTC in a second run
#                 recursive: 0 - find rise/set on the current local day. 
#                                This is set when doing the first call to this function
# 
########################################################################################################

sub SunRise($$$$$$$$){
  my ($JD, $deltaT, $lon, $lat, $zone, $horM, $horE, $recursive) = @_;
  
  my $jd0UT = floor($JD-0.5)+0.5;   # JD at 0 hours UT
  
  #-- calculations for noon
  my $sunCoor1 = SunPosition($jd0UT+   $deltaT/24./3600.,undef,undef);

  #-- calculations for next day's UTC midnight
  my $sunCoor2 = SunPosition($jd0UT+1.+$deltaT/24./3600.,undef,undef); 
  
  #-- rise/set time in UTC
  my ($transit,$rise,$set) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
    $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1,undef); 
  if( $transit eq "---" ){
    Log 3,"[FHEM::Astro::SunRise] no solution possible - maybe the sun never sets ?";
   return( ($transit,$rise,$set) ); 
  }
  
  my ($transittemp,$risetemp,$settemp);
  #-- check and adjust to have rise/set time on local calendar day
  if ( $recursive==0 ) { 
    if ($zone>0) {
      #rise time was yesterday local time -> calculate rise time for next UTC day
      if ($rise >=24-$zone || $transit>=24-$zone || $set>=24-$zone) {
        ($transittemp,$risetemp,$settemp) = SunRise($JD+1, $deltaT, $lon, $lat, $zone, $horM, $horE, 1);
        $transit = $transittemp
          if ($transit>=24-$zone);
        $rise = $risetemp
          if ($rise>=24-$zone);
        $set = $settemp
          if ($set>=24-$zone);
      }
    }elsif ($zone<0) {
      #rise time was yesterday local time -> calculate rise time for previous UTC day
      if ($rise<-$zone || $transit<-zone || $set<-zone) {
        ($transittemp,$risetemp,$settemp) = SunRise($JD-1, $deltaT, $lon, $lat, $zone, $horM, $horE, 1);
      $transit = $transittemp
        if ($transit<-$zone);
      $rise = $risetemp
        if ($rise<-$zone);
      $set  = $settemp
        if ($set <-$zone);
      }
    }
	
    $transit = _mod($transit+$zone, 24.);
    $rise    = _mod($rise   +$zone, 24.);
    $set     = _mod($set    +$zone, 24.);

	#-- Twilight calculation
	#-- civil twilight time in UTC. 
	my $CivilTwilightMorning;
	my $CivilTwilightEvening;
	($transittemp,$risetemp,$settemp) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
	   $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1, -6.*$DEG);
	if( $transittemp eq "---" ){
      Log 4,"[FHEM::Astro::SunRise] no solution possible for civil twilight - maybe the sun never sets below -6 degrees?";
      $CivilTwilightMorning = "---";
      $CivilTwilightEvening = "---";
    }else{
	  $CivilTwilightMorning = _mod($risetemp +$zone, 24.);
	  $CivilTwilightEvening = _mod($settemp  +$zone, 24.);
    }
    
	#-- nautical twilight time in UTC.
	my $NauticTwilightMorning;
	my $NauticTwilightEvening; 
	($transittemp,$risetemp,$settemp) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
	  $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1, -12.*$DEG);
	if( $transittemp eq "---" ){
      Log 4,"[FHEM::Astro::SunRise] no solution possible for nautical twilight - maybe the sun never sets below -12 degrees?";
      $NauticTwilightMorning = "---";
      $NauticTwilightEvening = "---";
    }else{
      $NauticTwilightMorning = _mod($risetemp +$zone, 24.);
	  $NauticTwilightEvening = _mod($settemp  +$zone, 24.);
	}

	#-- astronomical twilight time in UTC. 
	my $AstroTwilightMorning;
	my $AstroTwilightEvening;
	($transittemp,$risetemp,$settemp) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
	  $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1, -18.*$DEG);
	if( $transittemp eq "---" ){
      Log 4,"[FHEM::Astro::SunRise] no solution possible for astronomical twilight - maybe the sun never sets below -18 degrees?";
      $AstroTwilightMorning = "---";
      $AstroTwilightEvening = "---";
    }else{
	  $AstroTwilightMorning = _mod($risetemp +$zone, 24.);
	  $AstroTwilightEvening = _mod($settemp  +$zone, 24.);
	}
	
	#-- custom twilight time in UTC
	my $CustomTwilightMorning;
	my $CustomTwilightEvening;
    ($transittemp,$risetemp,$settemp) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
	  $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1, $horM*$DEG);
	if( $transittemp eq "---" ){
      Log 4,"[FHEM::Astro::SunRise] no solution possible for custom morning twilight - maybe the sun never sets below ".$horM." degrees?";
      $CustomTwilightMorning = "---";
    }else{
	  $CustomTwilightMorning = _mod($risetemp +$zone, 24.);
	}
    ($transittemp,$risetemp,$settemp) = RiseSet($jd0UT, $sunCoor1->{diameter}, $sunCoor1->{parallax}, 
    $sunCoor1->{ra}, $sunCoor1->{dec}, $sunCoor2->{ra}, $sunCoor2->{dec}, $lon, $lat, 1, $horE*$DEG);
  if( $transittemp eq "---" ){
      Log 4,"[FHEM::Astro::SunRise] no solution possible for custom evening twilight - maybe the sun never sets below ".$horE." degrees?";
      $CustomTwilightEvening = "---";
    }else{
    $CustomTwilightEvening = _mod($settemp  +$zone, 24.);
  }
	
	return( ($transit,$rise,$set,$CivilTwilightMorning,$CivilTwilightEvening,
	  $NauticTwilightMorning,$NauticTwilightEvening,$AstroTwilightMorning,$AstroTwilightEvening,$CustomTwilightMorning,$CustomTwilightEvening) );  
  }else{
    return( ($transit,$rise,$set) );  
  }
}

########################################################################################################
#
# MoonRise - Find local time of moonrise and moonset
# JD is the Julian Date of 0h local time (midnight)
# Accurate to about 5 minutes or better
# recursive: 1 - calculate rise/set in UTC
# recursive: 0 - find rise/set on the current local day (set could also be first)
# returns '' for moonrise/set does not occur on selected day
# 
########################################################################################################

sub MoonRise($$$$$$$){
  my ($JD, $deltaT, $lon, $lat, $radius, $zone, $recursive) = @_;
  my $timeinterval = 0.5;
  
  my $jd0UT = floor($JD-0.5)+0.5;   # JD at 0 hours UT
  #-- calculations for noon
  my $sunCoor1  = SunPosition($jd0UT+ $deltaT/24./3600.,undef,undef);
  my $moonCoor1 = MoonPosition($sunCoor1->{lon}, $sunCoor1->{anomalyMean}, $jd0UT+ $deltaT/24./3600.,undef,undef,undef,undef);
 
  #-- calculations for next day's midnight
  my $sunCoor2  = SunPosition($jd0UT +$timeinterval + $deltaT/24./3600.,undef,undef); 
  my $moonCoor2 = MoonPosition($sunCoor2->{lon}, $sunCoor2->{anomalyMean}, $jd0UT +$timeinterval + $deltaT/24./3600.,undef,undef,undef,undef); 

  # rise/set time in UTC, time zone corrected later.
  # Taking into account refraction, semi-diameter and parallax
  my ($transit,$rise,$set) = RiseSet($jd0UT, $moonCoor1->{diameter}, $moonCoor1->{parallax}, 
    $moonCoor1->{ra}, $moonCoor1->{dec}, $moonCoor2->{ra}, $moonCoor2->{dec}, $lon, $lat, $timeinterval,undef); 
  my ($transittemp,$risetemp,$settemp);
  my ($transitprev,$riseprev,$setprev);
  
  # check and adjust to have rise/set time on local calendar day
  if ( $recursive==0 ) { 
    if ($zone>0) {
      # recursive call to MoonRise returns events in UTC
      ($transitprev,$riseprev,$setprev) = MoonRise($JD-1., $deltaT, $lon, $lat, $radius, $zone, 1);  
      if ($transit >= 24.-$zone || $transit < -$zone) { # transit time is tomorrow local time
        if ($transitprev < 24.-$zone){
           $transit = ""; # there is no moontransit today
        }else{
           $transit  = $transitprev;
        }
      }
      
      if ($rise >= 24.-$zone || $rise < -$zone) { # rise time is tomorrow local time
        if ($riseprev < 24.-$zone){
          $rise = ""; # there is no moonrise today
        }else{ 
          $rise  = $riseprev;
        }
      }

      if ($set >= 24.-$zone || $set < -$zone) { # set time is tomorrow local time
        if ($setprev < 24.-$zone){
          $set = ""; # there is no moonset today
        }else{
          $set  = $setprev;
        }
      }

    }elsif ($zone<0) { # rise/set time was tomorrow local time -> calculate rise time for previous UTC day
      if ($rise<-$zone || $set<-$zone || $transit<-$zone) { 
        ($transittemp,$risetemp,$settemp) = MoonRise($JD+1., $deltaT, $lon, $lat, $radius, $zone, 1);  
        if ($transit < -$zone){
          if ($transittemp > -$zone){
            $transit = ''; # there is no moontransit today
          }else{
            $transit  = $transittemp;
          }
        }
        
        if ($rise < -$zone) {
          if ($risetemp > -$zone){
             $rise = ''; # there is no moonrise today
          }else{
             $rise = $risetemp;
          }
        }
        
        if ($set < -$zone){
          if ($settemp > -$zone){
            $set = ''; # there is no moonset today
          }else{
            $set  = $settemp;
          }
        }     
      }
    }
    #-- correct for time zone, if time is valid
    $transit = _mod($transit +$zone, 24.)
      if( $transit ne ""); 
    $rise = _mod($rise +$zone, 24.)
      if ($rise ne "");    
    $set  = _mod($set +$zone, 24.)
      if ($set ne "");   
  }
  return( ($transit,$rise,$set) );
}

########################################################################################################
#
# Season - Find astronomical season
# 
########################################################################################################

sub Season($$;$) {
    my ( $JD0, $deltaT, $lat ) = @_;

    # season starts during the day so we need to
    #  look for tomorrows quarter at noon
    my $sunCoor = SunPosition( $JD0 + 1. + $deltaT / 86400.0, undef, undef );
    my $quarter = ceil( rad2deg( $sunCoor->{lon} ) / 90. ) % 4;

    return
      wantarray
      ? ( $quarter, $seasons{ $lat && $lat < 0 ? 'S' : 'N' }[$quarter] )
      : $quarter;
}

########################################################################################################
#
# SetTime - update of the %Date hash for today
# 
########################################################################################################

sub SetTime (;$$$) {
    my ( $time, $tz, $lc_time ) = @_;

    #-- readjust locale
    my $old_lctime = setlocale(LC_TIME);
    setlocale(LC_TIME, $lc_time) if ($lc_time);
    use locale ':not_characters';

    #-- readjust timezone
    local $ENV{TZ} = $tz if ($tz);
    tzset() if ( exists &{'tzset'} );

    $time = gettimeofday() unless ( defined($time) );

    my ( $sec, $min, $hour, $day, $month, $year, $wday, $yday, $isdst ) =
      localtime($time);
    $year  += 1900;
    $month += 1;
    $Date{timestamp} = $time;
    $Date{timeday}   = $hour + $min / 60. + $sec / 3600.;
    $Date{year}      = $year;
    $Date{month}     = $month;
    $Date{day}       = $day;
    $Date{hour}      = $hour;
    $Date{min}       = $min;
    $Date{sec}       = $sec;
    $Date{isdst}     = $isdst;
    #-- broken on windows
    #$Date{zonedelta} = (strftime "%z", localtime($time))/100;
    $Date{zonedelta} = _tzoffset($time) / 100;
    #-- half broken in windows
    $Date{dayofyear} = 1 * strftime( "%j", localtime($time) );

    $Date{wdayl} = strftime( "%A", localtime($time) );
    $Date{wdays} = strftime( "%a", localtime($time) );
    $Date{monthl} = strftime( "%B", localtime($time) );
    $Date{months} = strftime( "%b", localtime($time) );
    $Date{datetime} = strftime( "%c", localtime($time) );
    $Date{week} = 1 * strftime( "%V", localtime($time) );
    $Date{wday} = 1 * strftime( "%w", localtime($time) );
    $Date{time} = strftime( "%X", localtime($time) );
    $Date{date} = strftime( "%x", localtime($time) );
    $Date{tz} = strftime( "%Z", localtime($time) );

    delete $Date{tz} if (!$Date{tz} || $Date{tz} eq "" || $Date{tz} eq " ");

    delete local $ENV{TZ};
    tzset() if ( exists &{'tzset'} );

    setlocale(LC_TIME, "");
    setlocale(LC_TIME, $old_lctime);
    no locale;

    return (undef);
}

########################################################################################################
#
# Compute - sequential calculation of properties
# 
########################################################################################################
  
sub Compute($;$){
  my ($hash,$params) = @_;
  undef %Astro;
  my $name = $hash->{NAME};
  SetTime() if (scalar keys %Date == 0); # fill %Date if it is still empty after restart to avoid warnings

  return undef if( !$init_done );

  #-- readjust language
  my $lang = uc(AttrVal($name,"language",AttrVal("global","language","EN")));
  if( defined($params->{"language"}) &&
      exists($transtable{uc($params->{"language"})})
  ){
    $lang = uc($params->{"language"});
    $tt = $transtable{$lang};
  }elsif( exists($transtable{uc($lang)}) ){
    $tt = $transtable{uc($lang)};
  }else{
    $tt = $transtable{EN};
  }

  #-- readjust timezone
  my $tz = AttrVal($name,"timezone",AttrVal("global","timezone",undef));
  $tz = $params->{"timezone"}
    if ( defined( $params->{"timezone"} ) );
  local $ENV{TZ} = $tz if ($tz);
  tzset() if ( exists &{'tzset'} );

  #-- geodetic latitude and longitude of observer on WGS84  
  if( defined($params->{"latitude"}) ){
    $Astro{ObsLat}  = $params->{"latitude"};
  }elsif( defined($attr{$name}) && defined($attr{$name}{"latitude"}) ){
    $Astro{ObsLat}  = $attr{$name}{"latitude"};
  }elsif( defined($attr{"global"}{"latitude"}) ){
    $Astro{ObsLat}  = $attr{"global"}{"latitude"};
  }else{
    $Astro{ObsLat}  = 50.0;
    Log3 $name,3,"[Astro] No latitude attribute set in global device, using 50.0°";
  }
  if( defined($params->{"longitude"}) ){
    $Astro{ObsLon}  = $params->{"longitude"};
  }elsif( defined($attr{$name}) && defined($attr{$name}{"longitude"}) ){
    $Astro{ObsLon}  = $attr{$name}{"longitude"};
  }elsif( defined($attr{"global"}{"longitude"}) ){
    $Astro{ObsLon}  = $attr{"global"}{"longitude"};
  }else{
    $Astro{ObsLon}  = 10.0;
    Log3 $name,3,"[Astro] No longitude attribute set in global device, using 10.0°";
  } 
  #-- altitude of observer in meters above WGS84 ellipsoid 
  if( defined($params->{"altitude"}) ){
    $Astro{ObsAlt}  = $params->{"altitude"};
  }elsif( defined($attr{$name}) && defined($attr{$name}{"altitude"}) ){
    $Astro{ObsAlt}  = $attr{$name}{"altitude"};
  }elsif( defined($attr{"global"}{"altitude"}) ){
    $Astro{ObsAlt}  = $attr{"global"}{"altitude"};
  }else{
    $Astro{ObsAlt}  = 0.0;
    Log3 $name,3,"[Astro] No altitude attribute set in global device, using 0.0 m above sea level";
  }
  #-- custom horizon of observer in degrees
  if( defined($params->{"horizon"}) &&
      $params->{"horizon"} =~ m/^([^:]+)(?::(.+))?$/
  ){
    $Astro{ObsHorMorning} = $1;
    $Astro{ObsHorEvening} = defined($2) ? $2 : $1;
  }elsif( defined($attr{$name}) && defined($attr{$name}{"horizon"}) &&
      $attr{$name}{"horizon"} =~ m/^([^:]+)(?::(.+))?$/
  ){
    $Astro{ObsHorMorning} = $1;
    $Astro{ObsHorEvening} = defined($2) ? $2 : $1;
  } else {
    $Astro{ObsHorMorning} = 0.0;
    $Astro{ObsHorEvening} = 0.0;
    Log3 $name,5,"[Astro] No horizon attribute defined, using 0.0° for morning and evening";
  }

  #-- internal variables converted to Radians and km 
  my $lat      = $Astro{ObsLat}*$DEG;
  my $lon      = $Astro{ObsLon}*$DEG;
  my $height   = $Astro{ObsAlt} * 0.001;   

  #if (eval(form.Year.value)<=1900 || eval(form.Year.value)>=2100 ) {
  #  alert("Dies Script erlaubt nur Berechnungen"+
  #  return;
  #}

  my $JD0 = Date2JD( $Date{day}, $Date{month}, $Date{year} );
  my $JD  = $JD0 + ( $Date{hour} - $Date{zonedelta} + $Date{min}/60. + $Date{sec}/3600.)/24;
  my $TDT = $JD  + $deltaT/86400.0; 
  
  $Astro{".ObsJD"}  = $JD;
  $Astro{ObsJD}     = _round($JD,2);

  my $gmst          = GMST($JD);
  my $lmst          = GMST2LMST($gmst, $lon); 
  $Astro{".ObsGMST"}  = $gmst;
  $Astro{".ObsLMST"}  = $lmst;
  $Astro{ObsGMST}     = HHMMSS($gmst);
  $Astro{ObsLMST}     = HHMMSS($lmst);
  
  #-- geocentric cartesian coordinates of observer
  my ($x,$y,$z,$radius) = Observer2EquCart($lon, $lat, $height, $gmst); 
 
  #-- calculate data for the sun at given time
  my $sunCoor     = SunPosition($TDT, $lat, $lmst*15.*$DEG);   
  $Astro{SunLon}  = _round($sunCoor->{lon}*$RAD,1);
  #$Astro{SunLat}  = $sunCoor->{lat}*$RAD;
  $Astro{".SunRa"}= _round($sunCoor->{ra} *$RAD/15,1);
  $Astro{SunRa}   = HHMM($Astro{".SunRa"});
  $Astro{SunDec}  = _round($sunCoor->{dec}*$RAD,1);
  $Astro{SunAz}   = _round($sunCoor->{az} *$RAD,1);
  $Astro{SunAlt}  = _round($sunCoor->{alt}*$RAD + Refraction($sunCoor->{alt}),1);  # including refraction WARNUNG => *RAD ???
  $Astro{SunSign} = $tt->{$sunCoor->{sig}};
  $Astro{SunSignN}= $sunCoor->{sign};
  $Astro{SunDiameter}=_round($sunCoor->{diameter}*$RAD*60,1); #angular diameter in arc seconds
  $Astro{SunDistance}=_round($sunCoor->{distance},0);
  
  #-- calculate distance from the observer (on the surface of earth) to the center of the sun
  my ($xs,$ys,$zs) = EquPolar2Cart($sunCoor->{ra}, $sunCoor->{dec}, $sunCoor->{distance});
  $Astro{SunDistanceObserver} = _round(sqrt( ($xs-$x)**2 + ($ys-$y)**2 + ($zs-$z)**2 ),0);
  
  my ($suntransit,$sunrise,$sunset,$CivilTwilightMorning,$CivilTwilightEvening,
    $NauticTwilightMorning,$NauticTwilightEvening,$AstroTwilightMorning,$AstroTwilightEvening,$CustomTwilightMorning,$CustomTwilightEvening) = 
    SunRise($JD0, $deltaT, $lon, $lat, $Date{zonedelta}, $Astro{ObsHorMorning}, $Astro{ObsHorEvening}, 0);
  $Astro{".SunTransit"}            = $suntransit;
  $Astro{".SunRise"}               = $sunrise;
  $Astro{".SunSet"}                = $sunset;
  $Astro{".CivilTwilightMorning"}  = $CivilTwilightMorning;
  $Astro{".CivilTwilightEvening"}  = $CivilTwilightEvening;
  $Astro{".NauticTwilightMorning"} = $NauticTwilightMorning;
  $Astro{".NauticTwilightEvening"} = $NauticTwilightEvening;
  $Astro{".AstroTwilightMorning"}  = $AstroTwilightMorning;
  $Astro{".AstroTwilightEvening"}  = $AstroTwilightEvening;
  $Astro{".CustomTwilightMorning"} = $CustomTwilightMorning;
  $Astro{".CustomTwilightEvening"} = $CustomTwilightEvening;
  $Astro{SunTransit}              = HHMM($suntransit);
  $Astro{SunRise}                 = HHMM($sunrise);
  $Astro{SunSet}                  = HHMM($sunset);
  $Astro{CivilTwilightMorning}    = HHMM($CivilTwilightMorning);
  $Astro{CivilTwilightEvening}    = HHMM($CivilTwilightEvening);
  $Astro{NauticTwilightMorning}   = HHMM($NauticTwilightMorning);
  $Astro{NauticTwilightEvening}   = HHMM($NauticTwilightEvening);
  $Astro{AstroTwilightMorning}    = HHMM($AstroTwilightMorning);
  $Astro{AstroTwilightEvening}    = HHMM($AstroTwilightEvening);
  $Astro{CustomTwilightMorning}   = HHMM($CustomTwilightMorning);
  $Astro{CustomTwilightEvening}   = HHMM($CustomTwilightEvening);

  #-- hours of day and night
  my $hoursofsunlight;
  my $hoursofnight;
  if (
      (!defined($sunset) && !defined($sunrise)) ||
      ($sunset !~ m/^\d+/ && $sunrise !~ m/^\d+/)
  ){
    if ($Astro{SunAlt} > 0.) {
      $hoursofsunlight = 24.;
      $hoursofnight = 0.;
    } else {
      $hoursofsunlight = 0.;
      $hoursofnight = 24.;
    }
  }
  elsif (!defined($sunset) || $sunset !~ m/^\d+/) {
    $hoursofsunlight = 24. - $sunrise;
    $hoursofnight = 24. - $hoursofsunlight;
  }
  elsif (!defined($sunrise) || $sunrise !~ m/^\d+/) {
    $hoursofsunlight = 24. - $sunset;
    $hoursofnight = 24. - $hoursofsunlight;
  } else {
    my $ss = $sunset;
    $ss += 24.
      if ($sunrise > $sunset);
    $hoursofsunlight = $ss - $sunrise;
    $hoursofnight = 24. - $hoursofsunlight;
  }
  $Astro{".SunHrsVisible"}   = $hoursofsunlight;
  $Astro{".SunHrsInvisible"} = $hoursofnight;
  $Astro{SunHrsVisible}   = HHMM($hoursofsunlight);
  $Astro{SunHrsInvisible} = HHMM($hoursofnight);
  
  #-- calculate data for the moon at given time
  my $moonCoor  = MoonPosition($sunCoor->{lon}, $sunCoor->{anomalyMean}, $TDT, $lon, $lat, $radius, $lmst*15.*$DEG);
  $Astro{MoonLon} = _round($moonCoor->{lon}*$RAD,1);
  $Astro{MoonLat} = _round($moonCoor->{lat}*$RAD,1);
  $Astro{".MoonRa"} = _round($moonCoor->{ra} *$RAD/15.,1);
  $Astro{MoonRa}    = HHMM($Astro{".MoonRa"});
  $Astro{MoonDec} = _round($moonCoor->{dec}*$RAD,1);
  $Astro{MoonAz}  = _round($moonCoor->{az} *$RAD,1);
  $Astro{MoonAlt} = _round($moonCoor->{alt}*$RAD + Refraction($moonCoor->{alt}),1);  # including refraction WARNUNG => *RAD ???
  $Astro{MoonSign}     = $tt->{$moonCoor->{sig}};
  $Astro{MoonSignN}    = $moonCoor->{sign};
  $Astro{MoonDistance} = _round($moonCoor->{distance},0);
  $Astro{MoonDiameter} = _round($moonCoor->{diameter}*$RAD*60.,1); # angular diameter in arc seconds
  $Astro{MoonAge}      = _round($moonCoor->{age}*$RAD,1);
  $Astro{MoonPhaseN}   = _round($moonCoor->{phasen},2);
  $Astro{MoonPhaseI}   = $moonCoor->{phasei};
  $Astro{MoonPhaseS}   = $tt->{$moonCoor->{phases}};
  
  #-- calculate distance from the observer (on the surface of earth) to the center of the moon
  my ($xm,$ym,$zm) = EquPolar2Cart($moonCoor->{ra}, $moonCoor->{dec}, $moonCoor->{distance});
  #Log 1,"  distance=".$moonCoor->{distance}."   test=".sqrt( ($xm)**2 + ($ym)**2 + ($zm)**2 )." $xm  $ym  $zm";
  #Log 1,"  distance=".$radius."   test=".sqrt( ($x)**2 + ($y)**2 + ($z)**2 )." $x  $y  $z";
  $Astro{".MoonDistanceObserver"} = sqrt( ($xm-$x)**2 + ($ym-$y)**2 + ($zm-$z)**2 );
  $Astro{MoonDistanceObserver}    = _round($Astro{".MoonDistanceObserver"},0);
  
  my ($moontransit,$moonrise,$moonset) = MoonRise($JD0, $deltaT, $lon, $lat, $radius, $Date{zonedelta}, 0);
  $Astro{".MoonTransit"} = $moontransit;
  $Astro{".MoonRise"}    = $moonrise;
  $Astro{".MoonSet"}     = $moonset;
  $Astro{MoonTransit}    = HHMM($moontransit);
  $Astro{MoonRise}       = HHMM($moonrise);
  $Astro{MoonSet}        = HHMM($moonset);

  #-- moon visiblity
  my $moonvisible;
  my $mooninvisible;
  if (
      (!defined($moonset) && !defined($moonrise)) ||
      ($moonset !~ m/^\d+/ && $moonrise !~ m/^\d+/)
  ){
    if ($Astro{MoonAlt} >= 0.) {
      $moonvisible = 24.;
      $mooninvisible = 0.;
    } else {
      $moonvisible = 0.;
      $mooninvisible = 24.;
    }
  }
  elsif (!defined($moonset) || $moonset !~ m/^\d+/) {
    $moonvisible = 24. - $moonrise;
    $mooninvisible = 24. - $moonvisible;
  }
  elsif (!defined($moonrise) || $moonrise !~ m/^\d+/) {
    $moonvisible = 24. - $moonset;
    $mooninvisible = 24. - $moonvisible;
  } else {
    my $ss = $moonset;
    $ss += 24.
      if ($moonrise > $moonset);
    $moonvisible = $ss - $moonrise;
    $mooninvisible = 24. - $moonvisible;
  }
  $Astro{".MoonHrsVisible"}   = $moonvisible;
  $Astro{".MoonHrsInvisible"} = $mooninvisible;
  $Astro{MoonHrsVisible}   = HHMM($moonvisible);
  $Astro{MoonHrsInvisible} = HHMM($mooninvisible);

  #-- fix date
  $Astro{ObsDate}      = $Date{date};
  $Astro{ObsTime}      = $Date{time};
  $Astro{ObsTimezone}  = $Date{zonedelta};
  $Astro{ObsTimezoneS} = $Date{tz} if ( $Date{tz} );
  $Astro{ObsDayofyear} = $Date{dayofyear};
  $Astro{ObsIsDST}     = $Date{isdst};
  $Astro{".timestamp"} = $Date{timestamp};
  $Astro{".timeday"}   = $Date{timeday};
  $Astro{".year"}      = $Date{year};
  $Astro{".month"}     = $Date{month};
  $Astro{".day"}       = $Date{day};
  $Astro{".hour"}      = $Date{hour};
  $Astro{".min"}       = $Date{min};
  $Astro{".sec"}       = $Date{sec};
  $Astro{".wdayl"}     = $Date{wdayl};
  $Astro{".wdays"}     = $Date{wdays};
  $Astro{".monthl"}    = $Date{monthl};
  $Astro{".months"}    = $Date{months};
  $Astro{".datetime"}  = $Date{datetime};
  $Astro{".week"}      = $Date{week};
  $Astro{".wday"}      = $Date{wday};

  #-- check astro season
  my ($seasonn, $season) = Season($JD0, $deltaT, $Astro{ObsLat});
  $Astro{ObsSeason}  = $tt->{$season};
  $Astro{ObsSeasonN} = $seasonn;

  delete local $ENV{TZ};
  tzset() if ( exists &{'tzset'} );

  return( undef );
};

########################################################################################################
#
# Moonwidget - SVG picture of the moon 
#
#  Parameter arg = argument array
#
########################################################################################################

sub Moonwidget($){
  my ($arg) = @_;
  my $name = $FW_webArgs{name};
  $name    =~ s/'//g if ($name);
  my $hash = $name && $name ne "" && defined($defs{$name}) ? $defs{$name} : ();
  
  my $mooncolor = 'rgb(255,220,100)';
  my $moonshadow = 'rgb(70,70,100)';

  $mooncolor = $FW_webArgs{mooncolor} 
    if ($FW_webArgs{mooncolor} );
  $moonshadow = $FW_webArgs{moonshadow}
    if ($FW_webArgs{moonshadow} );
    
  my @size = split('x', ($FW_webArgs{size} ? $FW_webArgs{size}  : '400x400'));
  
  $FW_RETTYPE = "image/svg+xml";
  $FW_RET="";
  FW_pO '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" width="'.$size[0].'px" height="'.$size[1].'px">';
  my $ma = Get($hash,("","text","MoonAge"));
  my $mb = Get($hash,("","text","MoonPhaseS"));

  my ($radius,$axis,$dir,$start,$middle);
  $radius = 250;
  $axis   = sin(($ma+90)*$DEG)*$radius;
  $axis   = -$axis
    if ($axis < 0);
    
  if( (0.0 <= $ma && $ma <= 90) || (270.0 < $ma && $ma <= 360.0) ){
    $dir = 1;
  }else{
    $dir = 0;
  }
  if( 0.0 < $ma && $ma <= 180 ){
    $start  = $radius;
    $middle = -$radius;
  }else{
    $start  = -$radius;
    $middle = $radius;
  }
 
  FW_pO '<g transform="translate(400,400) scale(-1,1)">';
  FW_pO '<circle cx="0" cy="0" r="250" fill="'.$moonshadow.'"/>';
  FW_pO '<path d="M 0 '.$start.' A '.$axis.' '.$radius.' 0 0 '.$dir.' 0 '.$middle.' A '.$radius.' '.$radius.' 0 0 0 0 '.$start.' Z" fill="'.$mooncolor.'"/>'; 
  FW_pO '</g>';
  #FW_pO '<text x="100" y="710" style="font-family:Helvetica;font-size:60px;font-weight:bold" fill="black">'.$mb.'</text>';
  FW_pO '</svg>';
  return ($FW_RETTYPE, $FW_RET); 
}	         

########################################################################################################
#
# Update - Update readings 
#
#  Parameter hash = hash of the bus master
#
########################################################################################################

sub Update($@) {
  my ($hash) = @_;
  
  my $name     = $hash->{NAME};
  RemoveInternalTimer($hash);
  delete $hash->{NEXTUPDATE};

  return undef if (IsDisabled($name));

  my $tz = AttrVal( $name, "timezone", AttrVal( "global", "timezone", undef ) );
  my $lang = AttrVal( $name, "language", AttrVal( "global", "language", undef ) );
  my $lc_time = AttrVal(
      $name,
      "lc_time",
      AttrVal(
          "global", "lc_time",
          ( $lang ? lc($lang) . "_" . uc($lang) . ".UTF-8" : undef )
      )
  );
  my $now = gettimeofday();    # conserve timestamp before recomputing

  SetTime(undef, $tz, $lc_time);
  Compute($hash);

  my @next;

  #-- add regular update interval time
  push @next, $now + $hash->{INTERVAL}
    if ( defined( $hash->{INTERVAL} ) && $hash->{INTERVAL} > 0 );

  #-- add event times
  foreach my $comp (
      defined( $hash->{RECOMPUTEAT} ) ? split( ',', $hash->{RECOMPUTEAT} ) : () )
  {
    if ( $comp eq 'NewDay' ) {
        my @dch = localtime($now + 86400 + 3600*((localtime($now))[8] - (localtime($now + 86400.))[8]));
        push @next,
          _timelocal_modern( 0, 0, 0, $dch[3], $dch[4], $dch[5]+1900. );
        next;
    }
    my $k = ".$comp";
    next unless ( defined( $Astro{$k} ) && $Astro{$k} =~ /^\d+(?:\.\d+)?$/ );
    my $t =
      _timelocal_modern( 0, 0, 0, (localtime($now))[3,4], (localtime($now))[5]+1900. ) + $Astro{$k} * 3600.;
    $t += 86400. if ( $t < $now );    # that is for tomorrow
    push @next, $t;
  }

  #-- set timer for next update
  if (@next) {
    my $n = minNum( $next[0], @next );
    $hash->{NEXTUPDATE} = FmtDateTime($n);
    InternalTimer( $n, "FHEM::Astro::Update", $hash, 1 );
  }

  readingsBeginUpdate($hash);
  foreach my $key (keys %Astro){
    readingsBulkUpdateIfChanged($hash,$key,encode_utf8($Astro{$key}));
  }
  readingsEndUpdate($hash,1); 
  readingsSingleUpdate($hash,"state","Updated",1);
}

########################################################################################################
#
# FormatReading - Convert a reading into text format
#
#  Parameter r = reading name, h = parameter hash, locale = locale string, val = optional value
#
########################################################################################################

sub FormatReading($$;$$) {
  my ( $r, $h, $lc_numeric, $val ) = @_;
  my $ret;
  $val = $Astro{$r} unless ( defined($val) );

  my $f = "%s";

  #-- number formatting
  $f = "%2.1f" if ( $r eq "MoonAge" );
  $f = "%2.1f" if ( $r eq "MoonAlt" );
  $f = "%2.1f" if ( $r eq "MoonAz" );
  $f = "%2.1f" if ( $r eq "MoonDec" );
  $f = "%2.1f" if ( $r eq "MoonDiameter" );
  $f = "%.0f"  if ( $r eq "MoonDistance" );
  $f = "%.0f"  if ( $r eq "MoonDistanceObserver" );
  $f = "%2.1f" if ( $r eq "MoonLat" );
  $f = "%2.1f" if ( $r eq "MoonLon" );
  $f = "%1.2f" if ( $r eq "MoonPhaseN" );
  $f = "%.0f"  if ( $r eq "ObsAlt" );
  $f = "%d"    if ( $r eq "ObsDayofyear" );
  $f = "%2.1f" if ( $r eq "ObsHorEvening" );
  $f = "%2.1f" if ( $r eq "ObsHorMorning" );
  $f = "%.2f"  if ( $r eq "ObsJD" );
  $f = "%.5f"  if ( $r eq "ObsLat" );
  $f = "%.5f"  if ( $r eq "ObsLon" );
  $f = "%2d"   if ( $r eq "ObsTimezone" );
  $f = "%2.1f" if ( $r eq "SunAlt" );
  $f = "%2.1f" if ( $r eq "SunAz" );
  $f = "%2.1f" if ( $r eq "SunDec" );
  $f = "%2.1f" if ( $r eq "SunDiameter" );
  $f = "%.0f"  if ( $r eq "SunDistance" );
  $f = "%.0f"  if ( $r eq "SunDistanceObserver" );
  $f = "%2.1f" if ( $r eq "SunLon" );

  $ret = $val ne "" ? sprintf( $f, $val ) : "";
  $ret = UConv::decimal_mark( $ret, $lc_numeric )
    unless ( $h && ref($h) && defined( $h->{html} ) && $h->{html} eq "0" );

  $ret = ( $val == 1. ? $tt->{"dst"} : $tt->{"nt"} )
    if ( $r eq "ObsIsDST" );

  if ( $h && ref($h) && ( !$h->{html} || $h->{html} ne "0" ) ) {

    #-- add unit if desired
    if ( $h->{unit}
        || ( $h->{long} && ( !defined( $h->{unit} ) || $h->{unit} ne "0" ) ) )
    {
      $ret .= "°"                         if ( $r eq "MoonAge" );
      $ret .= "°"                         if ( $r eq "MoonAlt" );
      $ret .= "°"                         if ( $r eq "MoonAz" );
      $ret .= "°"                         if ( $r eq "MoonDec" );
      $ret .= "′"                         if ( $r eq "MoonDiameter" );
      $ret .= chr(0x00A0) . "km"          if ( $r eq "MoonDistance" );
      $ret .= chr(0x00A0) . "km"          if ( $r eq "MoonDistanceObserver" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "MoonHrsInvisible" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "MoonHrsVisible" );
      $ret .= "°"                         if ( $r eq "MoonLat" );
      $ret .= "°"                         if ( $r eq "MoonLon" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "MoonRa" );
      $ret .= chr(0x00A0) . "m"           if ( $r eq "ObsAlt" );
      $ret .= "."                         if ( $r eq "ObsDayofyear" );
      $ret .= "°"                         if ( $r eq "ObsHorEvening" );
      $ret .= "°"                         if ( $r eq "ObsHorMorning" );
      $ret .= chr(0x00A0) . $tt->{"days"} if ( $r eq "ObsJD" );
      $ret .= "°"                         if ( $r eq "ObsLat" );
      $ret .= "°"                         if ( $r eq "ObsLon" );
      $ret .= "°"                         if ( $r eq "SunAlt" );
      $ret .= "°"                         if ( $r eq "SunAz" );
      $ret .= "°"                         if ( $r eq "SunDec" );
      $ret .= "′"                         if ( $r eq "SunDiameter" );
      $ret .= chr(0x00A0) . "km"          if ( $r eq "SunDistance" );
      $ret .= chr(0x00A0) . "km"          if ( $r eq "SunDistanceObserver" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "SunHrsInvisible" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "SunHrsVisible" );
      $ret .= "°"                         if ( $r eq "SunLon" );
      $ret .= chr(0x00A0) . "h"           if ( $r eq "SunRa" );
    }

    #-- add text if desired
    if ( $h->{long} ) {
      my $sep = " ";
      $sep = ": "
        if ( $h->{long} > 1. && ( $h->{long} < 4. || $r !~ /^Sun|Moon/ ) );
      $sep = "" if ($ret eq "");

      $ret = $tt->{"twilightastro"} . $sep . $ret
        if ( $r eq "AstroTwilightEvening" );
      $ret = $tt->{"twilightastro"} . $sep . $ret
        if ( $r eq "AstroTwilightMorning" );
      $ret = $tt->{"twilightcivil"} . $sep . $ret
        if ( $r eq "CivilTwilightEvening" );
      $ret = $tt->{"twilightcivil"} . $sep . $ret
        if ( $r eq "CivilTwilightMorning" );
      $ret = $tt->{"twilightcustom"} . $sep . $ret
        if ( $r eq "CustomTwilightEvening" );
      $ret = $tt->{"twilightcustom"} . $sep . $ret
        if ( $r eq "CustomTwilightMorning" );
      $ret = $tt->{"age"} . $sep . $ret      if ( $r eq "MoonAge" );
      $ret = $tt->{"alt"} . $sep . $ret      if ( $r eq "MoonAlt" );
      $ret = $tt->{"az"} . $sep . $ret       if ( $r eq "MoonAz" );
      $ret = $tt->{"dec"} . $sep . $ret      if ( $r eq "MoonDec" );
      $ret = $tt->{"diameter"} . $sep . $ret if ( $r eq "MoonDiameter" );
      $ret = $ret . $sep . $tt->{"toce"}
        if ( $r eq "MoonDistance" );
      $ret = $ret . $sep . $tt->{"toobs"}
        if ( $r eq "MoonDistanceObserver" );
      $ret = $tt->{"hoursofvisibility"} . $sep . $ret
        if ( $r eq "MoonHrsVisible" );
      $ret = $tt->{"latecl"} . $sep . $ret  if ( $r eq "MoonLat" );
      $ret = $tt->{"lonecl"} . $sep . $ret  if ( $r eq "MoonLon" );
      $ret = $tt->{"phase"} . $sep . $ret   if ( $r eq "MoonPhaseN" );
      $ret = $tt->{"phase"} . $sep . $ret   if ( $r eq "MoonPhaseS" );
      $ret = $tt->{"ra"} . $sep . $ret      if ( $r eq "MoonRa" );
      $ret = $tt->{"rise"} . $sep . $ret    if ( $r eq "MoonRise" );
      $ret = $tt->{"set"} . $sep . $ret     if ( $r eq "MoonSet" );
      $ret = $tt->{"sign"} . $sep . $ret    if ( $r eq "MoonSign" );
      $ret = $tt->{"transit"} . $sep . $ret if ( $r eq "MoonTransit" );
      $ret = $tt->{"twilightnautic"} . $sep . $ret
        if ( $r eq "NauticTwilightEvening" );
      $ret = $tt->{"twilightnautic"} . $sep . $ret
        if ( $r eq "NauticTwilightMorning" );
      $ret = $ret . $sep . $tt->{"altitude"}  if ( $r eq "ObsAlt" );
      $ret = $tt->{"date"} . $sep . $ret      if ( $r eq "ObsDate" );
      $ret = $ret . $sep . $tt->{"dayofyear"} if ( $r eq "ObsDayofyear" );
      $ret = $tt->{"alt"} . $sep . $ret       if ( $r eq "ObsHorEvening" );
      $ret = $tt->{"alt"} . $sep . $ret       if ( $r eq "ObsHorMorning" );
      $ret = ( $val == 1. ? $tt->{"switchtodst"} : $tt->{"switchtont"} )
        if ( $r eq "ObsIsDST" );
      $ret = $tt->{"jdate"} . $sep . $ret     if ( $r eq "ObsJD" );
      $ret = $tt->{"lmst"} . $sep . $ret      if ( $r eq "ObsLMST" );
      $ret = $ret . $sep . $tt->{"latitude"}  if ( $r eq "ObsLat" );
      $ret = $ret . $sep . $tt->{"longitude"} if ( $r eq "ObsLon" );
      $ret = $tt->{"season"} . $sep . $ret    if ( $r eq "ObsSeason" );
      $ret = $tt->{"time"} . $sep . $ret      if ( $r eq "ObsTime" );
      $ret = $tt->{"timezone"} . $sep . $ret  if ( $r eq "ObsTimezone" );
      $ret = $tt->{"alt"} . $sep . $ret       if ( $r eq "SunAlt" );
      $ret = $tt->{"az"} . $sep . $ret        if ( $r eq "SunAz" );
      $ret = $tt->{"dec"} . $sep . $ret       if ( $r eq "SunDec" );
      $ret = $tt->{"diameter"} . $sep . $ret  if ( $r eq "SunDiameter" );
      $ret = $ret . $sep . $tt->{"toce"}
        if ( $r eq "SunDistance" );
      $ret = $ret . $sep . $tt->{"toobs"}
        if ( $r eq "SunDistanceObserver" );
      $ret = $tt->{"hoursofnight"} . $sep . $ret
        if ( $r eq "SunHrsInvisible" );
      $ret = $tt->{"hoursofsunlight"} . $sep . $ret
        if ( $r eq "SunHrsVisible" );
      $ret = $tt->{"lonecl"} . $sep . $ret  if ( $r eq "SunLon" );
      $ret = $tt->{"ra"} . $sep . $ret      if ( $r eq "SunRa" );
      $ret = $tt->{"rise"} . $sep . $ret    if ( $r eq "SunRise" );
      $ret = $tt->{"set"} . $sep . $ret     if ( $r eq "SunSet" );
      $ret = $tt->{"sign"} . $sep . $ret    if ( $r eq "SunSign" );
      $ret = $tt->{"transit"} . $sep . $ret if ( $r eq "SunTransit" );

      #-- add prefix for Sun/Moon if desired
      if ( $h->{long} > 2. && $r =~ /^Sun|Moon/ ) {
        $ret = " " . $ret;

        #-- add a separator after prefix if desired
        $ret = ":" . $ret if ( $h->{long} > 3. );

        $ret = $tt->{"sun"} . $ret
          if ( $r =~ /^Sun/ );
        $ret = $tt->{"moon"} . $ret
          if ( $r =~ /^Moon/ );
      }
    }
  }

  return encode_utf8($ret);
}

########################################################################################################
#
# Set - Implements SetFn function 
#
#  Parameter hash = hash of device addressed, a = array of arguments, h = hash of parameters
#
########################################################################################################

sub Set($@) {
  my ($hash,$a,$h) = @_;

  my $name = shift @$a;

  if ( $a->[0] eq "update" ) {
    return "[FHEM::Astro::Set] $name is disabled" if ( IsDisabled($name) );
    RemoveInternalTimer($hash);
    InternalTimer( gettimeofday() + 1, "FHEM::Astro::Update", $hash, 1 );
  }
  else {
    return
      "[FHEM::Astro::Set] $name with unknown argument $a->[0], choose one of "
      . join( " ",
        map { defined( $sets{$_} ) ? "$_:$sets{$_}" : $_ }
        sort keys %sets );
  }

  return "";
}

########################################################################################################
#
# Get - Implements GetFn function 
#
#  Parameter hash = hash of the bus master a = argument array
#
########################################################################################################

sub Get($@) {
  my ($hash,$a,$h,@a) = @_;
  my $name = "#APIcall";
  my $type = "dummy";

  #-- backwards compatibility for non-parseParams requests
  if (!ref($a)) {
    $hash = exists($defs{$hash}) ? $defs{$hash} : ()
      if ($hash && !ref($hash));
    unshift @a, $h;
    $h = undef;
    $type = $a;
    $a = \@a;
  }
  else {
    $type = shift @$a;
  }
  if (defined($hash->{NAME})) {
    $name = $hash->{NAME};
  } else {
    $hash->{NAME} = $name;
  }

  my $wantsreading = 0;
  my $dayOffset = 0;
  my $now = gettimeofday();
  my $html = defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : undef;
  my $tz = AttrVal( $name, "timezone", AttrVal( "global", "timezone", undef ) );
  my $lang = AttrVal( $name, "language", AttrVal( "global", "language", undef ) );
  my $lc_numeric = AttrVal(
      $name,
      "lc_numeric",
      AttrVal(
          "global", "lc_numeric",
          ( $lang ? lc($lang) . "_" . uc($lang) . ".UTF-8" : undef )
      )
  );
  my $lc_time = AttrVal(
      $name,
      "lc_time",
      AttrVal(
          "global", "lc_time",
          ( $lang ? lc($lang) . "_" . uc($lang) . ".UTF-8" : undef )
      )
  );
  if ( $h && ref($h) ) {
    $html       = $h->{html}       if ( defined( $h->{html} ) );
    $tz         = $h->{timezone}   if ( defined( $h->{timezone} ) );
    $lc_numeric = $h->{lc_numeric} if ( defined( $h->{lc_numeric} ) );
    $lc_numeric = lc( $h->{language} ) . "_" . uc( $h->{language} ) . ".UTF-8"
      if ( !$lc_numeric && defined( $h->{language} ) );
    $lc_time = $h->{lc_time} if ( defined( $h->{lc_time} ) );
    $lc_time = lc( $h->{language} ) . "_" . uc( $h->{language} ) . ".UTF-8"
      if ( !$lc_time && defined( $h->{language} ) );
  }

  #-- fill %Astro if it is still empty after restart to avoid warnings
  Compute($hash, $h) if (scalar keys %Astro == 0);

  #-- second parameter may be one or many readings
  my @readings;
  if( (int(@$a)>1) ) {
    @readings = split(',', $a->[1]);
    foreach (@readings) {
      if(exists($Astro{$_})) {
        $wantsreading = 1;
        last;
      }
    }
  }

  #-- last parameter may be indicating day offset
  if(
    (int(@$a)>4+$wantsreading && $a->[4+$wantsreading] =~ /^\+?([-+]\d+|yesterday|tomorrow)$/i) ||
    (int(@$a)>3+$wantsreading && $a->[3+$wantsreading] =~ /^\+?([-+]\d+|yesterday|tomorrow)$/i) ||
    (int(@$a)>2+$wantsreading && $a->[2+$wantsreading] =~ /^\+?([-+]\d+|yesterday|tomorrow)$/i) ||
    (int(@$a)>1+$wantsreading && $a->[1+$wantsreading] =~ /^\+?([-+]\d+|yesterday|tomorrow)$/i)
  ){
    $dayOffset = $1;
    pop @$a;
    $dayOffset = -1 if (lc($dayOffset) eq "yesterday");
    $dayOffset = 1  if (lc($dayOffset) eq "tomorrow");
  }

  if( int(@$a) > (1+$wantsreading) ) {
    my $str = (int(@$a) == (3+$wantsreading)) ? $a->[1+$wantsreading]." ".$a->[2+$wantsreading] : $a->[1+$wantsreading];
    if( $str =~ /^(\d{2}):(\d{2})(?::(\d{2}))?$|^(?:(?:(\d{4})-)?(\d{2})-(\d{2}))(?:\D+(\d{2}):(\d{2})(?::(\d{2}))?)?$/){
      return "[FHEM::Astro::Get] hours can only be between 00 and 23" if (defined($1) && $1 > 23.);
      return "[FHEM::Astro::Get] minutes can only be between 00 and 59" if (defined($2) && $2 > 59.);
      return "[FHEM::Astro::Get] seconds can only be between 00 and 59" if (defined($3) && $3 > 59.);
      return "[FHEM::Astro::Get] month can only be between 01 and 12" if (defined($5) && ($5 > 12. || $5 < 1.));
      return "[FHEM::Astro::Get] day can only be between 01 and 31" if (defined($6) && ($6 > 31. || $6 < 1.));
      return "[FHEM::Astro::Get] hours can only be between 00 and 23" if (defined($7) && $7 > 23.);
      return "[FHEM::Astro::Get] minutes can only be between 00 and 59" if (defined($8) && $8 > 59.);
      return "[FHEM::Astro::Get] seconds can only be between 00 and 59" if (defined($9) && $9 > 59.);

      SetTime(
          _timelocal_modern(
              defined($3) ? $3 : (defined($9) ? $9 : 0),
              defined($2) ? $2 : (defined($8) ? $8 : 0),
              defined($1) ? $1 : (defined($7) ? $7 : 12),
              (defined($5)? ($6,$5-1.) : (localtime($now))[3,4]),
              (defined($4)? $4 : (localtime($now))[5]+1900.),
          ) + ( $dayOffset * 86400. ), $tz, $lc_time
        )
    }else{
      return "[FHEM::Astro::Get] $name has improper time specification $str, use [YYYY-]MM-DD [HH:MM[:SS]] [-1|yesterday|+1|tomorrow]";
    }
  }else{
    SetTime($now + ($dayOffset * 86400.), $tz, $lc_time);
  }

  #-- disable automatic links to FHEM devices
  delete $FW_webArgs{addLinks};

  if( $a->[0] eq "version") {
    return version->parse(FHEM::Astro::->VERSION())->normal;
    
  }elsif( $a->[0] eq "json") {
    Compute($hash, $h);

    #-- beautify JSON at cost of performance only when debugging
    if (ref($json) && AttrVal($name,"verbose",AttrVal("global","verbose",3)) > 3.) {
      $json->canonical;
      $json->pretty;
    }

    if( $wantsreading==1 ){
      my %ret;
      foreach (@readings) {
        next unless(exists($Astro{$_}) && !ref($Astro{$_}));
        if ($h && ref($h) && ($h->{text} || $h->{unit} || $h->{long})) {
          $ret{text}{$_} = FormatReading($_, $h, $lc_numeric);
        }
        $ret{$_} = $Astro{$_};
      }
      return $json->encode(\%ret) if (ref($json));
      return toJSON(\%ret);
    }else{
      if ($h && ref($h) && ($h->{text} || $h->{unit} || $h->{long})) {
        foreach (keys %Astro) {
          next if (ref($Astro{$_}) || $_ =~ /^\./);
          $Astro{text}{$_} = FormatReading($_, $h, $lc_numeric);
        }
      }
      return $json->encode(\%Astro) if (ref($json));
      return toJSON(\%Astro);
    }

  }elsif( $a->[0] eq "text") {
    Compute($hash, $h);
    my $ret = "";

    if ( $wantsreading==1 && $h && ref($h) && scalar keys %{$h} > 0 ) {
      foreach (@readings) {
        next if (!defined($Astro{$_}) || ref($Astro{$_}));
        $ret .= $html && $html eq "1" ? "<br/>\n" : "\n"
          if ( $ret ne "" );
        $ret .= FormatReading( $_, $h, $lc_numeric ) unless($_ =~ /^\./);
        $ret .= encode_utf8($Astro{$_}) if ($_ =~ /^\./);
      }
      $ret = "<html>" . $ret . "</html>" if (defined($html) && $html ne "0");
    }
    elsif ( $wantsreading==1 ) {
      foreach (@readings) {
        next if (!defined($Astro{$_}) || ref($Astro{$_}));
        $ret .= $html && $html eq "1" ? "<br/>\n" : "\n"
          if ( $ret ne "" );
        $ret .= encode_utf8($Astro{$_});
      }
      $ret = "<html>" . $ret . "</html>" if (defined($html) && $html ne "0");
    }
    else {
      $h->{long} = 1 unless ( defined( $h->{long} ) );

      $ret = FormatReading( "ObsDate", $h, $lc_numeric ) . " " . $Astro{ObsTime};
      $ret .= (
        ( $Astro{ObsIsDST} == 1 )
        ? " (" . FormatReading( "ObsIsDST", undef ) . ")"
        : ""
      );
      $ret .= ", " . FormatReading( "ObsTimezone", $h ) . "\n";
      $ret .= FormatReading( "ObsJD", $h, $lc_numeric ) . ", "
        . FormatReading( "ObsDayofyear", $h, $lc_numeric ) . "\n";
      $ret .= FormatReading( "ObsSeason", $h ) . "\n";
      $ret .=
          $tt->{"coord"} . ":  "
        . FormatReading( "ObsLon", $h, $lc_numeric ) . ", "
        . FormatReading( "ObsLat", $h, $lc_numeric ) . ", "
        . FormatReading( "ObsAlt", $h, $lc_numeric ) . "\n";
      $ret .= FormatReading( "ObsLMST", $h ) . "\n\n";

      $ret .= "\n" . $tt->{"sun"} . "\n";
      $ret .=
          FormatReading( "SunRise", $h ) . "   "
        . FormatReading( "SunSet",     $h ) . "   "
        . FormatReading( "SunTransit", $h ) . "\n";
      $ret .= FormatReading( "SunHrsVisible", $h ) . "   "
        . FormatReading( "SunHrsInvisible", $h ) . "\n";
      $ret .= FormatReading( "CivilTwilightMorning", $h ) . "  -  "
        . $Astro{CivilTwilightEvening} . "\n";
      $ret .= FormatReading( "NauticTwilightMorning", $h ) . "  -  "
        . $Astro{NauticTwilightEvening} . "\n";
      $ret .= FormatReading( "AstroTwilightMorning", $h ) . "  -  "
        . $Astro{AstroTwilightEvening} . "\n";
      $ret .=
          $tt->{"distance"} . ":  "
        . FormatReading( "SunDistance",         $h, $lc_numeric ) . " ("
        . FormatReading( "SunDistanceObserver", $h, $lc_numeric ) . ")\n";
      $ret .=
          $tt->{"position"} . ":  "
        . FormatReading( "SunLon", $h, $lc_numeric ) . ", "
        . FormatReading( "SunRa",  $h, $lc_numeric ) . ", "
        . FormatReading( "SunDec", $h, $lc_numeric ) . "; "
        . FormatReading( "SunAz",  $h, $lc_numeric ) . ", "
        . FormatReading( "SunAlt", $h, $lc_numeric ) . "\n";
      $ret .= FormatReading( "SunDiameter", $h, $lc_numeric ) . ", "
        . FormatReading( "SunSign", $h ) . "\n\n";

      $ret .= "\n" . $tt->{"moon"} . "\n";
      $ret .=
          FormatReading( "MoonRise", $h ) . "   "
        . FormatReading( "MoonSet",     $h ) . "   "
        . FormatReading( "MoonTransit", $h ) . "\n";
      $ret .= FormatReading( "MoonHrsVisible", $h ) . "\n";
      $ret .=
          $tt->{"distance"} . ":  "
        . FormatReading( "MoonDistance",         $h, $lc_numeric ) . " ("
        . FormatReading( "MoonDistanceObserver", $h, $lc_numeric ) . ")\n";
      $ret .=
          $tt->{"position"} . ":  "
        . FormatReading( "MoonLon", $h, $lc_numeric ) . ", "
        . FormatReading( "MoonLat", $h, $lc_numeric ) . "; "
        . FormatReading( "MoonRa",  $h, $lc_numeric ) . ", "
        . FormatReading( "MoonDec", $h, $lc_numeric ) . ", "
        . FormatReading( "MoonAz",  $h, $lc_numeric ) . ", "
        . FormatReading( "MoonAlt", $h, $lc_numeric ) . "\n";
      $ret .=
          FormatReading( "MoonDiameter", $h, $lc_numeric ) . ", "
        . FormatReading( "MoonAge",    $h, $lc_numeric ) . ", "
        . FormatReading( "MoonPhaseN", $h, $lc_numeric ) . " = "
        . $Astro{MoonPhaseS} . ", "
        . FormatReading( "MoonSign", $h );

        if ($html && $html eq "1") {
          $ret = "<html>" . $ret . "</html>";
          $ret =~ s/   /&nbsp;&nbsp;&nbsp;/g;
          $ret =~ s/  /&nbsp;&nbsp;/g;
          $ret =~ s/\n/<br\/>\n/g;
        }
    }

    return $ret;
  }else {
    return "[FHEM::Astro::Get] $name with unknown argument $a->[0], choose one of ". 
    join(" ", map { defined($gets{$_})?"$_:$gets{$_}":$_ } sort keys %gets);
  }
}

1;

=pod
=encoding utf8
=item helper
=item summary collection of various routines for astronomical data
=item summary_DE Sammlung verschiedener Routinen für astronomische Daten
=begin html

   <a name="Astro"></a>
        <h3>Astro</h3>
        <ul>
        <p> FHEM module with a collection of various routines for astronomical data</p>
        <a name="Astrodefine"></a>
        <h4>Define</h4>
        <p>
            <code>define &lt;name&gt; Astro [global]</code>
            <br />Defines the Astro device (only one is needed per FHEM installation).
            <br />
            Optional parameter 'global' will mark this device for global configuration options, see SUNRISE_EL compatibility mode below for details.</p>
        <p>
        Readings with prefix <i>Sun</i> refer to the sun, with prefix <i>Moon</i> refer to the moon.
        The suffixes for these readings are:
        <ul>
        <li><i>Age</i> = angle (in degrees) of body along its track</li>
        <li><i>Az,Alt</i> = azimuth and altitude angle (in degrees) of body above horizon</li>
        <li><i>Dec,Ra</i> = declination (in degrees) and right ascension (in HH:MM) of body position</li>
        <li><i>HrsVisible,HrsInvisible</i> = Hours of visiblity and invisiblity of the body</li>
        <li><i>Lat,Lon</i> = latitude and longituds (in degrees) of body position</li>
        <li><i>Diameter</i> = virtual diameter (in arc minutes) of body</li>
        <li><i>Distance,DistanceObserver</i> = distance (in km) of body to center of earth or to observer</li>
        <li><i>PhaseN,PhaseS</i> = Numerical and string value for phase of body</li>
	    <li><i>Sign,SignN</i> = Circadian sign for body along its track</li>
	    <li><i>Rise,Transit,Set</i> = times (in HH:MM) for rise and set as well as for highest position of body</li>
        </ul>
        <p>
        Readings with prefix <i>Obs</i> refer to the observer.
        In addition to some of the suffixes gives above, the following may occur:
        <ul>
        <li><i>Date,Dayofyear</i> = date</li>
        <li><i>JD</i> = Julian date</li>
        <li><i>Time,Timezone,TimezoneS</i> = obvious meaning</li>
        <li><i>IsDST</i> = 1 if running on daylight savings time, 0 otherwise</li>
        <li><i>GMST,LMST</i> = Greenwich and Local Mean Sidereal Time (in HH:MM)</li>
	    </ul>
	    <p>
	    An SVG image of the current moon phase may be obtained under the link 
	    <code>&lt;ip address of fhem&gt;/fhem/Astro_moonwidget?name='&lt;device name&gt;'</code>.
	    Optional web parameters are <code>[&amp;size='&lt;width&gt;x&lt;height&gt;'][&amp;mooncolor=&lt;color&gt;][&amp;moonshadow=&lt;color&gt;]</code>
	    <p>
        Notes: <ul>
        <li>Calculations are only valid between the years 1900 and 2100</li>
        <li>Attention: Timezone is taken from the local Perl settings, NOT automatically defined for a location</li>
        <li>This module uses the global attribute <code>language</code> to determine its output data<br/>
         (default: EN=english). For German output, set <code>attr global language DE</code>.<br/>
         If a local attribute on the device was set for language it will take precedence.</li>
        <li>The time zone is determined automatically from the local settings of the <br/>
        operating system. If geocordinates from a different time zone are used, the results are<br/>
        not corrected automatically.</li>
        <li>Some definitions determining the observer position are used<br/>
        from the global device, i.e.<br/>
        <ul>
        <code>attr global longitude &lt;value&gt;</code><br/>
        <code>attr global latitude &lt;value&gt;</code><br/>
        <code>attr global altitude &lt;value&gt;</code> (in m above sea level)
        </ul>
        These definitions are only used when there are no corresponding local attribute settings on the device.
        </li>
        <li>
        It is not necessary to define an Astro device to use the data provided by this module.<br/>
        To use its data in any other module, you just need to put <code>LoadModule("Astro");</code> <br/>
        at the start of your own code, and then may call, for example, the function<br/> 
        <ul><code>Astro_Get( SOME_HASH_REFERENCE,"dummy","text", "SunRise","2019-12-24");</code></ul>
        to acquire the sunrise on Christmas Eve 2019. The hash reference may also be undefined or an existing device name of any type. Note that device attributes of the respective device will be respected as long as their name matches those mentioned for an Astro device.
        attribute=value pairs may be added in text format to enforce
        settings like language that would otherwise be defined by a real device.<br/>
        You may also add parameters to customize your request:<br/>
        <ul><code>Astro_Get( SOME_HASH_REFERENCE, ["dummy","text"], {html=>1} );</code><br/>
        <code>Astro_Get( SOME_HASH_REFERENCE, ["dummy","text","SunRise,SunSet,SunAz,SunDistanceObserver"], {html=>1, long=>2} );</code></ul>
        </li>
        <li>Functions that can replace those from <a href="#SUNRISE_EL">SUNRISE_EL</a> are available under the same name and adding <i>a</i> as a prefix in front of it (for example, use <code>asunrise()</code> instead of <code>sunrise()</code>). A single Astro device can act as a global configuration device for such functions if it was created using the <i>global</i> define parameter. If you don't want to create any Astro device at all, you may put <code>LoadModule("Astro");</code> into your 99_myUtils.pm to only load the SUNRISE_EL replacement functions.
        </li>
        </ul>
        <a name="Astroset"></a>
        <h4>Set</h4>
        <ul>
            <li><a name="Astro_update"></a>
                <code>set &lt;name&gt; update</code>
                <br />trigger to recompute values immediately.</li>
        </ul>
        <a name="Astroget"></a>
        <h4>Get</h4>
        Attention: Get-calls are NOT written into the readings of the device. Readings change only through periodic updates.<br/>
        <ul>
            <li><a name="Astro_json"></a>
                <code>get &lt;name&gt; json [&lt;reading&gt;,[&lt;reading&gt;]] [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; json [&lt;reading&gt;,[&lt;reading&gt;]] MM-DD [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; json [&lt;reading&gt;,[&lt;reading&gt;]] YYYY-MM-DD [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; json [&lt;reading&gt;,[&lt;reading&gt;]] HH:MM[:SS] [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; json [&lt;reading&gt;,[&lt;reading&gt;]] YYYY-MM-DD HH:MM[:SS] [-1|yesterday|+1|tomorrow]</code>
                <br />returns the complete set of an individual reading of astronomical data either for the current time, or for a day and time given in the argument. <code>yesterday</code>, <code>tomorrow</code> or any other integer number may be given at the end to get data relative to the given day and time.<br/>
                Formatted values as described below may be generated in a subtree <code>text</code> by adding <code>text=1</code> to the request.</li>
            <li><a name="Astro_text"></a>
                <code>get &lt;name&gt; text [&lt;reading&gt;,[&lt;reading&gt;]] [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; text [&lt;reading&gt;,[&lt;reading&gt;]] MM-DD [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; text [&lt;reading&gt;,[&lt;reading&gt;]] YYYY-MM-DD [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; text [&lt;reading&gt;,[&lt;reading&gt;]] HH:MM[:SS] [-1|yesterday|+1|tomorrow]</code><br/>
                <code>get &lt;name&gt; text [&lt;reading&gt;,[&lt;reading&gt;]] YYYY-MM-DD HH:MM[:SS] [-1|yesterday|+1|tomorrow]</code>
                <br />returns the complete set of an individual reading of astronomical data either for the current time, or for a day and time given in the argument. <code>yesterday</code>, <code>tomorrow</code> or any other integer number may be given at the end to get data relative to the given day and time.<br/>
                The return value may be formatted and/or labeled by adding one or more of the following key=value pairs to the request:
                      <ul>
                      <li><i>unit=1</i> = Will add a unit to numerical values. Depending on attribute lc_numeric, the decimal separator will be in regional format as well.</li>
                      <li><i>long=1</i> = A describtive label will be added to the value.</li>
                      <li><i>long=2</i> = Same as long=1 but the label will be separated from the value by a colon.</li>
                      <li><i>long=3</i> = Same as long=2 but Sun or Moon will be added as a prefix.</li>
                      <li><i>long=4</i> = Same as long=3 but the separator will be used to separate Sun/Moon instead.</li>
                      </ul></li>
            <li><a name="Astro_version"></a>
                <code>get &lt;name&gt; version</code>
                <br />Display the version of the module</li>             
        </ul>
        <a name="Astroattr"></a>
        <h4>Attributes</h4>
        <ul>
            <li><a name="Astro_interval"></a>
                <code>&lt;interval&gt;</code>
                <br />Update interval in seconds. The default is 3600 seconds, a value of 0 disables the periodic update.</li>
            <li><a name="Astro_language"></a>
                <code>&lt;language&gt;</code>
                <br />A language may be set to overwrite global attribute settings.</li>
            <li><a name="Astro_lc_numeric"></a>
                <code>&lt;lc_numeric&gt;</code>
                <br />Set regional settings to format numerical values in textual output. If not set, it will generate the locale based on the attribute <i>language</i> (if set).</li>
            <li><a name="Astro_lc_time"></a>
                <code>&lt;lc_time&gt;</code>
                <br />Set regional settings to format time related values in textual output. If not set, it will generate the locale based on the attribute <i>language</i> (if set).</li>
            <li><a name="Astro_recomputeAt"></a>
                <code>&lt;recomputeAt&gt;</code>
                <br />Enforce recomputing values at specific event times, independant from update interval. This attribute contains a list of one or many of the following values:<br />
                      <ul>
                      <li><i>MoonRise,MoonSet,MoonTransit</i> = for moon rise, set, and transit</li>
                      <li><i>NewDay</i> = for 00:00:00 hours of the next calendar day</li>
                      <li><i>SunRise,SunSet,SunTransit</i> = for sun rise, set, and transit</li>
                      <li><i>*TwilightEvening,*TwilightMorning</i> = for the respective twilight stage begin</li>
                      </ul></li>
            <li><a name="Astro_timezone"></a>
                <code>&lt;timezone&gt;</code>
                <br />A timezone may be set to overwrite global and system settings. Format may depend on your local system implementation but is likely in the format similar to <code>Europe/Berlin</code>.</li>
            <li>Some definitions determining the observer position:<br/>
                <ul>
                <code>attr  &lt;name&gt;  longitude &lt;value&gt;</code><br/>
                <code>attr  &lt;name&gt;  latitude &lt;value&gt;</code><br/>
                <code>attr  &lt;name&gt;  altitude &lt;value&gt;</code> (in m above sea level)<br/>
                <code>attr  &lt;name&gt;  horizon &lt;value&gt;</code> custom horizon angle in degrees, default 0. Different values for morning/evening may be set as <code>&lt;morning&gt;:&lt;evening&gt;</code>
                </ul>
                These definitions take precedence over global attribute settings.</li>
            <li><a name="Astro_disable"></a>
                <code>&lt;disable&gt;</code>
                <br />When set, this will completely disable any device update.</li>
            <li>Standard attributes <a href="#alias">alias</a>, <a href="#comment">comment</a>, <a
                    href="#event-on-update-reading">event-on-update-reading</a>, <a
                    href="#event-on-change-reading">event-on-change-reading</a>, <a href="#room"
                    >room</a>, <a href="#eventMap">eventMap</a>, <a href="#loglevel">loglevel</a>,
                    <a href="#webCmd">webCmd</a></li>
        </ul>
        </ul>
=end html
=begin html_DE

<a name="Astro"></a>
<h3>Astro</h3>
<ul>
<a href="https://wiki.fhem.de/wiki/Modul_Astro">Deutsche Dokumentation im Wiki</a> vorhanden, die englische Version gibt es hier: <a href="commandref.html#Astro">Astro</a> 
</ul>
=end html_DE
=for :application/json;q=META.json 95_Astro.pm
{
  "version": "v2.2",
  "author": [
    "Prof. Dr. Peter A. Henning <>",
    "Julian Pawlowski <>",
    "Marko Oldenburg <>"
  ],
  "x_fhem_maintainer": [
    "pahenning",
    "loredo",
    "CoolTux"
  ],
  "resources": {
    "x_wiki": {
      "web": "https://wiki.fhem.de/wiki/Modul_Astro"
    }
  },
  "keywords": [
    "astrology",
    "astronomy",
    "constellation",
    "moon",
    "moonrise",
    "moonset",
    "sun",
    "sunrise",
    "sunset",
    "star sign",
    "twilight",
    "zodiac",
    "Astrologie",
    "Astronomie",
    "Mond",
    "Mondaufgang",
    "Monduntergang",
    "Sonne",
    "Sternbild",
    "Sternzeichen",
    "Sonnenaufgang",
    "Sonnenuntergang",
    "Tierkreiszeichen",
    "Zodiak"
  ],
  "prereqs": {
    "runtime": {
      "requires": {
        "Encode": 0,
        "GPUtils": 0,
        "Math::Trig": 0,
        "POSIX": 0,
        "Time::HiRes": 0,
        "Time::Local": 0,
        "strict": 0,
        "utf8": 0,
        "warnings": 0
      },
      "recommends": {
        "JSON": 0
      },
      "suggests": {
        "Cpanel::JSON::XS": 0,
        "JSON::XS": 0
      }
    }
  }
}
=end :application/json;q=META.json
=cut
