52_I2C_MCP342x.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. ##############################################
  2. # $Id: 52_I2C_MCP342x.pm 13424 2017-02-16 22:06:16Z klausw $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use Time::HiRes qw(usleep);
  7. use Scalar::Util qw(looks_like_number);
  8. #use Error qw(:try);
  9. use constant {
  10. MCP3422_I2C_ADDRESS => '0x68',
  11. };
  12. ##################################################
  13. # Forward declarations
  14. #
  15. sub I2C_MCP342x_Initialize($);
  16. sub I2C_MCP342x_Define($$);
  17. sub I2C_MCP342x_Attr(@);
  18. sub I2C_MCP342x_Poll($);
  19. sub I2C_MCP342x_Set($@);
  20. sub I2C_MCP342x_Undef($$);
  21. my %resols = (
  22. '12' => {
  23. code => 0b00000000,
  24. delay => 5690,
  25. lsb => 1000,
  26. },
  27. '14' => {
  28. code => 0b00000100,
  29. delay => 22730,
  30. lsb => 250,
  31. },
  32. '16' => {
  33. code => 0b00001000,
  34. delay => 90910,
  35. lsb => 62.5,
  36. },
  37. '18' => {
  38. code => 0b00001100,
  39. delay => 363640,
  40. lsb => 15.625,
  41. },
  42. );
  43. my %gains = (
  44. '1' => 0b00000000,
  45. '2' => 0b00000001,
  46. '4' => 0b00000010,
  47. '8' => 0b00000011,
  48. );
  49. sub I2C_MCP342x_Initialize($) {
  50. my ($hash) = @_;
  51. $hash->{DefFn} = 'I2C_MCP342x_Define';
  52. $hash->{InitFn} = 'I2C_MCP342x_Init';
  53. $hash->{AttrFn} = 'I2C_MCP342x_Attr';
  54. $hash->{GetFn} = 'I2C_MCP342x_Get';
  55. $hash->{UndefFn} = 'I2C_MCP342x_Undef';
  56. $hash->{I2CRecFn} = 'I2C_MCP342x_I2CRec';
  57. $hash->{AttrList} = 'IODev do_not_notify:0,1 showtime:0,1 poll_interval:1,2,5,10,20,30 ' .
  58. 'ch1roundDecimal:0,1,2,3 ch1gain:1,2,4,8 ch1resolution:12,14,16,18 ch1factor '.
  59. 'ch2roundDecimal:0,1,2,3 ch2gain:1,2,4,8 ch2resolution:12,14,16,18 ch2factor '.
  60. 'ch3roundDecimal:0,1,2,3 ch3gain:1,2,4,8 ch3resolution:12,14,16,18 ch3factor '.
  61. 'ch4roundDecimal:0,1,2,3 ch4gain:1,2,4,8 ch4resolution:12,14,16,18 ch4factor '.
  62. $readingFnAttributes;
  63. }
  64. sub I2C_MCP342x_Define($$) {
  65. my ($hash, $def) = @_;
  66. my @a = split('[ \t][ \t]*', $def);
  67. $hash->{STATE} = "defined";
  68. if ($main::init_done) {
  69. eval { I2C_MCP342x_Init( $hash, [ @a[ 2 .. scalar(@a) - 1 ] ] ); };
  70. return I2C_MCP342x_Catch($@) if $@;
  71. }
  72. return undef;
  73. }
  74. sub I2C_MCP342x_Init($$) {
  75. my ( $hash, $args ) = @_;
  76. my $name = $hash->{NAME};
  77. Log3 $hash, 5, "$hash->{NAME}: Init Argumente1: $args";
  78. if (defined $args && int(@$args) < 1)
  79. {
  80. Log3 $hash, 0, "Define: Wrong syntax. Usage:\n" .
  81. "define <name> MCP342x [<i2caddress>] [<type>]";
  82. }
  83. if (defined (my $address = shift @$args)) {
  84. $hash->{I2C_Address} = $address =~ /^0x.*$/ ? oct($address) : $address;
  85. Log3 $hash, 0, "$name: I2C Address not valid" unless ($hash->{I2C_Address} < 128 && $hash->{I2C_Address} > 3);
  86. } else {
  87. $hash->{I2C_Address} = hex(MCP3422_I2C_ADDRESS);
  88. }
  89. if (defined (my $channels = shift @$args)) {
  90. $hash->{channels} = ($channels == 4 ? 4 : 2);
  91. } else {
  92. $hash->{channels} = 2;
  93. }
  94. my $msg = '';
  95. # create default attributes
  96. if (AttrVal($name, 'poll_interval', '?') eq '?') {
  97. $msg = CommandAttr(undef, $name . ' poll_interval 5');
  98. if ($msg) {
  99. Log3 ($hash, 1, $msg);
  100. return $msg;
  101. }
  102. }
  103. AssignIoPort($hash);
  104. $hash->{STATE} = 'Initialized';
  105. # my %sendpackage = ( i2caddress => $hash->{I2C_Address}, direction => "i2cread" );
  106. # $sendpackage{reg} = hex("AA");
  107. # $sendpackage{nbyte} = 22;
  108. # return "$name: no IO device defined" unless ($hash->{IODev});
  109. # my $phash = $hash->{IODev};
  110. # my $pname = $phash->{NAME};
  111. # CallFn($pname, "I2CWrtFn", $phash, \%sendpackage);
  112. return undef;
  113. }
  114. sub I2C_MCP342x_Catch($) {
  115. my $exception = shift;
  116. if ($exception) {
  117. $exception =~ /^(.*)( at.*FHEM.*)$/;
  118. return $1;
  119. }
  120. return undef;
  121. }
  122. sub I2C_MCP342x_Attr (@) {# hier noch Werteueberpruefung einfuegen
  123. my ($command, $name, $attr, $val) = @_;
  124. my $hash = $defs{$name};
  125. my $msg = '';
  126. if ($command && $command eq "set" && $attr && $attr eq "IODev") {
  127. if ($main::init_done and (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $val)) {
  128. main::AssignIoPort($hash,$val);
  129. my @def = split (' ',$hash->{DEF});
  130. I2C_MCP342x_Init($hash,\@def) if (defined ($hash->{IODev}));
  131. }
  132. }
  133. if ($attr && $attr eq 'poll_interval') {
  134. #my $pollInterval = (defined($val) && looks_like_number($val) && $val > 0) ? $val : 0;
  135. if (!defined($val) ) {
  136. RemoveInternalTimer($hash);
  137. } elsif ($val > 0) {
  138. RemoveInternalTimer($hash);
  139. InternalTimer(1, 'I2C_MCP342x_Poll', $hash, 0);
  140. } else {
  141. $msg = 'Wrong poll intervall defined. poll_interval must be a number > 0';
  142. }
  143. } elsif ($attr eq 'roundDecimal') {
  144. $msg = 'Wrong $attr defined. Use one of 0, 1, 2' if defined($val) && $val <= 0 && $val >= 3 ;
  145. } elsif ($attr eq 'gain') {
  146. foreach (split (/,/,$val)) {
  147. my @pair = split (/=/,$_);
  148. $msg = "wrong value: $_ for \"attr $hash->{NAME} $attr\" use comma separated <channel>=1|2|4|8 where <channel> = 1-$hash->{channels}"
  149. unless ( ( scalar(@pair) == 2 &&
  150. $pair[0] =~ m/^[1-4]$/i && $pair[0] <= $hash->{channels} &&
  151. $pair[1] =~ m/^(1|2|4|8)$/i ) ||
  152. $val =~ m/^(1|2|4|8)$/i);
  153. }
  154. } elsif ($attr eq 'resolution') {
  155. foreach (split (/,/,$val)) {
  156. my @pair = split (/=/,$_);
  157. $msg = "wrong value: $_ for \"attr $hash->{NAME} $attr\" use comma separated <channel>=12|14|16|18 where <channel> = 1-$hash->{channels}"
  158. unless ( ( scalar(@pair) == 2 &&
  159. $pair[0] =~ m/^[1-4]$/i &&
  160. $pair[1] =~ m/^1(2|4|6|8)$/i ) &&
  161. $val =~ m/^1(2|4|6|8)$/i );
  162. }
  163. }
  164. return ($msg) ? $msg : undef;
  165. }
  166. sub I2C_MCP342x_Poll($) {
  167. my ($hash) = @_;
  168. my $name = $hash->{NAME};
  169. # Read values
  170. I2C_MCP342x_Get($hash, $name);
  171. my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 0);
  172. if ($pollInterval > 0) {
  173. InternalTimer(gettimeofday() + ($pollInterval * 60), 'I2C_MCP342x_Poll', $hash, 0);
  174. }
  175. }
  176. sub I2C_MCP342x_Get($@) {
  177. my ($hash, @a) = @_;
  178. my $name = $a[0];
  179. my $cmd = $a[1];
  180. my $rex = "^[1-" . $hash->{channels} . "]\$";
  181. if (defined $cmd && $cmd =~ m/$rex/i) {
  182. my $resol = defined $a[2] ? $a[2] : AttrVal($hash->{NAME},("ch" . $cmd . "resolution"),"12");
  183. return "Wrong resolution, use 12, 14, 16 or 18" unless $resol =~ m/^1(2|4|6|8)$/i;
  184. my $gain = defined $a[3] ? $a[3] : AttrVal($hash->{NAME},("ch" . $cmd . "gain"),"1");
  185. return "Wrong gain, use 1, 2, 4 or 8" unless $gain =~ m/^(1|2|4|8)$/i;
  186. my $ts = ReadingsTimestamp($hash->{NAME},("Channel".$cmd),0);
  187. I2C_MCP342x_readvoltage($hash,$cmd,$resol,$gain);
  188. foreach (1..400) { #max 2s warten
  189. usleep 5000;
  190. return ReadingsVal($hash->{NAME},("Channel".$cmd),undef) if $ts ne ReadingsTimestamp($hash->{NAME},("Channel".$cmd),0);
  191. }
  192. } else {
  193. foreach (1..$hash->{channels}) {
  194. my $resol = defined $a[3] ? $a[3] : AttrVal($hash->{NAME},("ch" . $_ . "resolution"),"12");
  195. return "Wrong resolution, use 12, 14, 16 or 18" unless $resol =~ m/^1(2|4|6|8)$/i;
  196. my $gain = defined $a[4] ? $a[4] : AttrVal($hash->{NAME},("ch" . $_ . "gain"),"1");
  197. return "Wrong gain, use 1, 2, 4 or 8" unless $gain =~ m/^(1|2|4|8)$/i;
  198. I2C_MCP342x_readvoltage($hash,$_,$resol,$gain);
  199. }
  200. my @gets = ('1', '2');
  201. push(@gets,('3', '4')) if $hash->{channels} == 4;
  202. return 'Unknown argument' . (defined $cmd ? (" " . $cmd) : "" ) . ', choose one of ' . join(' ', @gets)
  203. }
  204. }
  205. sub I2C_MCP342x_Undef($$) {
  206. my ($hash, $arg) = @_;
  207. RemoveInternalTimer($hash);
  208. return undef;
  209. }
  210. sub I2C_MCP342x_I2CRec ($$) {
  211. my ($hash, $clientmsg) = @_;
  212. my $name = $hash->{NAME};
  213. my $phash = $hash->{IODev};
  214. my $pname = $phash->{NAME};
  215. while ( my ( $k, $v ) = each %$clientmsg ) { #erzeugen von Internals fuer alle Keys in $clientmsg die mit dem physical Namen beginnen
  216. $hash->{$k} = $v if $k =~ /^$pname/ ;
  217. }
  218. #my $ankommen = "$hash->{NAME}: vom physical empfangen";
  219. # foreach my $av (keys %{$clientmsg}) { $ankommen .= "|" . $av . ": " . $clientmsg->{$av}; }
  220. #Log3 $hash, 1, $ankommen;
  221. if ($clientmsg->{direction} && $clientmsg->{$pname . "_SENDSTAT"} && $clientmsg->{$pname . "_SENDSTAT"} eq "Ok") {
  222. readingsSingleUpdate($hash,"state", "Ok", 1);
  223. if ( $clientmsg->{direction} eq "i2cread" && defined($clientmsg->{received}) ) {###hier noch normal read rein,wenn alles wieder ok
  224. #Log3 $hash, 1, "empfangen: $clientmsg->{received}";
  225. I2C_MCP342x_GetVoltage ($hash, $clientmsg->{received}); # if $clientmsg->{type} eq "temp" && $clientmsg->{nbyte} == 2;
  226. }
  227. } else {
  228. readingsSingleUpdate($hash,"state", "transmission error", 1);
  229. Log3 $hash, 3, "$name: failurei in message from $pname";
  230. Log3 $hash, 3,(defined($clientmsg->{direction}) ? "Direction: " . $clientmsg->{direction} : "Direction: undef").
  231. (defined($clientmsg->{i2caddress}) ? " I2Caddress: " . sprintf("0x%.2X", $clientmsg->{i2caddress}) : " I2Caddress: undef").
  232. (defined($clientmsg->{reg}) ? " Register: " . sprintf("0x%.2X", $clientmsg->{reg}) : " Register: undef").
  233. (defined($clientmsg->{data}) ? " Data: " . sprintf("0x%.2X", $clientmsg->{data}) : " Data: undef").
  234. (defined($clientmsg->{received}) ? " received: " . sprintf("0x%.2X", $clientmsg->{received}) : " received: undef");
  235. }
  236. }
  237. sub I2C_MCP342x_GetVoltage ($$) {
  238. my ($hash, $rawdata) = @_;
  239. my @raw = split(" ",$rawdata);
  240. if ( defined($raw[4]) ) {
  241. if ( ($raw[4] & 0b10000000) == 0 ) {
  242. my $channel = 1 + (($raw[4] & 0b01100000) >> 5 );
  243. my $resol = 2 * (($raw[4] & 0b00001100) >> 2 ) + 12;
  244. my $gain = 2 ** ($raw[4] & 0b00000011);
  245. my $rawvolt;
  246. if ($resol == 18) {
  247. $rawvolt = ($raw[0] & 0b00000011) << 16 | $raw[1] << 8 | $raw[2];
  248. } elsif ($resol == 14) {
  249. $rawvolt = ($raw[0] & 0b00111111) << 8 | $raw[1];
  250. } elsif ($resol == 12) {
  251. $rawvolt = ($raw[0] & 0b00001111) << 8 | $raw[1];
  252. } else {
  253. $rawvolt = $raw[0] << 8 | $raw[1];
  254. }
  255. Log3 $hash, 4, "Kanal: $channel, rawvolt: $rawvolt, Aufloesung: $resol, Gain: $gain, LSB: $resols{$resol}{lsb}";
  256. $rawvolt -= (1 << $resol) if $rawvolt >= (1 << ($resol - 1));
  257. Log3 $hash, 4, "Kanal: $channel, Signedrawvolt: $rawvolt";
  258. my $voltage = ( $rawvolt * $resols{$resol}{lsb} ) / $gain ;
  259. $voltage /= 1000000; # LSB Werte in µV
  260. $voltage *= AttrVal($hash->{NAME},("ch" . $channel . "factor"),"1");
  261. $voltage = sprintf(
  262. '%.' . AttrVal($hash->{NAME}, ('ch' . $channel . 'roundDecimal'), 3) . 'f',
  263. $voltage
  264. );
  265. $voltage .= " overflow" if ( $rawvolt == ( (1<<($resol-1)) - 1) || $rawvolt == (1<<($resol-1)) );
  266. readingsSingleUpdate($hash,"Channel$channel", $voltage, 1);
  267. } else {
  268. Log3 $hash, 3, $hash->{NAME} . " error, output conversion not finished";
  269. }
  270. }
  271. }
  272. sub I2C_MCP342x_readvoltage($@) {
  273. my ($hash, $channel, $resol, $gain) = @_;
  274. my $name = $hash->{NAME};
  275. return "$name: no IO device defined" unless ($hash->{IODev});
  276. my $phash = $hash->{IODev};
  277. my $pname = $phash->{NAME};
  278. #0b10010000
  279. my $confreg = 1 << 7; # 1|| |||| Initiate a new conversion
  280. $confreg |= ($channel - 1) << 5; # 11 |||| Channel Selection Bits
  281. $confreg |= $resols{$resol}{code}; # 11|| Sample Rate Selection Bit
  282. $confreg |= $gains{$gain}; # 11 PGA Gain Selection Bits
  283. #Log3 $hash, 1, "confinhalt: " . sprintf ('0b%08b', $confreg);
  284. # Write CONFIGURATION REGISTER to device. This requests a conversion process
  285. my $i2creq = { i2caddress => $hash->{I2C_Address}, direction => "i2cwrite" };
  286. $i2creq->{data} = $confreg;
  287. CallFn($pname, "I2CWrtFn", $phash, $i2creq);
  288. usleep($resols{$resol}{delay}); #Verzoegerung
  289. # Read the result from device
  290. my $i2cread = { i2caddress => $hash->{I2C_Address}, direction => "i2cread" };
  291. $i2cread->{nbyte} = 5;
  292. #$i2cread->{type} = "temp";
  293. CallFn($pname, "I2CWrtFn", $phash, $i2cread);
  294. return;
  295. }
  296. 1;
  297. =pod
  298. =item device
  299. =item summary reads the analog inputs from an via I2C connected MCP342x
  300. =item summary_DE lesen der Analogeing&aumlnge eines &uuml;ber I2C angeschlossenen MCP342x
  301. =begin html
  302. <a name="I2C_MCP342x"></a>
  303. <h3>I2C_MCP342x</h3>
  304. (en | <a href="commandref_DE.html#I2C_MCP342x">de</a>)
  305. <ul>
  306. <a name="I2C_MCP342x"></a>
  307. Provides an interface to the MCP3422/3/4 A/D converter.
  308. The I2C messages are send through an I2C interface module like <a href="#RPII2C">RPII2C</a>, <a href="#FRM">FRM</a>
  309. or <a href="#NetzerI2C">NetzerI2C</a> so this device must be defined first.<br>
  310. <b>attribute IODev must be set</b><br>
  311. <a name="I2C_MCP342xDefine"></a><br>
  312. <b>Define</b>
  313. <ul>
  314. <code>define &lt;name&gt; I2C_MCP342x [[&lt;I2C Address&gt;] &lt;n channels&gt;]</code><br>
  315. where &lt;I2C Address&gt; is without direction bit and &lt;n channels&gt; is the number of A/D channels<br><br>
  316. </ul>
  317. <a name="I2C_MCP342xSet"></a>
  318. <b>Get</b>
  319. <ul>
  320. <code>get &lt;name&gt; [[[&lt;channel&gt;] &lt;resolution&gt; ] &lt;gain&gt;]</code><br>
  321. Returns the level on specific &lt;channel&gt;. &lt;resolution&gt; and &lt;gain&gt; will override attibutes for actual operation.
  322. Without attributes only the readings will be refreshed.<br><br>
  323. </ul>
  324. <a name="I2C_MCP342xAttr"></a>
  325. <b>Attributes</b>
  326. <ul>
  327. <li>poll_interval<br>
  328. Set the polling interval in minutes to query data from sensor<br>
  329. Default: 5, valid values: 1,2,5,10,20,30<br><br>
  330. </li>
  331. Following attributes are separate for all channels.<br><br>
  332. <li>ch1resolution<br>
  333. resolution settings<br>
  334. the bigger the resolution the longer the conversion time.<br>
  335. Default: 12, valid values: 12,14,16,18<br><br>
  336. </li>
  337. <li>ch1gain<br>
  338. gain setting<br>
  339. Important: the gain setting will reduce the measurement range an may produce an overflow. In this case "overflow" will be added to reading<br>
  340. Default: 1, valid values: 1,2,4,8<br><br>
  341. </li>
  342. <li>ch1factor<br>
  343. correction factor (will be mutiplied to channel value)<br>
  344. Default: 1, valid values: number<br><br>
  345. </li>
  346. <li>ch1roundDecimal<br>
  347. Number of decimal places for value<br>
  348. Default: 3, valid values: 0,1,2,3<br><br>
  349. </li>
  350. <li><a href="#IODev">IODev</a></li>
  351. <li><a href="#do_not_notify">do_not_notify</a></li>
  352. <li><a href="#showtime">showtime</a></li>
  353. </ul><br>
  354. </ul>
  355. =end html
  356. =begin html_DE
  357. <a name="I2C_MCP342x"></a>
  358. <h3>I2C_MCP342x</h3>
  359. (<a href="commandref.html#I2C_MCP342x">en</a> | de)
  360. <ul>
  361. <a name="I2C_MCP342x"></a>
  362. Erm&ouml;glicht die Verwendung eines MCP3422/3/4 I2C A/D Wandler.
  363. I2C-Botschaften werden &uuml;ber ein I2C Interface Modul wie beispielsweise das <a href="#RPII2C">RPII2C</a>, <a href="#FRM">FRM</a>
  364. oder <a href="#NetzerI2C">NetzerI2C</a> gesendet. Daher muss dieses vorher definiert werden.<br>
  365. <b>Das Attribut IODev muss definiert sein.</b><br>
  366. <a name="I2C_MCP342xDefine"></a><br>
  367. <b>Define</b>
  368. <ul>
  369. <code>define &lt;name&gt; I2C_MCP342x [[&lt;I2C Address&gt;] &lt;n channels&gt;]</code><br>
  370. Der Wert <code>&lt;I2C Address&gt;</code> ist die I2C Adresse ohne Richtungsbit und &lt;n channels&gt; die Anzahl der A/D Kanäle.<br>
  371. </ul>
  372. <a name="I2C_MCP342xGet"></a>
  373. <b>Get</b>
  374. <ul>
  375. <code>get &lt;name&gt; [[[&lt;channel&gt;] &lt;resolution&gt; ] &lt;gain&gt;]</code><br>
  376. Aktuelle Werte vom entstrechenden &lt;channel&gt; lesen. &lt;resolution&gt; und &lt;gain&gt; &uuml;berschreiben die entsprechenden Attribute für diesen Lesevorgang<br><br>
  377. </ul>
  378. <a name="I2C_MCP342xAttr"></a>
  379. <b>Attribute</b>
  380. <ul>
  381. <li>poll_interval<br>
  382. Aktualisierungsintervall aller Werte in Minuten.<br>
  383. Standard: 5, g&uuml;ltige Werte: 1,2,5,10,20,30<br><br>
  384. </li>
  385. Folgende Attribute existieren separat f&uuml;r alle Kan&auml;le.<br><br>
  386. <li>ch1resolution<br>
  387. Aufl&ouml;sung des Kanals<br>
  388. Je gr&ouml;&szlig;er die Aufl&ouml;sung desto l&auml;nger die Lesezeit.<br>
  389. Standard: 12, g&uuml;ltige Werte: 12,14,16,18<br><br>
  390. </li>
  391. <li>ch1gain<br>
  392. Verst&auml;rkungsfaktor<br>
  393. Wichtig: Der Verst&auml;rkungsfaktor verringert den Messbereich entsprechend und kann zu einem &Uuml;berlauf f&uuml;hren. In diesem Fall wird "overflow" an das reading angeh&auml;ngt.<br>
  394. Standard: 1, g&uuml;ltige Werte: 1,2,4,8<br><br>
  395. </li>
  396. <li>ch1factor<br>
  397. Korrekturfaktor (Wird zum Kanalwert multipliziert.)<br>
  398. Standard: 1, g&uuml;ltige Werte: Zahl<br><br>
  399. </li>
  400. <li>ch1roundDecimal<br>
  401. Anzahl Dezimalstellen f&uuml;r den Messwert<br>
  402. Standard: 3, g&uuml;ltige Werte: 0,1,2,3<br><br>
  403. </li>
  404. <li><a href="#IODev">IODev</a></li>
  405. <li><a href="#do_not_notify">do_not_notify</a></li>
  406. <li><a href="#showtime">showtime</a></li>
  407. </ul><br>
  408. </ul>
  409. =end html_DE
  410. =cut