98_PHC.pm 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215
  1. ##############################################
  2. # $Id: 98_PHC.pm 17272 2018-09-04 17:01:40Z StefanStrobel $
  3. # fhem Modul für PHC
  4. #
  5. # This file is part of fhem.
  6. #
  7. # Fhem is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Fhem is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. # Changelog:
  22. #
  23. # 2017-02-11 started initial PoC version
  24. # 2017-08-08 optimized logging, silentReconnect, singleLastCommand
  25. # 2017-08-18 ping command, reset / config, sendRaw
  26. #
  27. #
  28. # Todo / Ideas:
  29. # =============
  30. #
  31. # command 01 (Ping), configure commands ...
  32. #
  33. # input validation for attrs (allowed EMD address)
  34. # EMD Resend falls kein ACK
  35. # Bei kaputten Frames nächstes Frame im Buffer suchen
  36. #
  37. # attr zum Steuern der Readings-Erzeugung
  38. # setze alle Modul-Ausgänge in Readings mit Moduladr als Namen
  39. # nur angegebenen Liste von Modulen in Readings setzen
  40. # Namen für Readings überschreben mit eigenem attr (stärker als desc falls gesetzt)
  41. # -Desc Readings erzeugen
  42. #
  43. # Parsen der ACK / Feedback Channelbits als Option? (Bei Umschalten wäre es aber nötig)
  44. # Per Default auf eindeutige Befehle Readings setzen - Wert aus Command Hash (bei Ein / Aus, nicht bei Umschalten)
  45. # Dim Feedback codes (ein / aus)
  46. #
  47. # Timer im STM von Fhem aus ändern?
  48. #
  49. # What does not work:
  50. # ===================
  51. # AMD direkt ansprechen
  52. # Problem: keine Unterscheidung im Protokoll zwischen Befehlen vom STM und solchen von Modulen. Erst ACK ist Unterscheid
  53. # bei direktem Befehl meint STM dass es ein Feedback-Befeh ist. Die Acks überschneiden sich.
  54. # -> direktes Ansprechen von AMDs scheint nicht sinnvoll.
  55. # also Fhem als virtuelles EMD, STM soll dann AMDs ansprechen.
  56. # AMD in Fhem simulieren?
  57. # Fhem auf RPI viel zu langsam - erster Empfang enthält bereits 8 Resends vom STM bis Fhem überhaupt Befehl verarbeitet
  58. # insgesamt kommen 63 Resends ...
  59. #
  60. package main;
  61. use strict;
  62. use warnings;
  63. use Time::HiRes qw( gettimeofday tv_interval time );
  64. use Encode qw(decode encode);
  65. sub PHC_Initialize($);
  66. sub PHC_Define($$);
  67. sub PHC_Undef($$);
  68. sub PHC_Set($@);
  69. sub PHC_Get($@);
  70. sub PHC_Read($);
  71. sub PHC_Ready($);
  72. sub PHC_ReadAnswer($$$);
  73. sub PHC_ParseFrames($);
  74. sub PHC_HandleSendQueue($);
  75. sub PHC_TimeoutSend($);
  76. my $PHC_Version = '0.42 - 31.8.2018';
  77. my %PHC_AdrType = (
  78. 0x00 => ['EMD'], # Class 0 starts at 0x00 (EMD, ...)
  79. 0x20 => ['MCC', 'UIM'], # Class 1 starts at 0x20 / 32 (UIM, MCC, ...)
  80. 0x40 => ['AMD', 'JRM'], # Class 2 starts at 0x40 / 64 (AMD, JRM)
  81. 0x60 => ['MFM'], # Class 3 starts at 0x60 / 96 (MFM, ...) FUI = MFM
  82. 0x80 => [], # Class 4 starts at 0x80 / 128 (?)
  83. 0xA0 => ['DIM'], # Class 5 starts at 0xA0 / 160 (DIM, ...)
  84. 0xC0 => [], # ?
  85. 0xE0 => ['CLK'] #
  86. );
  87. # shr bits for channel, &-Mask for function
  88. my %PHC_CodeSplit = (
  89. 'EMD' => [4, 0x0F],
  90. 'MCC' => [4, 0x0F],
  91. 'UIM' => [4, 0x0F],
  92. 'AMD' => [5, 0x1F],
  93. 'JRM' => [5, 0x1F],
  94. 'MFM' => [4, 0x0F],
  95. 'DIM' => [5, 0x1F],
  96. 'CLK' => [4, 0x0F]
  97. );
  98. #
  99. # Options:
  100. # cbm Channel bits in Message
  101. # cba Channel bits in Ack
  102. # o use name for output channel
  103. # i use name for input channel
  104. # t1 time in data bytes 1+2
  105. # t2 time in data bytes 2+3, 4+5 and 6+7 if present
  106. # p priority information in data byte 1
  107. #
  108. # format: 'Type Function Len Acklen' => ['Function Name', Options]
  109. # len=1 means just one byte for function/channel
  110. # e.g. a Frame with Adr, 81/01, Fkt/Chan CRC
  111. my %PHC_functions = (
  112. 'EMD02+01' => ['Ein > 0', 'i'],
  113. 'EMD03+01' => ['Aus < 1', 'i'],
  114. 'EMD04+01' => ['Ein > 1', 'i'],
  115. 'EMD05+01' => ['Aus > 1', 'i'],
  116. 'EMD06+01' => ['Ein > 2', 'i'],
  117. 'EMD07+01' => ['Aus', 'i'],
  118. 'EMD020103' => ['LED_Ein', 'o'],
  119. 'EMD030103' => ['LED_Aus', 'o'],
  120. 'AMD010102' => ['Ping', 'cba'],
  121. 'AMD020102' => ['Ein', 'cba'],
  122. 'AMD030102' => ['Aus', 'cba'],
  123. 'AMD040102' => ['An Lock', 'cba'],
  124. 'AMD050102' => ['Aus Lock', 'cba'],
  125. 'AMD060102' => ['Umschalten', 'cba'],
  126. 'AMD070102' => ['Unlock', 'cba'],
  127. 'AMD080302' => ['An verzögert', 'cba', 't1'],
  128. 'AMD090302' => ['Aus verzögert', 'cba', 't1'],
  129. 'AMD100302' => ['An mit Timer', 'cba', 't1'],
  130. 'AMD110302' => ['Aus mit Timer', 'cba', 't1'],
  131. 'AMD120302' => ['Umschalten verzögert', 'cba', 't1'],
  132. 'AMD130302' => ['Umschalten mit Timer', 'cba', 't1'],
  133. 'AMD140102' => ['Lock', 'cba'],
  134. 'AMD150102' => ['Lock for time running', 'cba'],
  135. 'AMD160302' => ['Timer Addieren', 'cba', 't1'],
  136. 'AMD170302' => ['Timer setzen', 'cba', 't1'],
  137. 'AMD180102' => ['Timer cancel', 'cba'],
  138. 'AMD020201' => ['FB_Ein', 'cbm'],
  139. 'AMD030201' => ['FB_Aus', 'cbm'],
  140. 'AMD290201' => ['FB_Timer_Aus', 'cbm'], # kommt nach F10 wenn Zeit abgelaufen ist todo: check aus mit timer feedback?
  141. 'JRM020202' => ['Stop'],
  142. 'JRM030402' => ['Umschalten heben stop', 'p', 't2'],
  143. 'JRM040402' => ['Umschalten senken stop', 'p', 't2'],
  144. 'JRM050402' => ['Heben', 'p', 't2'],
  145. 'JRM060402' => ['Senken', 'p', 't2'],
  146. 'JRM070402' => ['Flip auf', 'p', 't2'],
  147. 'JRM080402' => ['Flip ab', 'p', 't2'],
  148. 'JRM030201' => ['FB_Senken_Ein', 'cbm'],
  149. 'JRM040201' => ['FB_Heben_Aus', 'cbm'],
  150. 'JRM050201' => ['FB_Senken_Aus', 'cbm'],
  151. 'JRM060201' => ['FB_Timer_Ein', 'cbm'],
  152. 'JRM060301' => ['FB_Timer_Ein', 'cbm'],
  153. 'JRM070201' => ['FB_Timer_Cancel', 'cbm'],
  154. 'JRM080201' => ['FB_Timer_Aus', 'cbm'],
  155. 'JRM090202' => ['Prio lock', 'p'],
  156. 'JRM100202' => ['Prio unlock', 'p'],
  157. 'JRM11' => ['Lernen an'], # uses different communication
  158. 'JRM12' => ['Lernen aus'],
  159. 'JRM130202' => ['Prio setzen', 'p'],
  160. 'JRM140202' => ['Prio löschen', 'p'],
  161. 'JRM150602' => ['Sensor heben', 'p', 't2'],
  162. 'JRM160802' => ['Sensor heben flip', 'p', 't2'],
  163. 'JRM170602' => ['Sensor senken', 'p', 't2'],
  164. 'JRM180802' => ['Sensor senken flip', 'p', 't2'],
  165. 'JRM190302' => ['Zeitmessung verzögert an', 't1'],
  166. 'JRM200302' => ['Zeitmessung verzögert aus', 't1'],
  167. 'JRM210302' => ['Zeitmessung an mit Timer', 't1'],
  168. 'JRM220102' => ['Zeitmessung cancel'],
  169. 'DIM020102' => ['Ein Max mit Memory', 'cba'],
  170. 'DIM030102' => ['Ein Max ohne Memory', 'cba'],
  171. 'DIM040102' => ['Aus', 'cba'],
  172. 'DIM050102' => ['Umschalten Max mit Memory', 'cba'],
  173. 'DIM060102' => ['Umschalten Max ohne Memory', 'cba'],
  174. 'DIM070302' => ['Dimmen Gegenrichtung', 'cba'],
  175. 'DIM080302' => ['Heller Dimmen', 'cba'],
  176. 'DIM090302' => ['Dunkler Dimmen', 'cba'],
  177. 'DIM100102' => ['Speichern Memory', 'cba'],
  178. 'DIM110102' => ['Umschalten Memory', 'cba'],
  179. 'DIM120102' => ['Ein Memory', 'cba'],
  180. 'DIM130102' => ['Speichern DIA1', 'cba'],
  181. 'DIM140102' => ['Umschalten DIA1', 'cba'],
  182. 'DIM150102' => ['Ein DIA1', 'cba'],
  183. 'DIM160102' => ['Speichern DIA2', 'cba'],
  184. 'DIM170102' => ['Umschalten DIA2', 'cba'],
  185. 'DIM180102' => ['Ein DIA2', 'cba'],
  186. 'DIM190102' => ['Speichern DIA3', 'cba'],
  187. 'DIM200102' => ['Umschalten DIA3', 'cba'],
  188. 'DIM210102' => ['Ein DIA3', 'cba'],
  189. 'DIM220302' => ['Dimmwert und Zeit setzen', 'cba'],
  190. 'DIM020201' => ['FB_Ein', 'cba'],
  191. 'DIM030201' => ['FB_Aus', 'cba'],
  192. 'MFM020103' => ['Ein', 'o'],
  193. 'MFM030103' => ['Aus', 'o'],
  194. 'MFM040103' => ['Umschalten', 'o'],
  195. 'MFM050103' => ['Ein verzögert', 'o'],
  196. 'MFM060103' => ['Aus verzögert', 'o'],
  197. 'MFM070103' => ['Ein mit Timer', 'o'],
  198. 'MFM080103' => ['Aus mit Timer', 'o'],
  199. 'MFM090103' => ['Dimmen in Gegenrichtung', 'o'],
  200. 'MFM100103' => ['Heller Dimmen', 'o'],
  201. 'MFM110103' => ['Dunkler Dimmen', 'o'],
  202. 'MFM120103' => ['Dimmwert und Zeit setzen', 'o'],
  203. 'MFM130103' => ['Rollade heben', 'o'],
  204. 'MFM140103' => ['Rollade senken', 'o'],
  205. 'MFM150103' => ['Aktion abbrechen', 'o'],
  206. 'MFM020101' => ['Taste O Ein > 0', 'i'],
  207. 'MFM030101' => ['Taste I Ein > 0', 'i'],
  208. 'MFM040101' => ['Taste O Aus', 'i'],
  209. 'MFM050101' => ['Taste I Aus', 'i'],
  210. 'MFM060101' => ['unknown'],
  211. 'MFM070101' => ['unknown'],
  212. 'MFM080101' => ['unknown'],
  213. 'MFM090101' => ['unknown'],
  214. 'MFM100101' => ['unknown'],
  215. 'MFM110101' => ['unknown'],
  216. 'UIM020101' => ['On/Off Ein', 'i'],
  217. 'UIM040101' => ['Auf Ein', 'i'],
  218. 'UIM060101' => ['Ab Ein', 'i'],
  219. 'MCC020101' => ['Ein > 0', 'i'],
  220. 'MCC020103' => ['LED Ein', 'o', 'cba'],
  221. 'MCC030103' => ['LED Aus', 'o', 'cba'],
  222. 'MCC090103' => ['LED Blink'],
  223. 'CLK03' => ['Clock Request']
  224. );
  225. #####################################
  226. sub PHC_Initialize($)
  227. {
  228. my ($hash) = @_;
  229. require "$attr{global}{modpath}/FHEM/DevIo.pm";
  230. $hash->{ReadFn} = "PHC_Read";
  231. $hash->{ReadyFn} = "PHC_Ready";
  232. $hash->{DefFn} = "PHC_Define";
  233. $hash->{UndefFn} = "PHC_Undef";
  234. $hash->{SetFn} = "PHC_Set";
  235. $hash->{GetFn} = "PHC_Get";
  236. $hash->{AttrFn} = "PHC_Attr";
  237. $hash->{AttrList}= "do_not_notify:1,0 " .
  238. "queueDelay " .
  239. "timeout " .
  240. "queueMax " .
  241. "silentReconnect " .
  242. "singleLastCommandReading " .
  243. "sendEcho:1,0 " .
  244. "module[0-9]+description " .
  245. "module[0-9]+type " .
  246. "channel(EMD|AMD|JRM|DIM|UIM|MCC|MFM)[0-9]+[io]?[0-9]+description " .
  247. "virtEMD[0-9]+C[0-9]+Name " . # virtual emd channel for set
  248. $readingFnAttributes;
  249. }
  250. #####################################
  251. sub PHC_Define($$)
  252. {
  253. my ($hash, $def) = @_;
  254. my @a = split("[ \t]+", $def);
  255. my ($name, $PHC, $dev) = @a;
  256. return "wrong syntax: define <name> PHC [devicename]"
  257. if(@a < 3);
  258. eval "use Digest::CRC qw(crc crc32 crc16 crcccitt)";
  259. if($@) {
  260. return "Please install the Perl Digest Library (apt-get install libdigest-crc-perl) - error was $@";
  261. }
  262. $hash->{BUSY} = 0;
  263. $hash->{EXPECT} = "";
  264. $hash->{ModuleVersion} = $PHC_Version;
  265. if ($dev !~ /.+@([0-9]+)/) {
  266. $dev .= '@19200,8,N,2';
  267. } else {
  268. Log3 $name, 3, "$name: Warning: connection speed $1 is probably wrong for the PHC bus. Default is 19200,8,N,2"
  269. if ($1 != 19200);
  270. }
  271. $hash->{DeviceName} = $dev;
  272. $hash->{devioLoglevel} = (AttrVal($name, "silentReconnect", 0) ? 4 : 3);
  273. DevIo_CloseDev($hash);
  274. my $ret = DevIo_OpenDev($hash, 0, 0);
  275. return $ret;
  276. }
  277. #####################################
  278. sub PHC_Undef($$)
  279. {
  280. my ($hash, $arg) = @_;
  281. my $name = $hash->{NAME};
  282. DevIo_CloseDev($hash);
  283. return undef;
  284. }
  285. # Attr command
  286. #########################################################################
  287. sub PHC_Attr(@)
  288. {
  289. my ($cmd,$name,$aName,$aVal) = @_;
  290. # $cmd can be "del" or "set"
  291. # $name is device name
  292. # aName and aVal are Attribute name and value
  293. my $hash = $defs{$name};
  294. Log3 $name, 5, "$name: Attr called with @_";
  295. if ($cmd eq "set") {
  296. if ($aName =~ 'virtEMD([0-9]+)C([0-9]+)Name(.*)') {
  297. my $modAdr = $1;
  298. my $cnlAdr = $2;
  299. if ($modAdr >= 32) {
  300. return "illegal EMD module address $modAdr - address needs to be < 32 and it must not be used on the bus";
  301. }
  302. if ($cnlAdr >= 15) {
  303. return "illegal EMD channel address $cnlAdr - address needs to be < 16";
  304. }
  305. my @virtEMDList = grep (/virtEMD[0-9]+C[0-9]+Name/, keys %{$attr{$name}});
  306. my $emdAdr = "";
  307. my $emdChannel = "";
  308. foreach my $attrName (@virtEMDList) {
  309. if ($aVal eq $attr{$name}{$attrName}) { # ist es der im konkreten Attr verwendete Name?
  310. if ($attrName =~ /virtEMD([0-9]+)C([0-9]+)Name/) {
  311. return "Name $aVal is already used for virtual EMD $modAdr channel $cnlAdr";
  312. }
  313. }
  314. }
  315. }
  316. }
  317. }
  318. #####################################
  319. sub PHC_Get($@)
  320. {
  321. my ($hash, @a) = @_;
  322. my $name = $hash->{NAME};
  323. my $getName = $a[1];
  324. return "\"get $name\" needs at least one argument" if(@a < 2);
  325. return undef;
  326. }
  327. #####################################
  328. sub PHC_DoEMD($$$$)
  329. {
  330. my ($hash, $modAdr, $channel, $fName) = @_;
  331. my $name = $hash->{NAME};
  332. my $function = 0;
  333. Log3 $name, 3, "$name: DoEMD called for module $modAdr, channel $channel, function $fName";
  334. foreach my $hkey (grep (/EMD/, keys %PHC_functions)) {
  335. #Log3 $name, 5, "$name: hkey $hkey";
  336. my $fn = lc $PHC_functions{$hkey}[0];
  337. #Log3 $name, 5, "$name: fn $fn";
  338. $fn =~ s/ //g;
  339. if ($fn =~ /(.*),.*/) {
  340. $fn = $1;
  341. }
  342. #Log3 $name, 5, "$name: compare to $fn";
  343. if ($fn eq $fName) {
  344. if ($hkey =~ /EMD0([0-9]).*/) {
  345. $function = $1;
  346. last;
  347. }
  348. }
  349. }
  350. return "function $fName not found" if (!$function);
  351. Log3 $name, 5, "$name: found function $function";
  352. if ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 's') {
  353. $hash->{Toggle}{$modAdr} = 'c';
  354. Log3 $name, 3, "$name: toggle for module $modAdr was set and will now be cleared";
  355. } elsif ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 'c') {
  356. $hash->{Toggle}{$modAdr} = 's';
  357. Log3 $name, 5, "$name: toggle for module $modAdr was cleared and will now be set";
  358. } else {
  359. $hash->{Toggle}{$modAdr} = 'c';
  360. Log3 $name, 3, "$name: toggle for module $modAdr was unknown and will now be cleared";
  361. }
  362. my $len = 1 | (($hash->{Toggle}{$modAdr} eq 's' ? 1 : 0) << 7);
  363. #Log3 $name, 3, "$name: len is $len";
  364. my $code = ($function + ($channel << 4));
  365. my $frame = pack ('CCC', $modAdr, $len, $code);
  366. my $crc = pack ('v', crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0));
  367. $frame = $frame . $crc;
  368. my $hFrame = unpack ('H*', $frame);
  369. Log3 $name, 3, "$name: sends $hFrame";
  370. if (!AttrVal($name, "sendEcho", 0)) {
  371. my $now = gettimeofday();
  372. $hash->{helper}{buffer} .= $frame;
  373. $hash->{helper}{lastRead} = $now;
  374. }
  375. DevIo_SimpleWrite($hash, $frame, 0);
  376. }
  377. #####################################
  378. sub PHC_Set($@)
  379. {
  380. my ($hash, @a) = @_;
  381. return "\"set $a[0]\" needs at least an argument" if(@a < 2);
  382. my ($name, $setName, @setValArr) = @a;
  383. my $setVal = (@setValArr ? join(' ', @setValArr) : "");
  384. if ($setName eq 'importChannelList') {
  385. if ($setVal) {
  386. my $iFile;
  387. if (!open($iFile, "<", $setVal)) {
  388. Log3 $name, 3, "$name: Cannot open template file $setVal";
  389. return;
  390. };
  391. my $mType = 'unknown';
  392. my $mAdr = 'unknown';
  393. my $aAdr = 'unknown';
  394. my $mName = 'unknown';
  395. my $mDisp = 'unknown';
  396. my $cType = 'i';
  397. my ($key, $cAdr, $cName);
  398. while (<$iFile>) {
  399. Log3 $name, 5, "$name: import read line $_";
  400. if ($_ =~ /<MOD adr="([0-9]+)" name="([^"]+)" display="([^"]+)"/) {
  401. $aAdr = sprintf('%03d', $1);
  402. $mAdr = sprintf('%02d', $1 & 0x1f);
  403. $mName = $2;
  404. $mDisp = encode ('UTF-8', $3);
  405. $mType = substr($mName, 0, 3);
  406. CommandAttr(undef, "$name module${aAdr}description $mDisp");
  407. CommandAttr(undef, "$name module${aAdr}type $mType");
  408. } elsif ($_ =~ /<OUT>/) {
  409. $cType = 'o';
  410. } elsif ($_ =~ /<IN>/) {
  411. $cType = 'i';
  412. } elsif ($_ =~ /<CHA adr="([0-9]+)" name="([^"]+)" visu="([^"]+)"\/>/) {
  413. my $rAdr = $1 & 0x1f;
  414. $cAdr = sprintf('%02d', $rAdr);
  415. $cName = encode ('UTF-8', $2);
  416. $key = $mType . $mAdr . $cType . $cAdr;
  417. CommandAttr(undef, "$name channel${key}description $cName");
  418. }
  419. }
  420. } else {
  421. return "please specify a filename";
  422. }
  423. } elsif ($setName eq "emd") {
  424. my @arg = @setValArr;
  425. shift @arg; shift @arg;
  426. my $fName = lc join('', @arg);
  427. return PHC_DoEMD($hash, $setValArr[0], $setValArr[1], $fName);
  428. } elsif ($setName eq "sendRaw") {
  429. my $modAdr = $setValArr[0];
  430. my $hexCmd = $setValArr[1];
  431. if ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 's') {
  432. $hash->{Toggle}{$modAdr} = 'c';
  433. Log3 $name, 3, "$name: toggle for module $modAdr was set and will now be cleared";
  434. } elsif ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 'c') {
  435. $hash->{Toggle}{$modAdr} = 's';
  436. Log3 $name, 5, "$name: toggle for module $modAdr was cleared and will now be set";
  437. } else {
  438. $hash->{Toggle}{$modAdr} = 'c';
  439. Log3 $name, 3, "$name: toggle for module $modAdr was unknown and will now be cleared";
  440. }
  441. my $len = (length ($hexCmd) / 2) | (($hash->{Toggle}{$modAdr} eq 's' ? 1 : 0) << 7);
  442. #Log3 $name, 3, "$name: len is $len";
  443. my $frame = pack ('H2CH*', $modAdr, $len, $hexCmd);
  444. my $crc = pack ('v', crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0));
  445. $frame = $frame . $crc;
  446. my $hFrame = unpack ('H*', $frame);
  447. Log3 $name, 3, "$name: sends $hFrame";
  448. if (!AttrVal($name, "sendEcho", 0)) {
  449. my $now = gettimeofday();
  450. $hash->{helper}{buffer} .= $frame;
  451. $hash->{helper}{lastRead} = $now;
  452. }
  453. DevIo_SimpleWrite($hash, $frame, 0);
  454. } else {
  455. my @virtEMDList = grep (/virtEMD[0-9]+C[0-9]+Name/, keys %{$attr{$name}});
  456. my $emdAdr = "";
  457. my $emdChannel = "";
  458. foreach my $aName (@virtEMDList) {
  459. if ($setName eq $attr{$name}{$aName}) { # ist es der im konkreten Set verwendete setName?
  460. if ($aName =~ /virtEMD([0-9]+)C([0-9]+)Name/) {
  461. $emdAdr = $1; # gefunden -> merke Nummer X im Attribut
  462. $emdChannel = $2;
  463. }
  464. }
  465. }
  466. if ($emdAdr eq "") {
  467. # todo: map to values, add hints
  468. my $hints = ":ein>0,ein>1,ein>2,aus,aus<1,aus>1";
  469. return "Unknown argument $setName, choose one of importChannelList sendRaw " . join (' ', map ($attr{$name}{$_} . $hints, @virtEMDList));
  470. }
  471. return PHC_DoEMD($hash, $emdAdr, $emdChannel, $setVal);
  472. }
  473. return undef;
  474. }
  475. #####################################
  476. # Called from ParseCommands
  477. sub PHC_ParseCode($$)
  478. {
  479. my ($hash, $command) = @_;
  480. my $name = $hash->{NAME};
  481. my $fAdr = sprintf('%03d', $command->{ADR}); # formatted abs adr for attr lookup (mod type)
  482. my @typeArr = split (',', AttrVal($name, "module${fAdr}type", "")); # potential types from attr
  483. my $typeAttrLen = @typeArr; # number of potential types in attr
  484. @typeArr = @{$PHC_AdrType{$command->{ADR} & 0xE0}} if (!@typeArr); # fallback to types from AdrType hash
  485. my $mType = $typeArr[0]; # first option for split (same for all options)
  486. Log3 $name, 5, "$name: ParseCode called, fAdr $fAdr, typeArr = @typeArr, code " . sprintf ('x%02X', $command->{CODE});
  487. #Log3 $name, 5, "$name: ParseCode data = @{$command->{DATA}}";
  488. #Log3 $name, 5, "$name: ParseCode ackdata = @{$command->{ACKDATA}}";
  489. return PHC_LogCommand($hash, $command, "unknown module type", 3) if (!$mType);
  490. $command->{MTYPE} = $mType; # first idea unless we find a fit later
  491. # splitting and therefore channel and function are the same within one address class
  492. # so they are ok to calculate here regardless of the exact module type identified later
  493. my ($channel, $function) = PHC_SplitCode($hash, $mType, $command->{CODE});
  494. $command->{CHANNEL} = $channel;
  495. $command->{FUNCTION} = $function;
  496. my $key1 = sprintf('%02d', $function);
  497. my $key2 = sprintf('%02d', $command->{LEN});
  498. my $key3 = sprintf('%02d', $command->{ACKLEN});
  499. my $wldk = '+';
  500. my @keys = ("$mType$key1$key2$key3", "$mType$key1$wldk$key3", "$mType$key1$key2", "$mType$key1");
  501. Log3 $name, 5, "$name: ParseCode checks typelist @typeArr against" .
  502. " F=" . sprintf ('x%02X', $function) . " C=" . sprintf ('x%02X', $channel) . "Len=$command->{LEN}, ackLen=$command->{ACKLEN}";
  503. my $bestFit = 0; # any fit of key 3, 2 or 1 is better than 0
  504. foreach my $mTypePot (@typeArr) {
  505. #Log3 $name, 5, "$name: ParseCode checks if type of module at $fAdr can be $mTypePot";
  506. my $idx = 4; # fourlevels of abstraction in the PHC_functions hash
  507. # does this module type match better than a previously tested type?
  508. foreach my $key (@keys) {
  509. if ($PHC_functions{$key}) {
  510. #Log3 $name, 5, "$name: match: $key";
  511. if ($idx > $bestFit) { # longer = better matching type found
  512. $bestFit = $idx;
  513. my @parseOpts = @{$PHC_functions{$key}};
  514. $command->{MTYPE} = $mTypePot;
  515. $command->{FNAME} = shift @parseOpts;
  516. foreach (@parseOpts) {$command->{PARSEOPTS}{$_} = 1};
  517. Log3 $name, 5, "$name: ParseCode match $key / $command->{FNAME} " . join (',', @parseOpts);
  518. }
  519. last; # first match is the best for this potential type
  520. } else {
  521. if (!$idx) { # this was the last try for this type with $idx=0, $key=$mTypePot$key1
  522. @typeArr = grep {!/$mTypePot/} @typeArr; # module type is not an option any more
  523. Log3 $name, 5, "$name: ParseCode could not match to $mTypePot, delete this option";
  524. }
  525. }
  526. $idx--;
  527. }
  528. }
  529. Log3 $name, 4, "$name: ParseCode typelist after matching is @typeArr" if (@typeArr > 1);
  530. return PHC_LogCommand($hash, $command, "no parse info", 3) if (!$command->{FNAME});
  531. $command->{CTYPE} = ($command->{PARSEOPTS}{'i'} ? 'i' : 'o');
  532. if (!$typeAttrLen || (scalar(@typeArr) >= 1 && scalar(@typeArr) < $typeAttrLen)) {
  533. # no moduleType attr so far or we could eliminate an option -> set more specific new attr
  534. CommandAttr(undef, "$name module${fAdr}type " . join (',', @typeArr));
  535. Log3 $name, 4, "$name: set attr $name module${fAdr}type " . join (',', @typeArr);
  536. }
  537. return 1;
  538. }
  539. #####################################
  540. # Called from ParseCommands
  541. sub PHC_ParseOptions($$)
  542. {
  543. my ($hash, $command) = @_;
  544. my $name = $hash->{NAME};
  545. my $dLen = @{$command->{DATA}};
  546. if ($command->{PARSEOPTS}{'p'}) {
  547. $command->{PRIO} = unpack ('b6', pack ('C', $command->{DATA}[1] & 0x3F));
  548. $command->{PSET} = $command->{DATA}[1] & 0x40;
  549. }
  550. if ($command->{PARSEOPTS}{'t1'}) {
  551. $command->{TIME1} = $command->{DATA}[1] + ($command->{DATA}[2] << 8) if ($dLen > 2);
  552. }
  553. if ($command->{PARSEOPTS}{'t2'}) {
  554. $command->{TIME1} = $command->{DATA}[2] + ($command->{DATA}[3] << 8) if ($dLen > 3);
  555. $command->{TIME2} = $command->{DATA}[4] + ($command->{DATA}[5] << 8) if ($dLen > 5);
  556. $command->{TIME3} = $command->{DATA}[6] + ($command->{DATA}[7] << 8) if ($dLen > 7);
  557. }
  558. }
  559. # todo: zumindest bei emds können mehrere codes (channel/function) nacheinender in einer message kommen
  560. # wenn zwei tasten gleichzeitig gedrückt werden...
  561. #####################################
  562. # Called from ParseFrames
  563. sub PHC_ParseCommands($$)
  564. {
  565. my ($hash, $command) = @_;
  566. my $name = $hash->{NAME};
  567. return if (!PHC_ParseCode($hash, $command));
  568. PHC_ParseOptions($hash, $command);
  569. PHC_LogCommand($hash, $command, "", ($command->{MTYPE} eq "CLK" ? 5:4));
  570. readingsBeginUpdate($hash);
  571. readingsBulkUpdate($hash, 'LastCommand', PHC_CmdText($hash, $command));
  572. DoTrigger($name, PHC_ChannelText($hash, $command, $command->{CHANNEL}) . ": " . $command->{FNAME});
  573. # channel bits aus Feedback / Ack verarbeiten
  574. if ($command->{PARSEOPTS}{'cbm'} || $command->{PARSEOPTS}{'cba'}) {
  575. my $bin = unpack ("B8", pack ("C", ($command->{PARSEOPTS}{'cbm'} ? $command->{DATA}[1] : $command->{ACKDATA}[1])));
  576. Log3 $name, 5, "$name: ParseCommand channel map = $bin";
  577. my $channelBit = 7;
  578. foreach my $c (split //, $bin) {
  579. my $bitName = PHC_ChannelDesc($hash, $command, $channelBit);
  580. Log3 $name, 5, "$name: ParseCommand Reading for channel $channelBit is $c ($bitName)";
  581. readingsBulkUpdate($hash, $bitName, $c) if ($bitName);
  582. $channelBit --;
  583. }
  584. }
  585. my @data = @{$command->{DATA}};
  586. if ($command->{PARSEOPTS}{'i'} && @data > 1) {
  587. my $codeIdx = 1; # second code
  588. while ($codeIdx < @data) {
  589. Log3 $name, 5, "$name: ParseCommand now handles additional code at Index $codeIdx";
  590. $command->{CODE} = $data[$codeIdx];
  591. PHC_ParseCode($hash, $command);
  592. PHC_LogCommand($hash, $command, "", 4);
  593. DoTrigger($name, PHC_ChannelText($hash, $command, $command->{CHANNEL}) . ": " . $command->{FNAME});
  594. $codeIdx++;
  595. }
  596. Log3 $name, 5, "$name: ParseCommand done";
  597. }
  598. readingsEndUpdate($hash, 1);
  599. }
  600. #####################################
  601. # Called from the read functions
  602. sub PHC_ParseFrames($)
  603. {
  604. my $hash = shift;
  605. my $name = $hash->{NAME};
  606. my $frame;
  607. my $hFrame;
  608. my ($adr, $xAdr, $len, $tog, $rest, $pld, $crc, $crc1);
  609. my $rLen;
  610. my @data;
  611. #Log3 $name, 5, "$name: Parseframes called";
  612. use bytes;
  613. if (!$hash->{skipReason}) {
  614. $hash->{skipBytes} = "";
  615. $hash->{skipReason} = "";
  616. };
  617. while ($hash->{helper}{buffer}) {
  618. $hash->{RAWBUFFER} = unpack ('H*', $hash->{helper}{buffer});
  619. Log3 $name, 5, "$name: Parseframes: loop with raw buffer: $hash->{RAWBUFFER}" if (!$hash->{skipReason});
  620. $rLen = length($hash->{helper}{buffer});
  621. return if ($rLen < 4);
  622. ($adr, $len, $rest) = unpack ('CCa*', $hash->{helper}{buffer});
  623. $xAdr = unpack('H2', $hash->{helper}{buffer});
  624. $tog = $len >> 7;
  625. $len = $len & 0x7F;
  626. if ($len > 30) {
  627. Log3 $name, 5, "$name: Parseframes: len > 30, skip first byte of buffer $hash->{RAWBUFFER}";
  628. $hash->{skipBytes} .= substr ($hash->{helper}{buffer},0,1);
  629. $hash->{skipReason} = "Len > 30" if (!$hash->{skipReason});
  630. $hash->{helper}{buffer} = substr ($hash->{helper}{buffer}, 1);
  631. next;
  632. }
  633. if (($rLen < 20) && ($rLen < $len + 4)) {
  634. Log3 $name, 5, "$name: Parseframes: len is $len so frame shoud be " . ($len + 4) . " but only $rLen read. wait for more";
  635. return;
  636. }
  637. $frame = substr($hash->{helper}{buffer},0,$len+2); # the frame (adr, tog/len, cmd/data) without crc
  638. $hFrame = unpack ('H*', $frame);
  639. ($pld, $crc, $rest) = unpack ("a[$len]va*", $rest); # v = little endian unsigned short, n would be big endian
  640. @data = unpack ('C*', $pld);
  641. $crc1 = crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0);
  642. my $fcrc = unpack ("H*", pack ("v", $crc));
  643. my $fcrc1 = unpack ("H*", pack ("v", $crc1));
  644. if ($crc != $crc1) {
  645. #my $skip = $len + 4;
  646. my $skip = 1;
  647. Log3 $name, 5, "$name: Parseframes: CRC error for $hFrame $fcrc, calc $fcrc1) - skip $skip bytes of buffer $hash->{RAWBUFFER}";
  648. $hash->{skipBytes} .= substr ($hash->{helper}{buffer},0,$skip);
  649. $hash->{skipReason} = "CRC error" if (!$hash->{skipReason});
  650. $hash->{helper}{buffer} = substr ($hash->{helper}{buffer}, $skip);
  651. next;
  652. }
  653. Log3 $name, 4, "$name: Parseframes: skipped " .
  654. unpack ("H*", $hash->{skipBytes}) . " reason: $hash->{skipReason}"
  655. if $hash->{skipReason};
  656. $hash->{skipBytes} = "";
  657. $hash->{skipReason} = "";
  658. $hash->{helper}{buffer} = $rest;
  659. Log3 $name, 5, "$name: Parseframes: Adr $adr/x$xAdr Len $len T$tog Data " . unpack ('H*', $pld) . " (Frame $hFrame $fcrc) Rest " . unpack ('H*', $rest)
  660. if ($adr != 224); # todo: remove this filter later (hide noisy stuff)
  661. $hash->{Toggle}{$adr} = ($tog ? 's' : 'c'); # save toggle for potential own sending of data
  662. if ($hash->{COMMAND} && $hFrame eq $hash->{COMMAND}{FRAME}) {
  663. Log3 $name, 4, "$name: Parseframes: Resend of $hFrame $fcrc detected";
  664. next;
  665. }
  666. my $cmd = $data[0];
  667. if ($cmd == 1) {
  668. # Ping / Ping response
  669. if (!$hash->{COMMAND}) {
  670. Log3 $name, 5, "$name: Parseframes: Ping request received";
  671. # fall through until $hash->{COMMAND} is set
  672. } else {
  673. if ($hash->{COMMAND}{CODE} == 1 && $hash->{COMMAND}{ADR} == $adr) {
  674. # this must be the response
  675. Log3 $name, 5, "$name: Parseframes: Ping response received";
  676. $hash->{COMMAND}{ACKDATA} = \@data;
  677. $hash->{COMMAND}{ACKLEN} = $len;
  678. PHC_ParseCommands($hash, $hash->{COMMAND});
  679. next;
  680. } else {
  681. # no reply to last command - ping or something else - now we seem to have a new ping request
  682. Log3 $name, 4, "$name: Parseframes: new Frame $hFrame $fcrc but no ACK for valid last Frame $hash->{COMMAND}{FRAME} - dropping last one";
  683. delete $hash->{COMMAND}; # done with this command
  684. # fall through until $hash->{COMMAND} is set
  685. }
  686. }
  687. } elsif ($cmd == 254) {
  688. # reset
  689. # todo: get module name / type and show real type / adr in Log, add to comand reading or go through parsecommand with simulated acl len 0 ...
  690. # parse payload in parsecommand
  691. # por byte, many channel/ function bytes
  692. Log3 $name, 4, "$name: Parseframes: configuration request for adr $adr received - frame is $hFrame $fcrc";
  693. delete $hash->{COMMAND}; # done with this command
  694. next;
  695. } elsif ($cmd == 255) {
  696. # reset
  697. Log3 $name, 4, "$name: Parseframes: reset for adr $adr received - frame is $hFrame $fcrc";
  698. delete $hash->{COMMAND}; # done with this command
  699. next;
  700. } elsif ($cmd == 0) {
  701. # ACK received
  702. Log3 $name, 5, "$name: Parseframes: Ack received";
  703. if ($hash->{COMMAND}) {
  704. if ($hash->{COMMAND}{ADR} != $adr) {
  705. Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc does not match adr of last Frame $hash->{COMMAND}{FRAME}";
  706. } elsif ($hash->{COMMAND}{TOGGLE} != $tog) {
  707. Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc does not match toggle of last Frame $hash->{COMMAND}{FRAME}";
  708. } else { # this ack is fine
  709. $hash->{COMMAND}{ACKDATA} = \@data;
  710. $hash->{COMMAND}{ACKLEN} = $len;
  711. PHC_ParseCommands($hash, $hash->{COMMAND});
  712. }
  713. delete $hash->{COMMAND}; # done with this command
  714. next;
  715. } else {
  716. Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc without a preceeding request - dropping";
  717. next;
  718. }
  719. } else {
  720. # normal command - no ack, ping etc.
  721. if ($hash->{COMMAND}) {
  722. Log3 $name, 4, "$name: Parseframes: new Frame $hFrame $fcrc but no ACK for valid last Frame $hash->{COMMAND}{FRAME} - dropping last one";
  723. }
  724. Log3 $name, 5, "$name: Parseframes: $hFrame $fcrc is not an Ack frame, wait for ack to follow";
  725. # todo: set timeout timer if not ACK received
  726. }
  727. my @oldData = @data;
  728. $hash->{COMMAND}{ADR} = $adr;
  729. $hash->{COMMAND}{LEN} = $len;
  730. $hash->{COMMAND}{TOGGLE} = $tog;
  731. $hash->{COMMAND}{DATA} = \@oldData;
  732. $hash->{COMMAND}{CODE} = $oldData[0];
  733. $hash->{COMMAND}{FRAME} = $hFrame;
  734. }
  735. }
  736. #####################################
  737. # Called from the global loop, when the select for hash->{FD} reports data
  738. sub PHC_Read($)
  739. {
  740. my $hash = shift;
  741. my $name = $hash->{NAME};
  742. my $now = gettimeofday();
  743. # throw away old stuff
  744. if ($hash->{helper}{lastRead} && ($now - $hash->{helper}{lastRead}) > 1) {
  745. if ($hash->{helper}{buffer}) {
  746. Log3 $name, 5, "throw away " . unpack ('H*', $hash->{helper}{buffer});
  747. }
  748. $hash->{helper}{buffer} = "";
  749. }
  750. $hash->{helper}{lastRead} = $now;
  751. my $buf = DevIo_SimpleRead($hash);
  752. return if(!defined($buf));
  753. $hash->{helper}{buffer} .= $buf;
  754. PHC_ParseFrames($hash);
  755. }
  756. #####################################
  757. # Called from get / set to get a direct answer
  758. sub PHC_ReadAnswer($$$)
  759. {
  760. my ($hash, $arg, $expectReply) = @_;
  761. my $name = $hash->{NAME};
  762. return ("No FD", undef)
  763. if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD})));
  764. my ($buf, $framedata, $cmd);
  765. my $rin = '';
  766. my $to = AttrVal($name, "timeout", 2); # default is 2 seconds timeout
  767. Log3 $name, 5, "$name: ReadAnswer called for get $arg";
  768. for(;;) {
  769. if($^O =~ m/Win/ && $hash->{USBDev}) {
  770. $hash->{USBDev}->read_const_time($to*1000); # set timeout (ms)
  771. $buf = $hash->{USBDev}->read(999);
  772. if(length($buf) == 0) {
  773. Log3 $name, 3, "$name: Timeout in ReadAnswer for get $arg";
  774. return ("Timeout reading answer for $arg", undef);
  775. }
  776. } else {
  777. if(!$hash->{FD}) {
  778. Log3 $name, 3, "$name: Device lost in ReadAnswer for get $arg";
  779. return ("Device lost when reading answer for get $arg", undef);
  780. }
  781. vec($rin, $hash->{FD}, 1) = 1; # setze entsprechendes Bit in rin
  782. my $nfound = select($rin, undef, undef, $to);
  783. if($nfound < 0) {
  784. next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
  785. my $err = $!;
  786. DevIo_Disconnected($hash);
  787. Log3 $name, 3, "$name: ReadAnswer $arg: error $err";
  788. return("PHC_ReadAnswer $arg: $err", undef);
  789. }
  790. if($nfound == 0) {
  791. Log3 $name, 3, "$name: Timeout2 in ReadAnswer for $arg";
  792. return ("Timeout reading answer for $arg", undef);
  793. }
  794. $buf = DevIo_SimpleRead($hash);
  795. if(!defined($buf)) {
  796. Log3 $name, 3, "$name: ReadAnswer for $arg got no data";
  797. return ("No data", undef);
  798. }
  799. }
  800. if($buf) {
  801. $hash->{helper}{buffer} .= $buf;
  802. Log3 $name, 5, "$name: ReadAnswer got: " . unpack ("H*", $hash->{helper}{buffer});
  803. }
  804. $framedata = PHC_ParseFrames($hash);
  805. }
  806. }
  807. #####################################
  808. sub PHC_Ready($)
  809. {
  810. my ($hash) = @_;
  811. return DevIo_OpenDev($hash, 1, undef)
  812. if($hash->{STATE} eq "disconnected");
  813. # This is relevant for windows/USB only
  814. my $po = $hash->{USBDev};
  815. my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
  816. return ($InBytes>0);
  817. }
  818. #######################################
  819. sub PHC_TimeoutSend($)
  820. {
  821. my $param = shift;
  822. my (undef,$name) = split(':',$param);
  823. my $hash = $defs{$name};
  824. Log3 $name, 3, "$name: timeout waiting for reply" .
  825. ($hash->{EXPECT} ? " expecting " . $hash->{EXPECT} : "") .
  826. " Request was " . $hash->{LASTREQUEST};
  827. $hash->{BUSY} = 0;
  828. $hash->{EXPECT} = "";
  829. };
  830. ###############################################################
  831. # split code into channel and function depending on module type
  832. sub PHC_SplitCode($$$)
  833. {
  834. my ($hash, $mType, $code) = @_;
  835. #Log3 $hash->{NAME}, 5, "$hash->{NAME}: PHC_SplitCode called with code $code and type $mType";
  836. my @splitArr = @{$PHC_CodeSplit{$mType}};
  837. Log3 $hash->{NAME}, 5, "$hash->{NAME}: SplitCode splits code " .
  838. sprintf ('%02d', $code) . " for type $mType into " .
  839. " channel " . ($code >> $splitArr[0]) . " / function " . ($code & $splitArr[1]);
  840. return ($code >> $splitArr[0], $code & $splitArr[1]); # channel, function
  841. }
  842. ###############################################################
  843. # log message with command parse data
  844. sub PHC_LogCommand($$$$)
  845. {
  846. my ($hash, $command, $msg, $level) = @_;
  847. Log3 $hash->{NAME}, $level, "$hash->{NAME}: " . PHC_Caller() . ' ' . PHC_CmdText($hash, $command) . $msg;
  848. }
  849. ###############################################################
  850. # get Text like EMD12i01
  851. sub PHC_ChannelText($$$)
  852. {
  853. my ($hash, $command, $channel) = @_;
  854. my $fmAdr = sprintf('%02d', ($command->{ADR} & 0x1F)); # relative module address formatted with two digits
  855. my $mType = $command->{MTYPE};
  856. return ($mType ? $mType . $fmAdr : 'Module' . sprintf ("x%02X", $command->{ADR})) .
  857. ($command->{CTYPE} ? $command->{CTYPE} : "") .
  858. (defined($channel) ? sprintf('%02d', $channel) : "");
  859. }
  860. ###############################################################
  861. # full detail of a command for logging
  862. sub PHC_CmdText($$)
  863. {
  864. my ($hash, $command) = @_;
  865. my $adr = $command->{ADR};
  866. my $mAdr = $adr & 0x1F; # relative module address
  867. my $fmAdr = sprintf('%02d', $mAdr);
  868. my $mType = $command->{MTYPE};
  869. my $channel = $command->{CHANNEL};
  870. my $cDesc = PHC_ChannelDesc($hash, $command, $channel);
  871. my $start = PHC_ChannelText($hash, $command, $channel);
  872. return ($start ? $start : "") .
  873. ($command->{FUNCTION} ? " F$command->{FUNCTION}" : "") .
  874. ($command->{FNAME} ? " $command->{FNAME}" : "") .
  875. (defined($command->{PRIO}) ? " P$command->{PRIO}" : "") .
  876. (defined($command->{PRIO}) ? ($command->{PSET} ? " (Set)" : " (no Set)") : "") .
  877. (defined($command->{TIME1}) ? " Time1 $command->{TIME1}" : "") .
  878. (defined($command->{TIME2}) ? " Time2 $command->{TIME2}" : "") .
  879. (defined($command->{TIME3}) ? " Time3 $command->{TIME3}" : "") .
  880. " data " . join (",", map ({sprintf ("x%02X", $_)} @{$command->{DATA}})) .
  881. " ack " . join (",", map ({sprintf ("x%02X", $_)} @{$command->{ACKDATA}})) .
  882. " tg " . $command->{TOGGLE} .
  883. ($cDesc ? " $cDesc" : "");
  884. }
  885. ###############################################################
  886. # channel description or internal mod/chan text
  887. sub PHC_ChannelDesc($$$)
  888. {
  889. my ($hash, $command, $channel) = @_;
  890. my $name = $hash->{NAME};
  891. my $mAdr = $command->{ADR} & 0x1F; # relative module address
  892. my $fmAdr = sprintf('%02d', $mAdr);
  893. my $mType = $command->{MTYPE};
  894. my $aName = "channel" . PHC_ChannelText($hash, $command, $channel) . "description";
  895. my $bName = PHC_ChannelText($hash, $command, $channel);
  896. my $bitName = PHC_SanitizeReadingName(AttrVal($name, $aName, $bName));
  897. return $bitName;
  898. }
  899. ###############################################################
  900. # convert description into a usable reading name
  901. sub PHC_SanitizeReadingName($)
  902. {
  903. my ($bitName) = @_;
  904. $bitName =~ s/ä/ae/g;
  905. $bitName =~ s/ö/oe/g;
  906. $bitName =~ s/ü/ue/g;
  907. $bitName =~ s/Ä/Ae/g;
  908. $bitName =~ s/Ö/Oe/g;
  909. $bitName =~ s/Ü/Ue/g;
  910. $bitName =~ s/ß/ss/g;
  911. $bitName =~ s/ / /g;
  912. $bitName =~ s/ -/-/g;
  913. $bitName =~ s/- /-/g;
  914. $bitName =~ s/ /_/g;
  915. $bitName =~ s/[^A-Za-z0-9\-]/_/g;
  916. $bitName =~ s/__/_/g;
  917. $bitName =~ s/__/_/g;
  918. return $bitName;
  919. }
  920. ###########################################################
  921. # return the name of the caling function for debug output
  922. # todo: remove main from caller function
  923. sub PHC_Caller()
  924. {
  925. my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash) = caller 2;
  926. return $1 if ($subroutine =~ /main::PHC_(.*)/);
  927. return $1 if ($subroutine =~ /main::(.*)/);
  928. return "$subroutine";
  929. }
  930. 1;
  931. =pod
  932. =item device
  933. =item summary retrieves events / readings from PHC bus and simulates input modules
  934. =item summary_DE hört den PHC-Bus ab, erzeugt Events / Readings und simuliert EMDs
  935. =begin html
  936. <a name="PHC"></a>
  937. <h3>PHC</h3>
  938. <ul>
  939. PHC provides a way to communicate with the PHC bus from Peha. It listens to the communication on the PHC bus, tracks the state of output modules in readings and can send events to the bus / "Steuermodul" by simulating PHC input modules.<br>
  940. It can import the channel list file that is exportable from the Peha "Systemsoftware" to get names of existing modules and channels.<br>
  941. This module can not replace a Peha "Steuermodul". It is also not possible to directly send commands to output modules on the PHC bus. If you want to
  942. interact with output modules then you have to define the action on the Peha "Steuermodul" and send the input event to it through a virtual input module. <br>
  943. If you define a virtual input module then it needs to be given a unique address in the allowed range for input modules and this address must not be used by existing input modules.
  944. <br><br>
  945. <b>Prerequisites</b>
  946. <ul>
  947. <br>
  948. <li>
  949. This module requires the Perl modules Device::SerialPort or Win32::SerialPort and Digest::CRC.<br>
  950. To connect to the PHC bus it requires an RS485 adapter that connects directly to the PHC bus with GND, +data and -data.
  951. </li>
  952. </ul>
  953. <br>
  954. <a name="PHCDefine"></a>
  955. <b>Define</b>
  956. <ul>
  957. <code>define &lt;name&gt; PHC &lt;Device&gt;</code><br>
  958. The module connects to the PHC bus with the specified device (RS485 adapter)<br>
  959. <br>
  960. Examples:<br>
  961. <br>
  962. <ul><code>define MyPHC PHC /dev/ttyRS485</code></ul><br>
  963. </ul>
  964. <br>
  965. <a name="PHCConfiguration"></a>
  966. <b>Configuration of the module</b><br><br>
  967. <ul>
  968. The module doesn't need configuration to listen to the bus and create readings. <br>
  969. Only if you want to send input to the bus, you need to define a virtual input module with an address that is not used by real modules.<br>
  970. Virtual input modules and their channels are defined using attributes.<br>
  971. Example:<br>
  972. <pre>
  973. attr MyPHC virtEMD25C2Name VirtLightSwitch
  974. </pre>
  975. Defines a virtual light switch as channel 2 on the virtual input nodule with address 25. This light switch can then be used with set comands like
  976. <pre>
  977. set MyPHC VirtualLightSwitch ein>0
  978. </pre>
  979. The set options offered here are the event types that PHC knows for input modules. They are ein>0, ein>1, ein>2, aus, aus<1.
  980. To react on such events in the PHC system you need to add a reaction to the programming of your PHC control module.
  981. </ul>
  982. <br>
  983. <a name="PHCSet"></a>
  984. <b>Set-Commands</b><br>
  985. <ul>
  986. <li><b>importChannelList</b></li>
  987. reads an xml file that is exportable by the Peha "Systemsoftware" that contains addresses and names of existing modules and channels on the PHC bus.<br>
  988. The path to the filename to import is relative to the Fhem base directory.<br>
  989. Example:<br>
  990. <pre>
  991. set MyPHC importChannelList Kanalliste.xml
  992. </pre>
  993. If Kanalliste.xml is located in /opt/fhem.<br>
  994. <br><br>
  995. more set options are created based on the attributes defining virtual input modules / channels<br>
  996. Every input channel for which an attribute like <code>virtEMDxyCcName</code> is defined will create a valid set option with name specified in the attribute.<br>
  997. </ul>
  998. <br>
  999. <a name="PHCGet"></a>
  1000. <b>Get-Commands</b><br>
  1001. <ul>
  1002. none so far
  1003. </ul>
  1004. <br>
  1005. <a name="PHCattr"></a>
  1006. <b>Attributes</b><br><br>
  1007. <ul>
  1008. <li><a href="#do_not_notify">do_not_notify</a></li>
  1009. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  1010. <br>
  1011. <li><b>virtEMDxyCcName</b></li>
  1012. Defines a virtual input module with a given address and a name for a channel of that input module.<br>
  1013. For example:<br>
  1014. <pre>
  1015. attr MyPHC virtEMD25C1Name LivingRoomLightSwitch
  1016. attr MyPHC virtEMD25C2Name KitchenLightSwitch
  1017. </pre>
  1018. <li><b>module[0-9]+description</b></li>
  1019. this attribute is typically created when you import a channel list with <code>set MyPHCDevice importChannelList</code>.<br>
  1020. It gives a name to a module. This name is used for better readability when logging at verbose level 4 or 5.
  1021. <li><b>module[0-9]+type</b></li>
  1022. this attribute is typically created when you import a channel list with <code>set MyPHCDevice importChannelList</code>.<br>
  1023. It defines the type of a module. This information is needed since some module types (e.g. EMD and JRM) use the same address space but a different
  1024. protocol interpretation so parsing is only correct if the module type is known.
  1025. <li><b>channel(EMD|AMD|JRM|DIM|UIM|MCC|MFN)[0-9]+[io]?[0-9]+description</b></li>
  1026. this attribute is typically created when you import a channel list with <code>set MyPHCDevice importChannelList</code>.<br>
  1027. It defines names for channels of modules.
  1028. These names are used for better readability when logging at verbose level 4 or 5.
  1029. They also define the names of readings that are automatically created when the module listens to the PHC bus.
  1030. <br>
  1031. </ul>
  1032. <br>
  1033. </ul>
  1034. =end html
  1035. =cut