###############################################################################
# $Id: UConv.pm 21159 2020-02-09 14:04:27Z loredo $
package main;

# only to suppress file reload error in FHEM
sub UConv_Initialize() { }

package UConv;
use strict;
use warnings;
use POSIX;
use utf8;

use Math::Trig;
use Math::Trig ':pi';
use Math::Trig ':radial';
use Math::Trig ':great_circle';
use Scalar::Util qw(looks_like_number);
use Time::HiRes qw(gettimeofday);
use Time::Local;
use Encode;

#use Data::Dumper;

sub _ReplaceStringByHashKey($$;$);

####################
# Translations

our %compasspoints = (
    en => [
        [ "North",           "N",   '▲' ],
        [ "North-Northeast", "NNE", '⬈' ],
        [ "North-East",      "NE",  '⬈' ],
        [ "East-Northeast",  "ENE", '⬈' ],
        [ "East",            "E",   '▶' ],
        [ "East-Southeast",  "ESE", '⬊' ],
        [ "Southeast",       "SE",  '⬊' ],
        [ "South-Southeast", "SSE", '⬊' ],
        [ "South",           "S",   '▼' ],
        [ "South-Southwest", "SSW", '⬋' ],
        [ "Southwest",       "SW",  '⬋' ],
        [ "West-Southwest",  "WSW", '⬋' ],
        [ "West",            "W",   '◀' ],
        [ "West-Northwest",  "WNW", '⬉' ],
        [ "Northwest",       "NW",  '⬉' ],
        [ "North-Northwest", "NNW", '⬉' ],
    ],
    de => [
        [ "Norden",        "N",   '▲' ],
        [ "Nord-Nordost",  "NNO", '⬈' ],
        [ "Nordost",      "NO",  '⬈' ],
        [ "Ost-Nordost",   "ONO", '⬈' ],
        [ "Osten",         "O",   '▶' ],
        [ "Ost-Südost",   "OSO", '⬊' ],
        [ "Südost",       "SO",  '⬊' ],
        [ "Süd-Südost",  "SSO", '⬊' ],
        [ "Süden",        "S",   '▼' ],
        [ "Süd-Südwest", "SSW", '⬋' ],
        [ "Südwest",      "SW",  '⬋' ],
        [ "West-Südwest", "WSW", '⬋' ],
        [ "Westen",        "W",   '◀' ],
        [ "West-Nordwest", "WNW", '⬉' ],
        [ "Nordwest",      "NW",  '⬉' ],
        [ "Nord-Nordwest", "NNW", '⬉' ],
    ],
    es => [
        [ "Norte",          "N",   '▲' ],
        [ "Norte-Noreste",  "NNE", '⬈' ],
        [ "Noreste",        "NE",  '⬈' ],
        [ "Este-Noreste",   "ENE", '⬈' ],
        [ "Este",           "E",   '▶' ],
        [ "Este-Sureste",   "ESE", '⬊' ],
        [ "Sureste",        "SE",  '⬊' ],
        [ "Sur-Sureste",    "SSE", '⬊' ],
        [ "Sur",            "S",   '▼' ],
        [ "Sudoeste",       "SDO", '⬋' ],
        [ "Sur-Oeste",      "SO",  '⬋' ],
        [ "Oeste-Suroeste", "OSO", '⬋' ],
        [ "Oeste",          "O",   '◀' ],
        [ "Oeste-Noroeste", "ONO", '⬉' ],
        [ "Noroeste",       "NO",  '⬉' ],
        [ "Norte-Noroeste", "NNE", '⬉' ],
    ],
    it => [
        [ "Nord",             "N",   '▲' ],
        [ "Nord-Nord-Est",    "NNE", '⬈' ],
        [ "Nord-Est",         "NE",  '⬈' ],
        [ "Est-Nord-Est",     "ENE", '⬈' ],
        [ "Est",              "E",   '▶' ],
        [ "Est-Sud-Est",      "ESE", '⬊' ],
        [ "Sud-Est",          "SE",  '⬊' ],
        [ "Sud-Sud-Est",      "SSE", '⬊' ],
        [ "Sud",              "S",   '▼' ],
        [ "Sud-Sud-Ovest",    "SSO", '⬋' ],
        [ "Sud-Ovest",        "SO",  '⬋' ],
        [ "Ovest-Sud-Ovest",  "OSO", '⬋' ],
        [ "Ovest",            "O",   '◀' ],
        [ "Ovest-Nord-Ovest", "ONO", '⬉' ],
        [ "Nord-Ovest",       "NO",  '⬉' ],
        [ "Nord-Nord-Ovest",  "NNO", '⬉' ],
    ],
    nl => [
        [ "Noorden",           "N",   '▲' ],
        [ "Noord-Noordoosten", "NNO", '⬈' ],
        [ "Noordoosten",       "NO",  '⬈' ],
        [ "Oost-Noordoost",    "ONO", '⬈' ],
        [ "Oosten",            "O",   '▶' ],
        [ "Oost-Zuidoost",     "OZO", '⬊' ],
        [ "Zuidoosten",        "ZO",  '⬊' ],
        [ "Zuid-Zuidoost",     "ZZO", '⬊' ],
        [ "Zuiden",            "Z",   '▼' ],
        [ "Zuid-Zuidwest",     "ZZW", '⬋' ],
        [ "Zuidwest",          "ZW",  '⬋' ],
        [ "West-Zuidwest",     "WZW", '⬋' ],
        [ "West",              "W",   '◀' ],
        [ "West-Noord-West",   "WNW", '⬉' ],
        [ "Noord-West",        "NW",  '⬉' ],
        [ "Noord-Noord-West",  "NNW", '⬉' ],
    ],
    fr => [
        [ "Nord",             "N",   '▲' ],
        [ "Nord-Nord-Est",    "NNE", '⬈' ],
        [ "Nord-Est",         "NE",  '⬈' ],
        [ "Est-Nord-Est",     "ENE", '⬈' ],
        [ "Est",              "E",   '▶' ],
        [ "Est-Sud-Est",      "ESE", '⬊' ],
        [ "Sud-Est",          "SE",  '⬊' ],
        [ "Sud-Sud-Est",      "SSE", '⬊' ],
        [ "Sud",              "S",   '▼' ],
        [ "Sud-Sud-Ouest",    "SSW", '⬋' ],
        [ "Sud-Ouest",        "SW",  '⬋' ],
        [ "Ouest-Sud-Ouest",  "OSO", '⬋' ],
        [ "Ouest",            "O",   '◀' ],
        [ "Ouest-Nord-Ouest", "ONO", '⬉' ],
        [ "Nord-Ouest",       "NO",  '⬉' ],
        [ "Nord-Nord-Ouest",  "NNO", '⬉' ],
    ],
    pl => [
        [ "Północ",                        "N",   '▲' ],
        [ "Północny-Północny-Wschód",   "NNE", '⬈' ],
        [ "Północny-Wschód",              "NE",  '⬈' ],
        [ "Wschód-Północny-Wschód",      "ENE", '⬈' ],
        [ "Wschód",                         "E",   '▶' ],
        [ "Wschód-Południowy-Wschód",     "ESE", '⬊' ],
        [ "Południowy-Południowy-Wschód", "SE",  '⬊' ],
        [ "Południowy-Wschód",             "SSE", '⬊' ],
        [ "Południe",                       "S",   '▼' ],
        [ "Południowo-Południowy-Zachód", "SSW", '⬋' ],
        [ "Południowy-Zachód",             "SW",  '⬋' ],
        [ "Zachód-Południowy-Zachód",     "WSW", '⬋' ],
        [ "Zachód",                         "W",   '◀' ],
        [ "Zachód-Północny-Zachód",      "WNW", '⬉' ],
        [ "Północny-Zachód",              "NW",  '⬉' ],
        [ "Północno-Północny-Zachód",   "NNW", '⬉' ],
    ],
);

our %hr_formats = (

    # 1 234 567.89
    std => {
        delim => "\x{2009}",
        sep   => ".",
    },

    # 1 234 567,89
    'std-fr' => {
        delim => "\x{2009}",
        sep   => ",",
    },

    # 1,234,567.89
    'old-english' => {
        delim => ",",
        sep   => ".",
    },

    # 1.234.567,89
    'old-european' => {
        delim => ".",
        sep   => ",",
    },

    # 1'234'567.89
    ch => {
        delim => "'",
        sep   => ".",
    },

    ### lang ref ###
    #

    en => { ref => "std", },

    de => { ref => "std-fr", },

    de_at => {
        ref => "std-fr",
        min => 4,
    },

    de_ch => { ref => "std", },

    nl => { ref => "std-fr", },

    fr => { ref => "std-fr", },

    pl => { ref => "std-fr", },

    ### number ref ###
    #

    0 => { ref => "std", },
    1 => { ref => "std-fr", },
    2 => { ref => "old-english", },
    3 => { ref => "old-european", },
    4 => { ref => "ch", },
    5 => {
        ref => "std-fr",
        min => 4,
    },

);

our %daytimes = (
    en => [
        "morning", "midmorning", "noon", "afternoon",
        "evening", "midevening", "night",
    ],
    de => [
        "Morgen",   "Vormittag", "Mittag", "Nachmittag",
        "Vorabend", "Abend",     "Nacht",
    ],
    nl => [
        "Ochtend", "Vormiddag", "Middag", "Nachmiddag",
        "Avond",   "Midavond",  "Nacht",
    ],
    fr => [
        "Matin", "Martinée", "Midi", "Après-midi", "Veille", "Soir", "Nuit",
    ],
    pl => [
        "Ranek",   "Rano",     "Południe", "Popołudnie",
        "Wigilia", "Wieczór", "Noc",
    ],
    icons => [
        "weather_sunrise", "scene_day",
        "weather_sun",     "weather_summer",
        "weather_sunset",  "scene_night",
        "weather_moon_phases_8",
    ],
);

our %sdt2daytimes = (

    # User overwrite format:
    # <SeasonSrc><SeasonIndex><DST><daytimeStage>:<daytime>
    # M000:0
    # M001:0
    # M002:0
    # M003:1
    # M004:1
    # M005:2
    # M006:2
    # M007:3
    # M008:3
    # M009:3
    # M0010:3
    # M0011:4
    # M0012:5
    #
    # M010:0
    # M011:0
    # M012:0
    # M013:1
    # M014:1
    # M015:2
    # M016:2
    # M017:3
    # M018:3
    # M019:3
    # M0110:3
    # M0111:4
    # M0112:5

    # SPRING SEASON
    0 => {

        # DST = no
        0 => {
            1  => 0,
            4  => 1,
            6  => 2,
            8  => 3,
            12 => 4,
        },

        # DST = yes
        1 => {
            1  => 0,
            4  => 1,
            6  => 2,
            8  => 3,
            12 => 4,
        },
    },

    # SUMMER SEASON
    1 => {

        # DST = yes
        1 => {
            1  => 0,
            4  => 1,
            6  => 2,
            7  => 3,
            10 => 4,
            12 => 5,
        }
    },

    # FALL SEASON
    2 => {

        # DST = no
        0 => {
            1  => 0,
            4  => 1,
            6  => 2,
            7  => 3,
            11 => 4,
        },

        # DST = yes
        1 => {
            1  => 0,
            4  => 1,
            6  => 2,
            7  => 3,
            11 => 4,
        },
    },

    # WINTER SEASON
    3 => {

        # DST = no
        0 => {
            1  => 0,
            3  => 1,
            6  => 2,
            8  => 3,
            12 => 4,
        },
    },
);

our %seasons = (
    en    => [ "Spring",    "Summer", "Fall",    "Winter", ],
    de    => [ "Frühling", "Sommer", "Herbst",  "Winter", ],
    nl    => [ "Voorjaar",  "Zomer",  "Herfst",  "Winter", ],
    fr    => [ "Printemps", "Été",  "Automne", "Hiver", ],
    pl    => [ "Wiosna",    "Lato",   "Jesień", "Zima", ],
    pheno => [ 2,           4,        7,         9 ],
);

our %seasonsPheno = (
    en => [
        "Early Spring",
        "First Spring",
        "Full Spring",
        "Early Summer",
        "Midsummer",
        "Late Summer",
        "Early Fall",
        "Fall",
        "Late Fall",
        "Winter",
    ],
    de => [
        "Vorfrühling", "Erstfrühling", "Vollfrühling", "Frühsommer",
        "Hochsommer",   "Spätsommer",   "Frühherbst",   "Vollherbst",
        "Spätherbst",  "Winter",
    ],
    nl => [
        "Vroeg Voorjaar",
        "Eerste Voorjaar",
        "Voorjaar",
        "Vroeg Zomer",
        "Zomer",
        "Laat Zomer",
        "Vroeg Herfst",
        "Herfst",
        "Laat Herfst",
        "Winter",
    ],
    fr => [
        "Avant du printemps",
        "Début du printemps",
        "Printemps",
        "Avant de l'été",
        "Milieu de l'été",
        "Fin de l'été",
        "Avant de l'automne",
        "Automne",
        "Fin de l'automne",
        "Hiver",
    ],
    pl => [
        "Przedwiośnie",
        "Pierwsza Wiosna",
        "Wiosna",
        "Wczesne Lato",
        "Połowa Lata",
        "Późnym Latem",
        "Wczesną Jesienią",
        "Jesień",
        "Późną Jesienią",
        "Zima",
    ],
);

our %dst = (
    en => [ "standard",         "daylight" ],
    de => [ "Normalzeit",       "Sommerzeit" ],
    nl => [ "Standaardtijd",    "Zomertijd" ],
    fr => [ "Heure normale",    "L'heure d'été" ],
    pl => [ "Standardowy czas", "Czas Letni" ],
);

our %daystages = (
    en => [ "weekday",   "weekend",    "holiday",     "vacation", ],
    de => [ "Wochentag", "Wochenende", "Feiertag",    "Urlaubstag", ],
    nl => [ "Weekdag",   "Weekend",    "Vieringsdag", "Vakantiedag", ],
    fr => [ "Jour de la semaine", "Weekend", "Vacances", "Villégiature", ],
    pl => [ "dzień powszedni",   "Weekend", "święto", "Wakacjach", ],
);

our %reldays = (
    en => [ "yesterday", "today",       "tomorrow" ],
    de => [ "gestern",   "heute",       "morgen" ],
    nl => [ "gisteren",  "vandaag",     "morgen" ],
    fr => [ "hier",      "aujourd'hui", "demain" ],
    pl => [ "wczoraj",   "dzisiaj",     "jutro" ],
);

our %monthss = (
    en => [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
        "Sep", "Oct", "Nov", "Dec", "Jan"
    ],
    de => [
        "Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Aug",
        "Sep", "Okt", "Nov", "Dez", "Jan"
    ],
    nl => [
        "Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Aug",
        "Sep", "Okt", "Nov", "Dec", "Jan"
    ],
    fr => [
        "Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aût",
        "Sep", "Oct",  "Nov", "Dec", "Jan"
    ],
    pl => [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
        "Sep", "Oct", "Nov", "Dec", "Jan"
    ],
);

our %months = (
    en => [
        "January",   "Febuary", "March",    "April",
        "May",       "June",    "July",     "August",
        "September", "October", "November", "December",
        "January"
    ],
    de => [
        "Januar",    "Februar", "März",    "April",
        "Mai",       "Juni",    "Juli",     "August",
        "September", "Oktober", "November", "Dezember",
        "Januar"
    ],
    nl => [
        "Januari",   "Februari", "Maart",    "April",
        "Mei",       "Juni",     "Juli",     "Augustus",
        "September", "Oktober",  "November", "December",
        "Januari"
    ],
    fr => [
        "Janvier",   "Février", "Mars",     "Avril",
        "Mai",       "Juin",     "Juillet",  "Août",
        "Septembre", "Octobre",  "Novembre", "Décembre",
        "Janvier"
    ],
    pl => [
        "Styczeń",  "Luty",         "Marzec",   "Kwiecień",
        "Maj",       "Czerwiec",     "July",     "Lipiec",
        "Wrzesień", "Październik", "Listopad", "Grudzień",
        "Styczeń"
    ],
);

our %dayss = (
    en => [ "Sun", "Mon", "Tue", "Wed",  "Thu", "Fri",  "Sat", "Sun" ],
    de => [ "So",  "Mo",  "Di",  "Mi",   "Do",  "Fr",   "Sa",  "So" ],
    nl => [ "Zon", "Maa", "Din", "Woe",  "Don", "Vri",  "Zat", "Zon" ],
    fr => [ "Dim", "Lun", "Mar", "Mer",  "Jeu", "Ven",  "Sam", "Dim" ],
    pl => [ "Nie", "Pon", "Wto", "śro", "Czw", "Pią", "Sob", "Nie" ],
);

our %days = (
    en => [
        "Sunday",   "Monday", "Tuesday",  "Wednesday",
        "Thursday", "Friday", "Saturday", "Sunday"
    ],
    de => [
        "Sonntag",    "Montag",  "Dienstag", "Mittwoch",
        "Donnerstag", "Freitag", "Samstag",  "Sonntag"
    ],
    nl => [
        "Zondag",    "Maandag", "Dinsdag",  "Woensdag",
        "Donderdag", "Vrijdag", "Zaterdag", "Zondag"
    ],
    fr => [
        "Dimanche", "Lundi",    "Mardi",  "Mercredi",
        "Jeudi",    "Vendredi", "Samedi", "Dimanche"
    ],
    pl => [
        "Niedziela", "Poniedziałek", "Wtorek", "środa",
        "Czwartek",  "Piątek",       "Sobota", "Niedziela"
    ],
);

our %dateformats = (
    en => '%wday_long%, %mon_long% %mday%',
    de => '%wday_long%, %mday%. %mon_long%',
    nl => '%wday_long%, %mday%. %mon_long%',
    fr => '%wday_long%, %mday%. %mon_long%',
    pl => '%wday_long%, %mday%. %mon_long%',
);

our %dateformatss = (
    en => '%mon_long% %mday%',
    de => '%mday%. %mon_long%',
    nl => '%mday%. %mon_long%',
    fr => '%mday%. %mon_long%',
    pl => '%mday%. %mon_long%',
);

# https://www.luftfeuchtigkeit-raumklima.de/tabelle.php
our %ideal_clima = (
    bathroom => {
        c => [ -273.15, 6,  16, 20, 23, 27, ],
        h => [ 0,       40, 50, 70, 80 ],
    },
    living => {
        c => [ -273.15, 6,  16, 20, 23, 27, ],
        h => [ 0,       30, 40, 60, 70 ],
    },
    kitchen => {
        c => [ -273.15, 6,  16, 18, 20, 27, ],
        h => [ 0,       40, 50, 60, 70 ],
    },
    bedroom => {
        c => [ -273.15, 6,  12, 17, 20, 23, ],
        h => [ 0,       30, 40, 60, 70 ],
    },
    hallway => {
        c => [ -273.15, 6,  12, 15, 18, 23, ],
        h => [ 0,       30, 40, 60, 70 ],
    },
    cellar => {
        c => [ -273.15, 6,  7,  10, 15, 20, ],
        h => [ 0,       40, 50, 60, 70 ],
    },
    outdoor => {
        c => [ -273.15, 2.5, 5,  14, 30, 35, ],
        h => [ 0,       40,  50, 70, 80 ],
    },
);

our %clima_names = (
    c => {
        en => [ "freeze",  "cold",  "low",     "ideal",    "high", "hot" ],
        de => [ "frostig", "kalt",  "niedrig", "optimal",  "hoch", "heiß" ],
        nl => [ "kil",     "koude", "laag",    "optimale", "hoog", "heet" ],
        fr => [ "froid",   "froid", "faible",  "optimal",  "haut", "chaud" ],
        pl =>
          [ "chłodny", "zimno", "niski", "optymalny", "wysoki", "gorący" ],
        rgb => [ "0055BB", "0066CC", "009999", "4C9329", "E7652B", "C72A23" ],
    },
    h => {
        en  => [ "dry",     "low",     "ideal",     "high",   "wet" ],
        de  => [ "trocken", "niedrig", "optimal",   "hoch",   "nass" ],
        nl  => [ "droog",   "laag",    "optimale",  "hoog",   "nat" ],
        fr  => [ "sec",     "faible",  "optimal",   "haut",   "humide" ],
        pl  => [ "suchy",   "niski",   "optymalny", "wysoki", "mokro" ],
        rgb => [ "C72A23",  "E7652B",  "4C9329",    "009999", "0066CC" ],
    }
);

#################################
### Inner metric conversions
###

# Temperature: convert Celsius to Kelvin
sub c2k($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data + 273.15, $rnd );
}

# Temperature: convert Kelvin to Celsius
sub k2c($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data - 273.15, $rnd );
}

# Speed: convert km/h (kilometer per hour) to m/s (meter per second)
sub kph2mps($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data / 3.6, $rnd );
}

# Speed: convert m/s (meter per second) to km/h (kilometer per hour)
sub mps2kph($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 3.6, $rnd );
}

# Pressure: convert hPa (hecto Pascal) to mmHg (milimeter of Mercury)
sub hpa2mmhg($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.00750061561303, $rnd );
}

#################################
### Metric to angloamerican conversions
###

# Temperature: convert Celsius to Fahrenheit
sub c2f($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 1.8 + 32, $rnd );
}

# Temperature: convert Kelvin to Fahrenheit
sub k2f($;$) {
    my ( $data, $rnd ) = @_;
    return _round( ( $data - 273.15 ) * 1.8 + 32, $rnd );
}

# Pressure: convert hPa (hecto Pascal) to in (inches of Mercury)
sub hpa2inhg($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.02952998751, $rnd );
}

# Pressure: convert hPa (hecto Pascal) to PSI (Pound force per square inch)
sub hpa2psi($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 100.00014504, $rnd );
}

# Speed: convert km/h (kilometer per hour) to mph (miles per hour)
sub kph2mph($;$) {
    return km2mi(@_);
}

# Speed: convert m/s (meter per seconds) to mph (miles per hour)
sub mps2mph($;$) {
    my ( $data, $rnd ) = @_;
    return _round( kph2mph( mps2kph( $data, 9 ), 9 ), $rnd );
}

# Length: convert mm (milimeter) to in (inch)
sub mm2in($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.039370, $rnd );
}

# Length: convert cm (centimeter) to in (inch)
sub cm2in($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.39370, $rnd );
}

# Length: convert m (meter) to ft (feet)
sub m2ft($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 3.2808, $rnd );
}

# Length: convert km (kilometer) to miles (mi)
sub km2mi($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.621371192, $rnd );
}

#################################
### Inner Angloamerican conversions
###

# Speed: convert mph (miles per hour) to ft/s (feet per second)
sub mph2fts($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 1.467, $rnd );
}

# Speed: convert ft/s (feet per second) to mph (miles per hour)
sub fts2mph($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data / 1.467, $rnd );
}

#################################
### Angloamerican to Metric conversions
###

# Temperature: convert Fahrenheit to Celsius
sub f2c($;$) {
    my ( $data, $rnd ) = @_;
    return _round( ( $data - 32 ) * 0.5556, $rnd );
}

# Temperature: convert Fahrenheit to Kelvin
sub f2k($;$) {
    my ( $data, $rnd ) = @_;
    return _round( ( $data - 32 ) / 1.8 + 273.15, $rnd );
}

# Pressure: convert in (inches of Mercury) to hPa (hecto Pascal)
sub inhg2hpa($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 33.8638816, $rnd );
}

# Pressure: convert PSI (Pound force per square inch) to hPa (hecto Pascal)
sub psi2hpa($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data / 100.00014504, $rnd );
}

# Speed: convert mph (miles per hour) to km/h (kilometer per hour)
sub mph2kph($;$) {
    return mi2km(@_);
}

# Speed: convert mph (miles per hour) to m/s (meter per seconds)
sub mph2mps($;$) {
    my ( $data, $rnd ) = @_;
    return _round( kph2mps( mph2kph( $data, 9 ), 9 ), $rnd );
}

# Length: convert in (inch) to mm (milimeter)
sub in2mm($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 25.4, $rnd );
}

# Length: convert in (inch) to cm (centimeter)
sub in2cm($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data / 0.39370, $rnd );
}

# Length: convert ft (feet) to m (meter)
sub ft2m($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data / 3.2808, $rnd );
}

# Length: convert mi (miles) to km (kilometer)
sub mi2km($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 1.609344, $rnd );
}

#################################
### Plane angle conversions
###

# convert direction in degree to point of the compass
sub direction2compasspoint($;$$) {
    my ( $deg, $txt, $lang ) = @_;
    my $i = floor( ( ( $deg + 11.25 ) % 360 ) / 22.5 );
    return $i if ( !wantarray && defined($txt) && $txt == "0" );

    my $directions_txt_i18n;
    $lang = main::AttrVal( "global", "language", "EN" ) unless ($lang);

    if ( exists( $compasspoints{ lc($lang) } ) ) {
        $directions_txt_i18n = $compasspoints{ lc($lang) };
    }
    else {
        $directions_txt_i18n = $compasspoints{en};
    }

    return (
        encode_utf8 $directions_txt_i18n->[$i][0],
        $directions_txt_i18n->[$i][1],
        encode_utf8 $directions_txt_i18n->[$i][2]
    ) if wantarray;
    return encode_utf8 $directions_txt_i18n->[$i][2] if ( $txt && $txt eq "3" );
    return encode_utf8 $directions_txt_i18n->[$i][0] if ( $txt && $txt eq "2" );
    return $directions_txt_i18n->[$i][1];
}

# see below for compasspoint2compasspoint() text translations.

#################################
### Solar conversions
###

# Power: convert uW/cm2 (micro watt per square centimeter) to UV-Index
sub uwpscm2uvi($;$) {
    my ( $data, $rnd ) = @_;

    return 0 unless ($data);

    # Forum topic,44403.msg501704.html#msg501704
    return int( ( $data - 100 ) / 450 + 1 ) unless ( defined($rnd) );

    $rnd = 0 unless ( defined($rnd) );
    return _round( ( ( $data - 100 ) / 450 + 1 ), $rnd );
}

# Power: convert UV-Index to uW/cm2 (micro watt per square centimeter)
sub uvi2uwpscm($) {
    my ($data) = @_;

    return 0 unless ($data);
    return ( $data * ( 450 + 1 ) ) + 100;
}

# Power: convert lux to W/m2 (watt per square meter)
sub lux2wpsm($;$) {
    my ( $data, $rnd ) = @_;

    # Forum topic,44403.msg501704.html#msg501704
    return _round( $data / 126.7, $rnd );
}

# Power: convert W/m2 to lux
sub wpsm2lux($;$) {
    my ( $data, $rnd ) = @_;

    # Forum topic,44403.msg501704.html#msg501704
    return _round( $data * 126.7, $rnd );
}

#################################
### Nautic unit conversions
###

# Speed: convert smi (statute miles) to nmi (nautical miles)
sub smi2nmi($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.8684, $rnd );
}

# Speed: convert km (kilometer) to nmi (nautical miles)
sub km2nmi($;$) {
    my ( $data, $rnd ) = @_;
    return _round( smi2nmi( km2mi( $data, 9 ), 9 ), $rnd );
}

# Speed: convert km/h to knots
sub kph2kn($;$) {
    my ( $data, $rnd ) = @_;
    return _round( $data * 0.539956803456, $rnd );
}

# Speed: convert km/h to Beaufort wind force scale
sub kph2bft($) {
    my ($data) = @_;
    my $val = "0";

    if ( $data >= 118 ) {
        $val = "12";
    }
    elsif ( $data >= 103 ) {
        $val = "11";
    }
    elsif ( $data >= 89 ) {
        $val = "10";
    }
    elsif ( $data >= 75 ) {
        $val = "9";
    }
    elsif ( $data >= 62 ) {
        $val = "8";
    }
    elsif ( $data >= 50 ) {
        $val = "7";
    }
    elsif ( $data >= 39 ) {
        $val = "6";
    }
    elsif ( $data >= 29 ) {
        $val = "5";
    }
    elsif ( $data >= 20 ) {
        $val = "4";
    }
    elsif ( $data >= 12 ) {
        $val = "3";
    }
    elsif ( $data >= 6 ) {
        $val = "2";
    }
    elsif ( $data >= 1 ) {
        $val = "1";
    }

    if (wantarray) {
        my ( $cond, $rgb, $warn ) = bft2condition($val);
        return ( $val, $rgb, $cond, $warn );
    }
    return $val;
}

# Speed: convert mph (miles per hour) to Beaufort wind force scale
sub mph2bft($) {
    my ($data) = @_;
    my $val = "0";

    if ( $data >= 73 ) {
        $val = "12";
    }
    elsif ( $data >= 64 ) {
        $val = "11";
    }
    elsif ( $data >= 55 ) {
        $val = "10";
    }
    elsif ( $data >= 47 ) {
        $val = "9";
    }
    elsif ( $data >= 39 ) {
        $val = "8";
    }
    elsif ( $data >= 32 ) {
        $val = "7";
    }
    elsif ( $data >= 25 ) {
        $val = "6";
    }
    elsif ( $data >= 19 ) {
        $val = "5";
    }
    elsif ( $data >= 13 ) {
        $val = "4";
    }
    elsif ( $data >= 8 ) {
        $val = "3";
    }
    elsif ( $data >= 4 ) {
        $val = "2";
    }
    elsif ( $data >= 1 ) {
        $val = "1";
    }

    if (wantarray) {
        my ( $cond, $rgb, $warn ) = bft2condition($val);
        return ( $val, $rgb, $cond, $warn );
    }
    return $val;
}

#################################
### Differential conversions
###

sub distance($$$$;$$) {
    my ( $lat1, $lng1, $lat2, $lng2, $rnd, $unit ) = @_;
    return _round( "0.000000000", $rnd )
      if ( $lat1 eq $lat2 && $lng1 eq $lng2 );

    my $aearth = 6378.137;    # GRS80/WGS84 semi major axis of earth ellipsoid
    my $km = great_circle_distance(
        deg2rad($lat1), pi / 2. - deg2rad($lng1),
        deg2rad($lat2), pi / 2. - deg2rad($lng2),
        $aearth
    );

    return _round(
        (
            $unit && $unit eq "nmi" ? km2nmi($km) : ( $unit ? km2mi($km) : $km )
        ),
        $rnd
    );
}

sub duration ($$;$) {
    my ( $datetimeNow, $datetimeOld, $format ) = @_;

    if ( $datetimeNow eq "" || $datetimeOld eq "" ) {
        $datetimeNow = "1970-01-01 00:00:00";
        $datetimeOld = "1970-01-01 00:00:00";
    }

    my $timestampNow = main::time_str2num($datetimeNow);
    my $timestampOld = main::time_str2num($datetimeOld);
    my $timeDiff     = $timestampNow - $timestampOld;

    # return seconds
    return _round( $timeDiff, 0 )
      if ( defined($format) && $format eq "sec" );

    # return minutes
    return _round( $timeDiff / 60, 0 )
      if ( defined($format) && $format eq "min" );

    # return human readable format
    return s2hms( _round( $timeDiff, 0 ) );
}

#################################
### Textual unit conversions
###

# Translate point of the compass to other languages
#   Also works to convert from short text to long text and vice versa.
sub compasspoint2compasspoint($;$$$) {
    my ( $shortTxt, $fromLang, $txt, $toLang ) = @_;
    return $shortTxt if ( !$shortTxt || $shortTxt eq "" );
    return direction2compasspoint( $shortTxt, $txt,
        $toLang ? $toLang : $fromLang )
      if ( $shortTxt =~ m/^\d+(?:\.\d+)?$/ );
    return compasspoint2direction( $shortTxt, $fromLang )
      if ( defined($txt) && $txt eq "0" );

    my $fromDirections_txt_i18n;
    $fromLang = "EN" unless ($fromLang);
    if ( exists( $compasspoints{ lc($fromLang) } ) ) {
        $fromDirections_txt_i18n = $compasspoints{ lc($fromLang) };
    }
    else {
        $fromLang                = "EN";
        $fromDirections_txt_i18n = $compasspoints{en};
    }

    my $toDirections_txt_i18n;
    $toLang = main::AttrVal( "global", "language", "EN" )
      unless ($toLang);
    if ( exists( $compasspoints{ lc($toLang) } ) ) {
        $toDirections_txt_i18n = $compasspoints{ lc($toLang) };
    }
    else {
        $toLang                = "EN";
        $toDirections_txt_i18n = $compasspoints{en};
    }

    my $i;
    my $i2;
    my $f = 0;
    foreach my $a ( @{$fromDirections_txt_i18n} ) {
        my $f2 = 0;
        foreach my $b ( @{$a} ) {
            if ( lc($b) eq lc( decode_utf8($shortTxt) ) ) {
                $i2 = $f2;
                last;
            }
            $f2++;
        }
        if ( defined($i2) ) {
            $i = $f;
            last;
        }
        $f++;
    }

    unless ( defined($txt) ) {
        $txt = 1;
        $txt = 3 if ( $i2 eq "2" );
        $txt = 2 if ( $i2 eq "0" );
    }

    unless ( defined($i) ) {
        return ( "", "", "" ) if wantarray;
        return "";
    }

    return (
        encode_utf8 $toDirections_txt_i18n->[$i][0],
        $toDirections_txt_i18n->[$i][1],
        encode_utf8 $toDirections_txt_i18n->[$i][2]
    ) if wantarray;
    return encode_utf8 $toDirections_txt_i18n->[$i][2] if ( $txt eq "3" );
    return encode_utf8 $toDirections_txt_i18n->[$i][0] if ( $txt eq "2" );
    return $toDirections_txt_i18n->[$i][1];
}

# convert point of the compass to direction in degree
sub compasspoint2direction($;$) {
    my ( $shortTxt, $fromLang ) = @_;
    return $shortTxt if ( !$shortTxt || $shortTxt eq "" );

    my $fromDirections_txt_i18n;
    $fromLang = "EN" unless ($fromLang);
    if ( exists( $compasspoints{ lc($fromLang) } ) ) {
        $fromDirections_txt_i18n = $compasspoints{ lc($fromLang) };
    }
    else {
        $fromLang                = "EN";
        $fromDirections_txt_i18n = $compasspoints{en};
    }

    my $i;
    my $i2;
    my $f = 0;
    foreach my $a ( @{$fromDirections_txt_i18n} ) {
        my $f2 = 0;
        foreach my $b ( @{$a} ) {
            if ( lc($b) eq lc( decode_utf8($shortTxt) ) ) {
                $i2 = $f2;
                last;
            }
            $f2++;
        }
        if ( defined($i2) ) {
            $i = $f;
            last;
        }
        $f += 22.5;
    }

    unless ( defined($i) ) {
        return ("") if wantarray;
        return "";
    }

    return $i;
}

# Convert an arabic number to roman numerals
sub arabic2roman ($) {
    my ($n) = @_;
    my %items = ();
    my @r;
    return "" if ( !$n || $n eq "" || $n !~ m/^\d+(?:\.\d+)?$/ || $n eq "0" );
    return $n
      if ( $n >= 1000001. );    # numbers above cannot be displayed/converted

    my %roman = (
        1       => 'I',
        5       => 'V',
        10      => 'X',
        50      => 'L',
        100     => 'C',
        500     => 'D',
        1000    => 'M',
        5000    => '(V)',
        10000   => '(X)',
        50000   => '(L)',
        100000  => '(C)',
        500000  => '(D)',
        1000000 => '(M)',
    );

    for my $v ( sort { $b <=> $a } keys %roman ) {
        my $c = int( $n / $v );
        next unless ($c);
        $items{ $roman{$v} } = $c;
        $n -= $v * $c;
    }

    my @th = sort { $a <=> $b } keys %roman;

    for ( my $i = 0 ; $i < @th ; $i++ ) {
        my $v = $th[$i];
        next if ( $v >= 1000000. );    # numbers above have no greater icon
        my $k = $roman{$v};
        my $c = $items{$k};
        next unless ($c);

        my $gv = $th[ $i + 1. ];
        my $gk = $roman{$gv};

        if ( $c == 4 || ( $gv / $v == $c ) ) {
            $items{$gk}++;
            $c = $gv - $c * $v;
            $items{$k} = $c * -1;

        }
    }

    for my $v ( sort { $b <=> $a } keys %roman ) {
        my $l = $roman{$v};
        my $c = $items{$l};
        next unless ($c);

        if ( $c > 0 ) {
            push @r, $l for ( 1 .. $c );
        }
        else {
            push @r, ( $l, pop @r );
        }
    }

    return join '', @r;
}

######## humanReadable #########################################
# What  : Formats a number or text string to be more readable for humans
# Syntax: { humanReadable( <value>, [ <format> ] ) }
# Call  : { humanReadable(102345.6789) }
#         { humanReadable(102345.6789, 3) }
#         { humanReadable(102345.6789, "DE") }
#         { humanReadable(102345.6789, "si-fr") }
#         { humanReadable(102345.6789, {
#                      group=>3, delim=>".", sep=>"," } ) }
#         { humanReadable("DE44500105175407324931", {
#                      group=>4, rev=>0 } ) }
# Source: https://en.wikipedia.org/wiki/Decimal_mark
#         https://de.wikipedia.org/wiki/Schreibweise_von_Zahlen
#         https://de.wikipedia.org/wiki/Dezimaltrennzeichen
#         https://de.wikipedia.org/wiki/Zifferngruppierung
sub humanReadable($;$) {
    my ( $v, $f ) = @_;
    my $l =
      $main::attr{global}{humanReadable} ? $main::attr{global}{humanReadable}
      : (
        $main::attr{global}{language} ? $main::attr{global}{language}
        : "EN"
      );

    my $h =
      !$f || ref($f) || !$hr_formats{$f} ? $f
      : (
          $hr_formats{$f}{ref} ? $hr_formats{ $hr_formats{$f}{ref} }
        : $hr_formats{$f}
      );
    my $min =
      ref($h)
      && defined( $h->{min} ) ? $h->{min}
      : (
        !ref($f)
          && $hr_formats{$f}{min} ? $hr_formats{$f}{min}
        : 5
      );
    my $group =
      ref($h)
      && defined( $h->{group} ) ? $h->{group}
      : (
        !ref($f)
          && $hr_formats{$f}{group} ? $hr_formats{$f}{group}
        : 3
      );
    my $delim =
      ref($h)
      && $h->{delim} ? $h->{delim}
      : $hr_formats{
        (
            $l =~ /^de|nl|fr|pl/i ? "std-fr"
            : "std"
        )
      }{delim};
    my $sep =
      ref($h)
      && $h->{sep} ? $h->{sep}
      : $hr_formats{
        (
            $l =~ /^de|nl|fr|pl/i ? "std-fr"
            : "std"
        )
      }{sep};
    my $reverse = ref($h) && defined( $h->{rev} ) ? $h->{rev} : 1;

    my @p = split( /\./, $v, 2 );

    if ( length( $p[0] ) < $min && length( $p[1] ) < $min ) {
        $v =~ s/\./$sep/g;
        return $v;
    }

    $v =~ s/\./\*/g;

    # digits after thousands separator
    if ( ( $delim eq "\x{202F}" || $delim eq " " )
        && length( $p[1] ) >= $min )
    {
        $v =~ s/(\w{$group})(?=\w)(?!\w*\*)/$1$delim/g;
    }

    # digits before thousands separator
    if ( length( $p[0] ) >= $min ) {
        $v = reverse $v if ($reverse);
        $v =~ s/(\w{$group})(?=\w)(?!\w*\*)/$1$delim/g;
        if ($reverse) {
            $v =~ s/\*/$sep/g;
            return scalar reverse $v;
        }
    }

    $v =~ s/\*/$sep/g;
    return $v;
}

# ######## machineReadable #########################################
# # What  : find the first matching number in a string and make it
# #         machine readable.
# # Syntax: { machineReadable( <value>, [ <global>, [ <format> ]] ) }
# # Call  : { machineReadable("102 345,6789") }
# sub machineReadable($;$) {
#     my ( $v, $g ) = @_;
#
#     sub mrVal($$) {
#         my ( $n, $n2 ) = @_;
#         $n .= "." . $n2 if ($n2);
#         $n =~ s/[^\d\.]//g;
#         return $n;
#     }
#
#
#     foreach ( "std", "std-fr" ) {
#         my $delim = '\\' . $hr_formats{$_}{delim};
#         $delim .= ' ' if ($_ =~ /^std/);
#
#         if (   $g
#             && $v =~
# s/((-?)((?:\d+(?:[$delim]\d)*)+)([\.\,])((?:\d+(?:[$delim]\d)*)+)?)/$2.mrVal($3, $5)/eg
#           )
#         {
#             last;
#         }
#         elsif ( $v =~
#             m/^((\-?)((?:\d(?:[$delim]\d)*)+)(?:([\.\,])((?:\d(?:[$delim]\d)*)+))?)/ )
#         {
#             $v = $2 . mrVal( $3, $5 );
#             last;
#         }
#     }
#
#     return $v;
# }

# Condition: convert temperature (Celsius) to temperature condition
sub c2condition($;$$) {
    my ( $data, $roomType, $lang ) = @_;
    my $val = "?";
    my $rgb = "FFFFFF";
    $lang = "en" if ( !$lang );

    my $thresholds;

    if ($roomType) {
        if (   ref($roomType)
            && ref($roomType) eq 'ARRAY'
            && scalar @{$roomType} == 6 )
        {
            $thresholds = $roomType;
        }
        elsif (ref($roomType)
            || looks_like_number($roomType)
            || !defined( $ideal_clima{$roomType} ) )
        {
            $thresholds = $ideal_clima{living}{c};
        }
        else {
            $thresholds = $ideal_clima{$roomType}{c};
        }
    }
    else {
        $thresholds = $ideal_clima{outdoor}{c};
    }

    my $i = 0;
    foreach my $th ( @{$thresholds} ) {
        if ( $data > $th ) {
            $val = $clima_names{c}{ lc($lang) }[$i];
            $rgb = $clima_names{c}{rgb}[$i];
        }
        else {
            last;
        }
        $i++;
    }

    return ( $val, $rgb ) if (wantarray);
    return $val;
}

# Condition: convert humidity (percent) to humidity condition
sub humidity2condition($;$$) {
    my ( $data, $roomType, $lang ) = @_;
    my $val = "?";
    my $rgb = "FFFFFF";
    $lang = "en" if ( !$lang );

    my $thresholds;

    if ($roomType) {
        if (   ref($roomType)
            && ref($roomType) eq 'ARRAY'
            && scalar @{$roomType} == 5 )
        {
            $thresholds = $roomType;
        }
        elsif (ref($roomType)
            || looks_like_number($roomType)
            || !defined( $ideal_clima{$roomType} ) )
        {
            $thresholds = $ideal_clima{living}{h};
        }
        else {
            $thresholds = $ideal_clima{$roomType}{h};
        }
    }
    else {
        $thresholds = $ideal_clima{outdoor}{h};
    }

    my $i = 0;
    foreach my $th ( @{$thresholds} ) {
        if ( $data > $th ) {
            $val = $clima_names{h}{ lc($lang) }[$i];
            $rgb = $clima_names{h}{rgb}[$i];
        }
        else {
            last;
        }
        $i++;
    }

    return ( $val, $rgb ) if (wantarray);
    return $val;
}

# Condition: convert UV-Index to UV condition
sub uvi2condition($) {
    my ($data) = @_;
    my $val    = "low";
    my $rgb    = "4C9329";

    if ( $data > 11 ) {
        $val = "extreme";
        $rgb = "674BC4";
    }
    elsif ( $data > 8 ) {
        $val = "veryhigh";
        $rgb = "C72A23";
    }
    elsif ( $data > 6 ) {
        $val = "high";
        $rgb = "E7652B";
    }
    elsif ( $data > 3 ) {
        $val = "moderate";
        $rgb = "F4E54C";
    }

    return ( $val, $rgb ) if (wantarray);
    return $val;
}

# Condition: convert Beaufort to wind condition
sub bft2condition($) {
    my ($data) = @_;
    my $rgb    = "FEFEFE";
    my $cond   = "calm";
    my $warn   = " ";

    if ( $data == 12 ) {
        $rgb  = "E93323";
        $cond = "hurricane_force";
        $warn = "hurricane_force";
    }
    elsif ( $data == 11 ) {
        $rgb  = "EB4826";
        $cond = "violent_storm";
        $warn = "storm_force";
    }
    elsif ( $data == 10 ) {
        $rgb  = "E96E2C";
        $cond = "storm";
        $warn = "storm_force";
    }
    elsif ( $data == 9 ) {
        $rgb  = "F19E38";
        $cond = "strong_gale";
        $warn = "gale_force";
    }
    elsif ( $data == 8 ) {
        $rgb  = "F7CE46";
        $cond = "gale";
        $warn = "gale_force";
    }
    elsif ( $data == 7 ) {
        $rgb  = "FFFF54";
        $cond = "near_gale";
        $warn = "high_winds";
    }
    elsif ( $data == 6 ) {
        $rgb  = "D6FD51";
        $cond = "strong_breeze";
        $warn = "high_winds";
    }
    elsif ( $data == 5 ) {
        $rgb  = "B1FC4F";
        $cond = "fresh_breeze";
    }
    elsif ( $data == 4 ) {
        $rgb  = "B1FC7B";
        $cond = "moderate_breeze";
    }
    elsif ( $data == 3 ) {
        $rgb  = "B1FCA3";
        $cond = "gentle_breeze";
    }
    elsif ( $data == 2 ) {
        $rgb  = "B1FCD0";
        $cond = "light_breeze";
    }
    elsif ( $data == 1 ) {
        $rgb  = "D6FEFE";
        $cond = "light_air";
    }

    return ( $cond, $rgb, $warn ) if (wantarray);
    return $cond;
}

sub values2weathercondition($$$$$) {
    my ( $temp, $hum, $light, $isday, $israining ) = @_;
    my $val = "clear";

    if ($israining) {
        $val = "rain";
    }
    elsif ( $light > 40000 ) {
        $val = "sunny";
    }
    elsif ($isday) {
        $val = "cloudy";
    }

    $val = "nt_" . $val unless ($isday);
    return $val;
}

#################################
### Chronological conversions
###

sub hms2s($) {
    my $in = shift;
    my @a  = split( ":", $in );
    return 0 if ( scalar @a < 2 || $in !~ m/^[\d:]*$/ );
    return $a[0] * 3600 + $a[1] * 60 + ( $a[2] ? $a[2] : 0 );
}

sub hms2m($) {
    return hms2s(@_) / 60;
}

sub hms2h($) {
    return hms2m(@_) / 60;
}

sub s2hms($) {
    my ($in) = @_;
    my ( $h, $m, $s );
    $h = int( $in / 3600 );
    $m = int( ( $in - $h * 3600 ) / 60 );
    $s = int( $in - $h * 3600 - $m * 60 );
    return ( $h, $m, $s ) if (wantarray);
    return sprintf( "%02d:%02d:%02d", $h, $m, $s );
}

sub m2hms($) {
    my ($in) = @_;
    my ( $h, $m, $s );
    $h = int( $in / 60 );
    $m = int( $in - $h * 60 );
    $s = int( 60 * ( $in - $h * 60 - $m ) );
    return ( $h, $m, $s ) if (wantarray);
    return sprintf( "%02d:%02d:%02d", $h, $m, $s );
}

sub h2hms($) {
    my ($in) = @_;
    my ( $h, $m, $s );
    $h = int($in);
    $m = int( 60 * ( $in - $h ) );
    $s = int( 3600 * ( $in - $h ) - 60 * $m );
    return ( $h, $m, $s ) if (wantarray);
    return sprintf( "%02d:%02d:%02d", $h, $m, $s );
}

sub DaysOfMonth (;$$) {
    my $y = shift;
    my $m = shift;

    return undef
      unless (
          !$y
        || $y =~ /^\d{10}(?:\.\d+)?$/
        || (
            $y =~ /^[1-2]\d{3}$/
            && (  !$m
                || $m =~ /^\d{1,2}$/ )
        )
      );

    my $t = ( $y =~ /^\d{10}(?:\.\d+)?$/ ? $y : gettimeofday() );
    $y = 1900. + ( localtime($t) )[5] unless ($y);
    $m = 1. + ( localtime($t) )[4] if ( !$y || $y !~ /^[1-2]\d{3}$/ );
    $y = 1900. + ( localtime($t) )[5] if ( $y !~ /^[1-2]\d{3}$/ );

    if ( $m < 8. ) {
        if ( $m % 2 ) {
            return 31.;
        }
        else {
            return 28. + IsLeapYear($y)
              if ( $m == 2. );
            return 30.;
        }
    }
    elsif ( $m % 2. ) {
        return 30.;
    }
    else {
        return 31.;
    }
}

sub IsLeapYear (;$) {

    # Either the value 0 or the value 1 is returned.
    #     If 0, it is not a leap year. If 1, it is a
    #     leap year. (Works for Julian calendar,
    #     established in 1582)

    my $y = shift;

    return undef
      unless ( !$y || $y =~ /^\d{10}(?:\.\d+)?$/ || $y =~ /^[1-2]\d{3}$/ );

    my $t = ( $y =~ /^\d{10}(?:\.\d+)?$/ ? $y : gettimeofday() );
    $y = 1900. + ( localtime($t) )[5] unless ($y);
    $y = 1900. + ( localtime($t) )[5] if ( $y !~ /^[1-2]\d{3}$/ );

    # If $year is not evenly divisible by 4, it is
    #     not a leap year; therefore, we return the
    #     value 0 and do no further calculations in
    #     this subroutine. ("$year % 4" provides the
    #     remainder when $year is divided by 4.
    #     If there is a remainder then $year is
    #     not evenly divisible by 4.)

    return 0 if $y % 4;

    # At this point, we know $year is evenly divisible
    #     by 4. Therefore, if it is not evenly
    #     divisible by 100, it is a leap year --
    #     we return the value 1 and do no further
    #     calculations in this subroutine.

    return 1 if $y % 100;

    # At this point, we know $year is evenly divisible
    #     by 4 and also evenly divisible by 100. Therefore,
    #     if it is not also evenly divisible by 400, it is
    #     not leap year -- we return the value 0 and do no
    #     further calculations in this subroutine.

    return 0 if $y % 400;

    # Now we know $year is evenly divisible by 4, evenly
    #     divisible by 100, and evenly divisible by 400.
    #     We return the value 1 because it is a leap year.

    return 1;
}

# Get current stage of the daytime based on temporal hours
# https://de.wikipedia.org/wiki/Temporale_Stunden
sub GetDaytime(;$$$$) {
    my ( $time, $totalTemporalHours, $lang, $params ) = @_;
    $lang = (
          $main::attr{global}{language}
        ? $main::attr{global}{language}
        : "EN"
    ) unless ($lang);

    my $ret = ref($time) eq "HASH" ? $time : _time( $time, $lang, 1, $params );
    return undef unless ( ref($ret) eq "HASH" );

    $ret->{daytimeStages} = $totalTemporalHours
      && $totalTemporalHours =~ m/^\d+$/ ? $totalTemporalHours : 12;

    # TODO: consider srParams
    $ret->{sunrise}   = main::sunrise_abs_dat( $ret->{time_t} );
    $ret->{sunrise_s} = hms2s( $ret->{sunrise} );
    $ret->{sunrise_t} = $ret->{midnight_t} + $ret->{sunrise_s};
    $ret->{sunset}    = main::sunset_abs_dat( $ret->{time_t} );
    $ret->{sunset_s}  = hms2s( $ret->{sunset} );
    $ret->{sunset_t}  = $ret->{midnight_t} + $ret->{sunset_s};
    $ret->{isday}     = $ret->{time_t} >= $ret->{sunrise_t}
      && $ret->{time_t} < $ret->{sunset_t} ? 1 : 0;

    $ret->{daytimeRel_s} =
      hms2s("$ret->{hour}:$ret->{min}:$ret->{sec}") - $ret->{sunrise_s};
    $ret->{daytimeRel}       = s2hms( $ret->{daytimeRel_s} );
    $ret->{daytimeT_s}       = $ret->{sunset_s} - $ret->{sunrise_s};
    $ret->{daytimeT}         = s2hms( $ret->{daytimeT_s} );
    $ret->{daytimeStageLn_s} = $ret->{daytimeT_s} / $ret->{daytimeStages};
    $ret->{daytimeStageLn}   = s2hms( $ret->{daytimeStageLn_s} );
    $ret->{daytimeStage_float} =
      $ret->{daytimeRel_s} / $ret->{daytimeStageLn_s};
    $ret->{daytimeStage} =
      int( ( ( $ret->{daytimeRel_s} + 1 ) / $ret->{daytimeStageLn_s} ) + 1 );
    $ret->{daytimeStage} = 0
      if ( $ret->{daytimeStage} < 1
        || $ret->{daytimeStage} > $ret->{daytimeStages} );

    # change midnight event when season changes
    $ret->{events}{ $ret->{midnight_t} }{VALUE} = 1
      if ( $ret->{seasonMeteoChng} && $ret->{seasonMeteoChng} == 1 );
    $ret->{events}{ $ret->{midnight_t} }{DESC} .=
      ", Begin meteorological $ret->{seasonMeteo_long} season"
      if ( $ret->{seasonMeteoChng} && $ret->{seasonMeteoChng} == 1 );
    $ret->{events}{ $ret->{midnight_t} }{VALUE} = 2
      if ( $ret->{seasonAstroChng} && $ret->{seasonAstroChng} == 1 );
    $ret->{events}{ $ret->{midnight_t} }{DESC} .=
      ", Begin astronomical $ret->{seasonAstro_long} season"
      if ( $ret->{seasonAstroChng} && $ret->{seasonAstroChng} == 1 );
    $ret->{events}{ $ret->{midnight_t} }{VALUE} = 2
      if ( $ret->{seasonPhenoChng} && $ret->{seasonPhenoChng} == 1 );
    $ret->{events}{ $ret->{midnight_t} }{DESC} .=
      ", Begin phenological season $ret->{seasonPheno_long}"
      if ( $ret->{seasonPhenoChng} && $ret->{seasonPhenoChng} == 1 );

    # calculate daytime from daytimeStage, season and DST
    my $ds = $ret->{daytimeStage};
    while ( !defined( $ret->{daytime} ) ) {

        #TODO let user define %sdt2daytimes through attribute
        $ret->{daytime} = $sdt2daytimes{1}{ $ret->{isdst} }{$ds}
          if ( $sdt2daytimes{1}
            && $sdt2daytimes{1}{ $ret->{isdst} }
            && defined( $sdt2daytimes{1}{ $ret->{isdst} }{$ds} ) );
        $ds--;

        # when no relation was found
        unless ( defined( $ret->{daytime} ) || $ds > -1 ) {

            # assume midevening after sunset
            if ( $ret->{time_s} >= $ret->{sunset_s} ) {
                $ret->{daytime} = 5;
            }

            # assume night before sunrise
            else {
                $ret->{daytime} = 6;
            }
        }
    }

    # daytime during evening and night
    unless ( $ret->{daytimeStage} ) {
        $ret->{daytime} = 4 unless ( $ret->{daytime} > 4 );
        $ret->{daytime} = 5 unless ( $ret->{daytime} > 5 || $ret->{isday} );
        $ret->{daytime} = 6 if ( $ret->{time_s} < $ret->{sunrise_s} );
    }

    $ret->{daytime_long} = $daytimes{en}[ $ret->{daytime} ];

    my @langs = ('EN');
    push @langs, $lang unless ( $lang =~ /^EN/i );
    foreach (@langs) {
        my $l = lc($_);
        $l =~ s/^([a-z]+).*/$1/g;
        next unless ( $daytimes{$l} );
        my $h = $l eq "en" ? $ret : \%{ $ret->{$_} };

        $h->{daytime_long} = $daytimes{$l}[ $ret->{daytime} ];
    }

    # calculate daily schedule
    #

    # Midnight
    $ret->{events}{ $ret->{midnight_t} }{TYPE} = "dayshift";
    $ret->{events}{ $ret->{midnight_t} }{TIME} =
      main::FmtDateTime( $ret->{midnight_t} );
    $ret->{events}{ $ret->{midnight_t} }{DESC} =
      "Begin of night time and new calendar day";
    $ret->{events}{ $ret->{1}{midnight_t} }{TYPE} = "dayshift";
    $ret->{events}{ $ret->{1}{midnight_t} }{TIME} =
      $ret->{date} . " 24:00:00";
    $ret->{events}{ $ret->{1}{midnight_t} }{DESC} =
      "End of calendar day and begin night time";

    # Holidays
    $ret->{events}{ $ret->{midnight_t} }{DESC} .=
      ", $daystages{en}[2]: $ret->{day_desc}"
      if ( $ret->{isholiday} );
    $ret->{events}{ $ret->{1}{midnight_t} }{DESC} .=
      ", $daystages{en}[2]: $ret->{1}{day_desc}"
      if ( $ret->{1}{isholiday} );

    # DST change
    #FIXME TODO
    if ( $ret->{dstchange} && $ret->{dstchange} == 1 ) {
        my $t = $ret->{midnight_t} + 2 * 60 * 60;
        $ret->{events}{$t}{TYPE}  = "dstshift";
        $ret->{events}{$t}{VALUE} = $ret->{isdst};
        $ret->{events}{$t}{TIME}  = main::FmtDateTime($t);
        $ret->{events}{$t}{DESC}  = "Begin of standard time (-1h)"
          unless ( $ret->{isdst} );
        $ret->{events}{$t}{DESC} = "Begin of daylight saving time (+1h)"
          if ( $ret->{isdst} );
    }

    # daytime stage event forecast for today
    my $i = 1;
    my $b = $ret->{sunrise_t};
    while ( $i <= $ret->{daytimeStages} + 1 ) {

        # find daytime
        my $daytime;
        $daytime = $sdt2daytimes{1}{ $ret->{isdst} }{$i}
          if ( $sdt2daytimes{1}
            && $sdt2daytimes{1}{ $ret->{isdst} }
            && defined( $sdt2daytimes{1}{ $ret->{isdst} }{$i} ) );

        # create event
        my $t = int( $b + 0.5 );
        $ret->{events}{$t}{TIME} = main::FmtDateTime($t);
        if ( $i == $ret->{daytimeStages} + 1 ) {
            $ret->{events}{$t}{TYPE}  = "daytime";
            $ret->{events}{$t}{VALUE} = "midevening";
            $ret->{events}{$t}{DESC}  = "End of daytime";
        }
        else {
            $ret->{events}{$t}{TYPE}  = "daytimeStage";
            $ret->{events}{$t}{VALUE} = $i;
            $ret->{events}{$t}{DESC}  = "Begin of daytime stage $i"
              unless ($daytime);
            if ( defined($daytime) ) {
                $ret->{events}{$t}{TYPE}  = "daytime";
                $ret->{events}{$t}{VALUE} = $daytimes{en}[$daytime];
                $ret->{events}{$t}{DESC} =
                  "Begin of $daytimes{en}[$daytime] time and daytime stage $i";
            }
        }
        $i++;
        $b += $ret->{daytimeStageLn_s};
    }

    return $ret;
}

####################
# HELPER FUNCTIONS

sub decimal_mark ($;$) {
    my $s;
    my $i;
    my $f;
    if ( $_[0] =~ /^(\-|\+)?(\d+)(?:\.(\d+))?$/ ) {
        $s = $1;
        $i = reverse $2;
        $f = $3;
    }
    else {
        return $_[0];
    }

    my $locale = ( $_[1] ? $_[1] : undef );

    my $old_locale = setlocale(LC_NUMERIC);
    setlocale( LC_NUMERIC, $locale ) if ($locale);
    use locale ':not_characters';
    my ( $decimal_point, $thousands_sep, $grouping ) =
      @{ localeconv() }{ 'decimal_point', 'thousands_sep', 'grouping' };
    setlocale( LC_NUMERIC, "" );
    setlocale( LC_NUMERIC, $old_locale );
    no locale;

    $decimal_point = '.'
      unless ( defined($decimal_point) && $decimal_point ne '' );
    $thousands_sep = chr(0x202F)
      unless ( defined($thousands_sep) && $thousands_sep ne "" );
    my @grouping =
      $grouping && $grouping =~ /^\d+$/
      ? unpack( "C*", $grouping )
      : (3);

    $i =~ s/(\d{$grouping[0]})(?=\d)/$1$thousands_sep/g;
    $f =~ s/(\d{$grouping[0]})(?=\d)/$1$thousands_sep/g
      if ( defined($f) && $f ne '' );

    return
        ( $s ? $s : '' )
      . ( reverse $i )
      . ( defined($f) && $f ne '' ? $decimal_point . $f : '' );
}

sub _round($;$) {
    my ( $val, $n ) = @_;
    $n = 1 unless ( defined($n) );
    return sprintf( "%.${n}f", $val );
}

sub _time(;$$$$);

sub _time(;$$$$) {
    my ( $time, $lang, $dayOffset, $params ) = @_;
    $dayOffset = 1 if ( !defined($dayOffset) || $dayOffset !~ /^-?\d+$/ );
    $lang      = (
          $main::attr{global}{language}
        ? $main::attr{global}{language}
        : "EN"
    ) unless ($lang);

    return undef
      unless ( !$time || $time =~ /^\d{10}(?:\.\d+)?$/ );

    my %ret;
    $ret{time_t} = $time if ($time);
    $ret{time_t} = time unless ($time);
    $ret{params} = $params if ($params);

    my @t = localtime( $ret{time_t} );
    (
        $ret{sec},  $ret{min},  $ret{hour}, $ret{mday}, $ret{mon},
        $ret{year}, $ret{wday}, $ret{yday}, $ret{isdst}
    ) = @t;
    $ret{monISO} = $ret{mon} + 1;
    $ret{year} += 1900;

    $ret{date} =
      sprintf( "%04d-%02d-%02d", $ret{year}, $ret{monISO}, $ret{mday} );
    $ret{time} = sprintf( "%02d:%02d", $ret{hour}, $ret{min} );
    $ret{time_hms} =
      sprintf( "%02d:%02d:%02d", $ret{hour}, $ret{min}, $ret{sec} );
    $ret{time_s}     = hms2s( $ret{time_hms} );            #FIXME for DST change
    $ret{datetime}   = $ret{date} . " " . $ret{time_hms};
    $ret{midnight_t} = $ret{time_t} - $ret{time_s};        #FIXME for DST change

    # get leap year status
    $ret{isly} = IsLeapYear( $ret{year} );

    # remaining monthdays
    $ret{mdayrem} = 0;
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 1 );
    $ret{mdayrem} = 28 + $ret{isly} - $ret{mday}
      if ( $ret{monISO} == 2 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 3 );
    $ret{mdayrem} = 30 - $ret{mday} if ( $ret{monISO} == 4 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 5 );
    $ret{mdayrem} = 30 - $ret{mday} if ( $ret{monISO} == 6 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 7 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 8 );
    $ret{mdayrem} = 30 - $ret{mday} if ( $ret{monISO} == 9 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 10 );
    $ret{mdayrem} = 30 - $ret{mday} if ( $ret{monISO} == 11 );
    $ret{mdayrem} = 31 - $ret{mday} if ( $ret{monISO} == 12 );

    # remaining yeardays
    $ret{ydayrem} = 365 + $ret{isly} - $ret{yday};

    # ISO 8601 weekday as number with Monday as 1 (1-7)
    $ret{wdaynISO} = strftime( '%u', @t );

    # Week number with the first Sunday as the first day of week one (00-53)
    $ret{week} = strftime( '%U', @t );

    # ISO 8601 week number (00-53)
    $ret{weekISO} = strftime( '%V', @t );

    # weekend
    $ret{iswe} = ( $ret{wday} == 0 || $ret{wday} == 6 ) ? 1 : 0;

    # text strings
    my @langs = ('EN');
    push @langs, $lang unless ( $lang =~ /^EN/i );
    foreach (@langs) {
        my $l = lc($_);
        $l =~ s/^([a-z]+).*/$1/g;
        next unless ( $months{$l} );
        my $h = $l eq "en" ? \%ret : \%{ $ret{$_} };

        $h->{dst_long}   = $dst{$l}[ $ret{isdst} ];
        $h->{rday_long}  = $reldays{$l}[1];
        $h->{day_desc}   = $daystages{$l}[ $ret{iswe} ];
        $h->{wday_long}  = $days{$l}[ $ret{wday} ];
        $h->{wday_short} = $dayss{$l}[ $ret{wday} ];
        $h->{mon_long}   = $months{$l}[ $ret{mon} ];
        $h->{mon_short}  = $monthss{$l}[ $ret{mon} ];

        $h->{date_long} =
          _ReplaceStringByHashKey( \%ret, $dateformats{$l}, $_ );
        $h->{date_short} =
          _ReplaceStringByHashKey( \%ret, $dateformatss{$l}, $_ );
    }

    # holiday
    if ($dayOffset) {
        $ret{'-1'}{isholiday} = 0;
        $ret{1}{isholiday}    = 0;
    }
    $ret{isholiday} = 0;

    my $tod;
    my $ytd;
    my $tom;
    if ( main::AttrVal( 'global', 'holiday2we', undef ) ) {
        my $date = sprintf( "%02d-%02d", $ret{monISO}, $ret{mday} );
        $tod = main::IsWe($date);
        if ($dayOffset) {
            $ytd = main::IsWe('yesterday');
            $tom = main::IsWe('tomorrow');
        }

        if ( $tod ne "none" ) {
            $ret{iswe} += 2;
            $ret{isholiday} = 1;
            $ret{day_desc}  = $tod;

            foreach (@langs) {
                my $l = lc($_);
                $l =~ s/^([a-z]+).*/$1/g;
                next unless ( $months{$l} );
                my $h = $l eq "en" ? \%ret : \%{ $ret{$_} };

                $h->{day_desc} = $tod;
            }
        }
        if ($dayOffset) {
            if ( $ytd ne "none" && $ret{'-1'} ) {
                $ret{'-1'}{isholiday} = 1;
                $ret{'-1'}{day_desc}  = $ytd;

                foreach (@langs) {
                    my $l = lc($_);
                    $l =~ s/^([a-z]+).*/$1/g;
                    next unless ( $months{$l} );
                    my $h = $l eq "en" ? $ret{'-1'} : \%{ $ret{'-1'}{$_} };

                    $h->{day_desc} = $ytd;
                }
            }
            if ( $tom ne "none" && $ret{1} ) {
                $ret{1}{isholiday} = 1;
                $ret{1}{day_desc}  = $tom;

                foreach (@langs) {
                    my $l = lc($_);
                    $l =~ s/^([a-z]+).*/$1/g;
                    next unless ( $months{$l} );
                    my $h = $l eq "en" ? $ret{1} : \%{ $ret{1}{$_} };

                    $h->{day_desc} = $tom;
                }
            }
        }
    }

    if (wantarray) {
        my @a;

        foreach (
            'sec',      'min',     'hour',   'mday',     'mon',
            'year',     'wday',    'wdayn',  'yday',     'isdst',
            'mdayrem',  'monISO',  'week',   'weekISO',  'wdayISO',
            'wdaynISO', 'ydayrem', 'time_t', 'datetime', 'date',
            'time_hms', 'time',    'isly',
          )
        {
            push @a, $ret{$_};
        }

        return @a;
    }

    elsif ($dayOffset) {
        my $i = $dayOffset * -1;
        while ( $i < $dayOffset + 1 ) {
            $ret{$i} = _time( $ret{time_t} + ( 24 * 60 * 60 * $i ), $lang, 0 )
              unless ( $i == 0 );

            foreach (@langs) {
                my $l = $_;
                $l =~ s/^([A-Z-a-z]+).*/$1/g;
                $l = lc($l);
                next if ( $i == 0 || !$reldays{$l} );
                my $h = $l eq "en" ? \%{ $ret{$i} } : \%{ $ret{$i}{$l} };

                if ( $i == -1 || $i == 1 ) {
                    $h->{rday_long} = $reldays{$l}[ $i + 1 ];
                }
                else {
                    delete $h->{rday_long};
                }
            }

            $i++;
        }

        # DST change
        $ret{'-1'}{dstchange} = 0;
        $ret{dstchange}       = 0;
        $ret{1}{dstchange}    = 0;

        if ( $ret{isdst} ne $ret{1}{isdst} ) {
            $ret{dstchange} = 2;
            $ret{1}{dstchange} = 1;
        }
        elsif ( $ret{isdst} ne $ret{'-1'}{isdst} ) {
            $ret{'-1'}{dstchange} = 2;
            $ret{dstchange} = 1;
        }
    }

    return \%ret;
}

sub _GetIndexFromArray($$) {
    my ( $string, $array ) = @_;
    return undef unless ( ref($array) eq "ARRAY" );
    my ($index) = grep { $array->[$_] =~ /^$string$/i } ( 0 .. @$array - 1 );
    return defined $index ? $index : undef;
}

sub _ReplaceStringByHashKey($$;$) {
    my ( $hash, $string, $sublvl ) = @_;
    return $string unless ( $hash && ref($hash) eq "HASH" );

    $string = _ReplaceStringByHashKey( $hash->{$sublvl}, $string )
      if ( $sublvl && $hash->{$sublvl} );

    foreach my $key ( keys %{$hash} ) {
        next if ( ref( $hash->{$key} ) );
        my $val = $hash->{$key};
        $string =~ s/%$key%/$val/gi;
        $string =~ s/\$$key/$val/g;
    }

    return $string;
}

1;

=pod
=encoding utf8

=for :application/json;q=META.json UConv.pm
{
  "author": [
    "Julian Pawlowski <julian.pawlowski@gmail.com>"
  ],
  "x_fhem_maintainer": [
    "loredo"
  ],
  "x_fhem_maintainer_github": [
    "jpawlowski"
  ],
  "keywords": [
    "RType",
    "Unit"
  ]
}
=end :application/json;q=META.json

=cut
