diff --git a/fhem/FHEM/10_RESIDENTS.pm b/fhem/FHEM/10_RESIDENTS.pm index d1d22d951..3f4cf721d 100644 --- a/fhem/FHEM/10_RESIDENTS.pm +++ b/fhem/FHEM/10_RESIDENTS.pm @@ -19,6 +19,8 @@ sub RESIDENTS_Initialize($) { $hash->{AttrFn} = "RESIDENTStk_Attr"; $hash->{NotifyFn} = "RESIDENTStk_Notify"; + $hash->{AttrPrefix} = "rgr_"; + $hash->{AttrList} = "disable:1,0 disabledForIntervals do_not_notify:1,0 " . "rgr_states:multiple-strict,home,gotosleep,asleep,awoken,absent,gone rgr_lang:EN,DE rgr_noDuration:0,1 rgr_showAllStates:0,1 rgr_wakeupDevice " diff --git a/fhem/FHEM/20_GUEST.pm b/fhem/FHEM/20_GUEST.pm index 4f1789b03..8d5bae2c0 100644 --- a/fhem/FHEM/20_GUEST.pm +++ b/fhem/FHEM/20_GUEST.pm @@ -20,13 +20,15 @@ sub GUEST_Initialize($) { $hash->{AttrFn} = "RESIDENTStk_Attr"; $hash->{NotifyFn} = "RESIDENTStk_Notify"; + $hash->{AttrPrefix} = "rg_"; + $hash->{AttrList} = "disable:1,0 disabledForIntervals do_not_notify:1,0 " . "rg_states:multiple-strict,home,gotosleep,asleep,awoken,absent,none " . $readingFnAttributes; foreach (@RESIDENTStk_attr) { - $hash->{AttrList} .= " rg_" . $_; + $hash->{AttrList} .= " " . $hash->{AttrPrefix} . $_; } } diff --git a/fhem/FHEM/20_ROOMMATE.pm b/fhem/FHEM/20_ROOMMATE.pm index cec1c5748..fe6ff9f61 100644 --- a/fhem/FHEM/20_ROOMMATE.pm +++ b/fhem/FHEM/20_ROOMMATE.pm @@ -20,13 +20,15 @@ sub ROOMMATE_Initialize($) { $hash->{AttrFn} = "RESIDENTStk_Attr"; $hash->{NotifyFn} = "RESIDENTStk_Notify"; + $hash->{AttrPrefix} = "rr_"; + $hash->{AttrList} = "disable:1,0 disabledForIntervals do_not_notify:1,0 " . "rr_states:multiple-strict,home,gotosleep,asleep,awoken,absent,gone " . $readingFnAttributes; foreach (@RESIDENTStk_attr) { - $hash->{AttrList} .= " rr_" . $_; + $hash->{AttrList} .= " " . $hash->{AttrPrefix} . $_; } } diff --git a/fhem/FHEM/RESIDENTStk.pm b/fhem/FHEM/RESIDENTStk.pm index ec13ed534..a187a053f 100644 --- a/fhem/FHEM/RESIDENTStk.pm +++ b/fhem/FHEM/RESIDENTStk.pm @@ -6,6 +6,7 @@ use warnings; use Data::Dumper; use Time::Local; +use Unit; our (@RESIDENTStk_attr); # module variables ############################################################ @@ -1320,7 +1321,7 @@ m/^0|false|absent|disappeared|unavailable|unreachable|disconnected|1|true|presen my $d = $_; if ( $d =~ m/^([a-zA-Z\d._]+):(?:([A-Za-z\d_\.\-\/]*))?$/ ) { $d = $1; - $r = $2 if ($2 && $2 ne ""); + $r = $2 if ( $2 && $2 ne "" ); } my $presenceState = @@ -1446,16 +1447,15 @@ sub RESIDENTStk_DurationTimer($;$) { my $durPresence_hr = ( $durPresence > 0 ) - ? RESIDENTStk_sec2time($durPresence) + ? UConv::s2hms($durPresence) : "00:00:00"; my $durPresence_cr = ( $durPresence > 60 ) ? int( $durPresence / 60 + 0.5 ) : 0; my $durAbsence_hr = - ( $durAbsence > 0 ) ? RESIDENTStk_sec2time($durAbsence) : "00:00:00"; + ( $durAbsence > 0 ) ? UConv::s2hms($durAbsence) : "00:00:00"; my $durAbsence_cr = ( $durAbsence > 60 ) ? int( $durAbsence / 60 + 0.5 ) : 0; - my $durSleep_hr = - ( $durSleep > 0 ) ? RESIDENTStk_sec2time($durSleep) : "00:00:00"; + my $durSleep_hr = ( $durSleep > 0 ) ? UConv::s2hms($durSleep) : "00:00:00"; my $durSleep_cr = ( $durSleep > 60 ) ? int( $durSleep / 60 + 0.5 ) : 0; readingsBeginUpdate($hash) if ( !$silent ); @@ -2424,8 +2424,7 @@ return;;\ # earlier than wakeupDefaultTime if ( $wakeupEnforced == 3 && $wakeupDefaultTime - && RESIDENTStk_time2sec($wakeupDefaultTime) > - RESIDENTStk_time2sec($lastRun) ) + && UConv::hms2s($wakeupDefaultTime) > UConv::hms2s($lastRun) ) { Log3 $NAME, 4, "RESIDENTStk $NAME: " @@ -2649,15 +2648,15 @@ sub RESIDENTStk_wakeupGetBegin($;$) { } # Recalculate new wake-up value - my $seconds = RESIDENTStk_time2sec($wakeupTime) - $wakeupOffset * 60; + my $seconds = UConv::hms2s($wakeupTime) - $wakeupOffset * 60; if ( $seconds < 0 ) { $seconds = 86400 + $seconds } Log3 $NAME, 4, "RESIDENTStk $NAME: " . "wakeupGetBegin result: $wakeupTime = $seconds s - $wakeupOffset m = " - . RESIDENTStk_sec2time($seconds); + . UConv::s2hms($seconds); - return RESIDENTStk_sec2time($seconds); + return UConv::s2hms($seconds); } sub RESIDENTStk_wakeupRun($;$) { @@ -2880,8 +2879,7 @@ sub RESIDENTStk_wakeupRun($;$) { # earlier than wakeupDefaultTime if ( $wakeupEnforced == 3 && $wakeupDefaultTime - && RESIDENTStk_time2sec($wakeupDefaultTime) > - RESIDENTStk_time2sec($lastRun) ) + && UConv::hms2s($wakeupDefaultTime) > UConv::hms2s($lastRun) ) { Log3 $NAME, 4, "RESIDENTStk $NAME: " @@ -2933,7 +2931,7 @@ sub RESIDENTStk_wakeupRun($;$) { "RESIDENTStk $NAME: " . "created at-device $wakeupStopAtdevice to stop wake-up program in $wakeupOffset minutes"; fhem "define $wakeupStopAtdevice at +" - . RESIDENTStk_sec2time( $wakeupOffset * 60 + 1 ) + . UConv::s2hms( $wakeupOffset * 60 + 1 ) . " set $NAME:FILTER=running=1 stop triggerpost"; fhem "attr $wakeupStopAtdevice " . "comment Auto-created by RESIDENTS Toolkit: temp. at-device to stop wake-up program of timer $NAME when wake-up time is reached"; @@ -3008,7 +3006,7 @@ sub RESIDENTStk_wakeupGetNext($;$) { my $tomorrow = $today + 1; $tomorrow = 0 if ( $tomorrow == 7 ); - my $secNow = RESIDENTStk_time2sec( $hour . ":" . $min ) + $sec; + my $secNow = UConv::hms2s( $hour . ":" . $min ) + $sec; my $definitiveNextToday; my $definitiveNextTomorrow; my $definitiveNextTodayDev; @@ -3118,7 +3116,7 @@ sub RESIDENTStk_wakeupGetNext($;$) { && $wakeupAtNTM =~ /^([0-9]{2}:[0-9]{2})$/ ) { $nextRunSrc = "at"; - $nextRunSec = RESIDENTStk_time2sec($wakeupAtNTM); + $nextRunSec = UConv::hms2s($wakeupAtNTM); $nextRunSecTarget = $nextRunSec + $wakeupOffset * 60; if ( $wakeupOffset && $nextRunSecTarget >= 86400 ) { @@ -3139,7 +3137,7 @@ sub RESIDENTStk_wakeupGetNext($;$) { } else { $nextRunSrc = "dummy"; - $nextRunSecTarget = RESIDENTStk_time2sec($nextRun); + $nextRunSecTarget = UConv::hms2s($nextRun); $nextRunSec = $nextRunSecTarget - $wakeupOffset * 60; if ( $wakeupOffset && $nextRunSec < 0 ) { @@ -3327,24 +3325,24 @@ sub RESIDENTStk_wakeupGetNext($;$) { { Log3 $name, 4, "RESIDENTStk $name: 07 - next wake-up result: today at " - . RESIDENTStk_sec2time($definitiveNextToday) + . UConv::s2hms($definitiveNextToday) . ", wakeupDevice=" . $definitiveNextTodayDev; return ( $definitiveNextTodayDev, - substr( RESIDENTStk_sec2time($definitiveNextToday), 0, -3 ) ); + substr( UConv::s2hms($definitiveNextToday), 0, -3 ) ); } elsif (defined($definitiveNextTomorrowDev) && defined($definitiveNextTomorrow) ) { Log3 $name, 4, "RESIDENTStk $name: 07 - next wake-up result: tomorrow at " - . RESIDENTStk_sec2time($definitiveNextTomorrow) + . UConv::s2hms($definitiveNextTomorrow) . ", wakeupDevice=" . $definitiveNextTomorrowDev; return ( $definitiveNextTomorrowDev, - substr( RESIDENTStk_sec2time($definitiveNextTomorrow), 0, -3 ) ); + substr( UConv::s2hms($definitiveNextTomorrow), 0, -3 ) ); } return ( undef, undef ); @@ -3358,12 +3356,12 @@ sub RESIDENTStk_TimeSum($$) { return $val1; } else { - $timestamp1 = RESIDENTStk_time2sec($val1); + $timestamp1 = UConv::hms2s($val1); } if ( $val2 =~ /^([\+\-])([0-9]{2}):([0-9]{2})$/ ) { $math = $1; - $timestamp2 = RESIDENTStk_time2sec("$2:$3"); + $timestamp2 = UConv::hms2s("$2:$3"); } elsif ( $val2 =~ /^([\+\-])([0-9]*)$/ ) { $math = $1; @@ -3374,12 +3372,10 @@ sub RESIDENTStk_TimeSum($$) { } if ( $math eq "-" ) { - return - substr( RESIDENTStk_sec2time( $timestamp1 - $timestamp2 ), 0, -3 ); + return substr( UConv::s2hms( $timestamp1 - $timestamp2 ), 0, -3 ); } else { - return - substr( RESIDENTStk_sec2time( $timestamp1 + $timestamp2 ), 0, -3 ); + return substr( UConv::s2hms( $timestamp1 + $timestamp2 ), 0, -3 ); } } @@ -3405,32 +3401,7 @@ sub RESIDENTStk_TimeDiff ($$;$) { if ( defined($format) && $format eq "min" ); # return human readable format - return RESIDENTStk_sec2time( round( $timeDiff, 0 ) ); -} - -sub RESIDENTStk_sec2time($) { - my ($sec) = @_; - - # return human readable format - my $hours = - ( abs($sec) < 3600 ? 0 : int( abs($sec) / 3600 ) ); - $sec -= ( $hours == 0 ? 0 : ( $hours * 3600 ) ); - my $minutes = ( abs($sec) < 60 ? 0 : int( abs($sec) / 60 ) ); - my $seconds = abs($sec) % 60; - - $hours = "0" . $hours if ( $hours < 10 ); - $minutes = "0" . $minutes if ( $minutes < 10 ); - $seconds = "0" . $seconds if ( $seconds < 10 ); - - return "$hours:$minutes:$seconds"; -} - -sub RESIDENTStk_time2sec($) { - my ($s) = @_; - my @t = split /:/, $s; - $t[2] = 0 unless ( $t[2] ); - - return $t[0] * 3600 + $t[1] * 60 + $t[2]; + return UConv::s2hms( round( $timeDiff, 0 ) ); } sub RESIDENTStk_InternalTimer($$$$$) { @@ -3490,17 +3461,11 @@ sub RESIDENTStk_StopInternalTimers($) { RESIDENTStk_RemoveInternalTimer( "DurationTimer", $hash ); } -sub RESIDENTStk_findHomestateSlaves($) { - my ($hash) = @_; - my $ret = ""; - $ret .= AttrVal( $hash->{NAME}, "rh_residentsDevice", "" ); - - return $ret; -} - sub RESIDENTStk_findResidentSlaves($;$) { my ( $hash, $ret ) = @_; - my $ROOMMATES; + my @slaves; + + my @ROOMMATES; foreach ( devspec2array("TYPE=ROOMMATE") ) { next unless ( @@ -3508,11 +3473,10 @@ sub RESIDENTStk_findResidentSlaves($;$) { && grep { $hash->{NAME} eq $_ } split( /,/, $defs{$_}{RESIDENTGROUPS} ) ); - $ROOMMATES .= "," if ($ROOMMATES); - $ROOMMATES .= $_; + push @ROOMMATES, $_; } - my $GUESTS; + my @GUESTS; foreach ( devspec2array("TYPE=GUEST") ) { next unless ( @@ -3520,31 +3484,31 @@ sub RESIDENTStk_findResidentSlaves($;$) { && grep { $hash->{NAME} eq $_ } split( /,/, $defs{$_}{RESIDENTGROUPS} ) ); - $GUESTS .= "," if ($GUESTS); - $GUESTS .= $_; + push @GUESTS, $_; } - if ( !$ROOMMATES && $hash->{ROOMMATES} ) { + if ( scalar @ROOMMATES ) { + $hash->{ROOMMATES} = join( ",", @ROOMMATES ); + } + elsif ( $hash->{ROOMMATES} ) { delete $hash->{ROOMMATES}; } - elsif ( $ROOMMATES - && ( !$hash->{ROOMMATES} || $hash->{ROOMMATES} ne $ROOMMATES ) ) - { - $hash->{ROOMMATES} = $ROOMMATES; - } - if ( !$GUESTS && $hash->{GUESTS} ) { + if ( scalar @GUESTS ) { + $hash->{GUESTS} = join( ",", @GUESTS ); + } + elsif ( $hash->{GUESTS} ) { delete $hash->{GUESTS}; } - elsif ( $GUESTS && ( !$hash->{GUESTS} || $hash->{GUESTS} ne $GUESTS ) ) { - $hash->{GUESTS} = $GUESTS; - } - $ret = "" unless ($ret); - $ret .= "," unless ( $ret eq "" ); - $ret .= $hash->{ROOMMATES} if ( $hash->{ROOMMATES} ); - $ret .= "," unless ( $ret eq "" ); - $ret .= $hash->{GUESTS} if ( $hash->{GUESTS} ); + if ( $hash->{ROOMMATES} ) { + $ret .= "," if ($ret); + $ret .= $hash->{ROOMMATES}; + } + if ( $hash->{GUESTS} ) { + $ret .= "," if ($ret); + $ret .= $hash->{GUESTS}; + } return RESIDENTStk_findDummySlaves( $hash, $ret ); } @@ -3590,11 +3554,12 @@ sub RESIDENTStk_findDummySlaves($;$) { sub RESIDENTStk_GetPrefixFromType($) { my ($name) = @_; - return "" unless ( defined($name) ); - return "rh_" if ( GetType($name) eq "HOMESTATE" ); - return "rgr_" if ( GetType($name) eq "RESIDENTS" ); - return "rr_" if ( GetType($name) eq "ROOMMATE" ); - return "rg_" if ( GetType($name) eq "GUEST" ); + return $modules{ $defs{$name}{TYPE} }{AttrPrefix} + if ( $defs{$name} + && $defs{$name}{TYPE} + && $modules{ $defs{$name}{TYPE} } + && $modules{ $defs{$name}{TYPE} }{AttrPrefix} ); + return ""; } sub RESIDENTStk_DoModuleTrigger($$@) { diff --git a/fhem/FHEM/UConv.pm b/fhem/FHEM/UConv.pm index a68d8aa6f..fb413f977 100644 --- a/fhem/FHEM/UConv.pm +++ b/fhem/FHEM/UConv.pm @@ -5,82 +5,35 @@ sub UConv_Initialize() { } package UConv; use Scalar::Util qw(looks_like_number); +use POSIX qw(strftime); use Data::Dumper; #################### # Translations -my %compasspoint_txt = ( - "en" => [ +my %compasspoints = ( + en => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], - "de" => [ + de => [ 'N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], - "nl" => [ + nl => [ 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' ], - "fr" => [ + fr => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' ], - "pl" => [ + pl => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], ); -my %wdays_txt_en = ( - "en" => { - 'Mon' => 'Mon', - 'Tue' => 'Tue', - 'Wed' => 'Wed', - 'Thu' => 'Thu', - 'Fri' => 'Fri', - 'Sat' => 'Sat', - 'Sun' => 'Sun', - }, - "de" => { - 'Mon' => 'Mo', - 'Tue' => 'Di', - 'Wed' => 'Mi', - 'Thu' => 'Do', - 'Fri' => 'Fr', - 'Sat' => 'Sa', - 'Sun' => 'So', - }, - "nl" => { - 'Mon' => 'Maa', - 'Tue' => 'Din', - 'Wed' => 'Woe', - 'Thu' => 'Don', - 'Fri' => 'Vri', - 'Sat' => 'Zat', - 'Sun' => 'Zon', - }, - "fr" => { - 'Mon' => 'Lun', - 'Tue' => 'Mar', - 'Wed' => 'Mer', - 'Thu' => 'Jeu', - 'Fri' => 'Ven', - 'Sat' => 'Sam', - 'Sun' => 'Dim', - }, - "pl" => { - 'Mon' => 'Pon', - 'Tue' => 'Wt', - 'Wed' => 'Śr', - 'Thu' => 'Czw', - 'Fri' => 'Pt', - 'Sat' => 'Sob', - 'Sun' => 'Nie', - }, -); - ################################# ### Inner metric conversions ### @@ -145,8 +98,7 @@ sub hpa2psi($;$) { # Speed: convert km/h (kilometer per hour) to mph (miles per hour) sub kph2mph($;$) { - my ( $data, $rnd ) = @_; - return roundX( $data * 0.621, $rnd ); + return km2mi(@_); } # Speed: convert m/s (meter per seconds) to mph (miles per hour) @@ -173,6 +125,12 @@ sub m2ft($;$) { return roundX( $data * 3.2808, $rnd ); } +# Length: convert km (kilometer) to miles (mi) +sub km2mi($;$) { + my ( $data, $rnd ) = @_; + return roundX( $data * 0.621371192, $rnd ); +} + ################################# ### Inner Angloamerican conversions ### @@ -219,8 +177,7 @@ sub psi2hpa($;$) { # Speed: convert mph (miles per hour) to km/h (kilometer per hour) sub mph2kph($;$) { - my ( $data, $rnd ) = @_; - return roundX( $data * 1.609344, $rnd ); + return mi2km(@_); } # Speed: convert mph (miles per hour) to m/s (meter per seconds) @@ -247,6 +204,12 @@ sub ft2m($;$) { return roundX( $data / 3.2808, $rnd ); } +# Length: convert mi (miles) to km (kilometer) +sub mi2km($;$) { + my ( $data, $rnd ) = @_; + return roundX( $data * 1.609344, $rnd ); +} + ################################# ### Angular conversions ### @@ -254,14 +217,13 @@ sub ft2m($;$) { # convert direction in degree to point of the compass sub direction2compasspoint($;$) { my ( $azimuth, $lang ) = @_; - my $directions_txt_i18n; - if ( $lang && defined( $compasspoint_txt{$lang} ) ) { - $directions_txt_i18n = $compasspoint_txt{$lang}; + if ( $lang && defined( $compasspointss{ lc($lang) } ) ) { + $directions_txt_i18n = $compasspointss{ lc($lang) }; } else { - $directions_txt_i18n = $compasspoint_txt{en}; + $directions_txt_i18n = $compasspointss{en}; } return @$directions_txt_i18n[ @@ -418,10 +380,245 @@ sub mph2bft($) { return $val; } +################################# +### Differential conversions +### + +sub distance($$$$;$) { + my ( $lat1, $lng1, $lat2, $lng2, $miles ) = @_; + use constant M_PI => 4 * atan2( 1, 1 ); + my $pi80 = M_PI / 180; + $lat1 *= $pi80; + $lng1 *= $pi80; + $lat2 *= $pi80; + $lng2 *= $pi80; + + my $r = 6372.797; # mean radius of Earth in km + my $dlat = $lat2 - $lat1; + my $dlng = $lng2 - $lng1; + my $a = + sin( $dlat / 2 ) * sin( $dlat / 2 ) + + cos($lat1) * cos($lat2) * sin( $dlng / 2 ) * sin( $dlng / 2 ); + my $c = 2 * atan2( sqrt($a), sqrt( 1 - $a ) ); + my $km = $r * $c; + + return ( $miles ? km2mi($km) : $km ); +} + ################################# ### Textual unit conversions ### +######## %hr_formats ######################################### +# What : used by functions humanReadable and machineReadable +my %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, + }, + +); + +######## humanReadable ######################################### +# What : Formats a number or text string to be more readable for humans +# Syntax: { humanReadable( , [ ] ) } +# 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 = + $attr{global}{humanReadable} ? $attr{global}{humanReadable} + : ( + $attr{global}{language} ? $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( , [ , [ ]] ) } +# # 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, $indoor ) = @_; @@ -597,23 +794,282 @@ sub values2weathercondition($$$$$) { return $val; } -#TODO rewrite for Unit.pm -sub fmtTime($) { - my ($val) = @_; - my $suffix = ' s'; +################################# +### Chronological conversions +### - if ( $val >= 60 ) { - $val = sprintf( "%.1f", $val / 60 ); - $suffix = ' min'; +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 ); +} - if ( $val >= 60 ) { - $val = sprintf( "%.1f", $val / 60 ); - $suffix = ' h'; - } +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 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 $year = shift; + if ( !$year || $year !~ /^[1-2]\d{3}$/ ) { + my ( + $tsec, $tmin, $thour, $tmday, $tmon, + $tyear, $twday, $tyday, $tisdst + ) = GetTimeinfo($year); + $year = $tyear + 1900; } - return ( $val, $suffix ) if (wantarray); - return $val . $suffix; + # 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 $year % 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 $year % 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 $year % 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; +} + +sub IsDst (;$) { + my ( + $sec, $min, $hour, $mday, $month, + $monthISO, $year, $week, $weekISO, $wday, + $wdayISO, $yday, $isdst + ) = GetCalendarInfo(@_); + return $isdst; +} + +sub IsWeekend (;$) { + my ( + $sec, $min, $hour, $mday, $month, + $monthISO, $year, $week, $weekISO, $wday, + $wdayISO, $yday, $isdst, $iswe + ) = GetCalendarInfo(@_); + return $iswe; +} + +sub IsHoliday (;$) { + my ( + $sec, $min, $hour, + $mday, $month, $monthISO, + $year, $week, $weekISO, + $wday, $wdayISO, $yday, + $isdst, $iswe, $isHolidayYesterday, + $isHolidayToday, $isHolidayTomorrow + ) = GetCalendarInfo(@_); + return $isHolidayToday; +} + +sub IsHolidayTomorrow (;$) { + my ( + $sec, $min, $hour, + $mday, $month, $monthISO, + $year, $week, $weekISO, + $wday, $wdayISO, $yday, + $isdst, $iswe, $isHolidayYesterday, + $isHolidayToday, $isHolidayTomorrow + ) = GetCalendarInfo(@_); + return $isHolidayTomorrow; +} + +sub IsHolidayYesterday (;$) { + my ( + $sec, $min, $hour, + $mday, $month, $monthISO, + $year, $week, $weekISO, + $wday, $wdayISO, $yday, + $isdst, $iswe, $isHolidayYesterday, + $isHolidayToday, $isHolidayTomorrow + ) = GetCalendarInfo(@_); + return $isHolidayYesterday; +} + +sub GetCalendarInfo(;$$) { + my ( $time, $holidayDev ) = @_; + + my @t; + @t = localtime($time) if ($time); + @t = localtime() unless ($time); + my ( $sec, $min, $hour, $mday, $month, $year, $wday, $yday, $isdst ) = @t; + my $monthISO = $month + 1; + $year += 1900; + + # ISO 8601 weekday as number with Monday as 1 (1-7) + my $wdayISO = strftime( '%u', @t ); + + # Week number with the first Sunday as the first day of week one (00-53) + my $week = strftime( '%U', @t ); + + # ISO 8601 week number (00-53) + my $weekISO = strftime( '%V', @t ); + + my $iswe = ( $wday == 0 || $wday == 6 ) ? 1 : 0; + my $isHolidayYesterday; + my $isHolidayToday; + my $isHolidayTomorrow; + + $holidayDev = undef unless ( main::IsDevice( $holidayDev, "holiday" ) ); + $holidayDev = $main::attr{global}{holiday2we} + if ( !$holidayDev + && main::IsDevice( $main::attr{global}{holiday2we}, "holiday" ) ); + + if ($holidayDev) { + if ( main::ReadingsVal( $holidayDev, "state", "none" ) ne "none" ) { + $iswe = 1; + $isHolidayToday = 1; + } + $isHolidayYesterday = 1 + if ( + main::ReadingsVal( $holidayDev, "yesterday", "none" ) ne "none" ); + $isHolidayTomorrow = 1 + if ( main::ReadingsVal( $holidayDev, "tomorrow", "none" ) ne "none" ); + } + + return ( + $sec, $min, $hour, + $mday, $month, $monthISO, + $year, $week, $weekISO, + $wday, $wdayISO, $yday, + $isdst, $iswe, $isHolidayYesterday, + $isHolidayToday, $isHolidayTomorrow + ); +} + +# Get current stage of the daytime based on temporal hours +# https://de.wikipedia.org/wiki/Temporale_Stunden +sub GetDaytimeStage(@) { + my ( $date, $totalTemporalHours, @srParams ) = @_; + $date = time unless ($date); + $totalTemporalHours = 12 unless ($totalTemporalHours); + + # today + my ( + $sec, $min, $hour, + $mday, $month, $monthISO, + $year, $week, $weekISO, + $wday, $wdayISO, $yday, + $isdst, $iswe, $isHolidayYesterday, + $isHolidayToday, $isHolidayTomorrow + ) = GetCalendarInfo($date); + + # tomorrow + my ( + $tsec, $tmin, $thour, + $tmday, $tmonth, $tmonthISO, + $tyear, $tweek, $tweekISO, + $twday, $twdayISO, $tyday, + $tisdst, $tiswe, $tisHolidayYesterday, + $tisHolidayToday, $tisHolidayTomorrow + ) = GetCalendarInfo( $date + 24 * 60 * 60 ); + + my $secSr = hms2s( main::sunrise_abs_dat( $date, @srParams ) ); + my $secSs = hms2s( main::sunset_abs_dat( $date, @srParams ) ); + my $secNow = hms2s("$hour:$min:$sec") - $secSr; + my $dlength = $secSs - $secSr; + my $slength = $dlength / $totalTemporalHours; + my $currStage = int( $secNow / $slength ); + + my $dateSr = main::time_str2num("$year-$monthISO-$mday 00:00:00") + $secSr; + my %events; + my $i = 0; + until ( $i == $totalTemporalHours ) { + $events{$i}{timestamp} = int( $dateSr + 0.5 ); + $i++; + $dateSr += $slength; + } + $events{$totalTemporalHours}{timestamp} = int( + main::time_str2num("$year-$monthISO-$mday 00:00:00") + $secSs + 0.5 ); + + my $dateSrTomorrow = + main::time_str2num("$tyear-$tmonthISO-$tmday 00:00:00") + + hms2s( main::sunrise_abs_dat( $date + 86400, @srParams ) ); + my %eventsTomorrow; + $i = 0; + until ( $i == $totalTemporalHours ) { + $eventsTomorrow{$i}{timestamp} = int( $dateSrTomorrow + 0.5 ); + $i++; + $dateSrTomorrow += $slength; + } + + # early day after midnight + if ( $currStage < 0 ) { + return ( $totalTemporalHours, $slength, \%events ) + if (wantarray); + return $totalTemporalHours; + } + + # late day before midnight + elsif ( $currStage > $totalTemporalHours ) { + return ( $totalTemporalHours, $slength, \%eventsTomorrow ) + if (wantarray); + return $totalTemporalHours; + } + + # daytime + return ( $currStage, $slength, \%events ) if (wantarray); + return $currStage; } #################### diff --git a/fhem/FHEM/Unit.pm b/fhem/FHEM/Unit.pm index 1391d812d..4f10d4fc6 100644 --- a/fhem/FHEM/Unit.pm +++ b/fhem/FHEM/Unit.pm @@ -802,32 +802,32 @@ my $rtypes = { weekday => { ref_base => 900, symbol => { - de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' ], - en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], + de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], + en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt => { - de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' ], - en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], + de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], + en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt_long => { de => [ 'Sonntag', 'Montag', 'Dienstag', 'Mittwoch', - 'Donnerstag', 'Freitag', 'Samstag' + 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' ], en => [ - 'Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday' + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday', 'Sunday' ], }, scope => { de => [ - '^(So|Son|Sonntag|0)$', '^(Mo|Mon|Montag|1)$', + '^(So|Son|Sonntag|0|7)$', '^(Mo|Mon|Montag|1)$', '^(Di|Die|Dienstag|2)$', '^(Mi|Mit|Mittwoch|3)$', '^(Do|Don|Donnerstag|4)$', '^(Fr|Fre|Freitag|5)$', - '^(Sa|Sam|Samstag|6)$' + '^(Sa|Sam|Samstag|6)$', ], en => [ - '^(Sun|Su|Sunday|0)$', '^(Mon|Mo|Monday|1)$', + '^(Sun|Su|Sunday|0|7)$', '^(Mon|Mo|Monday|1)$', '^(Tue|Tu|Tuesday|2)$', '^(Wed|We|Wednesday|3)$', '^(Thu|Th|Thursday|4)$', '^(Fri|Fr|Friday|5)$', '^(Sat|Sa|Saturday|6)$' @@ -846,35 +846,35 @@ my $rtypes = { weekday_iso => { ref_base => 900, symbol => { - de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' ], - en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], + de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], + en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt => { - de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' ], - en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], + de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], + en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt_long => { de => [ 'Sonntag', 'Montag', 'Dienstag', 'Mittwoch', - 'Donnerstag', 'Freitag', 'Samstag' + 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' ], en => [ - 'Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday' + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday', 'Sunday' ], }, scope => { de => [ - '^(So|Son|Sonntag|6)$', '^(Mo|Mon|Montag|0)$', - '^(Di|Die|Dienstag|1)$', '^(Mi|Mit|Mittwoch|2)$', - '^(Do|Don|Donnerstag|3)$', '^(Fr|Fre|Freitag|4)$', - '^(Sa|Sam|Samstag|5)$' + '^(So|Son|Sonntag|7)$', '^(Mo|Mon|Montag|1)$', + '^(Di|Die|Dienstag|2)$', '^(Mi|Mit|Mittwoch|3)$', + '^(Do|Don|Donnerstag|4)$', '^(Fr|Fre|Freitag|5)$', + '^(Sa|Sam|Samstag|6)$' ], en => [ - '^(Sun|Su|Sunday|6)$', '^(Mon|Mo|Monday|0)$', - '^(Tue|Tu|Tuesday|1)$', '^(Wed|We|Wednesday|2)$', - '^(Thu|Th|Thursday|3)$', '^(Fri|Fr|Friday|4)$', - '^(Sat|Sa|Saturday|5)$' + '^(Sun|Su|Sunday|7)$', '^(Mon|Mo|Monday|1)$', + '^(Tue|Tu|Tuesday|2)$', '^(Wed|We|Wednesday|3)$', + '^(Thu|Th|Thursday|4)$', '^(Fri|Fr|Friday|5)$', + '^(Sat|Sa|Saturday|6)$' ], }, tmpl => '%txt%', @@ -1038,6 +1038,18 @@ my $rtypes = { 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], + nl => [ + 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', + 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' + ], + fr => [ + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' + ], + pl => [ + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' + ], }, txt_long => { de => [ @@ -1056,6 +1068,18 @@ my $rtypes = { 'West', 'West-Northwest', 'Northwest', 'North-Northwest' ], + nl => [ + 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', + 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' + ], + fr => [ + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' + ], + pl => [ + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' + ], }, scope => {