SHC_parser.pm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. #!/usr/bin/perl -w
  2. ##########################################################################
  3. # This file is part of the smarthomatic module for FHEM.
  4. #
  5. # Copyright (c) 2014 Uwe Freese
  6. #
  7. # You can find smarthomatic at www.smarthomatic.org.
  8. # You can find FHEM at www.fhem.de.
  9. #
  10. # This file is free software: you can redistribute it and/or modify it
  11. # under the terms of the GNU General Public License as published by the
  12. # Free Software Foundation, either version 3 of the License, or (at your
  13. # option) any later version.
  14. #
  15. # This file is distributed in the hope that it will be useful, but
  16. # WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
  18. # Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License along
  21. # with smarthomatic. If not, see <http://www.gnu.org/licenses/>.
  22. ##########################################################################
  23. # Usage:
  24. #
  25. # Init parser:
  26. # ------------
  27. # my $parser = new SHC_parser();
  28. #
  29. # Receiving packets:
  30. # ------------------
  31. # 1.) Receive string from base station (over UART).
  32. # 2.) Parse received string:
  33. # $parser->parse("Packet Data: SenderID=22;...");
  34. # 3.) Get MessageGroupName: my $grp = $parser->getMessageGroupName();
  35. # 4.) Get MessageName: my $msg = $parser->getMessageName();
  36. # 5.) Get data fields depending on MessageGroupName and MessageName, e.g.
  37. # $val = $parser->getField("Temperature");
  38. #
  39. # Sending packets:
  40. # ----------------
  41. # 1.) Init packet:
  42. # $parser->initPacket("PowerSwitch", "SwitchState", "Set");
  43. # 2.) Set fields:
  44. # $parser->setField("PowerSwitch", "SwitchState", "TimeoutSec", 8);
  45. # 3.) Get send string: $str = $parser->getSendString($receiverID);
  46. # 4.) Send string to base station (over UART).
  47. ##########################################################################
  48. # $Id: SHC_parser.pm 8190 2015-03-10 21:23:03Z rr2000 $
  49. package SHC_parser;
  50. use strict;
  51. use feature qw(switch);
  52. use XML::LibXML;
  53. use SHC_datafields;
  54. # Hash for data field definitions.
  55. my %dataFields = ();
  56. # Hashes used to translate between names and IDs.
  57. my %messageTypeID2messageTypeName = ();
  58. my %messageTypeName2messageTypeID = ();
  59. my %messageGroupID2messageGroupName = ();
  60. my %messageGroupName2messageGroupID = ();
  61. my %messageID2messageName = ();
  62. my %messageName2messageID = ();
  63. my %messageID2bits = ();
  64. # byte array to store data to send
  65. my @msgData = ();
  66. my $sendMode = 0;
  67. my $offset = 0;
  68. sub new
  69. {
  70. my $class = shift;
  71. init_datafield_positions();
  72. my $self = {
  73. _senderID => 0,
  74. _packetCounter => 0,
  75. _messageTypeID => 0,
  76. _messageGroupID => 0,
  77. _messageGroupName => "",
  78. _messageID => 0,
  79. _messageName => "",
  80. _messageData => "",
  81. };
  82. bless $self, $class;
  83. return $self;
  84. }
  85. sub init_datafield_positions_noarray($$$$$)
  86. {
  87. my ($messageGroupID, $messageID, $field, $arrayLength, $arrayElementBits) = @_;
  88. given ($field->nodeName) {
  89. when ('UIntValue') {
  90. my $id = ($field->findnodes("ID"))[0]->textContent;
  91. my $bits = ($field->findnodes("Bits"))[0]->textContent;
  92. # print "Data field " . $id . " starts at " . $offset . " with " . $bits . " bits.\n";
  93. $dataFields{$messageGroupID . "-" . $messageID . "-" . $id} =
  94. new UIntValue($id, $offset, $bits, $arrayLength, $arrayElementBits);
  95. $offset += $bits;
  96. }
  97. when ('IntValue') {
  98. my $id = ($field->findnodes("ID"))[0]->textContent;
  99. my $bits = ($field->findnodes("Bits"))[0]->textContent;
  100. # print "Data field " . $id . " starts at " . $offset . " with " . $bits . " bits.\n";
  101. $dataFields{$messageGroupID . "-" . $messageID . "-" . $id} =
  102. new IntValue($id, $offset, $bits, $arrayLength, $arrayElementBits);
  103. $offset += $bits;
  104. }
  105. when ('BoolValue') {
  106. my $id = ($field->findnodes("ID"))[0]->textContent;
  107. my $bits = 1;
  108. # print "Data field " . $id . " starts at " . $offset . " with " . $bits . " bits.\n";
  109. $dataFields{$messageGroupID . "-" . $messageID . "-" . $id} =
  110. new BoolValue($id, $offset, $arrayLength, $arrayElementBits);
  111. $offset += $bits;
  112. }
  113. when ('EnumValue') {
  114. my $id = ($field->findnodes("ID"))[0]->textContent;
  115. my $bits = ($field->findnodes("Bits"))[0]->textContent;
  116. # print "Data field " . $id . " starts at " . $offset . " with " . $bits . " bits.\n";
  117. my $object = new EnumValue($id, $offset, $bits, $arrayLength, $arrayElementBits);
  118. $dataFields{$messageGroupID . "-" . $messageID . "-" . $id} = $object;
  119. for my $element ($field->findnodes("Element")) {
  120. my $value = ($element->findnodes("Value"))[0]->textContent;
  121. my $name = ($element->findnodes("Name"))[0]->textContent;
  122. $object->addValue($name, $value);
  123. }
  124. $offset += $bits;
  125. }
  126. }
  127. }
  128. sub init_datafield_positions_array($$$)
  129. {
  130. my ($messageGroupID, $messageID, $field) = @_;
  131. my $offsetStartArray = $offset;
  132. my $arrayLength = int(($field->findnodes("Length"))[0]->textContent);
  133. my $arrayElementBits =
  134. calc_array_bits_ovr($field); # number of bits for one struct ("set of sub-elements") in a structured array
  135. # print "Next field is an array with " . $arrayLength . " elements (" . $arrayElementBits . " ovr bits per array element)!\n";
  136. for my $subfield ($field->findnodes("UIntValue|IntValue|BoolValue|EnumValue")) {
  137. my $bits =
  138. init_datafield_positions_noarray($messageGroupID, $messageID, $subfield, $arrayLength, $arrayElementBits);
  139. }
  140. $offset = $offsetStartArray + $arrayLength * $arrayElementBits;
  141. }
  142. # Calculate the overall bits for one struct in a structured array
  143. sub calc_array_bits_ovr($)
  144. {
  145. my ($field) = @_;
  146. my $bits = 0;
  147. for my $subfield ($field->findnodes("BoolValue")) {
  148. $bits += 1;
  149. }
  150. for my $subfield ($field->findnodes("UIntValue|IntValue|EnumValue")) {
  151. $bits += ($subfield->findnodes("Bits"))[0]->textContent;
  152. }
  153. return $bits;
  154. }
  155. # Read packet layout from XML file and remember the defined MessageGroups,
  156. # Messages and data fields (incl. positions, length).
  157. sub init_datafield_positions()
  158. {
  159. my $x = XML::LibXML->new() or die "new on XML::LibXML failed";
  160. my $d = $x->parse_file("FHEM/lib/SHC_packet_layout.xml") or die "parsing XML file failed";
  161. for my $element ($d->findnodes("/Packet/Header/EnumValue[ID='MessageType']/Element")) {
  162. my $value = ($element->findnodes("Value"))[0]->textContent;
  163. my $name = ($element->findnodes("Name"))[0]->textContent;
  164. $messageTypeID2messageTypeName{$value} = $name;
  165. $messageTypeName2messageTypeID{$name} = $value;
  166. }
  167. for my $messageGroup ($d->findnodes("/Packet/MessageGroup")) {
  168. my $messageGroupName = ($messageGroup->findnodes("Name"))[0]->textContent;
  169. my $messageGroupID = ($messageGroup->findnodes("MessageGroupID"))[0]->textContent;
  170. $messageGroupID2messageGroupName{$messageGroupID} = $messageGroupName;
  171. $messageGroupName2messageGroupID{$messageGroupName} = $messageGroupID;
  172. for my $message ($messageGroup->findnodes("Message")) {
  173. my $messageName = ($message->findnodes("Name"))[0]->textContent;
  174. my $messageID = ($message->findnodes("MessageID"))[0]->textContent;
  175. $messageID2messageName{$messageGroupID . "-" . $messageID} = $messageName;
  176. $messageName2messageID{$messageGroupName . "-" . $messageName} = $messageID;
  177. $offset = 0;
  178. for my $field ($message->findnodes("Array|UIntValue|IntValue|BoolValue|EnumValue")) {
  179. # When an array is detected, remember the array length and change the current field node
  180. # to the inner node for further processing.
  181. if ($field->nodeName eq 'Array') {
  182. init_datafield_positions_array($messageGroupID, $messageID, $field);
  183. } else {
  184. init_datafield_positions_noarray($messageGroupID, $messageID, $field, 1, 0);
  185. }
  186. }
  187. # DEBUG print "Remember packet length " . $offset . " bits for MessageGroupID " . $messageGroupID . ", MessageID " . $messageID . "\n";
  188. $messageID2bits{$messageGroupID . "-" . $messageID} = $offset;
  189. }
  190. }
  191. }
  192. sub parse
  193. {
  194. my ($self, $msg) = @_;
  195. $sendMode = 0;
  196. if (
  197. (
  198. $msg =~
  199. /^Packet Data: SenderID=(\d*);PacketCounter=(\d*);MessageType=(\d*);MessageGroupID=(\d*);MessageID=(\d*);MessageData=([^;]*);.*/
  200. )
  201. || ($msg =~
  202. /^Packet Data: SenderID=(\d*);PacketCounter=(\d*);MessageType=(\d*);AckSenderID=\d*;AckPacketCounter=\d*;Error=\d*;MessageGroupID=(\d*);MessageID=(\d*);MessageData=([^;]*);.*/
  203. )
  204. )
  205. {
  206. $self->{_senderID} = $1;
  207. $self->{_packetCounter} = $2;
  208. $self->{_messageTypeID} = $3;
  209. $self->{_messageGroupID} = $4;
  210. $self->{_messageID} = $5;
  211. $self->{_messageData} = $6;
  212. }
  213. else {
  214. return undef;
  215. }
  216. }
  217. sub getSenderID
  218. {
  219. my ($self) = @_;
  220. return $self->{_senderID};
  221. }
  222. sub getPacketCounter
  223. {
  224. my ($self) = @_;
  225. return $self->{_packetCounter};
  226. }
  227. sub getMessageTypeName
  228. {
  229. my ($self) = @_;
  230. return $messageTypeID2messageTypeName{$self->{_messageTypeID}};
  231. }
  232. sub getMessageGroupName
  233. {
  234. my ($self) = @_;
  235. return $messageGroupID2messageGroupName{$self->{_messageGroupID}};
  236. }
  237. sub getMessageName
  238. {
  239. my ($self) = @_;
  240. return $messageID2messageName{$self->{_messageGroupID} . "-" . $self->{_messageID}};
  241. }
  242. sub getMessageData
  243. {
  244. my ($self) = @_;
  245. if ($sendMode) {
  246. my $res = "";
  247. foreach (@msgData) {
  248. $res .= sprintf("%02X", $_);
  249. }
  250. return $res;
  251. } else {
  252. return $self->{_messageData};
  253. }
  254. }
  255. sub getField
  256. {
  257. my ($self, $fieldName, $index) = @_;
  258. if (!defined $index) {
  259. $index = 0;
  260. }
  261. my $obj = $dataFields{$self->{_messageGroupID} . "-" . $self->{_messageID} . "-" . $fieldName};
  262. # add 256 "empty" bytes to have enough data in the array because the message may be truncated
  263. my @tmpArray = map hex("0x$_"), ($self->{_messageData} . ("00" x 256)) =~ /(..)/g;
  264. return $obj->getValue(\@tmpArray, $index);
  265. }
  266. sub initPacket
  267. {
  268. my ($self, $messageGroupName, $messageName, $messageTypeName) = @_;
  269. $self->{_senderID} = 0; # base station SenderID
  270. $self->{_messageTypeID} = $messageTypeName2messageTypeID{$messageTypeName};
  271. $self->{_messageGroupID} = $messageGroupName2messageGroupID{$messageGroupName};
  272. $self->{_messageID} = $messageName2messageID{$messageGroupName . "-" . $messageName};
  273. my $lenBytes = int(($messageID2bits{$self->{_messageGroupID} . "-" . $self->{_messageID}} + 7) / 8);
  274. @msgData = (0) x $lenBytes;
  275. $sendMode = 1;
  276. }
  277. sub setField
  278. {
  279. my ($self, $messageGroupName, $messageName, $fieldName, $value, $index) = @_;
  280. if (!defined $index) {
  281. $index = 0;
  282. }
  283. my $gID = $messageGroupName2messageGroupID{$messageGroupName};
  284. my $mID = $messageName2messageID{$messageGroupName . "-" . $messageName};
  285. my $obj = $dataFields{$gID . "-" . $mID . "-" . $fieldName};
  286. $obj->setValue(\@msgData, $value, $index);
  287. }
  288. # sKK01RRRRGGMMDD
  289. # s0001003D3C0164 = SET Dimmer Switch Brightness 50%
  290. sub getSendString
  291. {
  292. my ($self, $receiverID, $aesKeyNr) = @_;
  293. # Right now the only way to set the AES key is by defining in in fhem.cfg
  294. # "define SHC_Dev_xx SHC_Dev xx aa" where xx = deviceID, aa = AES key
  295. #
  296. # TODO: Where to enter the AES key number? This is by device.
  297. # Add lookup table device -> AES key?
  298. # Automatically gather used AES key after reception from device?
  299. if (!defined $aesKeyNr) {
  300. $aesKeyNr = 0;
  301. }
  302. my $s = "s"
  303. . sprintf("%02X", $aesKeyNr)
  304. . sprintf("%02X", $self->{_messageTypeID})
  305. . sprintf("%04X", $receiverID)
  306. . sprintf("%02X", $self->{_messageGroupID})
  307. . sprintf("%02X", $self->{_messageID})
  308. . getMessageData();
  309. }
  310. 1;