Wolf ISM7 communication library
Jacek Kowalski
2018-06-24 bb2be605824d45f2733f2781606829d1dbb99588
commit | author | age
6276d7 1 <?php
JK 2 require_once(__DIR__ . '/vendor/autoload.php');
3
4 class ISM7_Connection {
5     protected $conn;
6     protected $buffer;
7     
8     protected $logger;
9     protected $dumper;
10     
11     public function __construct($host, $port=9091) {
12         $this->logger = Logger::getLogger('ism7.connection');
13         $this->logger->debug('Connecting to '.$host.':'.$port);
14         $this->dumper = new Clue\Hexdump\Hexdump();
15         
16         $context = stream_context_create(array(
17             'tls' => array(
18                 'verify_peer' => FALSE,
19             ),
20         ));
21         $this->conn = stream_socket_client('sslv3://'.$host.':'.$port,
22                 $errno, $errstr, ini_get("default_socket_timeout"),
23                 STREAM_CLIENT_CONNECT, $context);
24         if($this->conn === FALSE) {
25             throw new Exception('stream_socket_client() failed! '.$errstr.' ('.$errno.')');
26         }
27         
28         $this->logger->debug('Connected to '.$host.':'.$port);
29     }
30     
31     protected function send($data) {
32         $data = (string)$data;
33         $this->logger->debug('Trying to send '.strlen($data).' bytes');
34         $this->logger->trace('Send data dump:'."\n".$this->dumper->dump($data));
35         
36         for($written = 0; $written < strlen($data); $written += $length) {
37             $length = fwrite($this->conn, substr($data, $written));
38             if($length === FALSE) {
39                 throw new Exception('fwrite() failed!');
40             }
41         }
42         
43         $this->logger->debug('Sent '.$written.' bytes');
44     }
45     
46     protected function recv($bytes = 1) {
47         $this->logger->debug('Trying to receive '.$bytes.' bytes');
48         
49         $bytes = (int)$bytes;
50         while(strlen($this->buffer) < $bytes) {
51             $part = fread($this->conn, $bytes - strlen($this->buffer));
52             if($part === FALSE) {
53                 throw new Exception('fread() failed!');
54             }
55             $this->buffer .= $part;
56         }
57         
58         $data = substr($this->buffer, 0, $bytes);
59         $this->buffer = substr($this->buffer, $bytes);
60         
61         $this->logger->debug('Received '.strlen($data).' bytes');
62         $this->logger->trace('Received data dump:'."\n".$this->dumper->dump($data));
63         
64         return $data;
65     }
66     
67     public function communicate(ISM7_Request $request) {
68         $this->logger->debug('Sending request: '.get_class($request));
69         
70         $this->send($request->getMessage());
71         
72         $header = $this->recv(6);
73         $response = ISM7_Response::getByHeader($header);
74         $data = $this->recv($response->getLength());
75         $response->setData($data);
76         
77         $this->logger->debug('Got response: '.get_class($response));
78         
79         return $response;
80     }
81     
82     public function wait() {
83         $header = $this->recv(6);
84         $response = ISM7_Response::getByHeader($header);
85         $data = $this->recv($response->getLength());
86         $response->setData($data);
87         
88         $this->logger->debug('Got push request: '.get_class($response));
89         
90         return $response;
91     }
92     
93     public function close() {
94         fclose($this->conn);
95     }
96 }
97
98 abstract class ISM7_Message {
99     protected static $typeToId = array(
100         'PortalLogonRequest' => 0,
101         'PortalLogonResponse' => 1,
102         'ReadSystemconfigRequest' => 2,
103         'ReadSystemconfigResponse' => 3,
104         'TelegramBundleRequest' => 4,
105         'TelegramBundleResponse' => 5,
106         'DirectLogonRequest' => 8,
107         'DirectLogonResponse' => 9,
108     );
109     protected static $idToType = NULL;
110     
111     protected $address;
112     protected $type;
113     
114     protected static $logger;
115     
116     public function __construct($className, $address = 0) {
117         if(!self::$logger) {
118             self::$logger = Logger::getLogger('ism7.message');
119         }
120         
121         $this->address = (int)$address;
122         assert(0 <= $this->address && $this->address <= 0xFFFF);
123         $this->type = substr($className, strpos($className, '_') + 1);
124         assert(isset(self::$typeToId[$this->type]));
125     }
126     
127     public function getAddress() {
128         return $this->address;
129     }
130     public function getType() {
131         return $this->type;
132     }
133     public function getTypeId() {
134         return self::getIdByType($this->type);
135     }
136     
137     protected static function getIdByType($type) {
138         return self::$typeToId[$type];
139     }
140     protected static function getTypeById($id) {
141         if(self::$idToType === NULL) {
142             self::$idToType = array_flip(self::$typeToId);
143         }
144         return self::$idToType[$id];
145     }
146 }
147
148 abstract class ISM7_Request extends ISM7_Message {
149     public function __construct($className, $address = 0) {
150         parent::__construct($className, $address);
151     }
152     
153     function getMessage() {
154         $message = $this->getData();
155         $header = pack('nnn', $this->getAddress(), strlen($message), $this->getTypeId());
156         
157         return $header . $message;
158     }
159     
160     abstract function getData();
161 }
162
163 class ISM7_Response extends ISM7_Message {
164     private $length;
165     protected $data;
166     
167     public function __construct($className, $address = 0) {
168         parent::__construct($className, $address);
169     }
170     
171     public function getLength() {
172         return $this->length;
173     }
174     protected function setHeader($header) {
175         $this->address = $header['address'];
176         $this->length = $header['length'];
177     }
178     
179     public static function getByHeader($header) {
180         $headerDecoded = unpack('naddress/nlength/ntype', $header);
181         $className = 'ISM7_' . ISM7_Message::getTypeById($headerDecoded['type']);
182         self::$logger->debug('Response seems to be '.$className);
183         
184         $response = new $className();
185         $response->setHeader($headerDecoded);
186         return $response;
187     }
188     
189     protected function setData($data) {
190         $this->data = new DOMDocument('1.0', 'utf-8');
191         $this->data->loadXML($data);
192     }
193 }
194
195 class ISM7_DirectLogonRequest extends ISM7_Request {
196     private $password;
197     
198     public function __construct($password = '1111') {
199         parent::__construct(__CLASS__);
200         $this->password = (string)$password;
201     }
202     
203     public function getData() {
204         $xml = new DOMDocument('1.0', 'utf-8');
205         $rootNode = $xml->createElement('direct-logon-request');
206         $xml->appendChild($rootNode);
207         $passNode = $xml->createElement('password');
208         $rootNode->appendChild($passNode);
209         $passText = $xml->createTextNode($this->password);
210         $passNode-> appendChild($passText);
211         return $xml->saveXML();
212     }
213 }
214
215 class ISM7_DirectLogonResponse extends ISM7_Response {
216     protected $sid;
217     protected $installationName;
218     protected $serialNumber;
219     protected $dateTime;
220     protected $softwareVersion;
221     protected $hardwareVersion;
222     protected $moduleType;
223     protected $wlanConnected;
224     
225     public function __construct() {
226         parent::__construct(__CLASS__);
227     }
228     
229     public function setData($data) {
230         parent::setData($data);
231         $rootNode = $this->data->documentElement;
232         assert($rootNode->tagName == 'direct-logon-response');
233         assert($rootNode->hasAttribute('state'));
234         assert(strtolower($rootNode->getAttribute('state')) == 'ok');
235         assert($rootNode->hasAttribute('sid'));
236         $this->sid = (string)$rootNode->getAttribute('sid');
237         
238         foreach($rootNode->childNodes as $childNode) {
239             $text = trim((string)$childNode->textContent);
240             
241             self::$logger->trace('DirectLogonResponse::setData(): '.$childNode->nodeName.'='.$text);
242             
243             switch($childNode->nodeName) {
244                 case 'installationname':
245                     $this->installationName = $text;
246                     break;
247                 case 'serialnumber':
248                     $this->serialNumber = $text;
249                     break;
250                 case 'date-time':
251                     $this->dateTime = new DateTime($text);
252                     break;
253                 case 'ism-softwareversion':
254                     $this->softwareVersion = $text;
255                     break;
256                 case 'ism-hardwareversion':
257                     $this->hardwareVersion = $text;
258                     break;
259                 case 'type':
260                     $this->moduleType = $text;
261                     break;
262                 case 'wlan-connected':
263                     $this->wlanConnected = (strtolower($text) == 'true');
264                     break;
265                 default:
266                     self::$logger->info('DirectLogonResponse::setData(): unknown tag '.$childNode->nodeName);
267             }
268         }
269     }
270     
271     public function getSid() {
272         return $this->sid;
273     }
274 }
275
276 abstract class ISM7_Element {
277     protected static $logger;
278     
279     public function __construct() {
280         if(!self::$logger) {
281             self::$logger = Logger::getLogger('ism7.element');
282         }
283     }
284 }
285
286 class ISM7_Systemconfig_Gateway extends ISM7_Element {
287     protected $gatewayType;
288     protected $softwareNumber;
289     protected $softwareVersion;
290     protected $wlan;
291     protected $g3;
292     
293     public function __construct(DOMNode $node) {
294         parent::__construct();
295         
296         foreach($node->attributes as $attribute) {
297             $text = $attribute->nodeValue;
298             self::$logger->trace('Systemconfig_Gateway: '.$attribute->nodeName.'='.$text);
299             
300             switch($attribute->nodeName) {
301                 case 'type':
302                     $this->gatewayType = $text;
303                     break;
304                 case 'softwareNumber':
305                     $this->softwareNumber = $text;
306                     break;
307                 case 'softwareVersion':
308                     $this->softwareVersion = $text;
309                     break;
310                 case 'wlan':
311                     $this->wlan = ($text == 'true');
312                     break;
313                 case 'g3':
314                     $this->g3 = ($text == 'true');
315                     break;
316                 default:
317                     self::$logger->info('Systemconfig_Gateway: unknown attribute '.$childNode->nodeName);
318             }
319         }
320     }
321 }
322
323 class ISM7_Systemconfig_Bus extends ISM7_Element {
324     protected $busType;
325     protected $devices = array();
326     
327     public function __construct(DOMNode $node) {
328         parent::__construct();
329         
330         if($node->hasAttribute('type')) {
331             $this->busType = $node->getAttribute('type');
332         }
333         
334         foreach($node->childNodes as $childNode) {
335             self::$logger->trace('Systemconfig_Bus: '.$childNode->nodeName);
336             if($childNode->nodeName == 'busDevices') {
337                 foreach($childNode->childNodes as $device) {
338                     self::$logger->trace('Systemconfig_Bus: '.$device->nodeName);
339                     assert($device->nodeName == 'busDevice');
340                     $this->devices[] = new ISM7_Systemconfig_BusDevice($device);
341                 }
342             } else {
343                 self::$logger->info('Systemconfig_Bus: unknown tag '.$childNode->nodeName);
344             }
345         }
346     }
347 }
348
349 class ISM7_Systemconfig_BusDevice extends ISM7_Element {
350     protected $address;
351     protected $softwareNumber;
352     protected $softwareVersion;
353     protected $configuration;
354     protected $deviceId;
355     
356     public function __construct(DOMNode $node) {
357         parent::__construct();
358         
359         foreach($node->attributes as $attribute) {
360             $text = $attribute->nodeValue;
361             self::$logger->trace('Systemconfig_BusDevice: '.$attribute->nodeName.'='.$text);
362             
363             switch($attribute->nodeName) {
364                 case 'ba':
365                     $this->address = hexdec($text);
366                     break;
367                 case 'sv':
368                     $this->softwareNumber = hexdec($text);
369                     break;
370                 case 'sr':
371                     $this->softwareVersion = hexdec($text);
372                     break;
373                 case 'cfg':
374                     $this->configuration = hexdec($text);
375                     break;
376                 case 'did':
377                     $this->deviceId = hexdec($text);
378                     break;
379                 default:
380                     self::$logger->info('Systemconfig_BusDevice: unknown attribute '.$attribute->nodeName);
381             }
382         }
383     }
384 }
385
386 class ISM7_ReadSystemconfigRequest extends ISM7_Request {
387     private $sid;
388     
389     public function __construct($sid = '1') {
390         parent::__construct(__CLASS__);
391         $this->sid = (string)$sid;
392     }
393     
394     public function getData() {
395         $xml = new DOMDocument('1.0', 'utf-8');
396         $rootNode = $xml->createElement('read-systemconfig-request');
397         $rootNode->setAttribute('sid', $this->sid);
398         $xml->appendChild($rootNode);
399         return $xml->saveXML();
400     }
401 }
402
403 class ISM7_ReadSystemconfigResponse extends ISM7_Response {
404     protected $gateway;
405     protected $bus;
406     
407     public function __construct() {
408         parent::__construct(__CLASS__);
409     }
410     
411     public function setData($data) {
412         parent::setData($data);
413         
414         $rootNode = $this->data->documentElement;
415         assert($rootNode->tagName == 'read-systemconfig-response');
416         foreach($rootNode->childNodes as $childNode) {
417             $text = trim((string)$childNode->textContent);
418             
419             self::$logger->trace('ReadSystemconfigResponse::setData(): parsing '.$childNode->nodeName);
420             
421             switch($childNode->nodeName) {
422                 case 'gateway':
423                     $this->gateway = new ISM7_Systemconfig_Gateway($childNode);
424                     break;
425                 case 'busconfig':
426                     $this->bus = new ISM7_Systemconfig_Bus($childNode);
427                     break;
428                 default:
429                     self::$logger->info('ReadSystemconfigResponse::setData(): unknown tag '.$childNode->nodeName);
430             }
431         }
432     }
433 }
434
435 interface ISM7_XML_Part {
436     public function getXmlPart($xml = NULL);
437 }
438 interface ISM7_XML_Parent {
439     public function append(ISM7_XML_Part $part);
440 }
441
442 class ISM7_Telegram_InfonumberRead extends ISM7_Element implements ISM7_XML_Part {
443     protected $sequence;
444     protected $address;
445     protected $infonumber;
446     protected $interval;
447     
448     function __construct($sequence = NULL, $address = NULL, $infonumber = NULL, $interval = NULL) {
449         parent::__construct();
450         if($sequence === NULL && $address === NULL && $infonumber === NULL && $interval === NULL) {
451             return;
452         }
453         
454         if($address instanceof ISM7_Systemconfig_BusDevice) {
455             $address = $address->address;
456         }
457         
458         $this->sequence = (string)$sequence;
459         $this->address = (int)$address;
460         $this->infonumber = (int)$infonumber;
461         $this->interval = (int)$interval;
462     }
463     
464     public function getXmlPart($xml = NULL) {
465         if(!($xml instanceof DOMDocument)) {
466             $xml = new DOMDocument('1.0', 'utf-8');
467         }
468         
469         $rootNode = $xml->createElement('ird');
470         $rootNode->setAttribute('se', $this->sequence);
471         $rootNode->setAttribute('ba', '0x'.dechex($this->address));
472         $rootNode->setAttribute('in', $this->infonumber);
473         
474         if($this->interval) {
475             $rootNode->setAttribute('is', $this->interval);
476         }
477         
478         return $rootNode;
479     }
480 }
481
482 class ISM7_Telegram_Infonumber extends ISM7_Telegram_InfonumberRead {
483     protected $value = 0;
484     
485     public function __construct(DOMNode $node) {
486         parent::__construct();
487         
488         assert($node->hasAttribute('st'));
489         assert($node->getAttribute('st') == 'OK');
490         
491         assert($node->hasAttribute('dh'));
492         assert($node->hasAttribute('dl'));
493         $this->value = (hexdec($node->getAttribute('dh')) << 8) | hexdec($node->getAttribute('dl'));
494         if($this->value > 127 * 256) {
495             $value -= 256 * 256;
496         }
497         self::$logger->trace('Telegram_Infonumber: d='.$this->value);
498         
499         foreach($node->attributes as $attribute) {
500             $text = $attribute->nodeValue;
501             self::$logger->trace('Telegram_Infonumber: '.$attribute->nodeName.'='.$text);
502             
503             switch($attribute->nodeName) {
504                 case 'se':
505                     $this->sequence = $text;
506                     break;
507                 case 'ba':
508                     $this->address = hexdec($text);
509                     break;
510                 case 'in':
511                     $this->infonumber = (int)$text;
512                     break;
513                 case 'dh':
514                 case 'dl':
515                 case 'st':
516                     break;
517                 default:
518                     self::$logger->info('Systemconfig_BusDevice: unknown attribute '.$attribute->nodeName);
519             }
520         }
521     }
522     
523     public function __get($name) {
524         return $this->$name;
525     }
526 }
527
528 class ISM7_TelegramBundleRequest extends ISM7_Request implements ISM7_XML_Parent {
529     protected $xml;
530     protected $rootNode;
531     
532     protected $bundle;
533     protected $gateway;
534     protected $abortOnError;
535     protected $telegramType;
536     
537     public function __construct($bundle = 1, $gateway = 1, $abortOnError = FALSE, $telegramType = 'pull') {
538         parent::__construct(__CLASS__);
539         
540         assert(in_array($telegramType, array('pull', 'push', 'write', 'remove', 'clear')));
541         
542         $this->bundle = (int)$bundle;
543         $this->gateway = (int)$gateway;
544         $this->abortOnError = (bool)$abortOnError;
545         $this->telegramType = (string)$telegramType;
546         
547         $this->xml = new DOMDocument('1.0', 'utf-8');
548         $this->rootNode = $this->xml->createElement('tbreq');
549         $this->xml->appendChild($this->rootNode);
550     }
551     
552     public function append(ISM7_XML_Part $part) {
553         $node = $part->getXmlPart($this->xml);
554         assert($node->nodeName == 'ird' || $node->nodeName == 'erd');
555         
556         $this->rootNode->appendChild($node);
557     }
558     
559     public function getData() {
560         $this->rootNode->setAttribute('bn', $this->bundle);
561         $this->rootNode->setAttribute('gw', $this->gateway);
562         $this->rootNode->setAttribute('ae', ($this->abortOnError ? 'true' : 'false'));
563         $this->rootNode->setAttribute('ty', $this->telegramType);
564         
565         return $this->xml->saveXML();
566     }
567 }
568
569 class ISM7_TelegramBundleResponse extends ISM7_Response implements IteratorAggregate {
570     protected $bundle;
571     protected $gateway;
572     protected $dateTime;
573     protected $errorMessage;
574     
575     protected $infonumbers = array();
576     
577     public function __construct() {
578         parent::__construct(__CLASS__);
579     }
580     
581     public function setData($data) {
582         parent::setData($data);
583         
584         $rootNode = $this->data->documentElement;
585         assert($rootNode->tagName == 'tbres');
586         assert($rootNode->hasAttribute('st'));
587         assert($rootNode->getAttribute('st') == 'OK');
588         
589         assert($rootNode->hasAttribute('bn'));
590         assert(ctype_digit($rootNode->getAttribute('bn')));
591         $this->bundle = (int)$rootNode->getAttribute('bn');
592         
593         assert($rootNode->hasAttribute('gw'));
594         assert(ctype_digit($rootNode->getAttribute('gw')));
595         $this->gateway = (int)$rootNode->getAttribute('gw');
596         
597         if($rootNode->hasAttribute('ts')) {
598             $this->dateTime = new DateTime($rootNode->getAttribute('ts'));
599             self::$logger->trace('TelegramBundleResponse::setData(): timestamp: '.$this->dateTime->format('Y-m-d H:i:s'));
600         }
601         if($rootNode->hasAttribute('emsg') && $rootNode->getAttribute('emsg')) {
602             self::$logger->warn('TelegramBundleResponse::setData(): error message: '.$rootNode->getAttribute('emsg'));
603         }
604         
605         foreach($rootNode->childNodes as $childNode) {
606             $text = trim((string)$childNode->textContent);
607             
608             self::$logger->trace('TelegramBundleResponse::setData(): parsing '.$childNode->nodeName);
609             
610             switch($childNode->nodeName) {
611                 case 'irs':
612                     $this->infonumbers[] = new ISM7_Telegram_Infonumber($childNode);
613                     break;
614                 default:
615                     self::$logger->info('TelegramBundleResponse::setData(): unknown tag '.$childNode->nodeName);
616             }
617         }
618     }
619     
620     public function getIterator() {
621         return new ArrayIterator($this->infonumbers);
622     }
623 }