diff --git a/fhem/CHANGED b/fhem/CHANGED
index 838e74796..6c93ff254 100644
--- a/fhem/CHANGED
+++ b/fhem/CHANGED
@@ -1,6 +1,7 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
# Do not insert empty lines here, update check depends on it.
+ - new: 98_TRAFFIC: provide traffic details with Google Distance API
- feature: 98_Hyperion: set configFile only available if at least two files
are found while get configFiles
minor code improvements
diff --git a/fhem/FHEM/98_TRAFFIC.pm b/fhem/FHEM/98_TRAFFIC.pm
new file mode 100644
index 000000000..de8980938
--- /dev/null
+++ b/fhem/FHEM/98_TRAFFIC.pm
@@ -0,0 +1,531 @@
+#########################################################################
+# $Id$
+# fhem Modul which provides traffic details with Google Distance API
+#
+# 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 .
+#
+##############################################################################
+# Changelog:
+#
+# 2016-07-26 initial release
+# 2016-07-28 added eta, readings in minutes
+# 2016-08-01 changed JSON decoding/encofing, added stateReading attribute, added outputReadings attribute
+# 2016-08-02 added attribute includeReturn, round minutes & smart zero'ing, avoid negative values, added update burst
+# 2016-08-05 fixed 3 perl warnings
+# 2016-08-09 added auto-update if status returns UNKOWN_ERROR, added outputReading average
+# 2016-09-25 bugfix Blocking, improved errormessage
+# 2016-10-07 version 1.0, adding to SVN
+
+package main;
+
+use strict;
+use warnings;
+use Time::HiRes qw(gettimeofday);
+use Data::Dumper;
+use LWP::Simple qw($ua get);
+use JSON;
+use POSIX;
+use Blocking;
+
+sub TRAFFIC_Initialize($);
+sub TRAFFIC_Define($$);
+sub TRAFFIC_Undef($$);
+sub TRAFFIC_Set($@);
+sub TRAFFIC_Attr(@);
+sub TRAFFIC_GetUpdate($);
+
+my %TRcmds = (
+ 'update' => 'noArg',
+);
+my $TRVersion = '1.0';
+
+sub TRAFFIC_Initialize($){
+
+ my ($hash) = @_;
+
+ $hash->{DefFn} = "TRAFFIC_Define";
+ $hash->{UndefFn} = "TRAFFIC_Undef";
+ $hash->{SetFn} = "TRAFFIC_Set";
+ $hash->{AttrFn} = "TRAFFIC_Attr";
+ $hash->{AttrList} =
+ "disable:0,1 start_address end_address raw_data:0,1 language waypoints stateReading outputReadings includeReturn:0,1 " .
+ $readingFnAttributes;
+
+}
+
+sub TRAFFIC_Define($$){
+
+ my ($hash, $allDefs) = @_;
+
+ my @deflines = split('\n',$allDefs);
+ my @apiDefs = split('[ \t]+', shift @deflines);
+
+ if(int(@apiDefs) < 3) {
+ return "too few parameters: 'define TRAFFIC '";
+ }
+
+ $hash->{NAME} = $apiDefs[0];
+ $hash->{APIKEY} = $apiDefs[2];
+ $hash->{VERSION} = $TRVersion;
+
+ my $name = $hash->{NAME};
+
+ #clear all readings
+ foreach my $clearReading ( keys %{$hash->{READINGS}}){
+ Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
+ delete($hash->{READINGS}{$clearReading});
+ }
+
+ # basic update interval
+ if(scalar(@apiDefs) > 3 && $apiDefs[3] =~ m/^\d+$/){
+ $hash->{Interval} = $apiDefs[3];
+ }else{
+ $hash->{Interval} = 3600;
+ }
+ Log3 $hash, 3, "TRAFFIC: ($name) defined ".$hash->{NAME}.' with interval set to '.$hash->{Interval};
+
+ # put in default verbose level
+ $attr{$name}{"verbose"} = 1 if !$attr{$name}{"verbose"};
+ $attr{$name}{"outputReadings"} = "text" if !$attr{$name}{"outputReadings"};
+
+ readingsSingleUpdate( $hash, "state", "Initialized", 1 );
+
+ my $firstTrigger = gettimeofday() + 2;
+ $hash->{TRIGGERTIME} = $firstTrigger;
+ $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger);
+
+ RemoveInternalTimer($hash);
+ InternalTimer($firstTrigger, "TRAFFIC_StartUpdate", $hash, 0);
+ Log3 $hash, 5, "TRAFFIC: ($name) InternalTimer set to call GetUpdate in 2 seconds for the first time";
+ return undef;
+}
+
+
+sub TRAFFIC_Undef($$){
+
+ my ( $hash, $arg ) = @_;
+ RemoveInternalTimer ($hash);
+ return undef;
+}
+
+
+#
+# Attr command
+#########################################################################
+sub TRAFFIC_Attr(@){
+
+ my ($cmd,$name,$attrName,$attrValue) = @_;
+ # $cmd can be "del" or "set"
+ # $name is device name
+ my $hash = $defs{$name};
+
+ if ($cmd eq "set") {
+ addToDevAttrList($name, $attrName);
+ Log3 $hash, 3, "TRAFFIC: ($name) attrName $attrName set to attrValue $attrValue";
+ }
+ if($attrName eq "disable" && $attrValue eq "1"){
+ readingsSingleUpdate( $hash, "state", "disabled", 1 );
+ }
+ if($attrName eq "outputReadings" || $attrName eq "includeReturn"){
+ #clear all readings
+ foreach my $clearReading ( keys %{$hash->{READINGS}}){
+ Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
+ delete($hash->{READINGS}{$clearReading});
+ }
+ # start update
+ InternalTimer(gettimeofday() + 1, "TRAFFIC_StartUpdate", $hash, 0);
+ }
+ return undef;
+}
+
+sub TRAFFIC_Set($@){
+
+ my ($hash, @param) = @_;
+ return "\"set \" needs at least one argument: \n".join(" ",keys %TRcmds) if (int(@param) < 2);
+
+ my $name = shift @param;
+ my $set = shift @param;
+
+ $hash->{VERSION} = $TRVersion if $hash->{VERSION} != $TRVersion;
+
+ if(AttrVal($name, "disable", 0 ) == 1){
+ readingsSingleUpdate( $hash, "state", "disabled", 1 );
+ Log3 $hash, 3, "TRAFFIC: ($name) is disabled, $set not set!";
+ return undef;
+ }else{
+ Log3 $hash, 5, "TRAFFIC: ($name) set $name $set";
+ }
+
+ my $validCmds = join("|",keys %TRcmds);
+ if($set !~ m/$validCmds/ ) {
+ return join(' ', keys %TRcmds);
+
+ }elsif($set =~ m/update/){
+ Log3 $hash, 5, "TRAFFIC: ($name) update command recieved";
+
+ # if update burst ist specified
+ if( (my $burstCount = shift @param) && (my $burstInterval = shift @param)){
+ Log3 $hash, 5, "TRAFFIC: ($name) update burst is set to $burstCount $burstInterval";
+ $hash->{BURSTCOUNT} = $burstCount;
+ $hash->{BURSTINTERVAL} = $burstInterval;
+ }else{
+ Log3 $hash, 5, "TRAFFIC: ($name) no update burst set";
+ }
+
+ # update internal timer and update NOW
+ my $updateTrigger = gettimeofday() + 1;
+ $hash->{TRIGGERTIME} = $updateTrigger;
+ $hash->{TRIGGERTIME_FMT} = FmtDateTime($updateTrigger);
+ RemoveInternalTimer($hash);
+
+ # start update
+ InternalTimer($updateTrigger, "TRAFFIC_StartUpdate", $hash, 0);
+
+ return undef;
+ }
+}
+
+
+sub TRAFFIC_StartUpdate($){
+
+ my ( $hash ) = @_;
+ my $name = $hash->{NAME};
+
+ if(AttrVal($name, "disable", 0 ) == 1){
+ RemoveInternalTimer ($hash);
+ Log3 $hash, 3, "TRAFFIC: ($name) is disabled";
+ return undef;
+ }
+ if ( $hash->{Interval}) {
+ RemoveInternalTimer ($hash);
+ my $nextTrigger = gettimeofday() + $hash->{Interval};
+
+
+ if(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} > 0){
+ $nextTrigger = gettimeofday() + $hash->{BURSTINTERVAL};
+ $hash->{BURSTCOUNT}--;
+ }elsif(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} == 0){
+ delete($hash->{BURSTCOUNT});
+ delete($hash->{BURSTINTERVAL});
+ }
+
+ $hash->{TRIGGERTIME} = $nextTrigger;
+ $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
+ InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0);
+ Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call StartUpdate again in " . int($hash->{Interval}). " seconds";
+ }
+
+ if(defined(AttrVal($name, "start_address", undef )) && defined(AttrVal($name, "end_address", undef ))){
+
+ BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;normal',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
+
+ if(defined(AttrVal($name, "includeReturn", undef )) && AttrVal($name, "includeReturn", undef ) eq 1){
+ BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;return',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
+ }
+
+ }else{
+ readingsSingleUpdate( $hash, "state", "incomplete configuration", 1 );
+ Log3 $hash, 1, "TRAFFIC: ($name) is not configured correctly, please add start_address and end_address";
+ }
+}
+
+sub TRAFFIC_AbortUpdate($){
+
+}
+
+
+sub TRAFFIC_DoUpdate(){
+
+ my ($string) = @_;
+ my ($hName, $direction) = split(";;;", $string); # direction is normal or return
+ my $hash = $defs{$hName};
+
+ my $dotrigger = 1;
+ my $name = $hash->{NAME};
+ my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time);
+
+ Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate start";
+
+ if ( $hash->{Interval}) {
+ RemoveInternalTimer ($hash);
+ my $nextTrigger = gettimeofday() + $hash->{Interval};
+ $hash->{TRIGGERTIME} = $nextTrigger;
+ $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
+ InternalTimer($nextTrigger, "TRAFFIC_DoUpdate", $hash, 0);
+ Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call GetUpdate again in " . int($hash->{Interval}). " seconds";
+ }
+
+ my $returnJSON;
+
+ my $TRlanguage = '';
+ if(defined(AttrVal($name,"language",undef))){
+ $TRlanguage = '&language='.AttrVal($name,"language","");
+ }else{
+ Log3 $hash, 5, "TRAFFIC: ($name) no language specified";
+ }
+
+ my $TRwaypoints = '';
+ if(defined(AttrVal($name,"waypoints",undef))){
+ $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"waypoints",undef)));
+
+ if($direction eq "return"){
+ $TRwaypoints = '&waypoints=via:' . join('|via:', reverse split('\|', AttrVal($name,"waypoints",undef)));
+ Log3 $hash, 5, "TRAFFIC: ($name) reversing waypoints";
+ }
+ }else{
+ Log3 $hash, 5, "TRAFFIC: ($name) no waypoints specified";
+ }
+
+ my $origin = AttrVal($name, "start_address", 0 );
+ my $destination = AttrVal($name, "end_address", 0 );
+
+ if($direction eq "return"){
+ $origin = AttrVal($name, "end_address", 0 );
+ $destination = AttrVal($name, "start_address", 0 );
+ }
+
+ my $url = 'https://maps.googleapis.com/maps/api/directions/json?origin='.$origin.'&destination='.$destination.'&mode=driving'.$TRlanguage.'&departure_time=now'.$TRwaypoints.'&key='.$hash->{APIKEY};
+ Log3 $hash, 2, "TRAFFIC: ($name) using $url";
+
+ my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
+ $ua->default_header("HTTP_REFERER" => "www.google.de");
+ my $body = $ua->get($url);
+ my $json = decode_json($body->decoded_content);
+
+ my $duration_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'value'} ;
+ my $duration_in_traffic_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'value'};
+
+ $returnJSON->{'duration'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
+ $returnJSON->{'duration_in_traffic'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
+ $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
+ $returnJSON->{'state'} = $json->{'status'};
+ $returnJSON->{'status'} = $json->{'status'};
+ $returnJSON->{'eta'} = FmtTime( gettimeofday() + $duration_in_traffic_sec );
+
+ if($duration_in_traffic_sec && $duration_sec){
+ $returnJSON->{'delay'} = prettySeconds($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/text/;
+ Log3 $hash, 3, "TRAFFIC: ($name) delay in seconds = $duration_in_traffic_sec - $duration_sec";
+
+ $returnJSON->{'delay_min'} = int($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/min/;
+ if(defined($returnJSON->{'delay_min'})){
+ if( ( $returnJSON->{'delay_min'} && $returnJSON->{'delay_min'} =~ m/^-/ ) || $returnJSON->{'delay_min'} < 60){
+ Log3 $hash, 5, "TRAFFIC: ($name) delay_min was negative or less than 1min (".$returnJSON->{'delay_min'}."), set to 0";
+ $returnJSON->{'delay_min'} = 0;
+ }else{
+ $returnJSON->{'delay_min'} = int($returnJSON->{'delay_min'} / 60 + 0.5); #divide 60 and round
+ }
+ }
+ }else{
+ Log3 $hash, 1, "TRAFFIC: ($name) did not receive duration_in_traffic, not able to calculate delay";
+
+ }
+
+ # condition based values
+ $returnJSON->{'error_message'} = $json->{'error_message'} if $json->{'error_message'};
+ # output readings
+ $returnJSON->{'duration_min'} = int($duration_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/;
+ $returnJSON->{'duration_in_traffic_min'} = int($duration_in_traffic_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/;
+ $returnJSON->{'duration_sec'} = $duration_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
+ $returnJSON->{'duration_in_traffic_sec'} = $duration_in_traffic_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
+ # raw data (seconds)
+ $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'value'} if AttrVal($name, "raw_data", 0);
+
+
+ # average readings
+ if(AttrVal($name, "outputReadings", "" ) =~ m/average/){
+
+ # calc average
+ $returnJSON->{'average_duration_min'} = int($hash->{READINGS}{'average_duration_min'}{VAL} + $returnJSON->{'duration_min'}) / 2 if $returnJSON->{'duration_min'};
+ $returnJSON->{'average_duration_in_traffic_min'} = int($hash->{READINGS}{'average_duration_in_traffic_min'}{VAL} + $returnJSON->{'duration_in_traffic_min'}) / 2 if $returnJSON->{'duration_in_traffic_min'};
+ $returnJSON->{'average_delay_min'} = int($hash->{READINGS}{'average_delay_min'}{VAL} + $returnJSON->{'delay_min'}) / 2 if $returnJSON->{'delay_min'};
+
+ # override if this is the first average
+ $returnJSON->{'average_duration_min'} = $returnJSON->{'duration_min'} if !$hash->{READINGS}{'average_duration_min'}{VAL};
+ $returnJSON->{'average_duration_in_traffic_min'} = $returnJSON->{'duration_in_traffic_min'} if !$hash->{READINGS}{'average_duration_in_traffic_min'}{VAL};
+ $returnJSON->{'average_delay_min'} = $returnJSON->{'delay_min'} if !$hash->{READINGS}{'average_delay_min'}{VAL};
+ }
+
+
+ Log3 $hash, 5, "TRAFFIC: ($name) returning from TRAFFIC_DoUpdate: ".encode_json($returnJSON);
+ Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate done";
+ return "$name;;;$direction;;;".encode_json($returnJSON);
+}
+
+sub TRAFFIC_FinishUpdate($){
+ my ($name,$direction,$rawJson) = split(/;;;/,shift);
+ my $hash = $defs{$name};
+ my %sensors;
+ my $dotrigger = 1;
+
+ Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start";
+
+ my $json = decode_json($rawJson);
+ readingsBeginUpdate($hash);
+
+ foreach my $readingName (keys %{$json}){
+ Log3 $hash, 3, "TRAFFIC: ($name) ReadingsUpdate: $readingName - ".$json->{$readingName};
+ if($direction eq 'return'){
+ readingsBulkUpdate($hash,'return_'.$readingName,$json->{$readingName});
+ }else{
+ readingsBulkUpdate($hash,$readingName,$json->{$readingName});
+ }
+ }
+
+ if($json->{'status'} eq 'UNKNOWN_ERROR'){ # UNKNOWN_ERROR indicates a directions request could not be processed due to a server error. The request may succeed if you try again.
+ InternalTimer(gettimeofday() + 3, "TRAFFIC_StartUpdate", $hash, 0);
+ }
+
+ if(my $stateReading = AttrVal($name,"stateReading",undef)){
+ Log3 $hash, 5, "TRAFFIC: ($name) stateReading defined, override state";
+ if(!$json->{$stateReading}){
+ Log3 $hash, 1, "TRAFFIC: ($name) stateReading $stateReading not found";
+ }else{
+ readingsBulkUpdate($hash,'state',$json->{$stateReading});
+ }
+ }
+
+ readingsEndUpdate($hash, $dotrigger);
+ Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate done";
+}
+
+
+sub prettySeconds {
+ my $time = shift;
+
+ if($time =~ m/^-/){
+ return "0 min";
+ }
+ my $days = int($time / 86400);
+ $time -= ($days * 86400);
+ my $hours = int($time / 3600);
+ $time -= ($hours * 3600);
+ my $minutes = int($time / 60);
+ my $seconds = $time % 60;
+
+ $days = $days < 1 ? '' : $days .' days ';
+ $hours = $hours < 1 ? '' : $hours .' hours ';
+ $minutes = $minutes < 1 ? '' : $minutes . ' min ';
+ $time = $days . $hours . $minutes;
+ if(!$time){
+ return "0 min";
+ }else{
+ return $time;
+ }
+
+}
+
+
+1;
+
+#======================================================================
+#======================================================================
+#
+# HTML Documentation for help and commandref
+#
+#======================================================================
+#======================================================================
+=pod
+=item device
+=item summary provide traffic details with Google Distance API
+=item summary_DE stellt Verkehrsdaten mittels Google Distance API bereit
+=begin html
+
+
+TRAFFIC
+
+ TRAFFIC - google maps directions module
+
+
+ This FHEM module collects and displays data obtained via the google maps directions api
+ requirements:
+ perl JSON module
+ perl LWP::SIMPLE module
+ Google maps API key
+
+ Features:
+
+
+ - get distance between start and end location
+ - get travel time for route
+ - get travel time in traffic for route
+ - define additional waypoints
+ - calculate delay between travel-time and travel-time-in-traffic
+ - choose default language
+ - disable the device
+ - 5 log levels
+ - get outputs in seconds / meter (raw_data)
+ - state of google maps returned in error reading (i.e. The provided API key is invalid)
+ - customize update interval (default 3600 seconds)
+ - calculate ETA with localtime and delay
+ - configure the output readings with attribute outputReadings, text, min sec
+ - configure the state-reading
+ - optionally display the same route in return
+ - one-time-burst, specify the amount and interval between updates
+
+
+
+
+ Define:
+
+ define <name> TRAFFIC <YOUR-API-KEY> [UPDATE-INTERVAL]
+
+ example:
+ define muc2berlin TRAFFIC ABCDEFGHIJKLMNOPQRSTVWYZ 600
+
+
+
+ Attributes:
+
+ - "start_address" - Street, zipcode City (mandatory)
+ - "end_address" - Street, zipcode City (mandatory)
+ - "raw_data" - 0:1
+ - "language" - de, en etc.
+ - "waypoints" - Lat, Long coordinates, separated by |
+ - "disable" - 0:1
+ - "stateReading" - name the reading which will be used in device state
+ - "outputReadings" - define what kind of readings you want to get: text, min, sec, average
+ - "includeReturn" - 0:1
+
+
+
+
+
+ Readings:
+
+ - delay
+ - distance
+ - duration
+ - duration_in_traffic
+ - state
+ - eta
+ - delay_min
+ - duration_min
+ - duration_in_traffic_min
+ - error_message
+
+
+
+ Set
+
+ - update [burst-update-count] [burst-update-interval] - update readings manually
+
+
+
+
+
+=end html
+=cut
+
diff --git a/fhem/HISTORY b/fhem/HISTORY
index 7a3257c2f..f8f5e54ea 100644
--- a/fhem/HISTORY
+++ b/fhem/HISTORY
@@ -694,3 +694,6 @@
- Sat Oct 1 2016 (Christian.Kühnel)
- added new module 99_Venetian
+
+- Fri Oct 7 2016 (jmike)
+ - adding new module 98_TRAFFIC
diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt
index 34abd4044..cdb8582ad 100644
--- a/fhem/MAINTAINER.txt
+++ b/fhem/MAINTAINER.txt
@@ -347,6 +347,7 @@ FHEM/98_PID20.pm John http://forum.fhem.de Automatis
FHEM/98_RandomTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste/Kalendermodule
FHEM/98_SVG.pm rudolfkoenig http://forum.fhem.de Frontends/SVG/Plots/logProxy
FHEM/98_THRESHOLD.pm damian-s http://forum.fhem.de Automatisierung
+FHEM/98_TRAFFIC.pm jmike http://forum.fhem.de Unterstuetzende Dienste
FHEM/98_UbiquitiPM.pm Wzut http://forum.fhem.de Sonstige Systeme
FHEM/98_UbiquitiOut.pm Wzut http://forum.fhem.de Sonstige Systeme
FHEM/98_WeekdayTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste