# $Id$ ############################################################################## # # 59_Twilight.pm # Copyright by Sebastian Stuecker # erweitert von Dietmar Ortmann # Maintained by igami since 02-2018 # # used algorithm see: http://lexikon.astronomie.info/zeitgleichung/ # # Sun position computing # Copyright (C) 2013 Julian Pawlowski, julian.pawlowski AT gmail DOT com # based on Twilight.tcl http://www.homematic-wiki.info/mw/index.php/TCLScript:twilight # With contribution from http://www.ip-symcon.de/forum/threads/14925-Sonnenstand-berechnen-(Azimut-amp-Elevation) # # e-mail: omega at online dot de # # This file is part of fhem. # # Fhem 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. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; use POSIX; use HttpUtils; use Math::Trig; use Time::Local 'timelocal_nocheck'; sub Twilight_calc($$); sub Twilight_my_gmt_offset(); sub Twilight_midnight_seconds($); ################################################################################ sub Twilight_Initialize($) { my ($hash) = @_; # Consumer $hash->{DefFn} = "Twilight_Define"; $hash->{UndefFn} = "Twilight_Undef"; $hash->{GetFn} = "Twilight_Get"; $hash->{AttrList}= "$readingFnAttributes " ."useExtWeather"; return undef; } ################################################################################ sub Twilight_Get($@) { my ($hash, @a) = @_; return "argument is missing" if(int(@a) != 2); my $reading= $a[1]; my $value; if(defined($hash->{READINGS}{$reading})) { $value= $hash->{READINGS}{$reading}{VAL}; } else { return "no such reading: $reading"; } return "$a[0] $reading => $value"; } ################################################################################ sub Twilight_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "syntax: define Twilight [indoor_horizon [Weather]]" if(int(@a) < 4 && int(@a) > 6); $hash->{STATE} = "0"; my $latitude; my $longitude; my $name = $a[0]; if ($a[2] =~ /^[\+-]*[0-9]*\.*[0-9]*$/ && $a[2] !~ /^[\. ]*$/ ) { $latitude = $a[2]; if($latitude > 90){$latitude = 90;} if($latitude < -90){$latitude = -90;} }else{ return "Argument Latitude is not a valid number"; } if ($a[3] =~ /^[\+-]*[0-9]*\.*[0-9]*$/ && $a[3] !~ /^[\. ]*$/ ) { $longitude = $a[3]; if($longitude > 180){$longitude = 180;} if($longitude < -180){$longitude = -180;} }else{ return "Argument Longitude is not a valid number"; } my $weather = 0; my $indoor_horizon = 0; if(int(@a)>5) { $weather=$a[5] } if(int(@a)>4) { if ($a[4] =~ /^[\+-]*[0-9]*\.*[0-9]*$/ && $a[4] !~ /^[\. ]*$/ ) { $indoor_horizon = $a[4]; if($indoor_horizon > 20) { $indoor_horizon=20;} # minimal indoor_horizon makes values like civil_sunset and civil_sunrise if($indoor_horizon < -6) { $indoor_horizon= -6;} }else{ return "Argument Indoor_Horizon is not a valid number";} } $hash->{WEATHER_HORIZON} = 0; $hash->{INDOOR_HORIZON} = $indoor_horizon; $hash->{LATITUDE} = $latitude; $hash->{LONGITUDE} = $longitude; $hash->{WEATHER} = $weather; $hash->{VERSUCHE} = 0; $hash->{DEFINE} = 1; $hash->{CONDITION} = 50; $hash->{SUNPOS_OFFSET} = 5*60; $attr{$name}{verbose} = 4 if ($name =~ /^tst.*$/ ); my $mHash = { HASH=>$hash }; Twilight_sunpos($mHash); Twilight_Midnight($mHash); delete $hash->{DEFINE}; return undef; } ################################################################################ sub Twilight_Undef($$) { my ($hash, $arg) = @_; foreach my $key (keys %{$hash->{TW}}) { myRemoveInternalTimer($key, $hash); } myRemoveInternalTimer ("Midnight", $hash); myRemoveInternalTimer ("weather", $hash); myRemoveInternalTimer ("sunpos", $hash); return undef; } ################################################################################ sub myInternalTimer($$$$$) { my ($modifier, $tim, $callback, $hash, $waitIfInitNotDone) = @_; my $timerName = "$hash->{NAME}_$modifier"; my $mHash = { HASH=>$hash, NAME=>"$hash->{NAME}_$modifier", MODIFIER=>$modifier}; if (defined($hash->{TIMER}{$timerName})) { Log3 $hash, 1, "[$hash->{NAME}] possible overwriting of timer $timerName - please delete first"; stacktrace(); } else { $hash->{TIMER}{$timerName} = $mHash; } Log3 $hash, 5, "[$hash->{NAME}] setting Timer: $timerName " . FmtDateTime($tim); InternalTimer($tim, $callback, $mHash, $waitIfInitNotDone); return $mHash; } ################################################################################ sub myRemoveInternalTimer($$) { my ($modifier, $hash) = @_; my $timerName = "$hash->{NAME}_$modifier"; my $myHash = $hash->{TIMER}{$timerName}; if (defined($myHash)) { delete $hash->{TIMER}{$timerName}; Log3 $hash, 5, "[$hash->{NAME}] removing Timer: $timerName"; RemoveInternalTimer($myHash); } } ################################################################################ #sub myRemoveInternalTimerByName($){ # my ($name) = @_; # foreach my $a (keys %intAt) { # my $nam = ""; # my $arg = $intAt{$a}{ARG}; # if (ref($arg) eq "HASH" && defined($arg->{NAME}) ) { # $nam = $arg->{NAME} if (ref($arg) eq "HASH" && defined($arg->{NAME}) ); # } # delete($intAt{$a}) if($nam =~ m/^$name/g); # } #} ################################################################################ sub myGetHashIndirekt ($$) { my ($myHash, $function) = @_; if (!defined($myHash->{HASH})) { Log 3, "[$function] myHash not valid"; return undef; }; return $myHash->{HASH}; } ################################################################################ sub Twilight_midnight_seconds($) { my ($now) = @_; my @time = localtime($now); my $secs = ($time[2] * 3600) + ($time[1] * 60) + $time[0]; return $secs; } ################################################################################ #sub Twilight_ssTimeAsEpoch($) { # my ($zeit) = @_; # my ($hour, $min, $sec) = split(":",$zeit); # # my $days=0; # if ($hour>=24) {$days = 1; $hour -=24}; # # my @jetzt_arr = localtime(time()); # #Stunden Minuten Sekunden # $jetzt_arr[2] = $hour; $jetzt_arr[1] = $min; $jetzt_arr[0] = $sec; # $jetzt_arr[3] += $days; # my $next = timelocal_nocheck(@jetzt_arr); # # return $next; #} ################################################################################ sub Twilight_calc($$) { my ($deg, $idx) = @_; my $midnight = time() - Twilight_midnight_seconds(time()); my $sr = sunrise_abs("Horizon=$deg"); my $ss = sunset_abs ("Horizon=$deg"); my ($srhour, $srmin, $srsec) = split(":",$sr); $srhour -= 24 if($srhour>=24); my ($sshour, $ssmin, $sssec) = split(":",$ss); $sshour -= 24 if($sshour>=24); my $sr1 = $midnight + 3600*$srhour+60*$srmin+$srsec; my $ss1 = $midnight + 3600*$sshour+60*$ssmin+$sssec; return (0,0) if (abs ($sr1 - $ss1) < 30); #return Twilight_ssTimeAsEpoch($sr) + 0.01*$idx, # Twilight_ssTimeAsEpoch($ss) - 0.01*$idx; return ($sr1 + 0.01*$idx), ($ss1 - 0.01*$idx); } ################################################################################ sub Twilight_TwilightTimes(@) { my ($hash, $whitchTimes, $xml) = @_; my $Name = $hash->{NAME}; my $horizon = $hash->{HORIZON}; my $swip = $hash->{SWIP} ; my $lat = $attr{global}{latitude}; $attr{global}{latitude} = $hash->{LATITUDE}; my $long = $attr{global}{longitude}; $attr{global}{longitude} = $hash->{LONGITUDE}; # ------------------------------------------------------------------------------ my $idx = -1; my @horizons = ("_astro:-18", "_naut:-12", "_civil:-6",":0", "_indoor:$hash->{INDOOR_HORIZON}", "_weather:$hash->{WEATHER_HORIZON}"); foreach my $horizon (@horizons) { $idx++; next if ($whitchTimes eq "weather" && !($horizon =~ m/weather/) ); my ($name, $deg) = split(":", $horizon); my $sr = "sr$name"; my $ss = "ss$name"; $hash->{TW}{$sr}{NAME} = $sr; $hash->{TW}{$ss}{NAME} = $ss; $hash->{TW}{$sr}{DEG} = $deg; $hash->{TW}{$ss}{DEG} = $deg; $hash->{TW}{$sr}{LIGHT} = $idx+1;$hash->{TW}{$ss}{LIGHT} = $idx; $hash->{TW}{$sr}{STATE} = $idx+1;$hash->{TW}{$ss}{STATE} = 12 - $idx; $hash->{TW}{$sr}{SWIP} = $swip; $hash->{TW}{$ss}{SWIP} = $swip; ($hash->{TW}{$sr}{TIME}, $hash->{TW}{$ss}{TIME}) = Twilight_calc ($deg, $idx); if ($hash->{TW}{$sr}{TIME} == 0) { Log3 $hash, 4, "[$Name] hint: $hash->{TW}{$sr}{NAME}, $hash->{TW}{$ss}{NAME} are not defined(HORIZON=$deg)"; } } $attr{global}{latitude} = $lat; $attr{global}{longitude} = $long; # ------------------------------------------------------------------------------ readingsBeginUpdate ($hash); foreach my $ereignis (keys %{$hash->{TW}}) { next if ($whitchTimes eq "weather" && !($ereignis =~ m/weather/) ); readingsBulkUpdate($hash, $ereignis, $hash->{TW}{$ereignis}{TIME} == 0 ? "undefined" : FmtTime($hash->{TW}{$ereignis}{TIME})); } if ($hash->{CONDITION} != 50 ) { readingsBulkUpdate ($hash,"condition", $hash->{CONDITION}); readingsBulkUpdate ($hash,"condition_txt",$hash->{CONDITION_TXT}); } readingsEndUpdate ($hash, defined($hash->{LOCAL} ? 0 : 1)); # ------------------------------------------------------------------------------ my @horizonsOhneDeg = map {my($e, $deg)=split(":",$_); "$e"} @horizons; my @ereignisse = ((map {"sr$_"}@horizonsOhneDeg),(map {"ss$_"} reverse @horizonsOhneDeg),"sr$horizonsOhneDeg[0]"); map { $hash->{TW}{$ereignisse[$_]}{NAMENEXT} = $ereignisse[$_+1] } 0..$#ereignisse-1; # ------------------------------------------------------------------------------ my $myHash; my $now = time(); my $secSinceMidnight = Twilight_midnight_seconds($now); my $lastMitternacht = $now-$secSinceMidnight; my $nextMitternacht = ($secSinceMidnight > 12*3600) ? $lastMitternacht+24*3600 : $lastMitternacht; my $jetztIstMitternacht = abs($now+5-$nextMitternacht)<=10; my @keyListe = qw "DEG LIGHT STATE SWIP TIME NAMENEXT"; foreach my $ereignis (sort keys %{$hash->{TW}}) { next if ($whitchTimes eq "weather" && !($ereignis =~ m/weather/) ); myRemoveInternalTimer($ereignis, $hash); # if(!$jetztIstMitternacht); if($hash->{TW}{$ereignis}{TIME} > 0) { $myHash = myInternalTimer($ereignis, $hash->{TW}{$ereignis}{TIME}, "Twilight_fireEvent", $hash, 0); map {$myHash->{$_} = $hash->{TW}{$ereignis}{$_} } @keyListe; } } # ------------------------------------------------------------------------------ return 1; } ################################################################################ sub Twilight_fireEvent($) { my ($myHash) = @_; my $hash = myGetHashIndirekt($myHash, (caller(0))[3]); return if (!defined($hash)); my $name = $hash->{NAME}; my $event = $myHash->{MODIFIER}; my $deg = $myHash->{DEG}; my $light = $myHash->{LIGHT}; my $state = $myHash->{STATE}; my $swip = $myHash->{SWIP}; my $eventTime = $myHash->{TIME}; my $nextEvent = $myHash->{NAMENEXT}; my $delta = int($eventTime - time()); my $oldState = ReadingsVal($name,"state","0"); my $nextEventTime = ($hash->{TW}{$nextEvent}{TIME} > 0) ? FmtTime($hash->{TW}{$nextEvent}{TIME}) : "undefined"; my $doTrigger = !(defined($hash->{LOCAL})) && ( abs($delta)<6 || $swip && $state gt $oldState); #Log3 $hash, 3, "[$hash->{NAME}] swip-delta-oldState-doTrigger===>$swip/$delta/$oldState/$doTrigger"; Log3 $hash, 4, sprintf ("[$hash->{NAME}] %-10s %-19s ", $event, FmtDateTime($eventTime)). sprintf ("(%2d/$light/%+5.1f°/$doTrigger) ", $state, $deg). sprintf ("===> %-10s %-19s ", $nextEvent, $nextEventTime); readingsBeginUpdate($hash); readingsBulkUpdate ($hash, "state", $state); readingsBulkUpdate ($hash, "light", $light); readingsBulkUpdate ($hash, "horizon", $deg); readingsBulkUpdate ($hash, "aktEvent", $event); readingsBulkUpdate ($hash, "nextEvent", $nextEvent); readingsBulkUpdate ($hash, "nextEventTime", $nextEventTime); readingsEndUpdate ($hash, $doTrigger); } ################################################################################ sub Twilight_Midnight($) { my ($myHash) = @_; my $hash = myGetHashIndirekt($myHash, (caller(0))[3]); return if (!defined($hash)); $hash->{SWIP} = 0; my $param = Twilight_CreateHttpParameterAndGetData($myHash, "Mid"); } ################################################################################ # {Twilight_WeatherTimerUpdate( {HASH=$defs{"Twilight"}} ) } sub Twilight_WeatherTimerUpdate($) { my ($myHash) = @_; my $hash = myGetHashIndirekt($myHash, (caller(0))[3]); return if (!defined($hash)); $hash->{SWIP} = 1; my $param = Twilight_CreateHttpParameterAndGetData($myHash, "weather"); } ################################################################################ sub Twilight_CreateHttpParameterAndGetData($$) { my ($myHash, $mode) = @_; my $hash = myGetHashIndirekt($myHash, (caller(0))[3]); return if (!defined($hash)); my $location = $hash->{WEATHER}; my $verbose = AttrVal($hash->{NAME}, "verbose", 3 ); my $URL = "http://query.yahooapis.com/v1/public/yql?q=select%%20*%%20from%%20weather.forecast%%20where%%20woeid=%s%%20and%%20u=%%27c%%27&format=%s&env=store%%3A%%2F%%2Fdatatables.org%%2Falltableswithkeys"; my $url = sprintf($URL, $location, "json"); Log3 $hash, 4, "[$hash->{NAME}] url=$url"; my $param = { url => $url, timeout => defined($hash->{DEFINE}) ? 10 :10, hash => $hash, method => "GET", loglevel => 4-($verbose-3), header => "User-Agent: Mozilla/5.0\r\nAccept: application/xml", callback => \&Twilight_WeatherCallback, mode => $mode }; if (defined($hash->{DEFINE})) { delete $param->{callback}; my ($err, $result) = HttpUtils_BlockingGet($param); Twilight_WeatherCallback($param, $err, $result); } else { HttpUtils_NonblockingGet($param); } } ################################################################################ sub Twilight_WeatherCallback(@) { my ($param, $err, $result) = @_; my $hash = $param->{hash}; return if (!defined($hash)); if ($err) { Log3 $hash, 3, "[$hash->{NAME}] got no weather info from yahoo. Error code: $err"; $result = undef; } else { Log3 $hash, 4, "[$hash->{NAME}] got weather info from yahoo for $hash->{WEATHER}"; Log3 $hash, 5, "[$hash->{NAME}] answer=$result" if defined $result; } Twilight_getWeatherHorizon($hash, $result); #$hash->{CONDITION} = 50; if ($hash->{CONDITION} == 50 && $hash->{VERSUCHE} <= 10) { $hash->{VERSUCHE} += 1; Twilight_RepeatTimerSet($hash, $param->{mode}); return; } Twilight_TwilightTimes ($hash, $param->{mode}, $result); Log3 $hash, 3, "[$hash->{NAME}] " . ($hash->{VERSUCHE}+1) . " attempt(s) needed to get valid weather data from yahoo" if ($hash->{CONDITION} != 50 && $hash->{VERSUCHE} > 0); Log3 $hash, 3, "[$hash->{NAME}] " . ($hash->{VERSUCHE}+1) . " attempt(s) needed got NO valid weather data from yahoo" if ($hash->{CONDITION} == 50 && $hash->{VERSUCHE} > 0); $hash->{VERSUCHE} = 0; Twilight_StandardTimerSet ($hash); } ################################################################################ sub Twilight_RepeatTimerSet($$) { my ($hash, $mode) = @_; my $midnight = time() + 60; myRemoveInternalTimer("Midnight", $hash); if ($mode eq "Mid") { myInternalTimer ("Midnight", $midnight, "Twilight_Midnight", $hash, 0); } else { myInternalTimer ("Midnight", $midnight, "Twilight_WeatherTimerUpdate", $hash, 0); } } ################################################################################ sub Twilight_StandardTimerSet($) { my ($hash) = @_; my $midnight = time() - Twilight_midnight_seconds(time()) + 24*3600 + 1; myRemoveInternalTimer ("Midnight", $hash); myInternalTimer ("Midnight", $midnight, "Twilight_Midnight", $hash, 0); Twilight_WeatherTimerSet ($hash); } ################################################################################ sub Twilight_WeatherTimerSet($) { my ($hash) = @_; my $now = time(); myRemoveInternalTimer ("weather", $hash); foreach my $key ("sr_weather", "ss_weather") { my $tim = $hash->{TW}{$key}{TIME}; if ($tim-60*60>$now+60) { myInternalTimer ("weather", $tim-60*60, "Twilight_WeatherTimerUpdate", $hash, 0); last; } } } ################################################################################ sub Twilight_sunposTimerSet($) { my ($hash) = @_; myRemoveInternalTimer ("sunpos", $hash); myInternalTimer ("sunpos", time()+$hash->{SUNPOS_OFFSET}, "Twilight_sunpos", $hash, 0); } ################################################################################ sub Twilight_getWeatherHorizon(@) { my ($hash, $result) = @_; my $location=$hash->{WEATHER}; if ($location == 0) { $hash->{WEATHER_HORIZON}="0"; $hash->{CONDITION}="0"; return 1; } my $mod = "[".$hash->{NAME} ."] "; my @faktor_cond_code = (10,10,10,10, 9, 7, 7, 7, 7, 7, 7, 5, 5, 5, 5, 7, 7, 5, 5, 5, 7, 5, 5, 5, 5, 5, 2, 4, 4, 2, 2, 0, 0, 0, 0, 5, 0, 8, 8, 8, 6, 8, 6, 5, 2, 5, 6, 6, 0, 0, 0); # condition codes are described in FHEM wiki and in the documentation of the yahoo weather API my ($cond_code, $cond_txt, $temperatur, $aktTemp); if (defined($result)) { # ersetze in result(json) ": durch "=> # dadurch entsteht ein Perlausdruck, der direkt geparst werden kann my $perlAusdruck = $result; #$perlAusdruck = "

could"; $perlAusdruck =~ s/("[\w ]+")(\s*)(:)/$1=>/g; $perlAusdruck =~ s/null/undef/g; $perlAusdruck =~ s/true/1/g; $perlAusdruck =~ s/false/0/g; $perlAusdruck = 'return ' .$perlAusdruck; my $anonymSub = eval "sub {$perlAusdruck}"; Log3 $hash, 3, "[$hash->{NAME}] error $@ parsing $result" if($@); if (!$@) { my $resHash = $anonymSub->() if ($anonymSub gt ""); Log3 $hash, 3, "[$hash->{NAME}] error $@ parsing $result" if($@); #Log3 $hash, 3, "jsonAsPerl". Dumper $resHash->{query}{results}{channel}{item}{condition}; if (!$@) { $cond_code = $resHash->{query}{results}{channel}{item}{condition}{code}; $cond_txt = $resHash->{query}{results}{channel}{item}{condition}{text}; $temperatur = $resHash->{query}{results}{channel}{item}{condition}{temp}; } } } # wenn kein Code ermittelt werden kann, wird ein Pseudocode gesetzt if (!defined($cond_code) ) { $cond_code = "50"; # eigener neutraler Code $cond_txt = "undefined"; $temperatur = "undefined"; } else { $hash->{WEATHER_CORRECTION} = $faktor_cond_code[$cond_code] / 25 * 20; $hash->{WEATHER_HORIZON} = $hash->{WEATHER_CORRECTION} + $hash->{INDOOR_HORIZON}; $hash->{CONDITION} = $cond_code; $hash->{CONDITION_TXT} = $cond_txt; $hash->{TEMPERATUR} = $temperatur; Log3 $hash, 4, "[$hash->{NAME}] $cond_code=$cond_txt $temperatur, correction: $hash->{WEATHER_CORRECTION}°"; } my $doy = strftime("%j",localtime); my $declination = 0.4095*sin(0.016906*($doy-80.086)); if($hash->{WEATHER_HORIZON} > (89-$hash->{LATITUDE}+$declination) ){ $hash->{WEATHER_HORIZON} = 89-$hash->{LATITUDE}+$declination; } return 1; } ################################################################################ sub Twilight_sunpos($) { my ($myHash) = @_; my $hash = myGetHashIndirekt($myHash, (caller(0))[3]); return if (!defined($hash)); my $hashName = $hash->{NAME}; return "" if(AttrVal($hashName, "disable", undef)); my $tn = TimeNow(); my ($dSeconds,$dMinutes,$dHours,$iDay,$iMonth,$iYear,$wday,$yday,$isdst) = gmtime(time); $iMonth++; $iYear += 100; my $dLongitude = $hash->{LONGITUDE}; my $dLatitude = $hash->{LATITUDE}; Log3 $hash, 5, "Compute sunpos for latitude $dLatitude , longitude $dLongitude" if($dHours == 0 && $dMinutes <= 6 ); my $pi=3.14159265358979323846; my $twopi=(2*$pi); my $rad=($pi/180); my $dEarthMeanRadius=6371.01; # In km my $dAstronomicalUnit=149597890; # In km # Calculate difference in days between the current Julian Day # and JD 2451545.0, which is noon 1 January 2000 Universal Time # Calculate time of the day in UT decimal hours my $dDecimalHours=$dHours + $dMinutes/60.0 + $dSeconds/3600.0; # Calculate current Julian Day my $iYfrom2000=$iYear;#expects now as YY ; my $iA=(14 - ($iMonth)) / 12; my $iM=($iMonth) + 12 * $iA -3; my $liAux3=(153 * $iM + 2)/5; my $liAux4=365 * ($iYfrom2000 - $iA); my $liAux5=( $iYfrom2000 - $iA)/4; my $dElapsedJulianDays=($iDay + $liAux3 + $liAux4 + $liAux5 + 59)+ -0.5 + $dDecimalHours/24.0; # Calculate ecliptic coordinates (ecliptic longitude and obliquity of the # ecliptic in radians but without limiting the angle to be less than 2*Pi # (i.e., the result may be greater than 2*Pi) my $dOmega = 2.1429 - 0.0010394594 * $dElapsedJulianDays; my $dMeanLongitude = 4.8950630 + 0.017202791698 * $dElapsedJulianDays; # Radians my $dMeanAnomaly = 6.2400600 + 0.0172019699 * $dElapsedJulianDays; my $dEclipticLongitude = $dMeanLongitude + 0.03341607 * sin( $dMeanAnomaly ) + 0.00034894 * sin( 2 * $dMeanAnomaly ) -0.0001134 -0.0000203 * sin($dOmega); my $dEclipticObliquity = 0.4090928 - 6.2140e-9 * $dElapsedJulianDays +0.0000396 * cos($dOmega); # Calculate celestial coordinates ( right ascension and declination ) in radians # but without limiting the angle to be less than 2*Pi (i.e., the result may be # greater than 2*Pi) my $dSin_EclipticLongitude=sin( $dEclipticLongitude ); my $dY1=cos( $dEclipticObliquity ) * $dSin_EclipticLongitude; my $dX1=cos( $dEclipticLongitude ); my $dRightAscension=atan2( $dY1,$dX1 ); if ( $dRightAscension < 0.0 ) { $dRightAscension=$dRightAscension + $twopi }; my $dDeclination=asin( sin( $dEclipticObliquity )* $dSin_EclipticLongitude ); # Calculate local coordinates ( azimuth and zenith angle ) in degrees my $dGreenwichMeanSiderealTime=6.6974243242 + 0.0657098283 * $dElapsedJulianDays + $dDecimalHours; my $dLocalMeanSiderealTime=($dGreenwichMeanSiderealTime*15 + $dLongitude)* $rad; my $dHourAngle=$dLocalMeanSiderealTime - $dRightAscension; my $dLatitudeInRadians=$dLatitude * $rad; my $dCos_Latitude=cos( $dLatitudeInRadians ); my $dSin_Latitude=sin( $dLatitudeInRadians ); my $dCos_HourAngle=cos( $dHourAngle ); my $dZenithAngle=(acos( $dCos_Latitude * $dCos_HourAngle * cos($dDeclination) + sin( $dDeclination )* $dSin_Latitude)); my $dY=-sin( $dHourAngle ); my $dX=tan( $dDeclination )* $dCos_Latitude - $dSin_Latitude * $dCos_HourAngle; my $dAzimuth=atan2( $dY, $dX ); if ( $dAzimuth < 0.0 ) {$dAzimuth=$dAzimuth + $twopi}; $dAzimuth=$dAzimuth / $rad; # Parallax Correction my $dParallax=($dEarthMeanRadius / $dAstronomicalUnit) * sin( $dZenithAngle); $dZenithAngle=($dZenithAngle + $dParallax) / $rad; my $dElevation=90 - $dZenithAngle; my $twilight = int(($dElevation+12.0)/18.0 * 1000)/10; $twilight = 100 if ($twilight>100); $twilight = 0 if ($twilight< 0); my $twilight_weather ; if( (my $ExtWeather = AttrVal($hashName, "useExtWeather", "")) eq "") { $twilight_weather = int(($dElevation-$hash->{WEATHER_HORIZON}+12.0)/18.0 * 1000)/10; Log3 $hash, 5, "[$hash->{NAME}] " . "Original weather readings"; } else { my($extDev,$extReading) = split(":",$ExtWeather); my $extWeatherHorizont = ReadingsVal($extDev,$extReading,-1); if ($extWeatherHorizont >= 0){ $extWeatherHorizont = 100 if ($extWeatherHorizont > 100); Log3 $hash, 5, "[$hash->{NAME}] " . "New weather readings from: ".$extDev.":".$extReading.":".$extWeatherHorizont; $twilight_weather = $twilight - int(0.007 * ($extWeatherHorizont ** 2)); ## SCM: 100% clouds => 30% light (rough estimation) } else { $twilight_weather = int(($dElevation-$hash->{WEATHER_HORIZON}+12.0)/18.0 * 1000)/10; Log3 $hash, 3, "[$hash->{NAME}] " . "Error with external readings from: ".$extDev.":".$extReading." , taking original weather readings"; } } $twilight_weather = 100 if ($twilight_weather>100); $twilight_weather = 0 if ($twilight_weather< 0); # set readings $dAzimuth = int(100*$dAzimuth )/100; $dElevation = int(100*$dElevation)/100; my $compassPoint = Twilight_CompassPoint($dAzimuth); readingsBeginUpdate($hash); readingsBulkUpdate ($hash, "azimuth", $dAzimuth ); readingsBulkUpdate ($hash, "elevation", $dElevation ); readingsBulkUpdate ($hash, "twilight", $twilight ); readingsBulkUpdate ($hash, "twilight_weather", $twilight_weather ); readingsBulkUpdate ($hash, "compasspoint", $compassPoint); readingsEndUpdate ($hash, defined($hash->{LOCAL} ? 0 : 1)); Twilight_sunposTimerSet($hash); return undef; } ################################################################################ sub Twilight_CompassPoint($) { my ($azimuth) = @_; my $compassPoint = "unknown"; if ($azimuth < 22.5) { $compassPoint = "north"; } elsif ($azimuth < 45) { $compassPoint = "north-northeast"; } elsif ($azimuth < 67.5) { $compassPoint = "northeast"; } elsif ($azimuth < 90) { $compassPoint = "east-northeast"; } elsif ($azimuth < 112.5){ $compassPoint = "east"; } elsif ($azimuth < 135) { $compassPoint = "east-southeast"; } elsif ($azimuth < 157.5){ $compassPoint = "southeast"; } elsif ($azimuth < 180) { $compassPoint = "south-southeast"; } elsif ($azimuth < 202.5){ $compassPoint = "south"; } elsif ($azimuth < 225) { $compassPoint = "south-southwest"; } elsif ($azimuth < 247.5){ $compassPoint = "southwest"; } elsif ($azimuth < 270) { $compassPoint = "west-southwest"; } elsif ($azimuth < 292.5){ $compassPoint = "west"; } elsif ($azimuth < 315) { $compassPoint = "west-northwest"; } elsif ($azimuth < 337.5){ $compassPoint = "northwest"; } elsif ($azimuth <= 361) { $compassPoint = "north-northwest"; } return $compassPoint; } sub twilight($$$$) { my ($twilight, $reading, $min, $max) = @_; my $t = hms2h(ReadingsVal($twilight,$reading,0)); $t = hms2h($min) if(defined($min) && (hms2h($min) > $t)); $t = hms2h($max) if(defined($max) && (hms2h($max) < $t)); return h2hms_fmt($t); } 1; =pod =item device =item summary delivers twilight and other sun related events for use in notify =item summary_DE liefert Dämmerungs Sonnen basierte Ereignisse, für notify =begin html

Twilight


    Define
      define <name> Twilight <latitude> <longitude> [<indoor_horizon> [<Weather_Position>]]

      Defines a virtual device for Twilight calculations

      latitude, longitude
      The parameters latitude and longitude are decimal numbers which give the position on earth for which the twilight states shall be calculated.

      indoor_horizon
      The parameter indoor_horizon gives a virtual horizon, that shall be used for calculation of indoor twilight. Minimal value -6 means indoor values are the same like civil values. indoor_horizon 0 means indoor values are the same as real values. indoor_horizon > 0 means earlier indoor sunset resp. later indoor sunrise.

      Weather_Position
      The parameter Weather_Position is the yahoo weather id used for getting the weather condition. Go to http://weather.yahoo.com/ and enter a city or zip code. In the upcoming webpage, the id is a the end of the URL. Example: Munich, Germany -> 676757

      A Twilight device periodically calculates the times of different twilight phases throughout the day. It calculates a virtual "light" element, that gives an indicator about the amount of the current daylight. Besides the location on earth it is influenced by a so called "indoor horizon" (e.g. if there are high buildings, mountains) as well as by weather conditions. Very bad weather conditions lead to a reduced daylight for nearly the whole day. The light calculated spans between 0 and 6, where the values mean the following:

      light
      0 - total night, sun is at least -18 degree below horizon
      1 - astronomical twilight, sun is between -12 and -18 degree below horizon
      2 - nautical twilight, sun is between -6 and -12 degree below horizon
      3 - civil twilight, sun is between 0 and -6 degree below horizon
      4 - indoor twilight, sun is between the indoor_horizon and 0 degree below horizon (not used if indoor_horizon=0)
      5 - weather twilight, sun is between indoor_horizon and a virtual weather horizon (the weather horizon depends on weather conditions (optional)
      6 - maximum daylight

      Azimut, Elevation, Twilight
      The module calculates additionally the azimuth and the elevation of the sun. The values can be used to control a roller shutter.

      As a new (twi)light value the reading Twilight ist added. It is derived from the elevation of the sun with the formula: (Elevation+12)/18 * 100). The value allows a more detailed control of any lamp during the sunrise/sunset phase. The value ist betwenn 0% and 100% when the elevation is between -12° and 6°.

      You must know, that depending on the latitude, the sun will not reach any elevation. In june/july the sun never falls in middle europe below -18°. In more northern countries(norway ...) the sun may not go below 0°.

      Any control depending on the value of Twilight must consider these aspects.

      Example:
            define myTwilight Twilight 49.962529  10.324845 3 676757
          

    Set
      N/A

    Get
      get <name> <reading>

      lightthe current virtual daylight value
      nextEventthe name of the next event
      nextEventTimethe time when the next event will probably happen (during light phase 5 and 6 this is updated when weather conditions change
      sr_astrotime of astronomical sunrise
      sr_nauttime of nautical sunrise
      sr_civiltime of civil sunrise
      srtime of sunrise
      sr_indoortime of indoor sunrise
      sr_weathertime of weather sunrise
      ss_weathertime of weather sunset
      ss_indoortime of indoor sunset
      sstime of sunset
      ss_civiltime of civil sunset
      ss_nautictime of nautic sunset
      ss_astrotime of astro sunset
      azimuththe current azimuth of the sun 0° ist north 180° is south
      compasspointa textual representation of the compass point
      elevationthe elevaltion of the sun
      twilighta percetal value of a new (twi)light value: (elevation+12)/18 * 100)
      twilight_weathera percetal value of a new (twi)light value: (elevation-WEATHER_HORIZON+12)/18 * 100). So if there is weather, it is always a little bit darker than by fair weather
      conditionthe yahoo condition weather code
      condition_txtthe yahoo condition weather code as textual representation
      horizonvalue auf the actual horizon 0°, -6°, -12°, -18°

    Attributes
    • readingFnAttributes
    • useExtWeather <device>:<reading>
    • use data from other devices to calculate twilight_weather.
      The reading used shoud be in the range of 0 to 100 like the reading c_clouds in an openweathermap device, where 0 is clear sky and 100 are overcast clouds.
      With the use of this attribute weather effects like heavy rain or thunderstorms are neglegted for the calculation of the twilight_weather reading.

    Functions
    • twilight($twilight, $reading, $min, $max)
    • - implements a routine to compute the twilighttimes like sunrise with min max values.

      $twilightname of the twilight instance
      $readingname of the reading to use example: ss_astro, ss_weather ...
      $minparameter min time - optional
      $maxparameter max time - optional

    Example:
        define BlindDown at *{twilight("myTwilight","sr_indoor","7:30","9:00")} set xxxx position 100
        # xxxx is a defined blind
    
=end html =begin html_DE

Twilight


    Define
      define <name> Twilight <latitude> <longitude> [<indoor_horizon> [<Weather_Position>]]

      Erstellt ein virtuelles Device für die Dämmerungsberechnung (Zwielicht)

      latitude, longitude (geografische Länge & Breite)
      Die Parameter latitude und longitude sind Dezimalzahlen welche die Position auf der Erde bestimmen, für welche der Dämmerungs-Status berechnet werden soll.

      indoor_horizon
      Der Parameter indoor_horizon bestimmt einen virtuellen Horizont, der für die Berechnung der Dämmerung innerhalb von Rämen genutzt werden kann. Minimalwert ist -6 (ergibt gleichen Wert wie Zivile Dämmerung). Bei 0 fallen indoor- und realer Dämmerungswert zusammen. Werte grösser 0 ergeben frühere Werte für den Abend bzw. spätere für den Morgen.

      Weather_Position
      Der Parameter Weather_Position ist die Yahoo! Wetter-ID welche für den Bezug der Wetterinformationen gebraucht wird. Gehe auf http://weather.yahoo.com/ und gebe einen Ort (ggf. PLZ) ein. In der URL der daraufhin geladenen Seite ist an letzter Stelle die ID. Beispiel: München, Deutschland -> 676757

      Ein Twilight-Device berechnet periodisch die Dämmerungszeiten und -phasen während des Tages. Es berechnet ein virtuelles "Licht"-Element das einen Indikator für die momentane Tageslichtmenge ist. Neben der Position auf der Erde wird es vom sog. "indoor horizon" (Beispielsweise hohe Gebäde oder Berge) und dem Wetter beeinflusst. Schlechtes Wetter führt zu einer Reduzierung des Tageslichts für den ganzen Tag. Das berechnete Licht liegt zwischen 0 und 6 wobei die Werte folgendes bedeuten:

      light
      0 - Totale Nacht, die Sonne ist mind. -18 Grad hinter dem Horizont
      1 - Astronomische Dämmerung, die Sonne ist zw. -12 und -18 Grad hinter dem Horizont
      2 - Nautische Dämmerung, die Sonne ist zw. -6 and -12 Grad hinter dem Horizont
      3 - Zivile/Bürgerliche Dämmerung, die Sonne ist zw. 0 and -6 hinter dem Horizont
      4 - "indoor twilight", die Sonne ist zwischen dem Wert indoor_horizon und 0 Grad hinter dem Horizont (wird nicht verwendet wenn indoor_horizon=0)
      5 - Wetterbedingte Dämmerung, die Sonne ist zwischen indoor_horizon und einem virtuellen Wetter-Horizonz (der Wetter-Horizont ist Wetterabhängig (optional)
      6 - Maximales Tageslicht

      Azimut, Elevation, Twilight (Seitenwinkel, Höhenwinkel, Dämmerung)
      Das Modul berechnet zusätzlich Azimuth und Elevation der Sonne. Diese Werte können zur Rolladensteuerung verwendet werden.

      Das Reading Twilight wird als neuer "(twi)light" Wert hinzugefügt. Er wird aus der Elevation der Sonne mit folgender Formel abgeleitet: (Elevation+12)/18 * 100). Das erlaubt eine detailliertere Kontrolle der Lampen während Sonnenauf - und untergang. Dieser Wert ist zwischen 0% und 100% wenn die Elevation zwischen -12° und 6°

      Wissenswert dazu ist, dass die Sonne, abhägnig vom Breitengrad, bestimmte Elevationen nicht erreicht. Im Juni und Juli liegt die Sonne in Mitteleuropa nie unter -18°. In nördlicheren Gebieten (Norwegen, ...) kommt die Sonne beispielsweise nicht über 0°.

      All diese Aspekte müssen berücksichtigt werden bei Schaltungen die auf Twilight basieren.

      Beispiel:
            define myTwilight Twilight 49.962529  10.324845 3 676757
          

    Set
      N/A

    Get
      get <name> <reading>

      lightder aktuelle virtuelle Tageslicht-Wert
      nextEventName des nächsten Events
      nextEventTimedie Zeit wann das nächste Event wahrscheinlich passieren wird (während Lichtphase 5 und 6 wird dieser Wert aktualisiert wenn sich das Wetter ändert)
      sr_astroZeit des astronomitschen Sonnenaufgangs
      sr_nautZeit des nautischen Sonnenaufgangs
      sr_civilZeit des zivilen/bürgerlichen Sonnenaufgangs
      srZeit des Sonnenaufgangs
      sr_indoorZeit des "indoor" Sonnenaufgangs
      sr_weather"Wert" des Wetters beim Sonnenaufgang
      ss_weather"Wert" des Wetters beim Sonnenuntergang
      ss_indoorZeit des "indoor" Sonnenuntergangs
      ssZeit des Sonnenuntergangs
      ss_civilZeit des zivilen/bürgerlichen Sonnenuntergangs
      ss_nauticZeit des nautischen Sonnenuntergangs
      ss_astroZeit des astro. Sonnenuntergangs
      azimuthaktueller Azimuth der Sonne. 0° ist Norden 180° ist Süden
      compasspointEin Wortwert des Kompass-Werts
      elevationthe elevaltion of the sun
      twilightProzentualer Wert eines neuen "(twi)light" Wertes: (elevation+12)/18 * 100)
      twilight_weatherProzentualer Wert eines neuen "(twi)light" Wertes: (elevation-WEATHER_HORIZON+12)/18 * 100). Wenn ein Wetterwert vorhanden ist, ist es immer etwas dunkler als bei klarem Wetter.
      conditionYahoo! Wetter code
      condition_txtYahoo! Wetter code als Text
      horizonWert des aktuellen Horizont 0°, -6°, -12°, -18°

    Attributes
    • readingFnAttributes
    • useExtWeather <device>:<reading>
    • Nutzt Daten von einem anderen Device um twilight_weather zu berechnen.
      Das Reading sollte sich im Intervall zwischen 0 und 100 bewegen, z.B. das Reading c_clouds in einemopenweathermap device, bei dem 0 heiteren und 100 bedeckten Himmel bedeuten. Wird diese Attribut genutzt , werden Wettereffekte wie Starkregen oder Gewitter fuer die Berechnung von twilight_weather nicht mehr herangezogen.

    Functions
    • twilight($twilight, $reading, $min, $max)
    • - implementiert eine Routine um die Dämmerungszeiten wie Sonnenaufgang mit min und max Werten zu berechnen.

      $twilightName der twiligh Instanz
      $readingName des zu verwendenden Readings. Beispiel: ss_astro, ss_weather ...
      $minParameter min time - optional
      $maxParameter max time - optional

    Anwendungsbeispiel:
        define BlindDown at *{twilight("myTwilight","sr_indoor","7:30","9:00")} set xxxx position 100
        # xxxx ist ein definiertes Rollo
    
=end html_DE =cut