########################################################################################################################
# $Id: 60_Watches.pm 22591 2020-08-12 21:13:44Z DS_Starter $
#########################################################################################################################
#       60_Watches.pm
#
#       (c) 2018-2021 by Heiko Maaz
#       e-mail: Heiko dot Maaz at t-online dot de
# 
#       This script 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 <http://www.gnu.org/licenses/>.
#
#       The script is based on sources from following sites:
#       # modern clock: https://www.w3schools.com/graphics/canvas_clock_start.asp
#       # station clock: http://www.3quarks.com/de/Bahnhofsuhr/
#       # digital clock: http://www.3quarks.com/de/Segmentanzeige/index.html
#
#########################################################################################################################
package FHEM::Watches;                                 ## no critic 'package'

use strict;
use warnings;
use Time::HiRes qw(time gettimeofday tv_interval);
use GPUtils qw(GP_Import GP_Export);                   # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
eval "use FHEM::Meta;1" or my $modMetaAbsent = 1;      ## no critic 'eval'

# Run before module compilation
BEGIN {
  # Import from main::
  GP_Import( 
      qw(
          AttrVal
          defs
          FW_makeImage
          FW_ME
          FW_subdir
          IsDisabled
          Log3 
          modules          
          ReadingsVal
          readingsDelete 
          readingsBeginUpdate
          readingsBulkUpdate
          readingsEndUpdate
          readingFnAttributes
          readingsSingleUpdate   
          sortTopicNum         
        )
  );
  
  # Export to main context with different name
  #     my $pkg  = caller(0);
  #     my $main = $pkg;
  #     $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/gx;
  #     foreach (@_) {
  #         *{ $main . $_ } = *{ $pkg . '::' . $_ };
  #     }
  GP_Export(
      qw(
          Initialize
        )
  );  
}

# Versions History intern 
my %vNotesIntern = (
  "0.27.1" => "09.01.2021  remove usage of sscam_tooltip.js in sub controlPanel ,Forum: https:/topic,93454.msg1119567.html#msg1119567",
  "0.27.0" => "12.08.2020  control buttons, new attr hideButtons, controlButtonSize, some more changes according PBP".
                           "fix Random triggering of alarm or random time display at start / resume ",
  "0.26.0" => "01.08.2020  add attr timeAsReading -> write into reading 'currtime' if time is displayed, ".
                           "add \$readingFnAttributes, set release_status to stable ",
  "0.25.0" => "03.06.2020  set reading 'stoptime' in type 'stopwatch' ",                               
  "0.24.0" => "26.05.2020  entry of countDownInit can be in format <seconds> ",
  "0.23.2" => "20.05.2020  english commandref ",
  "0.23.1" => "10.05.2020  some more changes for PBP severity 3 ",
  "0.23.0" => "10.05.2020  attr 'digitalBorderDistance' now also valid for digital watches, some changes for PBP ", 
  "0.22.0" => "09.05.2020  new attr 'digitalBorderDistance' for left and rigtht border distance of digital text ", 
  "0.21.1" => "09.05.2020  fix calculate forerun of 'text' dynamically if digitalTextDigitNumber=0 ",
  "0.21.0" => "08.05.2020  support of alarm time of model digital 'watch' ",
  "0.20.1" => "08.05.2020  asynchron read digital text and much more fixes, set client as default timeSource ",
  "0.20.0" => "07.05.2020  asynchron read alarmTime reading, some fixes ",
  "0.19.0" => "06.05.2020  alarm event creation for watch models 'Station' and 'Station' ",
  "0.18.0" => "06.05.2020  attr 'digitalTextTicker' deleted and switched to setter 'textTicker', default text switched to blank ",
  "0.17.0" => "05.05.2020  new attr 'digitalTextTicker', 'digitalTextDigitNumber' ",
  "0.16.0" => "04.05.2020  delete attr 'digitalDisplayText', new setter 'displayText', 'displayTextDel' ",
  "0.15.1" => "04.05.2020  fix permanently events when no alarmTime is set in countdownwatch and countdown is finished ",
  "0.15.0" => "04.05.2020  new attribute 'digitalSegmentType' for different segement count, also new attributes ".
                           "'digitalDigitAngle', 'digitalDigitDistance', 'digitalDigitHeight', 'digitalDigitWidth', 'digitalSegmentDistance' ".
                           "'digitalSegmentWidth', stopwatches don't stop when alarm is triggered (use notify to do it) ",
  "0.14.0" => "03.05.2020  switch to packages, use setVersionInfo, support of Meta.pm ",
  "0.13.0" => "03.05.2020  set resume for countdownwatch, set 'continue' removed ",
  "0.12.0" => "03.05.2020  set resume for stopwatch, new 'alarmDel' command for stop watches, alarmHMS renamed to 'alarmHMSdelset' ",
  "0.11.0" => "02.05.2020  alarm event stabilized, reset command for 'countdownwatch', event alarmed contains alarm time ",
  "0.10.0" => "02.05.2020  renamed 'countDownDone' to 'alarmed', bug fix ",
  "0.9.0"  => "02.05.2020  new attribute 'timeSource' for selection of client/server time ",
  "0.8.0"  => "01.05.2020  new values 'countdownwatch' for attribute digitalDisplayPattern, switch all watches to server time ",
  "0.7.0"  => "30.04.2020  new set 'continue' for stopwatch ",
  "0.6.0"  => "29.04.2020  new set 'reset' for stopwatch, read 'state' and 'starttime' from readings, add csrf token support ",
  "0.5.0"  => "28.04.2020  new values 'stopwatch', 'staticwatch' for attribute digitalDisplayPattern ",
  "0.4.0"  => "20.11.2018  text display ",
  "0.3.0"  => "19.11.2018  digital clock added ",
  "0.2.0"  => "14.11.2018  station clock added ",
  "0.1.0"  => "13.11.2018  initial Version with modern analog clock"
);

my %hcb = (                                                                  # Hash der Steuertastendefinition
  1  => {cmd => "start",  img => "default/remotecontrol/black_btn_GREEN.png",  },
  2  => {cmd => "stop",   img => "default/remotecontrol/black_btn_RED.png",    },
  3  => {cmd => "resume", img => "default/remotecontrol/black_btn_YELLOW.png", },
  4  => {cmd => "reset",  img => "default/remotecontrol/black_btn_STOP.png",   },
);
                
my %hset = (                                                                  # Hash der Set-Werte
  staticwatch    => {set => "time"                                                                                  },
  stopwatch      => {set => "alarmSet alarmDel:noArg reset:noArg resume:noArg start:noArg stop:noArg"               },
  countdownwatch => {set => "alarmSet alarmDel:noArg reset:noArg resume:noArg start:noArg stop:noArg countDownInit" },
  watch          => {set => "alarmSet alarmDel:noArg"                                                               },
  text           => {set => "displayTextSet displayTextDel:noArg textTicker:on,off"                                 },
  time           => {fn  => \&_setTime                                                                              }, 
  reset          => {fn  => \&_setReset                                                                             }, 
  textTicker     => {fn  => \&_setTextTicker                                                                        },
  displayTextDel => {fn  => \&_setDisplayTextDel                                                                    },
  displayTextSet => {fn  => \&_setDisplayTextSet                                                                    },
  stop           => {fn  => \&_setStop                                                                              },
  resume         => {fn  => \&_setResume                                                                            },
  countDownInit  => {fn  => \&_setCountDownInit                                                                     },
  alarmDel       => {fn  => \&_setAlarmDel                                                                          },
  alarmSet       => {fn  => \&_setAlarmSet                                                                          },
  start          => {fn  => \&_setStart                                                                             },
);

##############################################################################
#         Initialize Funktion
##############################################################################
sub Initialize {
  my ($hash) = @_;
  
  $hash->{DefFn}              = \&Define;
  $hash->{SetFn}              = \&Set;
  $hash->{FW_summaryFn}       = \&FWebFn;
  $hash->{FW_detailFn}        = \&FWebFn;
  $hash->{AttrFn}             = \&Attr;
  $hash->{AttrList}           = "controlButtonSize:selectnumbers,50,5,150,0,lin ".
                                "digitalBorderDistance:slider,0,1,40 ".
                                "digitalColorBackground:colorpicker ".
                                "digitalColorDigits:colorpicker ".
                                "digitalDisplayPattern:countdownwatch,staticwatch,stopwatch,text,watch ".
                                "digitalDigitAngle:slider,-30,0.5,30,1 ".
                                "digitalDigitDistance:slider,0.5,0.1,10,1 ".
                                "digitalDigitHeight:slider,5,0.1,50,1 ".
                                "digitalDigitWidth:slider,5,0.1,50,1 ".
                                "digitalSegmentDistance:slider,0,0.1,5,1 ".
                                "digitalSegmentType:7,14,16 ".
                                "digitalSegmentWidth:slider,0.3,0.1,3.5,1 ".
                                "digitalTextDigitNumber ".
                                "disable:1,0 ".
                                "hideButtons:1,0 ".
                                "hideDisplayName:1,0 ".
                                "htmlattr ".
                                "modernColorBackground:colorpicker ".
                                "modernColorHand:colorpicker ".
                                "modernColorFigure:colorpicker ".
                                "modernColorFace:colorpicker ".
                                "modernColorRing:colorpicker ".
                                "modernColorRingEdge:colorpicker ".
                                "stationSecondHand:Bar,HoleShaped,NewHoleShaped,No ".
                                "stationSecondHandBehavoir:Bouncing,Overhasty,Creeping,ElasticBouncing ".
                                "stationMinuteHandBehavoir:Bouncing,Creeping,ElasticBouncing ".
                                "stationBoss:Red,Black,Vienna,No ".
                                "stationMinuteHand:Bar,Pointed,Swiss,Vienna ".
                                "stationHourHand:Bar,Pointed,Swiss,Vienna ".
                                "stationStrokeDial:GermanHour,German,Austria,Swiss,Vienna,No ".
                                "stationBody:Round,SmallWhite,RoundGreen,Square,Vienna,No ".
                                "timeAsReading:1,0 ".
                                "timeSource:server,client ".
                                $readingFnAttributes;;

  $hash->{FW_hideDisplayName} = 1;                        # Forum 88667
  # $hash->{FW_addDetailToSummary} = 1;
  $hash->{FW_atPageEnd}       = 1;                        # wenn 1 -> kein Longpoll ohne informid in HTML-Tag
  # $hash->{FW_deviceOverview}  = 1;
  
  eval { FHEM::Meta::InitMod( __FILE__, $hash ) };        ## no critic 'eval' # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html)
  
return;
}

##############################################################################
#         Define Funktion
##############################################################################
sub Define {
  my ($hash, $def) = @_;
  my $name = $hash->{NAME};
  my @a    = split m{\s+}x, $def;
  
  if(!$a[2]) {
      return "You need to specify more parameters.\n". "Format: define <name> Watches [Modern | Station | Digital]";
  }
  
  $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent);      # Modul Meta.pm nicht vorhanden
  $hash->{MODEL}                 = uc($a[2]);
  
  setVersionInfo($hash);                                      # Versionsinformationen setzen
  
  readingsSingleUpdate($hash,"state", "initialized", 1);      # Init für "state" 
  
return;
}

##############################################################################
#                                 Set Funktion
##############################################################################
sub Set {
  my ($hash, @a) = @_;
  return qq{"set X" needs at least an argument} if ( @a < 2 );
  my $name  = $a[0];
  my $opt   = $a[1];
  my $prop  = $a[2];
  my $prop1 = $a[3];
  my $prop2 = $a[4];
    
  return if(IsDisabled($name));
  
  my $addp = AttrVal($name, "digitalDisplayPattern", "watch");
  if (!$hset{$addp}) {
      Log3($name, 1, "$name - ERROR - The attribute 'digitalDisplayPattern' value '$addp' is not known by module '$hash->{TYPE}'");
      return;
  }
                                                           
  my $setlist = "Unknown argument $opt, choose one of "; 
  $setlist   .= "$hset{$addp}{set} ";

  my $params = {
      hash  => $hash,
      name  => $name,
      opt   => $opt,
      prop  => $prop,
      prop1 => $prop1,
      prop2 => $prop2,
      addp  => $addp,
      aref  => \@a,
  };
  
  no strict "refs";                                                        ## no critic 'NoStrict'  
  if($hset{$opt}) {
      my $ret = "";
      $ret = &{$hset{$opt}{fn}} ($params) if(defined &{$hset{$opt}{fn}}); 
      return $ret;
  }
  use strict "refs";

return $setlist;
}

################################################################
#                      Setter start
################################################################
sub _setStart {                          
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $name  = $paref->{name};
  my $addp  = $paref->{addp};
  
  return qq{Please set "countDownInit" before !} if($addp =~ /countdownwatch/x && !ReadingsVal($name, "countInitVal", ""));
  
  my $ms = int(time*1000);
      
  readingsBeginUpdate ($hash);
  readingsBulkUpdate  ($hash, "alarmed", 0)      if($addp =~ /stopwatch|countdownwatch/x); 
  readingsBulkUpdate  ($hash, "starttime", $ms);
  readingsBulkUpdate  ($hash, "state", "started");
  readingsEndUpdate   ($hash, 1);

return;
}

################################################################
#                      Setter alarmSet
################################################################
sub _setAlarmSet {                       
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $name  = $paref->{name};
  my $opt   = $paref->{opt};
  my $prop  = $paref->{prop}  // 70;
  my $prop1 = $paref->{prop1} // 70;
  my $prop2 = $paref->{prop2} // 70;
  
  my $msg = qq{The value for "$opt" is invalid. Use parameter "hh mm ss" like "19 45 13".}; 
  
  return $msg if($prop>23 || $prop1>59 || $prop2>59);
  
  my $at = sprintf("%02d",$prop).":".sprintf("%02d",$prop1).":".sprintf("%02d",$prop2);
        
  readingsSingleUpdate($hash, "alarmed",     0, 0);
  readingsSingleUpdate($hash, "alarmTime", $at, 1);

return;
}

################################################################
#                      Setter alarmDel
################################################################
sub _setAlarmDel {                       
  my $paref = shift;
  my $name  = $paref->{name};
  
  delReadings ($name, "alarmTime");
  delReadings ($name, "alarmed");

return;
}

################################################################
#                      Setter countDownInit
################################################################
sub _setCountDownInit {                  
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $name  = $paref->{name};
  my $opt   = $paref->{opt};
  my $prop  = $paref->{prop};
  my $prop1 = $paref->{prop1};
  my $prop2 = $paref->{prop2};
  
  my $ct;
  my $msg = qq{The value for "$opt" is invalid. Use parameter "hh mm ss" like "19 45 13" \nor alternatively only one entry in seconds.};
  
  if($prop && $prop1) {                                                # Format: hh mm ss
      $prop2 = defined $prop2 ? $prop2 : 70;                           # Sekunden
      return $msg if($prop>23 || $prop1>59 || $prop2>59);
      $ct = $prop*3600 + $prop1*60 + $prop2;                           # in Sekunden umgewandelt !     
  } 
  elsif ($prop && !$prop1) {                                           # Format: Sekundenangabe
      $ct = $prop; 
  } 
  else {
     return $msg;   
  
  }
  
  delReadings         ($name, "countInitVal");
  
  readingsBeginUpdate ($hash);
  readingsBulkUpdate  ($hash, "countInitVal", $ct          ); 
  readingsBulkUpdate  ($hash, "state",        "initialized");
  readingsEndUpdate   ($hash, 1);

return;
}

################################################################
#                      Setter resume
################################################################
sub _setResume {                         
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $name  = $paref->{name};
  my $addp  = $paref->{addp};
  
  return qq{Please set "countDownInit" before !} if($addp =~ /countdownwatch/x && !ReadingsVal($name, "countInitVal", ""));
  return if(ReadingsVal($name, "state", "") eq "started");
  
  my $ms = int(time*1000);
  readingsSingleUpdate($hash, "starttime", $ms,       0);
  readingsSingleUpdate($hash, "state",     "resumed", 1);

return;
}

################################################################
#                      Setter stop
################################################################
sub _setStop {                           
  my $paref = shift;
  my $hash  = $paref->{hash};
  
  readingsSingleUpdate($hash, "state", "stopped",  1);

return;
}

################################################################
#                      Setter displayTextSet
################################################################
sub _setDisplayTextSet {                 
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $aref  = $paref->{aref};
  
  my @a = @$aref;
  
  shift @a; shift @a;
  
  my $txt = join (" ", @a);
  $txt    =~ s/[\r\n]//gx;
  readingsSingleUpdate($hash, "displayText", $txt, 1);

return;
}

################################################################
#                      Setter displayTextDel
################################################################
sub _setDisplayTextDel {                 
  my $paref = shift;
  my $name  = $paref->{name};
  
  delReadings ($name, "displayText");

return;
}

################################################################
#                      Setter textTicker
################################################################
sub _setTextTicker {                     
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $prop  = $paref->{prop};
  
  if($prop eq "on") {
      readingsSingleUpdate($hash, "displayTextTicker", "on", 1); 
  } 
  else {
      readingsSingleUpdate($hash, "displayTextTicker", "off", 1); 
  }

return;
}

################################################################
#                      Setter reset
################################################################
sub _setReset {                          
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $name  = $paref->{name};
  
  delReadings         ($name);
  readingsSingleUpdate($hash, "state", "initialized", 1);

return;
}

################################################################
#                      Setter time
################################################################
sub _setTime {                           
  my $paref = shift;
  my $hash  = $paref->{hash};
  my $opt   = $paref->{opt};
  my $prop  = sprintf("%0d", $paref->{prop}  // 0);
  my $prop1 = sprintf("%0d", $paref->{prop1} // 0);
  my $prop2 = sprintf("%0d", $paref->{prop2} // 0);
  
  my $msg   = qq{The value for "$opt" is invalid. Use parameter "hh mm ss" like "19 45 13"};
  
  return $msg if($prop>23 || $prop1>59 || $prop2>59);

  readingsBeginUpdate ($hash); 
  readingsBulkUpdate  ($hash, "hour",   $prop);
  readingsBulkUpdate  ($hash, "minute", $prop1);
  readingsBulkUpdate  ($hash, "second", $prop2);                    
  readingsEndUpdate   ($hash, 1);

return;
}

##############################################################################
#         Attributfunktion
##############################################################################
sub Attr {                                           ## no critic 'complexity'
    my ($cmd,$name,$aName,$aVal) = @_;
    my $hash                     = $defs{$name};
    my ($do,$val);
      
    # $cmd can be "del" or "set"
    # $name is device name
    # aName and aVal are Attribute name and value
    
    if ($cmd eq "set" && $hash->{MODEL} !~ /modern/ix && $aName =~ /^modern/x) {
         return qq{"$aName" is only valid for Watches model "Modern"};
    }
    
    if ($cmd eq "set" && $hash->{MODEL} !~ /station/ix && $aName =~ /^station/x) {
         return qq{"$aName" is only valid for Watches model "Station"};
    }
    
    if ($cmd eq "set" && $hash->{MODEL} !~ /digital/ix && $aName =~ /^digital/x) {
         return qq{"$aName" is only valid for Watches model "Digital"};
    }
    
    if ($aName eq "disable") {
        if($cmd eq "set") {
            $do = ($aVal) ? 1 : 0;
        }
        $do  = 0 if($cmd eq "del");
        $val = ($do == 1 ? "disabled" : "initialized");
    
        readingsSingleUpdate($hash, "state", $val, 1);
    }
    
    if ($aName eq "timeAsReading") {
        if($cmd eq "set") {
            $do = $aVal;
        }
        $do = 0 if($cmd eq "del");
            
        if(!$do) {
            delReadings ($name, "currtime"); 
        }
    }
    
    if ($aName eq "digitalDisplayPattern") {
        if($cmd eq "set") {
            $do = $aVal;
        }
        $do = 0 if($cmd eq "del");
        
        if($do ne "text") {
            delReadings ($name); 
        } 
        else {
            delReadings ($name,undef,"^display.*"); 
        }        
 
        readingsSingleUpdate($hash, "state", "initialized", 1); 
        
        if($do =~ /\bstopwatch\b/x) {
            my $ms = int(time*1000);
            readingsSingleUpdate($hash, "starttime", $ms, 0);
        }
    } 

    if ($cmd eq "set") {
        if ($aName =~ /digitalTextDigitNumber|digitalBorderDistance/x && $aVal !~ /^[0-9]+$/x) {
            return qq{The value of "$aName" is not valid. Only integers are allowed !};
        }
    }    

return;
}

##############################################################################
#                      Webanzeige des Devices
##############################################################################
sub FWebFn {
  my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  my $hash  = $defs{$d};
  
  my $alias = AttrVal($d, "alias", $d);                            # Linktext als Aliasname oder Devicename setzen
  my $dlink = qq{<a href="$FW_ME?detail=$d">$alias</a>}; 
  
  my $ret = "";
  $ret   .= "<span>$dlink </span><br>"  if(!AttrVal($d,"hideDisplayName",0));
  if(IsDisabled($d)) {
      if(AttrVal($d,"hideDisplayName",0)) {
          $ret .= qq{Watch <a href="$FW_ME?detail=$d">$d</a> is disabled};
      } 
      else {
          $ret .= "<html>Watch is disabled</html>";
      }  
  } 
  else {
      $ret .= modernWatch ($d) if($hash->{MODEL} =~ /modern/ix);
      $ret .= stationWatch($d) if($hash->{MODEL} =~ /station/ix);
      $ret .= digitalWatch($d) if($hash->{MODEL} =~ /digital/ix);
  }
  
  my $ddp = AttrVal($d, "digitalDisplayPattern", "");              # Steuertastenpaneel 
  my $hb  = AttrVal($d, "hideButtons", 0);
  if($ddp =~ /countdownwatch|stopwatch/x && !$hb) {
      $ret .= controlPanel ($d);
  }
  
return $ret;
}

###############################################################################
#                    Paneel für Stoppuhr Steuertasten
###############################################################################
sub controlPanel {
  my $name     = shift;
  my $pbs      = AttrVal($name,"controlButtonSize", 100);                                       # Größe der Druckbuttons in %
  my $iconpath = "www/images"; 
  my $ret      = "";
  
  $ret .= "<style>TD.controlv1 { padding: 3px 3px; } </style>";
  $ret .= "<style>TD.controlv2 { padding: 8px 8px; } </style>";
  $ret .= "<style>.defsize { font-size:16px; } </style>";
  
  $ret .= '<table class="defsize">';
  $ret .= "<tr>";
 
  for my $btn (sort keys %hcb) {                 
      my $cmd = $hcb{$btn}{cmd};
      my $img = $hcb{$btn}{img}; 
      next if(!$cmd || !$img);
      
      $ret .= "<td class='controlv1'>" if($pbs <= 100);
      $ret .= "<td class='controlv2'>" if($pbs >= 105);
      
      if ($img =~ m/\.svg/x) {                                                                               # Verwendung für SVG's
          $img = FW_makeImage($img, $cmd, "rc-button");   
      } 
      else {                                                                                                 # $FW_ME = URL-Pfad unter dem der FHEMWEB-Server via HTTP erreichbar ist, z.B. /fhem                                 
          $img = "<img src=\"$FW_ME/$iconpath/$img\" height=\"$pbs%\" width=\"$pbs%\">";  
      }

      my $cmd1 = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $cmd')";                                      # $FW_subdir = Sub-path in URL, used by FLOORPLAN/weblink 
      $ret    .= "<a onClick=\"$cmd1\" title=\"$cmd\">$img</a>";  

      $ret .= "</td>";    
  }
  
  $ret .= "</tr>";  
  $ret .= "</table>";
  
return $ret;
}

##############################################################################
#         löscht alle oder das spezifizierte Reading (außer state)
#         $todel   = nur dieses Reading löschen
#         $supress = Reading (Regex) nicht löschen
##############################################################################
sub delReadings {
  my ($name,$todel,$supress) = @_;
  my $hash = $defs{$name};
  
  my $addp = AttrVal($name, "digitalDisplayPattern", "watch");
  
  if($todel) {
      readingsDelete($hash,$todel);
      return;
  }
  
  my @allrds = keys%{$hash->{READINGS}};
  for my $key(@allrds) {
      next if($key =~ /\bstate\b/x);     
      next if(defined $supress && $key =~ /$supress/x);        
      readingsDelete($hash,$key);
  }    
    
return;
} 

##############################################################################
#                      Digitale Uhr / Anzeige aus:
#            http://www.3quarks.com/de/Segmentanzeige/index.html
#
##############################################################################
sub digitalWatch {
  my ($d)      = @_;
  my $hash     = $defs{$d};
  my $alarmdef = "00:00:00";
  my $bgc      = AttrVal($d, "digitalColorBackground", "C4C4C4");
  my $dcd      = AttrVal($d, "digitalColorDigits",     "000000"); 
  my $addp     = AttrVal($d, "digitalDisplayPattern",  "watch");  
  my $adst     = AttrVal($d, "digitalSegmentType",     7);
  my $adsw     = AttrVal($d, "digitalSegmentWidth",    1.5);
  my $addh     = AttrVal($d, "digitalDigitHeight",     20);
  my $addw     = AttrVal($d, "digitalDigitWidth",      12);
  my $addd     = AttrVal($d, "digitalDigitDistance",   2);
  my $adsd     = AttrVal($d, "digitalSegmentDistance", 0.5);
  my $adda     = AttrVal($d, "digitalDigitAngle",      9);
  my $adtdn    = AttrVal($d, "digitalTextDigitNumber", 0);
  my $abdist   = AttrVal($d, "digitalBorderDistance",  8);
  my $hattr    = AttrVal($d, "htmlattr",               "width='150' height='50'");
  my $tsou     = AttrVal($d, "timeSource",             "client");
  my $showct   = AttrVal($d, "timeAsReading",          0);
  
  my $deftxt   =  "    ";
  my $rdtt     =  ReadingsVal ($d, "displayTextTicker", "off");
  my $ddt      =  ReadingsVal ($d, "displayText",       $deftxt);
  my $alarm    =  ReadingsVal ($d, "alarmTime",         "aa:bb:cc");
  
  my ($h,$m,$s,$txtc) = (0,0,0,0);
  
  my $bdist = "";                                                             # Abstand zum linken und rechten Rand
  for (my $i=0; $i<=$abdist; $i++ ) {
      $bdist .= " ";
  }
  
  if ($addp eq "stopwatch") {
      $alarmdef = "aa:bb:cc";                                                 # Stoppuhr bei Start 00:00:00 nicht Alerm auslösen
  }
  
  if ($addp eq "staticwatch") {                                               # statische Uhrzeitanzeige
      $h   = ReadingsVal($d, "hour"  , 0);
      $m   = ReadingsVal($d, "minute", 0);
      $s   = ReadingsVal($d, "second", 0);
  }

  my $back = << "END_JS";
     <html>
     <body>
     
     <canvas id='display_$d' $hattr style='background-color:#$bgc'></canvas>
    
     <script>
    // Segment display types
    SegmentDisplay_$d.SevenSegment    = 7;
    SegmentDisplay_$d.FourteenSegment = 14;
    SegmentDisplay_$d.SixteenSegment  = 16;

    // Segment corner types
    SegmentDisplay_$d.SymmetricCorner = 0;
    SegmentDisplay_$d.SquaredCorner   = 1;
    SegmentDisplay_$d.RoundedCorner   = 3;

    // Definition variables
    var state_$d;
    var st_$d;
    var ct_$d;
    var ci_$d;
    var csrf;
    var url_$d;
    var devName_$d;
    var selVal_$d;
    var hours_$d;
    var minutes_$d;
    var seconds_$d;
    var startDate_$d;
    var ddt_$d;
    var almtime0_$d        = '$alarm';                // Alarmzeit initialisieren
    var digitxt_$d         = '$deftxt';               // default Digitaltext initialisieren
    var tticker_$d         = '$rdtt';                 // Tickereinstellung initialisieren
    var zmodulo_$d         = 0;                       // Hilfszähler
    var showCurrTime_$d    = '$showct';               // Reading currtime schreiben oder nicht
    var distBorderright_$d = '$bdist';                // Abstand zum rechten Rand
    var distBorderleft_$d  = '$bdist';                // Abstand zum linken Rand 
    var asydone1_$d        = 0;                       // Statusbit dass asynchrone Datenkommunikation stattgefunden hat
    var asydone2_$d        = 0;                       // Statusbit dass asynchrone Datenkommunikation stattgefunden hat
    var allowSetStopTime;                             // erlaube / verbiete Setzen Reading stoptime 

    function SegmentDisplay_$d(displayId_$d) {
        this.displayId_$d    = displayId_$d;
        this.pattern         = '##:##:##';
        this.value           = '12:34:56';
        this.digitHeight     = 20;
        this.digitWidth      = 10;
        this.digitDistance   = 2.5;
        this.displayAngle    = 12;
        this.segmentWidth    = 2.5;
        this.segmentDistance = 0.2;

        if ($adst == '7') {
            this.segmentCount = SegmentDisplay_$d.SevenSegment;
        }
        
        if ($adst == '14') {
            this.segmentCount = SegmentDisplay_$d.FourteenSegment;
        }
        
        if ($adst == '16') {
            this.segmentCount = SegmentDisplay_$d.SixteenSegment;
        }
        
        this.cornerType      = SegmentDisplay_$d.RoundedCorner;
        this.colorOn         = 'rgb(233, 93, 15)';
        this.colorOff        = 'rgb(75, 30, 5)';
    };
    
    var display_$d = new SegmentDisplay_$d('display_$d');
    display_$d.cornerType      = 2;
    display_$d.displayAngle    = $adda;                          // Zeichenwinkel:  -30 - 30 (9)
    display_$d.digitHeight     = $addh;                          // Zeichenhöhe:    5   - 50 (20)
    display_$d.digitWidth      = $addw;                          // Zeichenbreite:  5   - 50 (12)
    display_$d.digitDistance   = $addd;                          // Zeichenabstand: 0.5 - 10 (2)
    display_$d.segmentWidth    = $adsw;                          // Stärke des einzelnen Segments: 0.3 - 3.5 (1.5)
    display_$d.segmentDistance = $adsd;                          // Abstand der Einzelsegmente:    0   - 5   (0.5)
    display_$d.colorOn         = '#$dcd';                        // original: display_$d.colorOn = 'rgba(0, 0, 0, 0.9)';
    display_$d.colorOff        = 'rgba(0, 0, 0, 0.1)';

    SegmentDisplay_$d.prototype.setValue = function(value) {
                                               this.value = value;
                                               this.draw();
                                           };

    SegmentDisplay_$d.prototype.draw = function() {
        var display_$d = document.getElementById(this.displayId_$d);
        if (display_$d) {
            var context = display_$d.getContext('2d');
            if (context) {
                // clear canvas
                context.clearRect(0, 0, display_$d.width, display_$d.height);
          
                // compute and check display width
                var width = 0;
                var first = true;
                if (this.pattern) {
                    for (var i = 0; i < this.pattern.length; i++) {
                        var c = this.pattern.charAt(i).toLowerCase();
                        if (c == '#') {
                            width += this.digitWidth;
                        } else if (c == '.' || c == ':') {
                            width += this.segmentWidth;
                        } else if (c != ' ') {
                            return;
                        }
                        width += first ? 0 : this.digitDistance;
                        first = false;
                    }
                }
                if (width <= 0) {
                    return;
                }
          
                // compute skew factor
                var angle = -1.0 * Math.max(-45.0, Math.min(45.0, this.displayAngle));
                var skew  = Math.tan((angle * Math.PI) / 180.0);
          
                // compute scale factor
                var scale = Math.min(display_$d.width / (width + Math.abs(skew * this.digitHeight)), display_$d.height / this.digitHeight);
          
                // compute display offset
                var offsetX = (display_$d.width - (width + skew * this.digitHeight) * scale) / 2.0;
                var offsetY = (display_$d.height - this.digitHeight * scale) / 2.0;
          
                // context transformation
                context.save();
                context.translate(offsetX, offsetY);
                context.scale(scale, scale);
                context.transform(1, 0, skew, 1, 0, 0);

                // draw segments
                var xPos = 0;
                var size = (this.value) ? this.value.length : 0;
                for (var i = 0; i < this.pattern.length; i++) {
                    var mask  = this.pattern.charAt(i);
                    var value = (i < size) ? this.value.charAt(i).toLowerCase() : ' ';
                    xPos += this.drawDigit(context, xPos, mask, value);
                }

                // finish drawing
                context.restore();
            }
        }
    };

    SegmentDisplay_$d.prototype.drawDigit = function(context, xPos, mask, c) {
        switch (mask) {
            case '#':
            var r = Math.sqrt(this.segmentWidth * this.segmentWidth / 2.0);
            var d = Math.sqrt(this.segmentDistance * this.segmentDistance / 2.0);
            var e = d / 2.0; 
            var f = (this.segmentWidth - d) * Math.sin((45.0 * Math.PI) / 180.0);
            var g = f / 2.0;
            var h = (this.digitHeight - 3.0 * this.segmentWidth) / 2.0;
            var w = (this.digitWidth - 3.0 * this.segmentWidth) / 2.0;
            var s = this.segmentWidth / 2.0;
            var t = this.digitWidth / 2.0;

            // draw segment a (a1 and a2 for 16 segments)
            if (this.segmentCount == 16) {
                var x = xPos;
                var y = 0;
                context.fillStyle = this.getSegmentColor_$d(c, null, '02356789abcdefgiopqrstz@%');
                context.beginPath();
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.moveTo(x + s + d, y + s);
                        context.lineTo(x + this.segmentWidth + d, y);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.moveTo(x + s + e, y + s - e);
                        context.lineTo(x + this.segmentWidth, y);
                        break;
                    default:
                        context.moveTo(x + this.segmentWidth - f, y + this.segmentWidth - f - d);
                        context.quadraticCurveTo(x + this.segmentWidth - g, y, x + this.segmentWidth, y);
                }
                context.lineTo(x + t - d - s, y);
                context.lineTo(x + t - d, y + s);
                context.lineTo(x + t - d - s, y + this.segmentWidth);
                context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                context.fill();
            
                var x = xPos;
                var y = 0;
                context.fillStyle = this.getSegmentColor_$d(c, null, '02356789abcdefgiopqrstz\@');
                context.beginPath();
                context.moveTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                context.lineTo(x + t + d + s, y + this.segmentWidth);
                context.lineTo(x + t + d, y + s);
                context.lineTo(x + t + d + s, y);
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                        context.lineTo(x + this.digitWidth - s - d, y + s);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y);
                        context.lineTo(x + this.digitWidth - s - e, y + s - e);
                        break;
                    default:
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y);
                        context.quadraticCurveTo(x + this.digitWidth - this.segmentWidth + g, y, x + this.digitWidth - this.segmentWidth + f, y + this.segmentWidth - f - d);
                }
                context.fill();
            
            } else {
                var x = xPos;
                var y = 0;
                context.fillStyle = this.getSegmentColor_$d(c, '02356789acefp', '02356789abcdefgiopqrstz\@');
                context.beginPath();
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.moveTo(x + s + d, y + s);
                        context.lineTo(x + this.segmentWidth + d, y);
                        context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                        context.lineTo(x + this.digitWidth - s - d, y + s);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.moveTo(x + s + e, y + s - e);
                        context.lineTo(x + this.segmentWidth, y);
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y);
                        context.lineTo(x + this.digitWidth - s - e, y + s - e);
                        break;
                    default:
                        context.moveTo(x + this.segmentWidth - f, y + this.segmentWidth - f - d);
                        context.quadraticCurveTo(x + this.segmentWidth - g, y, x + this.segmentWidth, y);
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y);
                        context.quadraticCurveTo(x + this.digitWidth - this.segmentWidth + g, y, x + this.digitWidth - this.segmentWidth + f, y + this.segmentWidth - f - d);
                }
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                context.fill();
            }
          
            // draw segment b
            x = xPos + this.digitWidth - this.segmentWidth;
            y = 0;
            context.fillStyle = this.getSegmentColor_$d(c, '01234789adhpy', '01234789abdhjmnopqruwy');
            context.beginPath();
            switch (this.cornerType) {
                case SegmentDisplay_$d.SymmetricCorner:
                    context.moveTo(x + s, y + s + d);
                    context.lineTo(x + this.segmentWidth, y + this.segmentWidth + d);
                    break;
                case SegmentDisplay_$d.SquaredCorner:
                    context.moveTo(x + s + e, y + s + e);
                    context.lineTo(x + this.segmentWidth, y + this.segmentWidth);
                    break;
                default:
                    context.moveTo(x + f + d, y + this.segmentWidth - f);
                    context.quadraticCurveTo(x + this.segmentWidth, y + this.segmentWidth - g, x + this.segmentWidth, y + this.segmentWidth);
            }
            context.lineTo(x + this.segmentWidth, y + h + this.segmentWidth - d);
            context.lineTo(x + s, y + h + this.segmentWidth + s - d);
            context.lineTo(x, y + h + this.segmentWidth - d);
            context.lineTo(x, y + this.segmentWidth + d);
            context.fill();
          
            // draw segment c
            x = xPos + this.digitWidth - this.segmentWidth;
            y = h + this.segmentWidth;
            context.fillStyle = this.getSegmentColor_$d(c, '013456789abdhnouy', '01346789abdghjmnoqsuw\@', '%');
            context.beginPath();
            context.moveTo(x, y + this.segmentWidth + d);
            context.lineTo(x + s, y + s + d);
            context.lineTo(x + this.segmentWidth, y + this.segmentWidth + d);
            context.lineTo(x + this.segmentWidth, y + h + this.segmentWidth - d);
            switch (this.cornerType) {
                case SegmentDisplay_$d.SymmetricCorner:
                    context.lineTo(x + s, y + h + this.segmentWidth + s - d);
                    context.lineTo(x, y + h + this.segmentWidth - d);
                    break;
                case SegmentDisplay_$d.SquaredCorner:
                    context.lineTo(x + s + e, y + h + this.segmentWidth + s - e);
                    context.lineTo(x, y + h + this.segmentWidth - d);
                    break;
                default:
                    context.quadraticCurveTo(x + this.segmentWidth, y + h + this.segmentWidth + g, x + f + d, y + h + this.segmentWidth + f);
                    context.lineTo(x, y + h + this.segmentWidth - d);
            }
            context.fill();
          
            // draw segment d (d1 and d2 for 16 segments)
            if (this.segmentCount == 16) {
                x = xPos;
                y = this.digitHeight - this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, '0235689bcdegijloqsuz_=\@');
                context.beginPath();
                context.moveTo(x + this.segmentWidth + d, y);
                context.lineTo(x + t - d - s, y);
                context.lineTo(x + t - d, y + s);
                context.lineTo(x + t - d - s, y + this.segmentWidth);
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                        context.lineTo(x + s + d, y + s);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.lineTo(x + this.segmentWidth, y + this.segmentWidth);
                        context.lineTo(x + s + e, y + s + e);
                        break;
                    default:
                        context.lineTo(x + this.segmentWidth, y + this.segmentWidth);
                        context.quadraticCurveTo(x + this.segmentWidth - g, y + this.segmentWidth, x + this.segmentWidth - f, y + f + d);
                        context.lineTo(x + this.segmentWidth - f, y + f + d);
                }
                context.fill();

                x = xPos;
                y = this.digitHeight - this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, '0235689bcdegijloqsuz_=\@', '%');
                context.beginPath();
                context.moveTo(x + t + d + s, y + this.segmentWidth);
                context.lineTo(x + t + d, y + s);
                context.lineTo(x + t + d + s, y);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.lineTo(x + this.digitWidth - s - d, y + s);
                        context.lineTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.lineTo(x + this.digitWidth - s - e, y + s + e);
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y + this.segmentWidth);
                        break;
                    default:
                        context.lineTo(x + this.digitWidth - this.segmentWidth + f, y + f + d);
                        context.quadraticCurveTo(x + this.digitWidth - this.segmentWidth + g, y + this.segmentWidth, x + this.digitWidth - this.segmentWidth, y + this.segmentWidth);
                }
                context.fill();
            
            } else {
                x = xPos;
                y = this.digitHeight - this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, '0235689bcdelotuy_', '0235689bcdegijloqsuz_=\@');
                context.beginPath();
                context.moveTo(x + this.segmentWidth + d, y);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                switch (this.cornerType) {
                    case SegmentDisplay_$d.SymmetricCorner:
                        context.lineTo(x + this.digitWidth - s - d, y + s);
                        context.lineTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                        context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                        context.lineTo(x + s + d, y + s);
                        break;
                    case SegmentDisplay_$d.SquaredCorner:
                        context.lineTo(x + this.digitWidth - s - e, y + s + e);
                        context.lineTo(x + this.digitWidth - this.segmentWidth, y + this.segmentWidth);
                        context.lineTo(x + this.segmentWidth, y + this.segmentWidth);
                        context.lineTo(x + s + e, y + s + e);
                        break;
                    default:
                        context.lineTo(x + this.digitWidth - this.segmentWidth + f, y + f + d);
                        context.quadraticCurveTo(x + this.digitWidth - this.segmentWidth + g, y + this.segmentWidth, x + this.digitWidth - this.segmentWidth, y + this.segmentWidth);
                        context.lineTo(x + this.segmentWidth, y + this.segmentWidth);
                        context.quadraticCurveTo(x + this.segmentWidth - g, y + this.segmentWidth, x + this.segmentWidth - f, y + f + d);
                        context.lineTo(x + this.segmentWidth - f, y + f + d);
                }
                context.fill();
            }
          
            // draw segment e
            x = xPos;
            y = h + this.segmentWidth;
            context.fillStyle = this.getSegmentColor_$d(c, '0268abcdefhlnoprtu', '0268acefghjklmnopqruvw\@');
            context.beginPath();
            context.moveTo(x, y + this.segmentWidth + d);
            context.lineTo(x + s, y + s + d);
            context.lineTo(x + this.segmentWidth, y + this.segmentWidth + d);
            context.lineTo(x + this.segmentWidth, y + h + this.segmentWidth - d);
            switch (this.cornerType) {
                case SegmentDisplay_$d.SymmetricCorner:
                    context.lineTo(x + s, y + h + this.segmentWidth + s - d);
                    context.lineTo(x, y + h + this.segmentWidth - d);
                    break;
                case SegmentDisplay_$d.SquaredCorner:
                    context.lineTo(x + s - e, y + h + this.segmentWidth + s - d + e);
                    context.lineTo(x, y + h + this.segmentWidth);
                    break;
                default:
                    context.lineTo(x + this.segmentWidth - f - d, y + h + this.segmentWidth + f); 
                    context.quadraticCurveTo(x, y + h + this.segmentWidth + g, x, y + h + this.segmentWidth);
            }
            context.fill();
          
            // draw segment f
            x = xPos;
            y = 0;
            context.fillStyle = this.getSegmentColor_$d(c, '045689abcefhlpty', '045689acefghklmnopqrsuvwy\@', '%');
            context.beginPath();
            context.moveTo(x + this.segmentWidth, y + this.segmentWidth + d);
            context.lineTo(x + this.segmentWidth, y + h + this.segmentWidth - d);
            context.lineTo(x + s, y + h + this.segmentWidth + s - d);
            context.lineTo(x, y + h + this.segmentWidth - d);
            switch (this.cornerType) {
                case SegmentDisplay_$d.SymmetricCorner:
                    context.lineTo(x, y + this.segmentWidth + d);
                    context.lineTo(x + s, y + s + d);
                    break;
                case SegmentDisplay_$d.SquaredCorner:
                    context.lineTo(x, y + this.segmentWidth);
                    context.lineTo(x + s - e, y + s + e);
                    break;
                default:
                    context.lineTo(x, y + this.segmentWidth);
                    context.quadraticCurveTo(x, y + this.segmentWidth - g, x + this.segmentWidth - f - d, y + this.segmentWidth - f); 
                    context.lineTo(x + this.segmentWidth - f - d, y + this.segmentWidth - f); 
            }
            context.fill();

            // draw segment g for 7 segments
            if (this.segmentCount == 7) {
                x = xPos;
                y = (this.digitHeight - this.segmentWidth) / 2.0;
                context.fillStyle = this.getSegmentColor_$d(c, '2345689abdefhnoprty-=');
                context.beginPath();
                context.moveTo(x + s + d, y + s);
                context.lineTo(x + this.segmentWidth + d, y);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                context.lineTo(x + this.digitWidth - s - d, y + s);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                context.fill();
            }
                
            // draw inner segments for the fourteen- and sixteen-segment-display
            if (this.segmentCount != 7) {
                // draw segment g1
                x = xPos;
                y = (this.digitHeight - this.segmentWidth) / 2.0;
                context.fillStyle = this.getSegmentColor_$d(c, null, '2345689aefhkprsy-+*=', '%');
                context.beginPath();
                context.moveTo(x + s + d, y + s);
                context.lineTo(x + this.segmentWidth + d, y);
                context.lineTo(x + t - d - s, y);
                context.lineTo(x + t - d, y + s);
                context.lineTo(x + t - d - s, y + this.segmentWidth);
                context.lineTo(x + this.segmentWidth + d, y + this.segmentWidth);
                context.fill();
                
                // draw segment g2
                x = xPos;
                y = (this.digitHeight - this.segmentWidth) / 2.0;
                context.fillStyle = this.getSegmentColor_$d(c, null, '234689abefghprsy-+*=\@', '%');
                context.beginPath();
                context.moveTo(x + t + d, y + s);
                context.lineTo(x + t + d + s, y);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y);
                context.lineTo(x + this.digitWidth - s - d, y + s);
                context.lineTo(x + this.digitWidth - this.segmentWidth - d, y + this.segmentWidth);
                context.lineTo(x + t + d + s, y + this.segmentWidth);
                context.fill();
                
                // draw segment j 
                x = xPos + t - s;
                y = 0;
                context.fillStyle = this.getSegmentColor_$d(c, null, 'bdit+*', '%');
                context.beginPath();
                if (this.segmentCount == 14) {
                    context.moveTo(x, y + this.segmentWidth + this.segmentDistance);
                    context.lineTo(x + this.segmentWidth, y + this.segmentWidth + this.segmentDistance);
                } else {
                    context.moveTo(x, y + this.segmentWidth + d);
                    context.lineTo(x + s, y + s + d);
                    context.lineTo(x + this.segmentWidth, y + this.segmentWidth + d);
                }
                context.lineTo(x + this.segmentWidth, y + h + this.segmentWidth - d);
                context.lineTo(x + s, y + h + this.segmentWidth + s - d);
                context.lineTo(x, y + h + this.segmentWidth - d);
                context.fill();
            
                // draw segment m
                x = xPos + t - s;
                y = this.digitHeight;
                context.fillStyle = this.getSegmentColor_$d(c, null, 'bdity+*\@', '%');
                context.beginPath();
                if (this.segmentCount == 14) {
                    context.moveTo(x, y - this.segmentWidth - this.segmentDistance);
                    context.lineTo(x + this.segmentWidth, y - this.segmentWidth - this.segmentDistance);
                } else {
                    context.moveTo(x, y - this.segmentWidth - d);
                    context.lineTo(x + s, y - s - d);
                    context.lineTo(x + this.segmentWidth, y - this.segmentWidth - d);
                }
                context.lineTo(x + this.segmentWidth, y - h - this.segmentWidth + d);
                context.lineTo(x + s, y - h - this.segmentWidth - s + d);
                context.lineTo(x, y - h - this.segmentWidth + d);
                context.fill();
            
                // draw segment h
                x = xPos + this.segmentWidth;
                y = this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, 'mnx\\\\*');
                context.beginPath();
                context.moveTo(x + this.segmentDistance, y + this.segmentDistance);
                context.lineTo(x + this.segmentDistance + r, y + this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance , y + h - this.segmentDistance - r);
                context.lineTo(x + w - this.segmentDistance , y + h - this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance - r , y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + this.segmentDistance + r);
                context.fill();
                
                // draw segment k
                x = xPos + w + 2.0 * this.segmentWidth;
                y = this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, '0kmvxz/*', '%');
                context.beginPath();
                context.moveTo(x + w - this.segmentDistance, y + this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance, y + this.segmentDistance + r);
                context.lineTo(x + this.segmentDistance + r, y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + h - this.segmentDistance - r);
                context.lineTo(x + w - this.segmentDistance - r, y + this.segmentDistance);
                context.fill();
            
                // draw segment l
                x = xPos + w + 2.0 * this.segmentWidth;
                y = h + 2.0 * this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, '5knqrwx\\\\*');
                context.beginPath();
                context.moveTo(x + this.segmentDistance, y + this.segmentDistance);
                context.lineTo(x + this.segmentDistance + r, y + this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance , y + h - this.segmentDistance - r);
                context.lineTo(x + w - this.segmentDistance , y + h - this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance - r , y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + this.segmentDistance + r);
                context.fill();
                
                // draw segment n
                x = xPos + this.segmentWidth;
                y = h + 2.0 * this.segmentWidth;
                context.fillStyle = this.getSegmentColor_$d(c, null, '0vwxz/*', '%');
                context.beginPath();
                context.moveTo(x + w - this.segmentDistance, y + this.segmentDistance);
                context.lineTo(x + w - this.segmentDistance, y + this.segmentDistance + r);
                context.lineTo(x + this.segmentDistance + r, y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + h - this.segmentDistance);
                context.lineTo(x + this.segmentDistance, y + h - this.segmentDistance - r);
                context.lineTo(x + w - this.segmentDistance - r, y + this.segmentDistance);
                context.fill();
            }
          
            return this.digitDistance + this.digitWidth;
          
            case '.':
                context.fillStyle = (c == '#') || (c == '.') ? this.colorOn : this.colorOff;
                this.drawPoint(context, xPos, this.digitHeight - this.segmentWidth, this.segmentWidth);
                return this.digitDistance + this.segmentWidth;
          
            case ':':
                context.fillStyle = (c == '#') || (c == ':') ? this.colorOn : this.colorOff;
                var y = (this.digitHeight - this.segmentWidth) / 2.0 - this.segmentWidth;
                this.drawPoint(context, xPos, y, this.segmentWidth);
                this.drawPoint(context, xPos, y + 2.0 * this.segmentWidth, this.segmentWidth);
                return this.digitDistance + this.segmentWidth;
          
            default:
                return this.digitDistance;    
        }
    };

    SegmentDisplay_$d.prototype.drawPoint = function(context, x1, y1, size) {
        var x2 = x1 + size;
        var y2 = y1 + size;
        var d  = size / 4.0;
      
        context.beginPath();
        context.moveTo(x2 - d, y1);
        context.quadraticCurveTo(x2, y1, x2, y1 + d);
        context.lineTo(x2, y2 - d);
        context.quadraticCurveTo(x2, y2, x2 - d, y2);
        context.lineTo(x1 + d, y2);
        context.quadraticCurveTo(x1, y2, x1, y2 - d);
        context.lineTo(x1, y1 + d);
        context.quadraticCurveTo(x1, y1, x1 + d, y1);
        context.fill();
    }; 

    SegmentDisplay_$d.prototype.getSegmentColor_$d = function(c, charSet7, charSet14, charSet16) {
                                                         if (c == '#') {
                                                             return this.colorOn;
                                                         } else {
                                                             switch (this.segmentCount) {
                                                                 case 7:  return (charSet7.indexOf(c) == -1) ? this.colorOff : this.colorOn;
                                                                 case 14: return (charSet14.indexOf(c) == -1) ? this.colorOff : this.colorOn;
                                                                 case 16: var pattern = charSet14 + (charSet16 === undefined ? '' : charSet16);
                                                                 return (pattern.indexOf(c) == -1) ? this.colorOff : this.colorOn;
                                                                 default: return this.colorOff;
                                                             }
                                                         }
                                                     };
    
    // CSRF-Token auslesen
    var body = document.querySelector("body");
    if( body != null ) {
        csrf = body.getAttribute("fwcsrf");
    }
 
    // get the base url
    function getBaseUrl () {
        var url = window.location.href.split("?")[0];
        url += "?";
        if( csrf != null ) {
            url += "fwcsrf="+csrf+"&";
        }
        return url;
    }

    function makeCommand (cmd) {
        return getBaseUrl()+"cmd="+encodeURIComponent(cmd)+"&XHR=1";
    }
    
    // Template digital time display
    function buildtime (hours, minutes, seconds) {
        var ddt = ((hours   < 10) ? '0' : '') + hours   + ':' + 
                  ((minutes < 10) ? '0' : '') + minutes + ':' + 
                  ((seconds < 10) ? '0' : '') + seconds
                  ;   
        return ddt;
    }
    
    // localStorage Set 
    function localStoreSet_$d (hours, minutes, seconds, sumsecs, aSetStopT) {
        if (Number.isInteger(hours))     { localStorage.setItem('h_$d',   hours);     }
        if (Number.isInteger(minutes))   { localStorage.setItem('m_$d',   minutes);   }
        if (Number.isInteger(seconds))   { localStorage.setItem('s_$d',   seconds);   }
        if (Number.isInteger(sumsecs))   { localStorage.setItem('ss_$d',  sumsecs);   }
        if (Number.isInteger(aSetStopT)) { localStorage.setItem('ast_$d', aSetStopT); }
    }
    
    // localStorage speichern letzte Alarmzeit
    function localStoreSetLastalm_$d (dev, lastalmtime) {
        localStorage.setItem('lastalmtime_'+dev, lastalmtime);
    }
    
    // Reading currtime setzen
    function setrcurrtime (h_$d, m_$d, s_$d) {
        var time_$d = buildtime (h_$d, m_$d, s_$d);
        if (modulo2_$d != zmodulo_$d && showCurrTime_$d != 0 && time_$d != 'NaN:NaN:NaN') {
            command = '{ CommandSetReading(undef, "$d currtime '+time_$d+'") }';  
            url_$d  = makeCommand(command);
            \$.get(url_$d);
        }   
    }
    
    // Check ob Alarm ausgelöst werden soll und ggf. Alarmevent triggern
    function checkAndDoAlm_$d (dev, acttime, almtime) {
        lastalmtime_$d = localStorage.getItem('lastalmtime_$d');                              // letzte Alarmzeit laden        
        if ( (acttime == almtime || acttime == '$alarmdef') && acttime != lastalmtime_$d ) {
            command = '{ CommandSetReading(undef, "$d alarmed '+acttime+'") }';
            url_$d  = makeCommand(command);
              
            localStoreSetLastalm_$d (dev, acttime);                                           // aktuelle Alarmzeit sichern 
              
            if(acttime == almtime) {
               \$.get(url_$d);
            
            } else {
                \$.get(url_$d, function (data) {
                                command = '{ CommandSetReading(undef, "$d state stopped") }';
                                url_$d  = makeCommand(command);
                                \$.get(url_$d);
                            }
                      );                                       
            }
        }
    }
    
    animate_$d();
    
    function animate_$d() {
        var watchkind_$d = '$addp';
        var cycletime    = new Date();
        var cycleseconds = cycletime.getSeconds();
        modulo2_$d       = cycleseconds % 2;                                           // Taktung für Reading lesen/schreiben (Serverauslastung reduzieren)

        if (watchkind_$d == 'watch') {                  
            if (modulo2_$d != zmodulo_$d) {
                command = '{ReadingsVal("$d","alarmTime","+almtime0_$d+")}';     // alarmTime Reading lesen
                url_$d  = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    almtime0_$d = data.replace(/\\n/g, '');
                                    zmodulo_$d = modulo2_$d;
                                    return (almtime0_$d, zmodulo_$d);
                                } 
                      );
            }            
            
            // Zeitsteuerung - aktueller Timestamp in Millisekunden
            if ('$tsou' == 'server') {                   // Serverzeit
                command = '{ int(time*1000) }';
                url_$d  = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    data = data.replace(/\\n/g, ''); 
                                    ct_$d = parseInt(data); 
                                    return ct_$d;
                                } 
                      ); 
              
                time_$d  = new Date(ct_$d);
              
            } else {
                time_$d  = new Date();                                              // Clientzeit
            }
          
            if (typeof ct_$d === 'undefined') {                                     // wenn Zeit noch undef mit lokaler Zeit initialisieren -> springen Zeiger verhindern
                time_$d  = new Date();                   
            } else {
                time_$d  = new Date(ct_$d);
            }                  
                  
            var hours_$d   = time_$d.getHours();
            var minutes_$d = time_$d.getMinutes();
            var seconds_$d = time_$d.getSeconds();
            
            acttime_$d = ((hours_$d   < 10) ? '0' : '') + hours_$d   + ':' + 
                         ((minutes_$d < 10) ? '0' : '') + minutes_$d + ':' + 
                         ((seconds_$d < 10) ? '0' : '') + seconds_$d;
                         
            if (acttime_$d == '00:00:00') {
                localStoreSetLastalm_$d ('$d', 'NaN');                              // letzte Alarmzeit zurücksetzen zum Tageswechsel            
            }

            checkAndDoAlm_$d ('$d', acttime_$d, almtime0_$d);
            
            setrcurrtime (hours_$d, minutes_$d, seconds_$d);                        // Reading currtime mit angezeigter Zeit setzen
        }
        
        if (watchkind_$d == 'staticwatch') {
            var hours_$d   = '$h';
            var minutes_$d = '$m';
            var seconds_$d = '$s';
            
            setrcurrtime (hours_$d, minutes_$d, seconds_$d);                         // Reading currtime mit angezeigter Zeit setzen
        }
        
        if (watchkind_$d == 'stopwatch') {           
            command = '{ReadingsVal("$d","state","")}';                              // state Reading lesen
            url_$d  = makeCommand(command);
            \$.get( url_$d, function (data) {
                                state_$d = data.replace(/\\n/g, '');                                
                                return state_$d;
                            } 
                  );
            
            if (state_$d == 'started' || state_$d == 'resumed') { 
                localStoreSet_$d        (NaN, NaN, NaN, NaN, 1);                     // set Reading stoptime freischalten
                
                if (modulo2_$d != zmodulo_$d) {
                    command = '{ReadingsVal("$d","alarmTime","+almtime0_$d+")}';     // alarmTime Reading lesen
                    url_$d  = makeCommand(command);
                    \$.get( url_$d, function (data) {
                                        almtime0_$d = data.replace(/\\n/g, '');
                                        zmodulo_$d  = modulo2_$d;
                                        return (almtime0_$d, zmodulo_$d);
                                    } 
                          );
                }
                
                // == Startzeit ==            
                command   = '{ReadingsNum("$d","starttime", 0)}';
                url_$d    = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    data        = data.replace(/\\n/g, ''); 
                                    st_$d       = parseInt(data); 
                                    asydone1_$d = 1;
                                    return (st_$d, asydone1_$d);
                                } 
                      );
                
                startDate_$d = new Date(st_$d);
                
                // aktueller Timestamp in Millisekunden      
                currDate_$d  = new Date();            
                elapsesec_$d = ((currDate_$d.getTime() - startDate_$d.getTime()))/1000;    // vergangene Millisekunden in Sekunden
                
                if (state_$d == 'resumed') {
                    lastsumsec_$d = localStorage.getItem('ss_$d');                                                      
                    elapsesec_$d  = parseInt(elapsesec_$d) + parseInt(lastsumsec_$d);
                } else {
                    elapsesec_$d  = parseInt(elapsesec_$d);
                }
                
                if (state_$d == 'started' && elapsesec_$d <= 5) {
                    localStoreSetLastalm_$d ('$d', 'NaN');                               // letzte Alarmzeit zurücksetzen bis 2 s nach Start            
                }
                
                hours_$d       = parseInt(elapsesec_$d / 3600);
                elapsesec_$d  -= hours_$d * 3600;
                minutes_$d     = parseInt(elapsesec_$d / 60);
                seconds_$d     = parseInt(elapsesec_$d - minutes_$d * 60);
                
                ddt_$d = buildtime (hours_$d, minutes_$d, seconds_$d);
                
                if(asydone1_$d == 1) {                                                  // vermeidet zufällige Alarmierung bei start / resume
                    checkAndDoAlm_$d ('$d', ddt_$d, almtime0_$d);                       // Alarm auslösen wenn zutreffend
                }
                
                localStoreSet_$d (hours_$d, minutes_$d, seconds_$d, NaN);
                
                setrcurrtime (hours_$d, minutes_$d, seconds_$d);                        // Reading currtime mit angezeigter Zeit setzen
                
                if(asydone1_$d == 0) {                                                  // vermeidet zufällige Zeitanzeige bei start / resume
                    hours_$d   = 'NaN';
                    minutes_$d = 'NaN';
                    seconds_$d = 'NaN';                 
                }
            }
            
            if (state_$d == 'stopped') {
                hours_$d   = localStorage.getItem('h_$d');
                minutes_$d = localStorage.getItem('m_$d');
                seconds_$d = localStorage.getItem('s_$d');
                
                sumsecs_$d = parseInt(hours_$d*3600) + parseInt(minutes_$d*60) + parseInt(seconds_$d);
                localStoreSet_$d        (NaN, NaN, NaN, sumsecs_$d);
                
                allowSetStopTime = localStorage.getItem('ast_$d');
                if(allowSetStopTime == 1) {
                    ddt_$d = buildtime (hours_$d, minutes_$d, seconds_$d);             // Reading mit Stoppzeit setzen

                    command = '{ CommandSetReading(undef, "$d stoptime '+ddt_$d+'") }';
                    url_$d  = makeCommand(command);
                    \$.get(url_$d);
                }
                localStoreSet_$d        (NaN, NaN, NaN, NaN, 0);                      // set Reading stoptime verbieten
                
                asydone1_$d = 0;
            }

            if (state_$d == 'initialized') {
                hours_$d   = 0;
                minutes_$d = 0;
                seconds_$d = 0;

                localStoreSet_$d (hours_$d, minutes_$d, seconds_$d);
                localStoreSetLastalm_$d ('$d', 'NaN');                                  // letzte Alarmzeit zurücksetzen 

                asydone1_$d = 0;                
            }
        }
        
        if (watchkind_$d == 'countdownwatch') {                                    
            command = '{ReadingsVal("$d","state","")}';
            url_$d  = makeCommand(command);
            \$.get( url_$d, function (data) {
                                state_$d = data.replace(/\\n/g, '');                      
                                return state_$d;
                            } 
                  );
            
            if (state_$d == 'started' || state_$d == 'resumed') {                
                if (modulo2_$d != zmodulo_$d) {
                    command = '{ReadingsVal("$d","alarmTime","+almtime0_$d+")}';     // alarmTime Reading lesen
                    url_$d  = makeCommand(command);
                    \$.get( url_$d, function (data) {
                                        almtime0_$d = data.replace(/\\n/g, '');
                                        zmodulo_$d = modulo2_$d;  
                                        return (almtime0_$d, zmodulo_$d);
                                    } 
                          );
                }
                
                // == Ermittlung Countdown Startwert ==
                if (modulo2_$d != zmodulo_$d) {                
                    command   = '{ReadingsNum("$d","countInitVal", 0)}';
                    url_$d    = makeCommand(command);
                    \$.get( url_$d, function (data) {
                                        data  = data.replace(/\\n/g, ''); 
                                        ci_$d = parseInt(data); 
                                        return ci_$d;
                                    } 
                          );
                }
                      
                if (state_$d == 'resumed') {
                    countInitVal_$d = localStorage.getItem('ss_$d');
                } else {
                    countInitVal_$d = parseInt(ci_$d);                                   // Initialwert Countdown in Sekunden 
                }
                
                // == Ermittlung vergangene Sekunden ==                  
                command = '{ReadingsNum("$d","starttime", 0)}';
                url_$d  = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    data        = data.replace(/\\n/g, ''); 
                                    st_$d       = parseInt(data);
                                    asydone1_$d = 1;                                    
                                    return (st_$d, asydone1_$d);
                                } 
                      );                     
                startDate_$d = new Date(st_$d);
                
                // aktueller Timestamp in Millisekunden 
                command   = '{ int(time*1000) }';
                url_$d    = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    data        = data.replace(/\\n/g, ''); 
                                    ct_$d       = parseInt(data);
                                    asydone2_$d = 1;                                    
                                    return (ct_$d, asydone2_$d);
                                } 
                      );   
                currDate_$d  = new Date(ct_$d);
                
                elapsesec_$d = (currDate_$d.getTime() - startDate_$d.getTime())/1000;    // vergangene Millisekunden in Sekunden umrechnen
                
                if (state_$d == 'started' && elapsesec_$d <= 5) {
                    localStoreSetLastalm_$d ('$d', 'NaN');                               // letzte Alarmzeit zurücksetzen bis 2 s nach Start            
                }                
                
                // == Countdown errechnen ==
                countcurr_$d = parseInt(countInitVal_$d) - parseInt(elapsesec_$d);
                //log("countInitVal_$d: "+countInitVal_$d+", elapsesec_$d"+elapsesec_$d+", countcurr_$d: "+countcurr_$d);
                
                hours_$d       = parseInt(countcurr_$d / 3600);
                countcurr_$d  -= hours_$d * 3600;
                minutes_$d     = parseInt(countcurr_$d / 60);
                seconds_$d     = parseInt(countcurr_$d - minutes_$d * 60);
     
                if (countcurr_$d >= 0) {
                    ddt_$d = buildtime (hours_$d, minutes_$d, seconds_$d);
                    if(asydone1_$d == 1 && asydone2_$d == 1) {                          // vermeidet zufällige Alarmierung bei start / resume
                        checkAndDoAlm_$d ('$d', ddt_$d, almtime0_$d);                   // Alarm auslösen wenn zutreffend
                    }
                    localStoreSet_$d (hours_$d, minutes_$d, seconds_$d, NaN);
                } else {
                    hours_$d   = 0;
                    minutes_$d = 0;
                    seconds_$d = 0;
                }
                
                setrcurrtime (hours_$d, minutes_$d, seconds_$d);                        // Reading currtime mit angezeigter Zeit setzen
            
                if(asydone1_$d == 0 && asydone2_$d == 0) {                              // vermeidet zufällige Zeitanzeige bei start / resume
                    hours_$d   = 'NaN';
                    minutes_$d = 'NaN';
                    seconds_$d = 'NaN';                 
                }
            }
            
            if (state_$d == 'stopped') {
                hours_$d   = localStorage.getItem('h_$d');
                minutes_$d = localStorage.getItem('m_$d');
                seconds_$d = localStorage.getItem('s_$d');
                
                pastsumsec_$d = parseInt(hours_$d*3600) + parseInt(minutes_$d*60) + parseInt(seconds_$d);
                localStoreSet_$d (NaN, NaN, NaN, pastsumsec_$d);
                
                asydone1_$d = 0;
                asydone2_$d = 0;
            }

            if (state_$d == 'initialized') {
                hours_$d   = 0;
                minutes_$d = 0;
                seconds_$d = 0;
                
                localStoreSet_$d (hours_$d, minutes_$d, seconds_$d);
                localStoreSetLastalm_$d ('$d', 'NaN');                                 // letzte Alarmzeit zurücksetzen
                
                asydone1_$d = 0;
                asydone2_$d = 0;
            }
        }
        
        if (watchkind_$d == 'text') {  
            tlength_$d = digitxt_$d.length;                                            // Länge des Textes
            if($adtdn > 0) {
                tlength_$d = $adtdn;
            }  
            
            display_$d.pattern = '';                                          // Template erstellen
            forerun_$d         = '';                                          // Vorlauf Textticker initialisieren
            for (var i = 0; i <= tlength_$d; i++) {
                display_$d.pattern += '#';
                forerun_$d         += ' ';
            }
            display_$d.pattern += distBorderright_$d;                         // Abstand Text zum rechten Rand  
            display_$d.pattern  = distBorderleft_$d + display_$d.pattern      
                        
            if (tticker_$d == 'on') {                                         // Text als Laufband ?                         
                var rttime    = new Date();
                var rthours   = rttime.getHours();
                var rtminutes = rttime.getMinutes();
                var rtseconds = rttime.getSeconds();
                var rtmillis  = rttime.getMilliseconds();
                
                var text_$d  = forerun_$d+digitxt_$d+'      ';
                var index_$d = ( 2 * (rtseconds + 60*rtminutes + 24*60*rthours) + Math.floor(rtmillis / 500) ) % (text_$d.length - 6);
                value_$d     = distBorderleft_$d + text_$d.substr(index_$d, tlength_$d+1);
            
            } else {
                value_$d = distBorderleft_$d + digitxt_$d;
            }
            
            if (modulo2_$d != zmodulo_$d) {
                command = '{ReadingsVal("$d","displayText", "$deftxt")}';           // Text dynamisch aus Reading lesen
                url_$d  = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    digitxt_$d = data.replace(/\\n/g, '');
                                    return (digitxt_$d);                                     
                                } 
                      );
            }
                  
            if (modulo2_$d != zmodulo_$d) {
                command = '{ReadingsVal("$d","displayTextTicker", "off")}';         // Textticker Einstellung aus Reading lesen
                url_$d  = makeCommand(command);
                \$.get( url_$d, function (data) {
                                    tticker_$d = data.replace(/\\n/g, '');
                                    zmodulo_$d = modulo2_$d;  
                                    return (tticker_$d, zmodulo_$d);                                    
                                } 
                      );
            }
        
        } else { 
            display_$d.pattern = distBorderleft_$d + '##:##:##' + distBorderright_$d;    // Textschablone initialisieren        
            ddt_$d   = buildtime (hours_$d, minutes_$d, seconds_$d);            
            value_$d = distBorderleft_$d + ddt_$d;
            
            if(value_$d == distBorderleft_$d + 'undefined:undefined:undefined' || value_$d == distBorderleft_$d + 'NaN:NaN:NaN') {
               value_$d = distBorderleft_$d + '  :  :  '; 
            }
        }
        
        display_$d.setValue(value_$d);
        window.setTimeout('animate_$d()', 200);
    }

    </script>
    </body>
    </html>      
  
END_JS

return qq{$back};
}

##############################################################################
#                            Bahnhofsuhr aus:
#                  http://www.3quarks.com/de/Bahnhofsuhr
#
##############################################################################
sub stationWatch {
  my ($d)    = @_;
  my $hash   = $defs{$d};
  my $ssh    = AttrVal ($d, "stationSecondHand",         "Bar").      "SecondHand";
  my $shb    = AttrVal ($d, "stationSecondHandBehavoir", "Bouncing"). "SecondHand";
  my $smh    = AttrVal ($d, "stationMinuteHand",         "Pointed").  "MinuteHand";
  my $mhb    = AttrVal ($d, "stationMinuteHandBehavoir", "Bouncing"). "MinuteHand";
  my $shh    = AttrVal ($d, "stationHourHand",           "Pointed").  "HourHand";
  my $sb     = AttrVal ($d, "stationBoss",               "Red").      "Boss"; 
  my $ssd    = AttrVal ($d, "stationStrokeDial",         "Swiss").    "StrokeDial";
  my $sbody  = AttrVal ($d, "stationBody",               "Round").    "Body";
  my $hattr  = AttrVal ($d, "htmlattr",                  "width='150' height='150'");
  my $tsou   = AttrVal ($d, "timeSource",                "client"); 
  my $showct = AttrVal ($d, "timeAsReading",             0);
  
  my $alarm  = ReadingsVal($d, "alarmTime", "aa:bb:cc");

  my $back = << "END_JS";
      <html>
      <body>  
      <canvas id='clock_$d' $hattr> 
      </canvas>
      
      <script>  
      
      var ct_$d;
      var almtime0_$d     = '$alarm';
      var time_$d;       
      var zmodulo_$d      = 0;                       // Hilfszähler 
      var showCurrTime_$d = '$showct';               // Reading currtime schreiben oder nicht      
      
      // clock body (Uhrgehäuse)
      StationClock_$d.NoBody         = 0;
      StationClock_$d.SmallWhiteBody = 1;
      StationClock_$d.RoundBody      = 2;
      StationClock_$d.RoundGreenBody = 3;
      StationClock_$d.SquareBody     = 4;
      StationClock_$d.ViennaBody     = 5;

      // stroke dial (Zifferblatt)
      StationClock_$d.NoDial               = 0;
      StationClock_$d.GermanHourStrokeDial = 1;
      StationClock_$d.GermanStrokeDial     = 2;
      StationClock_$d.AustriaStrokeDial    = 3;
      StationClock_$d.SwissStrokeDial      = 4;
      StationClock_$d.ViennaStrokeDial     = 5;

      //clock hour hand (Stundenzeiger)
      StationClock_$d.PointedHourHand = 1;
      StationClock_$d.BarHourHand     = 2;
      StationClock_$d.SwissHourHand   = 3;
      StationClock_$d.ViennaHourHand  = 4;

      //clock minute hand (Minutenzeiger)
      StationClock_$d.PointedMinuteHand = 1;
      StationClock_$d.BarMinuteHand     = 2;
      StationClock_$d.SwissMinuteHand   = 3;
      StationClock_$d.ViennaMinuteHand  = 4;

      //clock second hand (Sekundenzeiger)
      StationClock_$d.NoSecondHand            = 0;
      StationClock_$d.BarSecondHand           = 1;
      StationClock_$d.HoleShapedSecondHand    = 2;
      StationClock_$d.NewHoleShapedSecondHand = 3;
      StationClock_$d.SwissSecondHand         = 4;

      // clock boss (Zeigerabdeckung)
      StationClock_$d.NoBoss     = 0;
      StationClock_$d.BlackBoss  = 1;
      StationClock_$d.RedBoss    = 2;
      StationClock_$d.ViennaBoss = 3;

      // minute hand behavoir
      StationClock_$d.CreepingMinuteHand        = 0;
      StationClock_$d.BouncingMinuteHand        = 1;
      StationClock_$d.ElasticBouncingMinuteHand = 2;

      // second hand behavoir
      StationClock_$d.CreepingSecondHand        = 0;
      StationClock_$d.BouncingSecondHand        = 1;
      StationClock_$d.ElasticBouncingSecondHand = 2;
      StationClock_$d.OverhastySecondHand       = 3;
      
      // CSRF-Token auslesen
      var body = document.querySelector("body");
      if( body != null ) {
          csrf = body.getAttribute("fwcsrf");
      }
 
      // get the base url
      function getBaseUrl () {
          var url = window.location.href.split("?")[0];
          url += "?";
          if( csrf != null ) {
              url += "fwcsrf="+csrf+"&";
          }
          return url;
      }

      function makeCommand (cmd) {
          return getBaseUrl()+"cmd="+encodeURIComponent(cmd)+"&XHR=1";
      }
      
      // Template digital time display
      function buildtime (hours, minutes, seconds) {
          var ddt = ((hours   < 10) ? '0' : '') + hours   + ':' + 
                    ((minutes < 10) ? '0' : '') + minutes + ':' + 
                    ((seconds < 10) ? '0' : '') + seconds
                    ;   
          return ddt;
      }
      
      // Reading currtime setzen
      function setrcurrtime (h_$d, m_$d, s_$d) {
          var time_$d = buildtime (h_$d, m_$d, s_$d);
          if (modulo2_$d != zmodulo_$d && showCurrTime_$d != 0 && time_$d != 'NaN:NaN:NaN') {
              command = '{ CommandSetReading(undef, "$d currtime '+time_$d+'") }';  
              url_$d  = makeCommand(command);
              \$.get(url_$d);
          }   
      }
      
      // localStorage speichern letzte Alarmzeit
      function localStoreSetLastalm_$d (dev, lastalmtime) {
          localStorage.setItem('lastalmtime_'+dev, lastalmtime);
      }
    
      // Check ob Alarm ausgelöst werden soll und ggf. Alarmevent triggern
      function checkAndDoAlm_$d (dev, acttime, almtime) {
          lastalmtime_$d = localStorage.getItem('lastalmtime_$d');                             // letzte Alarmzeit laden   
          if ( acttime == almtime && acttime != lastalmtime_$d ) {
              command = '{ CommandSetReading(undef, "$d alarmed '+acttime+'") }';
              url_$d  = makeCommand(command);
              
              localStoreSetLastalm_$d (dev, acttime);                                          // aktuelle Alarmzeit sichern 
              
              if(acttime == almtime) {
                 \$.get(url_$d);
            
              } else {
                  \$.get(url_$d, function (data) {
                                     command = '{ CommandSetReading(undef, "$d state stopped") }';
                                     url_$d  = makeCommand(command);
                                     \$.get(url_$d);
                                }
                        );                                       
              }
          }
      }

      function StationClock_$d(clockId_$d) {
          this.clockId_$d = clockId_$d; 
          this.radius     = 0;

          // hour offset
          this.hourOffset = 0;
          
          // clock body
          this.body              = StationClock_$d.RoundBody;
          this.bodyShadowColor   = 'rgba(0,0,0,0.5)';
          this.bodyShadowOffsetX = 0.03;
          this.bodyShadowOffsetY = 0.03;
          this.bodyShadowBlur    = 0.06;
          
          // body dial
          this.dial              = StationClock_$d.GermanStrokeDial;
          this.dialColor         = 'rgb(60,60,60)';
          
          // clock hands
          this.hourHand          = StationClock_$d.PointedHourHand;
          this.minuteHand        = StationClock_$d.PointedMinuteHand;
          this.secondHand        = StationClock_$d.HoleShapedSecondHand;
          this.handShadowColor   = 'rgba(0,0,0,0.3)';
          this.handShadowOffsetX = 0.03;
          this.handShadowOffsetY = 0.03;
          this.handShadowBlur    = 0.04;
            
          // clock colors
          this.hourHandColor     = 'rgb(0,0,0)';
          this.minuteHandColor   = 'rgb(0,0,0)';
          this.secondHandColor   = 'rgb(200,0,0)';
          
          // clock boss
          this.boss              = StationClock_$d.NoBoss;
          this.bossShadowColor   = 'rgba(0,0,0,0.2)';
          this.bossShadowOffsetX = 0.02;
          this.bossShadowOffsetY = 0.02;
          this.bossShadowBlur    = 0.03;
          
          // hand behavoir
          this.minuteHandBehavoir = StationClock_$d.CreepingMinuteHand;
          this.secondHandBehavoir = StationClock_$d.OverhastySecondHand;
          
          // hand animation
          this.minuteHandAnimationStep = 0;
          this.secondHandAnimationStep = 0;
          this.lastMinute              = 0;
          this.lastSecond              = 0;
      };

      StationClock_$d.prototype.draw = function() {
          var clock_$d = document.getElementById(this.clockId_$d);
          
          if (clock_$d) {
              var context = clock_$d.getContext('2d');
              if (context) {
                  this.radius = 0.75 * (Math.min(clock_$d.width, clock_$d.height) / 2);
          
                  // clear canvas and set new origin
                  context.clearRect(0, 0, clock_$d.width, clock_$d.height);
                  context.save();
                  context.translate(clock_$d.width / 2, clock_$d.height / 2);
                  
                  // draw body
                  if (this.body != StationClock_$d.NoStrokeBody) {
                      context.save();
                      switch (this.body) {
                          case StationClock_$d.SmallWhiteBody:
                            this.fillCircle(context, 'rgb(255,255,255)', 0, 0, 1);
                            break;
                          case StationClock_$d.RoundBody:
                            this.fillCircle(context, 'rgb(255,255,255)', 0, 0, 1.1);
                            context.save();
                            this.setShadow(context, this.bodyShadowColor, this.bodyShadowOffsetX, this.bodyShadowOffsetY, this.bodyShadowBlur);
                            this.strokeCircle(context, 'rgb(0,0,0)', 0, 0, 1.1, 0.07);
                            context.restore();
                            break;
                          case StationClock_$d.RoundGreenBody:
                            this.fillCircle(context, 'rgb(235,236,212)', 0, 0, 1.1);
                            context.save();
                            this.setShadow(context, this.bodyShadowColor, this.bodyShadowOffsetX, this.bodyShadowOffsetY, this.bodyShadowBlur);
                            this.strokeCircle(context, 'rgb(180,180,180)', 0, 0, 1.1, 0.2);
                            context.restore();
                            this.strokeCircle(context, 'rgb(29,84,31)', 0, 0, 1.15, 0.1);
                            context.save();
                            this.setShadow(context, 'rgba(235,236,212,100)', -0.02, -0.02, 0.09);
                            this.strokeCircle(context, 'rgb(76,128,110)', 0, 0, 1.1, 0.08);
                            context.restore();
                            break;
                          case StationClock_$d.SquareBody:
                            context.save();
                            this.setShadow(context, this.bodyShadowColor, this.bodyShadowOffsetX, this.bodyShadowOffsetY, this.bodyShadowBlur);
                            this.fillSquare(context, 'rgb(237,235,226)', 0, 0, 2.4);
                            this.strokeSquare(context, 'rgb(38,106,186)', 0, 0, 2.32, 0.16);
                            context.restore();
                            context.save();
                            this.setShadow(context, this.bodyShadowColor, this.bodyShadowOffsetX, this.bodyShadowOffsetY, this.bodyShadowBlur);
                            this.strokeSquare(context, 'rgb(42,119,208)', 0, 0, 2.24, 0.08);
                            context.restore();
                            break;
                          case StationClock_$d.ViennaBody:
                            context.save();
                            this.fillSymmetricPolygon(context, 'rgb(156,156,156)', [[-1.2,1.2],[-1.2,-1.2]],0.1);
                            this.fillPolygon(context, 'rgb(156,156,156)', 0,1.2 , 1.2,1.2 , 1.2,0);
                            this.fillCircle(context, 'rgb(255,255,255)', 0, 0, 1.05, 0.08);
                            this.strokeCircle(context, 'rgb(0,0,0)', 0, 0, 1.05, 0.01);
                            this.strokeCircle(context, 'rgb(100,100,100)', 0, 0, 1.1, 0.01);
                            this.fillPolygon(context, 'rgb(100,100,100)', 0.45,1.2 , 1.2,1.2 , 1.2,0.45);
                            this.fillPolygon(context, 'rgb(170,170,170)', 0.45,-1.2 , 1.2,-1.2 , 1.2,-0.45);
                            this.fillPolygon(context, 'rgb(120,120,120)', -0.45,1.2 , -1.2,1.2 , -1.2,0.45);
                            this.fillPolygon(context, 'rgb(200,200,200)', -0.45,-1.2 , -1.2,-1.2 , -1.2,-0.45);
                            this.strokeSymmetricPolygon(context, 'rgb(156,156,156)', [[-1.2,1.2],[-1.2,-1.2]],0.01);
                            this.fillPolygon(context, 'rgb(255,0,0)', 0.05,-0.6 , 0.15,-0.6 , 0.15,-0.45 , 0.05,-0.45);
                            this.fillPolygon(context, 'rgb(255,0,0)', -0.05,-0.6 , -0.15,-0.6 , -0.15,-0.45 , -0.05,-0.45);
                            this.fillPolygon(context, 'rgb(255,0,0)', 0.05,-0.35 , 0.15,-0.35 , 0.15,-0.30 ,  0.10,-0.20 , 0.05,-0.20);
                            this.fillPolygon(context, 'rgb(255,0,0)', -0.05,-0.35 , -0.15,-0.35 , -0.15,-0.30 ,  -0.10,-0.20 , -0.05,-0.20);
                            context.restore();
                            break;
                      }
                      context.restore();
                  }
                  
                  // draw dial
                  for (var i = 0; i < 60; i++) {
                      context.save();
                      context.rotate(i * Math.PI / 30);
                      switch (this.dial) {
                          case StationClock_$d.SwissStrokeDial:
                          if ((i % 5) == 0) {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.75, 0.07);
                          } else {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.92, 0.026);
                          }
                          break;
                          case StationClock_$d.AustriaStrokeDial:
                          if ((i % 5) == 0) {
                              this.fillPolygon(context, this.dialColor, -0.04, -1.0, 0.04, -1.0, 0.03, -0.78, -0.03, -0.78);
                          } else {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.94, 0.02);
                          }
                          break;
                          case StationClock_$d.GermanStrokeDial:
                          if ((i % 15) == 0) {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.70, 0.08);
                          } else if ((i % 5) == 0) {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.76, 0.08);
                          } else {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.92, 0.036);
                          }
                          break;
                          case StationClock_$d.GermanHourStrokeDial:
                          if ((i % 15) == 0) {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.70, 0.10);
                          } else if ((i % 5) == 0) {
                              this.strokeLine(context, this.dialColor, 0.0, -1.0, 0.0, -0.74, 0.08);
                          }
                          break;
                          case StationClock_$d.ViennaStrokeDial:
                          if ((i % 15) == 0) {
                              this.fillPolygon(context, this.dialColor, 0.7,-0.1, 0.6,0, 0.7,0.1,  1,0.03,  1,-0.03);
                          } else if ((i % 5) == 0) {
                              this.fillPolygon(context, this.dialColor, 0.85,-0.06, 0.78,0, 0.85,0.06,  1,0.03,  1,-0.03);
                          }
                          this.fillCircle(context, this.dialColor, 0.0, -1.0, 0.03);
                          break;
                      }
                      context.restore();
                  }
                  
                  var cycletime       = new Date();
                  var cycleseconds    = cycletime.getSeconds();
                  modulo2_$d          = cycleseconds % 2;                                    // Taktung für Readingabruf (Serverauslastung reduzieren)
      
                  if (modulo2_$d != zmodulo_$d) {
                      command = '{ReadingsVal("$d","alarmTime","+almtime0_$d+")}';           // alarmTime Reading lesen
                      url_$d  = makeCommand(command);
                      \$.get( url_$d, function (data) {
                                          almtime0_$d = data.replace(/\\n/g, '');
                                          zmodulo_$d = modulo2_$d;  
                                          return (almtime0_$d, zmodulo_$d);
                                      } 
                            );
                  }
                
                  // Zeitsteuerung
                  if ('$tsou' == 'server') {                   // Serverzeit
                      // aktueller Timestamp in Millisekunden 
                      command = '{ int(time*1000) }';
                      url_$d  = makeCommand(command);
                      \$.get( url_$d, function (data) {
                                          data = data.replace(/\\n/g, ''); 
                                          ct_$d = parseInt(data); 
                                          return ct_$d;
                                      } 
                            ); 
                      
                      time_$d  = new Date(ct_$d);
                      
                  } else {
                      time_$d  = new Date();                   // Clientzeit
                  }
                  
                  if (typeof ct_$d === 'undefined') {          // wenn Zeit noch undef mit lokaler Zeit initialisieren -> springen Zeiger verhindern
                      time_$d  = new Date();                   
                  } else {
                      time_$d  = new Date(ct_$d);
                  }

                  var millis_$d  = time_$d.getMilliseconds() / 1000.0;
                  var seconds_$d = time_$d.getSeconds();
                  var minutes_$d = time_$d.getMinutes();
                  var hours_$d   = time_$d.getHours() + this.hourOffset;
                  
                  acttime_$d = ((hours_$d   < 10) ? '0' : '') + hours_$d   + ':' + 
                               ((minutes_$d < 10) ? '0' : '') + minutes_$d + ':' + 
                               ((seconds_$d < 10) ? '0' : '') + seconds_$d;
                               
                  if (acttime_$d == '00:00:00') {
                      localStoreSetLastalm_$d ('$d', 'NaN');                   // letzte Alarmzeit zurücksetzen zum Tageswechsel            
                  }

                  checkAndDoAlm_$d ('$d', acttime_$d, almtime0_$d);  

                  setrcurrtime (hours_$d, minutes_$d, seconds_$d);             // Reading currtime mit angezeigter Zeit setzen                  

                  // draw hour hand
                  context.save();
                  context.rotate(hours_$d * Math.PI / 6 + minutes_$d * Math.PI / 360);
                  this.setShadow(context, this.handShadowColor, this.handShadowOffsetX, this.handShadowOffsetY, this.handShadowBlur);
                  switch (this.hourHand) {
                    case StationClock_$d.BarHourHand:
                      this.fillPolygon(context, this.hourHandColor, -0.05, -0.6, 0.05, -0.6, 0.05, 0.15, -0.05, 0.15);
                      break;
                    case StationClock_$d.PointedHourHand:
                      this.fillPolygon(context, this.hourHandColor, 0.0, -0.6,  0.065, -0.53, 0.065, 0.19, -0.065, 0.19, -0.065, -0.53);
                      break;
                    case StationClock_$d.SwissHourHand:
                      this.fillPolygon(context, this.hourHandColor, -0.05, -0.6, 0.05, -0.6, 0.065, 0.26, -0.065, 0.26);
                      break;
                    case StationClock_$d.ViennaHourHand:
                      this.fillSymmetricPolygon(context, this.hourHandColor, [[-0.02,-0.72],[-0.08,-0.56],[-0.15,-0.45],[-0.06,-0.30],[-0.03,0],[-0.1,0.2],[-0.05,0.23],[-0.03,0.2]]);
                  }
                  context.restore();
                  
                  // draw minute hand
                  context.save();
                  switch (this.minuteHandBehavoir) {
                    case StationClock_$d.CreepingMinuteHand:
                      context.rotate((minutes_$d + seconds_$d / 60) * Math.PI / 30);
                      break;
                    case StationClock_$d.BouncingMinuteHand:
                      context.rotate(minutes_$d * Math.PI / 30);
                      break;
                    case StationClock_$d.ElasticBouncingMinuteHand:
                      if (this.lastMinute != minutes_$d) {
                          this.minuteHandAnimationStep = 3;
                          this.lastMinute = minutes_$d;
                      }
                      context.rotate((minutes_$d + this.getAnimationOffset(this.minuteHandAnimationStep)) * Math.PI / 30);
                      this.minuteHandAnimationStep--;
                      break;
                  }
                  this.setShadow(context, this.handShadowColor, this.handShadowOffsetX, this.handShadowOffsetY, this.handShadowBlur);
                  switch (this.minuteHand) {
                    case StationClock_$d.BarMinuteHand:
                      this.fillPolygon(context, this.minuteHandColor, -0.05, -0.9, 0.035, -0.9, 0.035, 0.23, -0.05, 0.23);
                      break;
                    case StationClock_$d.PointedMinuteHand:
                      this.fillPolygon(context, this.minuteHandColor, 0.0, -0.93,  0.045, -0.885, 0.045, 0.23, -0.045, 0.23, -0.045, -0.885);
                      break;
                    case StationClock_$d.SwissMinuteHand:
                      this.fillPolygon(context, this.minuteHandColor, -0.035, -0.93, 0.035, -0.93, 0.05, 0.25, -0.05, 0.25);
                      break;
                    case StationClock_$d.ViennaMinuteHand:
                      this.fillSymmetricPolygon(context, this.minuteHandColor, [[-0.02,-0.98],[-0.09,-0.7],[-0.03,0],[-0.05,0.2],[-0.01,0.4]]);
                  }
                  context.restore();
                  
                  // draw second hand
                  context.save();
                  switch (this.secondHandBehavoir) {
                    case StationClock_$d.OverhastySecondHand:
                      context.rotate(Math.min((seconds_$d + millis_$d) * (60.0 / 58.5), 60.0) * Math.PI / 30);
                      break;
                    case StationClock_$d.CreepingSecondHand:
                      context.rotate((seconds_$d + millis_$d) * Math.PI / 30);
                      break;
                    case StationClock_$d.BouncingSecondHand:
                      context.rotate(seconds_$d * Math.PI / 30);
                      break;
                    case StationClock_$d.ElasticBouncingSecondHand:
                      if (this.lastSecond != seconds_$d) {
                          this.secondHandAnimationStep = 3;
                          this.lastSecond = seconds_$d;
                      }
                      context.rotate((seconds_$d + this.getAnimationOffset(this.secondHandAnimationStep)) * Math.PI / 30);
                      this.secondHandAnimationStep--;
                      break;
                  }
                  this.setShadow(context, this.handShadowColor, this.handShadowOffsetX, this.handShadowOffsetY, this.handShadowBlur);
                  switch (this.secondHand) {
                    case StationClock_$d.BarSecondHand:
                      this.fillPolygon(context, this.secondHandColor, -0.006, -0.92, 0.006, -0.92, 0.028, 0.23, -0.028, 0.23);
                      break;
                    case StationClock_$d.HoleShapedSecondHand:
                      this.fillPolygon(context, this.secondHandColor, 0.0, -0.9, 0.011, -0.889, 0.01875, -0.6, -0.01875, -0.6, -0.011, -0.889);
                      this.fillPolygon(context, this.secondHandColor, 0.02, -0.4, 0.025, 0.22, -0.025, 0.22, -0.02, -0.4);
                      this.strokeCircle(context, this.secondHandColor, 0, -0.5, 0.083, 0.066);
                      break;
                    case StationClock_$d.NewHoleShapedSecondHand:
                      this.fillPolygon(context, this.secondHandColor, 0.0, -0.95, 0.015, -0.935, 0.0187, -0.65, -0.0187, -0.65, -0.015, -0.935);
                      this.fillPolygon(context, this.secondHandColor, 0.022, -0.45, 0.03, 0.27, -0.03, 0.27, -0.022, -0.45);
                      this.strokeCircle(context, this.secondHandColor, 0, -0.55, 0.085, 0.07);
                      break;
                    case StationClock_$d.SwissSecondHand:
                      this.strokeLine(context, this.secondHandColor, 0.0, -0.6, 0.0, 0.35, 0.026);
                      this.fillCircle(context, this.secondHandColor, 0, -0.64, 0.1);
                      break;
                    case StationClock_$d.ViennaSecondHand:
                      this.strokeLine(context, this.secondHandColor, 0.0, -0.6, 0.0, 0.35, 0.026);
                      this.fillCircle(context, this.secondHandColor, 0, -0.64, 0.1);
                      break;
                  }
                  context.restore();
                  
                  // draw clock boss
                  if (this.boss != StationClock_$d.NoBoss) {
                      context.save();
                      this.setShadow(context, this.bossShadowColor, this.bossShadowOffsetX, this.bossShadowOffsetY, this.bossShadowBlur);
                      switch (this.boss) {
                          case StationClock_$d.BlackBoss:
                            this.fillCircle(context, 'rgb(0,0,0)', 0, 0, 0.1);
                            break;
                          case StationClock_$d.RedBoss:
                            this.fillCircle(context, 'rgb(220,0,0)', 0, 0, 0.06);
                            break;
                          case StationClock_$d.ViennaBoss:
                            this.fillCircle(context, 'rgb(0,0,0)', 0, 0, 0.07);
                            break;
                      }
                      context.restore();
                  }
                  context.restore();
              }
          }
      };

      StationClock_$d.prototype.getAnimationOffset = function(animationStep) {
          switch (animationStep) {
            case 3: return  0.2;
            case 2: return -0.1;
            case 1: return  0.05;
          }
      return 0;
      };

      StationClock_$d.prototype.setShadow = function(context, color, offsetX, offsetY, blur) {
          if (color) {
              context.shadowColor   = color;
              context.shadowOffsetX = this.radius * offsetX;
              context.shadowOffsetY = this.radius * offsetY;
              context.shadowBlur    = this.radius * blur;
          }
      };

      StationClock_$d.prototype.fillCircle = function(context, color, x, y, radius) {
          if (color) {
              context.beginPath();
              context.fillStyle = color;
              context.arc(x * this.radius, y * this.radius, radius * this.radius, 0, 2 * Math.PI, true);
              context.fill();
          }
      };

      StationClock_$d.prototype.strokeCircle = function(context, color, x, y, radius, lineWidth) {
          if (color) {
              context.beginPath();
              context.strokeStyle = color;
              context.lineWidth = lineWidth * this.radius;
              context.arc(x * this.radius, y * this.radius, radius * this.radius, 0, 2 * Math.PI, true);
              context.stroke();
          }
      };

      StationClock_$d.prototype.fillSquare = function(context, color, x, y, size) {
          if (color) {
              context.fillStyle = color;
              context.fillRect((x - size / 2) * this.radius, (y -size / 2) * this.radius, size * this.radius, size * this.radius);
          }
      };

      StationClock_$d.prototype.strokeSquare = function(context, color, x, y, size, lineWidth) {
          if (color) {
              context.strokeStyle = color;
              context.lineWidth = lineWidth * this.radius;
              context.strokeRect((x - size / 2) * this.radius, (y -size / 2) * this.radius, size * this.radius, size * this.radius);
          }
      };

      StationClock_$d.prototype.strokeLine = function(context, color, x1, y1, x2, y2, width) {
          if (color) {
              context.beginPath();
              context.strokeStyle = color;
              context.moveTo(x1 * this.radius, y1 * this.radius);
              context.lineTo(x2 * this.radius, y2 * this.radius);
              context.lineWidth = width * this.radius;
              context.stroke();
          }
      };

      StationClock_$d.prototype.fillPolygon = function(context, color, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5) {
          if (color) {
              context.beginPath();
              context.fillStyle = color;
              context.moveTo(x1 * this.radius, y1 * this.radius);
              context.lineTo(x2 * this.radius, y2 * this.radius);
              context.lineTo(x3 * this.radius, y3 * this.radius);
              context.lineTo(x4 * this.radius, y4 * this.radius);
              if ((x5 != undefined) && (y5 != undefined)) {
                 context.lineTo(x5 * this.radius, y5 * this.radius);
              }
              context.lineTo(x1 * this.radius, y1 * this.radius);
              context.fill();
          }
      };

      StationClock_$d.prototype.fillSymmetricPolygon = function(context, color, points) {
          context.beginPath();
          context.fillStyle = color;
          context.moveTo(points[0][0] * this.radius, points[0][1] * this.radius);
          for (var i = 1; i < points.length; i++) {
              context.lineTo(points[i][0] * this.radius, points[i][1] * this.radius);
          }
          for (var i = points.length - 1; i >= 0; i--) {
              context.lineTo(0 - points[i][0] * this.radius, points[i][1] * this.radius);
          }
          context.lineTo(points[0][0] * this.radius, points[0][1] * this.radius);
          context.fill();
      };

      StationClock_$d.prototype.strokeSymmetricPolygon = function(context, color, points, width) {
          context.beginPath();
          context.strokeStyle = color;
          context.moveTo(points[0][0] * this.radius, points[0][1] * this.radius);
          for (var i = 1; i < points.length; i++) {
              context.lineTo(points[i][0] * this.radius, points[i][1] * this.radius);
          }
          for (var i = points.length - 1; i >= 0; i--) {
              context.lineTo(0 - points[i][0] * this.radius, points[i][1] * this.radius);
          }
          context.lineTo(points[0][0] * this.radius, points[0][1] * this.radius);
          context.lineWidth = width * this.radius;
          context.stroke();
      };
        
      var clock_$d                = new StationClock_$d('clock_$d');
      clock_$d.body               = StationClock_$d.$sbody;
      clock_$d.dial               = StationClock_$d.$ssd;
      clock_$d.hourHand           = StationClock_$d.$shh;
      clock_$d.minuteHand         = StationClock_$d.$smh;
      clock_$d.secondHand         = StationClock_$d.$ssh;
      clock_$d.boss               = StationClock_$d.$sb;
      clock_$d.minuteHandBehavoir = StationClock_$d.$mhb;
      clock_$d.secondHandBehavoir = StationClock_$d.$shb;

      function animate(clock_$d) {
          clock_$d.draw();
          // window.setTimeout(function(){animate(clock_$d)}, 50);     // alte Variante
          window.setTimeout(function(){animate(clock_$d)}, 100);
      }

      animate(clock_$d);   
      </script>
      
      </body>
      </html>      

END_JS

return qq{$back};
}

##############################################################################
#                            Moderne Uhr aus:
#         https://www.w3schools.com/graphics/canvas_clock_start.asp
#
##############################################################################
sub modernWatch {
  my ($d)    = @_;
  my $hash   = $defs{$d};
  my $facec  = AttrVal($d, "modernColorFace",       "FFFEFA");
  my $bgc    = AttrVal($d, "modernColorBackground", "333");
  my $fc     = AttrVal($d, "modernColorFigure",     "333");
  my $hc     = AttrVal($d, "modernColorHand",       "333");
  my $fr     = AttrVal($d, "modernColorRing",       "FFFFFF");
  my $fre    = AttrVal($d, "modernColorRingEdge",   "333");
  my $hattr  = AttrVal($d, "htmlattr",              "width='150' height='150'");
  my $tsou   = AttrVal($d, "timeSource",            "client");
  my $showct = AttrVal($d, "timeAsReading",         0);

  my $alarm  = ReadingsVal($d, "alarmTime", "aa:bb:cc");  

  my $back = << "END_JS";
      <html>
      <body>

      <canvas id='canvas_$d' $hattr style='background-color:#$bgc'>
      </canvas>

      <script>
      
      var ct_$d;
      var almtime0_$d     = '$alarm';
      var zmodulo_$d      = 0;                       // Hilfszähler 
      var modulo2_$d      = 0;                       // Hilfszähler
      var showCurrTime_$d = '$showct';               // Reading currtime schreiben oder nicht
      
      // CSRF-Token auslesen
      var body = document.querySelector("body");
      if( body != null ) {
          csrf = body.getAttribute("fwcsrf");
      }
 
      // get the base url
      function getBaseUrl () {
          var url = window.location.href.split("?")[0];
          url += "?";
          if( csrf != null ) {
              url += "fwcsrf="+csrf+"&";
          }
          return url;
      }

      function makeCommand (cmd) {
          return getBaseUrl()+"cmd="+encodeURIComponent(cmd)+"&XHR=1";
      }
      
      // Template digital time display
      function buildtime (hours, minutes, seconds) {
          var ddt = ((hours   < 10) ? '0' : '') + hours   + ':' + 
                    ((minutes < 10) ? '0' : '') + minutes + ':' + 
                    ((seconds < 10) ? '0' : '') + seconds
                    ;   
          return ddt;
      }
    
    
      // Reading currtime setzen
      function setrcurrtime (h_$d, m_$d, s_$d) {
          var time_$d = buildtime (h_$d, m_$d, s_$d);
          if (modulo2_$d != zmodulo_$d && showCurrTime_$d != 0 && time_$d != 'NaN:NaN:NaN') {
              command = '{ CommandSetReading(undef, "$d currtime '+time_$d+'") }';  
              url_$d  = makeCommand(command);
              \$.get(url_$d);
          }   
      }
      
      // localStorage speichern letzte Alarmzeit
      function localStoreSetLastalm_$d (dev, lastalmtime) {
          localStorage.setItem('lastalmtime_'+dev, lastalmtime);
      }
    
      // Check ob Alarm ausgelöst werden soll und ggf. Alarmevent triggern
      function checkAndDoAlm_$d (dev, acttime, almtime) {
          lastalmtime_$d = localStorage.getItem('lastalmtime_$d');                             // letzte Alarmzeit laden   
          if ( acttime == almtime && acttime != lastalmtime_$d ) {
              command = '{ CommandSetReading(undef, "$d alarmed '+acttime+'") }';
              url_$d  = makeCommand(command);
              
              localStoreSetLastalm_$d (dev, acttime);                                        // aktuelle Alarmzeit sichern 
              
              if(acttime == almtime) {
                 \$.get(url_$d);
            
              } else {
                  \$.get(url_$d, function (data) {
                                     command = '{ CommandSetReading(undef, "$d state stopped") }';
                                     url_$d  = makeCommand(command);
                                     \$.get(url_$d);
                                }
                        );                                       
              }
          }
      }     
      
      var canvas_$d = document.getElementById('canvas_$d');
      var ctx_$d    = canvas_$d.getContext('2d');
      var radius_$d = canvas_$d.height / 2;
      
      ctx_$d.translate(radius_$d, radius_$d);
      radius_$d = radius_$d * 0.90
      // setInterval(drawClock_$d, 1000);                // alte Variante
      setInterval(drawClock_$d, 100);

      function drawClock_$d() {
          drawFace_$d    (ctx_$d, radius_$d);
          drawnumbers_$d (ctx_$d, radius_$d);
          drawTime_$d    (ctx_$d, radius_$d);
      }

      function drawFace_$d(ctx_$d, radius_$d) {
          var grad_$d;
          ctx_$d.beginPath();
          ctx_$d.arc(0, 0, radius_$d, 0, 2*Math.PI);
          ctx_$d.fillStyle = '#$facec';                    // Füllung Uhr
          ctx_$d.fill();
          grad_$d = ctx_$d.createRadialGradient(0,0,radius_$d*0.95, 0,0,radius_$d*1.05);
          grad_$d.addColorStop(0, '#$hc');                // Farbe Zeiger und innere Ringgrenze
          grad_$d.addColorStop(0.5, '#$fr');              // Farbe Ziffernblattring
          grad_$d.addColorStop(1, '#$fre');               // Farbe äußere Ringgrenze
          ctx_$d.strokeStyle = grad_$d;
          ctx_$d.lineWidth   = radius_$d*0.1;
          ctx_$d.stroke();
          ctx_$d.beginPath();
          ctx_$d.arc(0, 0, radius_$d*0.1, 0, 2*Math.PI);
          ctx_$d.fillStyle = '#$fc';                      // Farbe Ziffern und Zeigerwelle
          ctx_$d.fill();
      }

      function drawnumbers_$d(ctx_$d, radius_$d) {
          var ang_$d;
          var num_$d;
          ctx_$d.font         = radius_$d*0.15 + 'px arial';
          ctx_$d.textBaseline ='middle';
          ctx_$d.textAlign    ='center';
          
          for(num_$d = 1; num_$d < 13; num_$d++){
              ang_$d = num_$d * Math.PI / 6;
              ctx_$d.rotate(ang_$d);
              ctx_$d.translate(0, -radius_$d*0.85);
              ctx_$d.rotate(-ang_$d);
              ctx_$d.fillText(num_$d.toString(), 0, 0);
              ctx_$d.rotate(ang_$d);
              ctx_$d.translate(0, radius_$d*0.85);
              ctx_$d.rotate(-ang_$d);
          }
      }

      function drawTime_$d(ctx_$d, radius_$d){
          var cycletime    = new Date();
          var cycleseconds = cycletime.getSeconds();
          modulo2_$d       = cycleseconds % 2;                                       // Taktung für Readingabruf (Serverauslastung reduzieren)

          if (modulo2_$d != zmodulo_$d) {
              command = '{ReadingsVal("$d","alarmTime","+almtime0_$d+")}';     // alarmTime Reading lesen
              url_$d  = makeCommand(command);
              \$.get( url_$d, function (data) {
                                  almtime0_$d = data.replace(/\\n/g, '');
                                  zmodulo_$d = modulo2_$d;  
                                  return (almtime0_$d, zmodulo_$d);
                              } 
                    );
          }
          // Zeitsteuerung
          if ('$tsou' == 'server') {                   // Serverzeit
              // aktueller Timestamp in Millisekunden 
              command = '{ int(time*1000) }';
              url_$d  = makeCommand(command);
              \$.get( url_$d, function (data) {
                                  data = data.replace(/\\n/g, ''); 
                                  ct_$d = parseInt(data); 
                                  return ct_$d;
                              } 
                    ); 
              
              time_$d  = new Date(ct_$d);
              
          } else {
              time_$d  = new Date();                   // Clientzeit
          }
          
          if (typeof ct_$d === 'undefined') {          // wenn Zeit noch undef mit lokaler Zeit initialisieren -> springen Zeiger verhindern
              time_$d  = new Date();                   
          } else {
              time_$d  = new Date(ct_$d);
          }
          
          var hour_$d   = time_$d.getHours();
          var minute_$d = time_$d.getMinutes();
          var second_$d = time_$d.getSeconds();
          
          acttime_$d = ((hour_$d   < 10) ? '0' : '') + hour_$d   + ':' + 
                       ((minute_$d < 10) ? '0' : '') + minute_$d + ':' + 
                       ((second_$d < 10) ? '0' : '') + second_$d;
                       
          if (acttime_$d == '00:00:00') {
              localStoreSetLastalm_$d ('$d', 'NaN');               // letzte Alarmzeit zurücksetzen zum Tageswechsel            
          }

          checkAndDoAlm_$d ('$d', acttime_$d, almtime0_$d);
          
          setrcurrtime (hour_$d, minute_$d, second_$d);            // Reading currtime mit angezeigter Zeit setzen
          
          //hour_$d
          hour_$d = hour_$d%12;
          hour_$d = (hour_$d*Math.PI/6)+
                    (minute_$d*Math.PI/(6*60))+
                    (second_$d*Math.PI/(360*60));
          drawHand_$d(ctx_$d, hour_$d, radius_$d*0.5, radius_$d*0.07);
          
          //minute_$d
          minute_$d = (minute_$d*Math.PI/30)+(second_$d*Math.PI/(30*60));
          drawHand_$d(ctx_$d, minute_$d, radius_$d*0.8, radius_$d*0.07);
          
          // second_$d
          second_$d = (second_$d*Math.PI/30);
          drawHand_$d(ctx_$d, second_$d, radius_$d*0.9, radius_$d*0.02);
      }

      function drawHand_$d(ctx_$d, pos, length, width) {
          ctx_$d.beginPath();
          ctx_$d.lineWidth  = width;
          ctx_$d.lineCap    = 'round';
          ctx_$d.moveTo(0,0);
          ctx_$d.rotate(pos);
          ctx_$d.lineTo(0, -length);
          ctx_$d.stroke();
          ctx_$d.rotate(-pos);
      }
      </script>

      </body>
      </html>
      
END_JS

return qq{$back};
}

##############################################################################
#               Versionierungen des Moduls setzen
#  Die Verwendung von Meta.pm und Packages wird berücksichtigt
#
##############################################################################
sub setVersionInfo {
  my ($hash) = @_;
  my $name   = $hash->{NAME};

  my $v                    = (sortTopicNum("desc",keys %vNotesIntern))[0];
  my $type                 = $hash->{TYPE};
  $hash->{HELPER}{PACKAGE} = __PACKAGE__;
  $hash->{HELPER}{VERSION} = $v;
  
  if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) {
      # META-Daten sind vorhanden
      $modules{$type}{META}{version} = "v".$v;                                         # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}}
      if($modules{$type}{META}{x_version}) {                                                                             
          $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/gx;
      } else {
          $modules{$type}{META}{x_version} = $v; 
      }
      return $@ unless (FHEM::Meta::SetInternals($hash));                                                                
      if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) {
          # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen
          # mit {<Modul>->VERSION()} im FHEMWEB kann Modulversion abgefragt werden
          use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' );        ## no critic 'VERSION'                                      
      }
  } else {
      # herkömmliche Modulstruktur
      $hash->{VERSION} = $v;
  }
  
return;
}

1;

=pod
=item helper
=item summary    Clock display in different variants
=item summary_DE Uhrenanzeige in verschiedenen Varianten

=begin html

<a name="Watches"></a>
<h3>Watches</h3>

<br>
The module Watches provides watches in different styles as Device. The module is a JavaScript application that runs on a 
client (browser) and not on the FHEM server. Attributes and readings are read asynchronously from the server and possibly 
also written, but only if the application is currently running in the browser. <br>
The user can influence the design of the watches via attributes. <br>
The clocks are based on scripts of these pages: <br>

<a href='https://www.w3schools.com/graphics/canvas_clock_start.asp'>modern watch</a>, 
<a href='http://www.3quarks.com/de/Bahnhofsuhr/'>Station clock</a>, 
<a href='http://www.3quarks.com/de/Segmentanzeige/'>Digital display</a> 
<br><br>

A device of the model <b>Digital</b> can also be used as stopwatch, countdown timer or
universal text display (for sixteen segment mode see attribute <a href="#digitalSegmentType">digitalSegmentType</a>). <br>
As time source the client (browser time) as well as the FHEM server can be set 
(attribute <a href="#timeSource">timeSource</a>). <br>

<ul>
  <a name="WatchesDefine"></a>
  <b>Define</b>
  
  <ul>
    define &lt;name&gt; Watches [Modern | Station | Digital]  
    <br><br>
    
  <table>  
     <colgroup> <col width=5%> <col width=95%> </colgroup>
     <tr><td> <b>Modern</b>     </td><td>: creates an analog clock with a modern design </td></tr>
     <tr><td> <b>Station</b>    </td><td>: creates a station clock </td></tr>
     <tr><td> <b>Digital</b>    </td><td>: creates a digital display (clock, (CountDown)stop watch, static time display or text) </td></tr>
  </table>
  <br>
  <br>
  </ul>

  <a name="WatchesSet"></a>
  <b>Set</b> 
  
  <ul>
  <ul>
  
    <a name="alarmSet"></a>
    <li><b>alarmSet &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; </b><br>
      Sets the alarm time in the format hh hours, mm minutes and ss seconds. <br>
      If the time reaches the defined value, an event of the reading "alarmed" is triggered. <br>
      This set command is only available for digital stopwatches. <br><br>
      
      <ul>
      <b>Example</b> <br>
      set &lt;name&gt; alarmSet 0 30 10
      </ul>
      <br>
      
    </li>
    <br>
    
    <a name="alarmDel"></a>
    <li><b>alarmDel</b><br>
      Clears the set alarm time and its status. <br>
      This set command is only available for digital stopwatches. <br>
    </li>
    <br>
    
    <a name="countDownInit"></a>
    <li><b>countDownInit &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; | &lt;seconds&gt; </b><br>
      Sets the start time of a countdown stopwatch.
      The format can be &lt;hh&gt; hours, &lt;mm&gt; minutes and &lt;ss&gt; seconds or alternatively only one entry in seconds. <br>
      This set command is only available with a digital countdown stopwatch. <br><br>
      
      <ul>
      <b>Examples</b> <br>
      set &lt;name&gt; countDownInit 0 30 10 <br>
      set &lt;name&gt; countDownInit 3600
      </ul>
      <br>
      
    </li>
    <br>
    
    <a name="displayTextSet"></a>
    <li><b>displayTextSet</b><br>
      Sets the text to be displayed. <br> 
      This set command is only available for a digital segment display with "digitalDisplayPattern = text". <br>
      (default: blank display) <br><br>
      
      <b>Note:</b> <br>
      The displayable characters depend on the attribute "digitalSegmentType". <br>
      With the (default) seven-segment display, only numbers, hyphen, underscore and the letters 
      A, b, C, d, E, F, H, L, n, o, P, r, t, U and Y are displayed. 
      In addition to numbers, short texts such as "Error", "HELP", "run" or "PLAY" can also be displayed. <br>
      For text display it is recommended to set the sixteen segment display with the attribute "digitalSegmentType" ! 
    </li>
    <br>
    
    <a name="displayTextDel"></a>
    <li><b>displayTextDel</b><br>
      Deletes the display text. <br>
      This set command is only available for a digital segment display with "digitalDisplayPattern = text". <br>
    </li>
    <br>
    
    <a name="reset"></a>
    <li><b>reset</b><br>
      Stops the stopwatch (if running) and clears all specific readings or resets it to initialized. <br>
      This set command is only available for digital stopwatches. <br>
    </li>
    <br>
    
    <a name="resume"></a>
    <li><b>resume</b><br>
      Resumes counting a stopped stopwatch. <br>
      This set command is only available for digital stopwatches. <br>
    </li>
    <br>
    
    <a name="start"></a>
    <li><b>start</b><br>
      Starts the stopwatch. <br>
      This set command is only available for digital stopwatches. <br>
    </li>
    <br>  
    
    <a name="stop"></a>
    <li><b>stop</b><br>
      Stop the stopwatch. The achieved time is retained. <br>
      This set command is only available for digital stopwatches. <br>
    </li>
    <br>
    
    <a name="textTicker"></a>
    <li><b>textTicker on | off </b><br>
      Switches the ticker mode of a text display (see attribute digitalDisplayPattern) on or off. <br>
      (default: off)      
    </li>
    <br>
    
    <a name="time"></a>
    <li><b>time &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; </b><br>
      Sets a static time display with hh hours, mm minutes and ss seconds. <br>
      This set command is only available for a digital clock with static time display. <br><br>
      
      <ul>
      <b>Example</b> <br>
      set &lt;name&gt; time 8 15 3
      </ul>
      
    </li>
    <br>
  
  </ul>
  </ul>
  <br>

  <a name="WatchesGet"></a>
  <b>Get</b> 
  
  <ul>
  
    N/A
    
  </ul>
  <br>

  <a name="WatchesAttr"></a>
  <b>Attributes</b>
  <br><br>
  
  <ul>
  <ul>
  
    <a name="controlButtonSize"></a>
    <li><b>controlButtonSize</b><br>
      Changes the size of the control buttons if the clock type has control buttons.
    </li>
    <br>
  
    <a name="disable"></a>
    <li><b>disable</b><br>
      Activates/deactivates the Device.
    </li>
    <br>
    
    <a name="hideButtons"></a>
    <li><b>hideButtons</b><br>
      Hides the control buttons if the watch type has control buttons. 
    </li>
    <br>
    
    <a name="hideDisplayName"></a>
    <li><b>hideDisplayName</b><br>
      Hides the Device/Alias name (link to detail view).    
    </li>
    <br>
    
    <a name="htmlattr"></a>
    <li><b>htmlattr</b><br>
      Additional HTML tags to resize the clock / display. <br><br>
      <ul>
        <b>Example: </b><br>
        attr &lt;name&gt; htmlattr width="125" height="125" <br>
      </ul>
    </li>
    <br>
    
    <a name="timeAsReading"></a>
    <li><b>timeAsReading</b><br>
      If set, a displayed time is written to the reading currtime.
    </li>
    <br>
    
    <a name="timeSource"></a>
    <li><b>timeSource</b><br>
      Selects the time source. The local client time (browser) or the FHEM server time can be displayed. <br>
      This setting is not relevant for (countdown) stopwatches. <br>
      [default: client]
    </li>
    <br>
    
  </ul>  
    
  The following attributes must be set specifically for a clock type. <br>
  <br>
   
  <b>Model: Modern</b>  <br><br>
  
  <ul>
    <a name="modernColorBackground"></a>
    <li><b>modernColorBackground</b><br>
      Background color of the clock.    
    </li>
    <br>
    
    <a name="modernColorFace"></a>
    <li><b>modernColorFace</b><br>
      Colouring of the dial.    
    </li>
    <br>
    
    <a name="modernColorFigure"></a>
    <li><b>modernColorFigure</b><br>
      Colour of the numbers on the dial and the pointer axle cover.    
    </li>
    <br>
    
    <a name="modernColorHand"></a>
    <li><b>modernColorHand</b><br>
      Colour of the watch hands.    
    </li>
    <br>
    
    <a name="modernColorRing"></a>
    <li><b>modernColorRing</b><br>
      Colour of the dial frame.    
    </li>
    <br>
    
    <a name="modernColorRingEdge"></a>
    <li><b>modernColorRingEdge</b><br>
      Colour of the outer ring of the dial frame.  
    </li>
    <br>
  </ul>
  <br>
  
  <b>Model: Station</b>  <br><br>
  
  <ul>
    <a name="stationBody"></a>
    <li><b>stationBody</b><br>
      Type of watch case.     
    </li>
    <br>
    
    <a name="stationBoss"></a>
    <li><b>stationBoss</b><br>
      Type and colour of the pointer axle cover.    
    </li>
    <br>

    <a name="stationHourHand"></a>
    <li><b>stationHourHand</b><br>
      Type of hour hand.    
    </li>
    <br>    
    
    <a name="stationMinuteHand"></a>
    <li><b>stationMinuteHand</b><br>
      Type of minute hand.    
    </li>
    <br> 

    <a name="stationMinuteHandBehavoir"></a>
    <li><b>stationMinuteHandBehavoir</b><br>
      Behavior of the minute hand.    
    </li>
    <br>   

    <a name="stationSecondHand"></a>
    <li><b>stationSecondHand</b><br>
      Type of second hand.   
    </li>
    <br>  

    <a name="stationSecondHandBehavoir"></a>
    <li><b>stationSecondHandBehavoir</b><br>
      Behavior of the second hand.    
    </li>
    <br>  

    <a name="stationStrokeDial"></a>
    <li><b>stationStrokeDial</b><br>
      Selection of the dial.    
    </li>
    <br>      
    
  </ul>
  <br>
  
  <b>Model: Digital</b>  <br><br>
  
  <ul>
    <a name="digitalBorderDistance"></a>
    <li><b>digitalBorderDistance</b><br>
      Left and right distance of the digital text display from the background edge. <br>
      (default: 8)      
    </li>
    <br>  
  
    <a name="digitalColorBackground"></a>
    <li><b>digitalColorBackground</b><br>
      Digital clock background color.    
    </li>
    <br>  

    <a name="digitalColorDigits"></a>
    <li><b>digitalColorDigits</b><br>
      Color of the bar display in a digital watch.    
    </li>
    <br> 

    <a name="digitalDigitAngle"></a>
    <li><b>digitalDigitAngle </b><br>
      Adjusts the tilt angle of the displayed characters. <br>
      (default: 9)      
    </li>
    <br> 
    
    <a name="digitalDigitDistance"></a>
    <li><b>digitalDigitDistance </b><br>
      Adjusts the character spacing. <br>
      (default: 2)     
    </li>
    <br>  
    
    <a name="digitalDigitHeight"></a>
    <li><b>digitalDigitHeight </b><br>
      Adjusts the character height. <br>
      (default: 20)      
    </li>
    <br>

    <a name="digitalDigitWidth"></a>
    <li><b>digitalDigitWidth </b><br>
      Adjusts the character width. <br>
      (default: 12)      
    </li>
    <br>     
  
    <a name="digitalDisplayPattern"></a>
    <li><b>digitalDisplayPattern [countdownwatch | staticwatch | stopwatch | text | watch]</b><br>
      Switching the digital display between a clock (default), a stopwatch, static time display or text display. 
      The text to be displayed in text display mode can be defined with <br>
      <b>set &lt;name&gt displayText</b>. <br><br>
      
      <b>Note:</b> For text display it is recommended to set the attribute "digitalSegmentType" to "16". <br><br>
      
    <ul>
    <table>  
       <colgroup> <col width=5%> <col width=95%> </colgroup>
       <tr><td> <b>countdownwatch </b> </td><td>: CountDown Stopwatch            </td></tr>
       <tr><td> <b>staticwatch</b>     </td><td>: static time display            </td></tr>
       <tr><td> <b>stopwatch</b>       </td><td>: Stopwatch                      </td></tr>
       <tr><td> <b>text</b>            </td><td>: Display of a definable text    </td></tr>
       <tr><td> <b>watch</b>           </td><td>: Watch                          </td></tr>
    </table>
    </ul>
    <br>
    <br>
    </li> 
    
    <a name="digitalSegmentDistance"></a>
    <li><b>digitalSegmentDistance </b><br>
      Defines the distance between the segments. <br>
      (default: 0.5)      
    </li>
    <br> 
    
    <a name="digitalSegmentType"></a>
    <li><b>digitalSegmentType </b><br>
      Switches the segment number of the digital display. <br>
      (default: 7)      
    </li>
    <br> 

    <a name="digitalSegmentWidth"></a>
    <li><b>digitalSegmentWidth </b><br>
      Changes the width of the individual segments. <br>
      (default: 1.5)     
    </li>
    <br> 

    <a name="digitalTextDigitNumber"></a>
    <li><b>digitalTextDigitNumber &lt;Quantity&gt; </b><br>
      If &lt;Quantity&gt > 0, the number of digits of a text display (digitalDisplayPattern = text) is fixed. 
      If &lt;Quantity&gt = 0 or not set, the setting is made automatically. In this case an adaptation is made
      of the character size to the number depending on the set display size (see htmlattr). <br>
      (default: 0)      
    </li>
    <br>      
    
  </ul>
  </ul>
  
</ul>

=end html
=begin html_DE

<a name="Watches"></a>
<h3>Watches</h3>

<br>
Das Modul Watches stellt Uhren in unterschiedlichen Stilen als Device zur Verfügung. Das Modul ist eine JavaScript Anwendung 
die auf einem Client (Browser) ausgeführt wird und nicht auf dem FHEM Server. Attribute und Readings werden asynchron vom 
Server gelesen und evtl. auch geschrieben, allerdings nur dann wenn die Anwendung aktuell im Browser ausgeführt wird. <br> 
Der Nutzer kann das Design der Uhren über Attribute beeinflussen. <br>
Die Uhren basieren auf Skripten dieser Seiten: <br>

<a href='https://www.w3schools.com/graphics/canvas_clock_start.asp'>moderne Uhr</a>, 
<a href='http://www.3quarks.com/de/Bahnhofsuhr/'>Bahnhofsuhr</a>, 
<a href='http://www.3quarks.com/de/Segmentanzeige/'>Digitalanzeige</a> 
<br><br>

Ein Device vom Model <b>Digital</b> kann ebenfalls als Stoppuhr, CountDown-Timer oder
universelle Textanzeige (für Sechzehnsegment-Modus siehe Attribut <a href="#digitalSegmentType">digitalSegmentType</a>) 
verwendet werden. <br>
Als Zeitquelle können sowohl der Client (Browserzeit) als auch der FHEM-Server eingestellt werden 
(Attribut <a href="#timeSource">timeSource</a>). <br>

<ul>
  <a name="WatchesDefine"></a>
  <b>Define</b>
  
  <ul>
    define &lt;name&gt; Watches [Modern | Station | Digital]  
    <br><br>
    
  <table>  
     <colgroup> <col width=5%> <col width=95%> </colgroup>
     <tr><td> <b>Modern</b>     </td><td>: erstellt eine analoge Uhr im modernen Design </td></tr>
     <tr><td> <b>Station</b>    </td><td>: erstellt eine Bahnhofsuhr </td></tr>
     <tr><td> <b>Digital</b>    </td><td>: erstellt eine Digitalanzeige (Uhr, (CountDown)Stoppuhr, statische Zeitanzeige oder Text) </td></tr>
  </table>
  <br>
  <br>
  </ul>

  <a name="WatchesSet"></a>
  <b>Set</b> 
  
  <ul>
  <ul>
  
    <a name="alarmSet"></a>
    <li><b>alarmSet &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; </b><br>
      Setzt die Alarmzeit im Format hh-Stunden, mm-Minuten und ss-Sekunden. <br>
      Erreicht die Zeit den definierten Wert, wird ein Event des Readings "alarmed" ausgelöst. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br><br>
      
      <ul>
      <b>Beispiel</b> <br>
      set &lt;name&gt; alarmSet 0 30 10
      </ul>
      <br>
      
    </li>
    <br>
    
    <a name="alarmDel"></a>
    <li><b>alarmDel</b><br>
      Löscht die gesetzte Alarmzeit und deren Status. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br>
    </li>
    <br>
    
    <a name="countDownInit"></a>
    <li><b>countDownInit &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; | &lt;Sekunden&gt; </b><br>
      Setzt die Startzeit einer CountDown-Stoppuhr.
      Das Format kann sein &lt;hh&gt; Stunden, &lt;mm&gt; Minuten und &lt;ss&gt; Sekunden oder alternativ nur eine Angabe in Sekunden. <br>
      Dieses Set-Kommando ist nur bei einer digitalen CountDown-Stoppuhr vorhanden. <br><br>
      
      <ul>
      <b>Beispiel</b> <br>
      set &lt;name&gt; countDownInit 0 30 10 <br>
      set &lt;name&gt; countDownInit 3600
      </ul>
      <br>
      
    </li>
    <br>
    
    <a name="displayTextSet"></a>
    <li><b>displayTextSet</b><br>
      Stellt den anzuzeigenden Text ein. <br> 
      Dieses Set-Kommando ist nur bei einer digitalen Segmentanzeige mit "digitalDisplayPattern = text" vorhanden. <br>
      (default: leere Anzeige) <br><br>
      
      <b>Hinweis:</b> <br>
      Die darstellbaren Zeichen sind vom Attribut "digitalSegmentType" abhängig. <br>
      Mit der (default) Siebensegmentanzeige können lediglich Ziffern, Bindestrich, Unterstrich und die Buchstaben 
      A, b, C, d, E, F, H, L, n, o, P, r, t, U und Y angezeigt werden. 
      Damit lassen sich außer Zahlen auch kurze Texte wie „Error“, „HELP“, „run“ oder „PLAY“ anzeigen. <br>
      Für Textdarstellung wird empfohlen die Sechzehnsegmentanzeige mit dem Attribut "digitalSegmentType" einzustellen !    
    </li>
    <br>
    
    <a name="displayTextDel"></a>
    <li><b>displayTextDel</b><br>
      Löscht den Anzeigetext. <br>
      Dieses Set-Kommando ist nur bei einer digitalen Segmentanzeige mit "digitalDisplayPattern = text" vorhanden. <br>
    </li>
    <br>
    
    <a name="reset"></a>
    <li><b>reset</b><br>
      Stoppt die Stoppuhr (falls sie läuft) und löscht alle spezifischen Readings bzw. setzt sie auf initialized zurück. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br>
    </li>
    <br>
    
    <a name="resume"></a>
    <li><b>resume</b><br>
      Setzt die Zählung einer angehaltenen Stoppuhr fort. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br>
    </li>
    <br>
    
    <a name="start"></a>
    <li><b>start</b><br>
      Startet die Stoppuhr. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br>
    </li>
    <br>  
    
    <a name="stop"></a>
    <li><b>stop</b><br>
      Stoppt die Stoppuhr. Die erreichte Zeit bleibt erhalten. <br>
      Dieses Set-Kommando ist nur bei digitalen Stoppuhren vorhanden. <br>
    </li>
    <br>
    
    <a name="textTicker"></a>
    <li><b>textTicker on | off </b><br>
      Schaltet den Laufschriftmodus einer Textanzeige (siehe Attribut digitalDisplayPattern) ein bzw. aus. <br>
      (default: off)      
    </li>
    <br>
    
    <a name="time"></a>
    <li><b>time &lt;hh&gt; &lt;mm&gt; &lt;ss&gt; </b><br>
      Setzt eine statische Zeitanzeige mit hh-Stunden, mm-Minuten und ss-Sekunden. <br>
      Dieses Set-Kommando ist nur bei einer Digitaluhr mit statischer Zeitanzeige vorhanden. <br><br>
      
      <ul>
      <b>Beispiel</b> <br>
      set &lt;name&gt; time 8 15 3
      </ul>
      
    </li>
    <br>
  
  </ul>
  </ul>
  <br>

  <a name="WatchesGet"></a>
  <b>Get</b> 
  
  <ul>
  
    N/A
    
  </ul>
  <br>

  <a name="WatchesAttr"></a>
  <b>Attribute</b>
  <br><br>
  
  <ul>
  <ul>
  
    <a name="controlButtonSize"></a>
    <li><b>controlButtonSize</b><br>
      Ändert die Größe der Steuerdrucktasten sofern der Uhrentyp über Steuerdrucktasten verfügt.
    </li>
    <br>
    
    <a name="disable"></a>
    <li><b>disable</b><br>
      Aktiviert/deaktiviert das Device.
    </li>
    <br>
    
    <a name="hideButtons"></a>
    <li><b>hideButtons</b><br>
      Verbirgt die Steuerdrucktasten sofern der Uhrentyp über Steuerdrucktasten verfügt. 
    </li>
    <br>
    
    <a name="hideDisplayName"></a>
    <li><b>hideDisplayName</b><br>
      Verbirgt den Device/Alias-Namen (Link zur Detailansicht).    
    </li>
    <br>
    
    <a name="htmlattr"></a>
    <li><b>htmlattr</b><br>
      Zusätzliche HTML Tags zur Größenänderung der Uhr / Anzeige. <br><br>
      <ul>
        <b>Beispiel: </b><br>
        attr &lt;name&gt; htmlattr width="125" height="125" <br>
      </ul>
    </li>
    <br>
    
    <a name="timeAsReading"></a>
    <li><b>timeAsReading</b><br>
      Wenn gesetzt, wird eine angezeigte Uhrzeit in das Reading currtime geschrieben.
    </li>
    <br>
    
    <a name="timeSource"></a>
    <li><b>timeSource</b><br>
      Wählt die Zeitquelle aus. Es kann die lokale Clientzeit (Browser) oder die Zeit des FHEM-Servers angezeigt werden. <br>
      Diese Einstellung ist bei (CountDown-)Stoppuhren nicht relevant. <br>
      (default: client)
    </li>
    <br>
    
  </ul>  
    
  Die nachfolgenden Attribute sind spezifisch für einen Uhrentyp zu setzen. <br>
  <br>
   
  <b>Model: Modern</b>  <br><br>
  
  <ul>
    <a name="modernColorBackground"></a>
    <li><b>modernColorBackground</b><br>
      Hintergrundfarbe der Uhr.    
    </li>
    <br>
    
    <a name="modernColorFace"></a>
    <li><b>modernColorFace</b><br>
      Einfärbung des Ziffernblattes.    
    </li>
    <br>
    
    <a name="modernColorFigure"></a>
    <li><b>modernColorFigure</b><br>
      Farbe der Ziffern im Ziffernblatt und der Zeigerachsabdeckung.    
    </li>
    <br>
    
    <a name="modernColorHand"></a>
    <li><b>modernColorHand</b><br>
      Farbe der UhrenZeiger.    
    </li>
    <br>
    
    <a name="modernColorRing"></a>
    <li><b>modernColorRing</b><br>
      Farbe des Ziffernblattrahmens.    
    </li>
    <br>
    
    <a name="modernColorRingEdge"></a>
    <li><b>modernColorRingEdge</b><br>
      Farbe des Außenringes vom Ziffernblattrahmen.    
    </li>
    <br>
  </ul>
  <br>
  
  <b>Model: Station</b>  <br><br>
  
  <ul>
    <a name="stationBody"></a>
    <li><b>stationBody</b><br>
      Art des Uhrengehäuses.    
    </li>
    <br>
    
    <a name="stationBoss"></a>
    <li><b>stationBoss</b><br>
      Art und Farbe der Zeigerachsabdeckung.    
    </li>
    <br>

    <a name="stationHourHand"></a>
    <li><b>stationHourHand</b><br>
      Art des Stundenzeigers.    
    </li>
    <br>    
    
    <a name="stationMinuteHand"></a>
    <li><b>stationMinuteHand</b><br>
      Art des Minutenzeigers.    
    </li>
    <br> 

    <a name="stationMinuteHandBehavoir"></a>
    <li><b>stationMinuteHandBehavoir</b><br>
      Verhalten des Minutenzeigers.    
    </li>
    <br>   

    <a name="stationSecondHand"></a>
    <li><b>stationSecondHand</b><br>
      Art des Sekundenzeigers.    
    </li>
    <br>  

    <a name="stationSecondHandBehavoir"></a>
    <li><b>stationSecondHandBehavoir</b><br>
      Verhalten des Sekundenzeigers.    
    </li>
    <br>  

    <a name="stationStrokeDial"></a>
    <li><b>stationStrokeDial</b><br>
      Auswahl des Ziffernblattes.    
    </li>
    <br>      
    
  </ul>
  <br>
  
  <b>Model: Digital</b>  <br><br>
  
  <ul>
    <a name="digitalBorderDistance"></a>
    <li><b>digitalBorderDistance</b><br>
      Linker und rechter Abstand der digitalen Textanzeige vom Hintergrundrand. <br>
      (default: 8)      
    </li>
    <br>  
  
    <a name="digitalColorBackground"></a>
    <li><b>digitalColorBackground</b><br>
      Digitaluhr Hintergrundfarbe.    
    </li>
    <br>  

    <a name="digitalColorDigits"></a>
    <li><b>digitalColorDigits</b><br>
      Farbe der Balkenanzeige in einer Digitaluhr.    
    </li>
    <br> 

    <a name="digitalDigitAngle"></a>
    <li><b>digitalDigitAngle </b><br>
      Stellt den Neigungswinkel der dargestellten Zeichen ein. <br>
      (default: 9)      
    </li>
    <br> 
    
    <a name="digitalDigitDistance"></a>
    <li><b>digitalDigitDistance </b><br>
      Stellt den Zeichenabstand ein. <br>
      (default: 2)      
    </li>
    <br>  
    
    <a name="digitalDigitHeight"></a>
    <li><b>digitalDigitHeight </b><br>
      Stellt die Zeichenhöhe ein. <br>
      (default: 20)      
    </li>
    <br>

    <a name="digitalDigitWidth"></a>
    <li><b>digitalDigitWidth </b><br>
      Stellt die Zeichenbreite ein. <br>
      (default: 12)      
    </li>
    <br>     
  
    <a name="digitalDisplayPattern"></a>
    <li><b>digitalDisplayPattern [countdownwatch | staticwatch | stopwatch | text | watch]</b><br>
      Umschaltung der Digitalanzeige zwischen einer Uhr (default), einer Stoppuhr, statischen Zeitanzeige oder Textanzeige. 
      Der anzuzeigende Text im Modus Textanzeige kann mit <br>
      <b>set &lt;name&gt displayText</b>. <br><br>
      
      <b>Hinweis:</b> Bei Textanzeige wird empfohlen das Attribut "digitalSegmentType" auf "16" zu stellen. <br><br>
      
    <ul>
    <table>  
       <colgroup> <col width=5%> <col width=95%> </colgroup>
       <tr><td> <b>countdownwatch </b> </td><td>: CountDown Stoppuhr                 </td></tr>
       <tr><td> <b>staticwatch</b>     </td><td>: statische Zeitanzeige              </td></tr>
       <tr><td> <b>stopwatch</b>       </td><td>: Stoppuhr                           </td></tr>
       <tr><td> <b>text</b>            </td><td>: Anzeige eines definierbaren Textes </td></tr>
       <tr><td> <b>watch</b>           </td><td>: Uhr                                </td></tr>
    </table>
    </ul>
    <br>
    <br>
    </li> 
    
    <a name="digitalSegmentDistance"></a>
    <li><b>digitalSegmentDistance </b><br>
      Legt den Abstand zwischen den Segmenten fest. <br>
      (default: 0.5)      
    </li>
    <br> 
    
    <a name="digitalSegmentType"></a>
    <li><b>digitalSegmentType </b><br>
      Schaltet die Segmentanzahl der Digitalanzeige um. <br>
      (default: 7)      
    </li>
    <br> 

    <a name="digitalSegmentWidth"></a>
    <li><b>digitalSegmentWidth </b><br>
      Verändert die Breite der einzelnen Segmente. <br>
      (default: 1.5)      
    </li>
    <br> 

    <a name="digitalTextDigitNumber"></a>
    <li><b>digitalTextDigitNumber &lt;Anzahl&gt; </b><br>
      Wenn &lt;Anzahl&gt > 0 wird die Anzahl der Stellen einer Textanzeige (digitalDisplayPattern = text) fest eingestellt. 
      Wenn &lt;Anzahl&gt = 0 oder nicht gesetzt erfolgt die Festlegung automatisch. In diesem Fall erfolgt eine Adaption
      der Zeichengröße an die Anzahl abhängig von der eingestellten Displaygröße (siehe htmlattr). <br>
      (default: 0)      
    </li>
    <br>      
    
  </ul>
  </ul>
  
</ul>

=end html_DE

=for :application/json;q=META.json 60_Watches.pm
{
  "abstract": "Clock display in different variants",
  "x_lang": {
    "de": {
      "abstract": "Uhrenanzeige in verschiedenen Varianten"
    }
  },
  "keywords": [
    "Watch",
    "Modern clock",
    "clock",
    "Station clock",
    "Digital display"
  ],
  "version": "v1.1.1",
  "release_status": "stable",
  "author": [
    "Heiko Maaz <heiko.maaz@t-online.de>",
    null
  ],
  "x_fhem_maintainer": [
    "DS_Starter"
  ],
  "x_fhem_maintainer_github": [
    "nasseeder1"
  ],
  "prereqs": {
    "runtime": {
      "requires": {
        "FHEM": 5.00918799,
        "perl": 5.014,
        "Time::HiRes": 0,
        "GPUtils": 0
      },
      "recommends": {
        "FHEM::Meta": 0
      },
      "suggests": {
      }
    }
  }
}
=end :application/json;q=META.json

=cut