Wrapper around alarmdecoder to make it Twisted compatible.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

221 lines
7.2 KiB

  1. #!/usr/bin/env python
  2. __author__ = 'John-Mark Gurney'
  3. __copyright__ = 'Copyright 2017 John-Mark Gurney. All rights reserved.'
  4. __license__ = '2-clause BSD license'
  5. # Copyright 2017, John-Mark Gurney
  6. # All rights reserved.
  7. #
  8. # Redistribution and use in source and binary forms, with or without
  9. # modification, are permitted provided that the following conditions are met:
  10. #
  11. # 1. Redistributions of source code must retain the above copyright notice, this
  12. # list of conditions and the following disclaimer.
  13. # 2. Redistributions in binary form must reproduce the above copyright notice,
  14. # this list of conditions and the following disclaimer in the documentation
  15. # and/or other materials provided with the distribution.
  16. #
  17. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  21. # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. #
  28. # The views and conclusions contained in the software and documentation are those
  29. # of the authors and should not be interpreted as representing official policies,
  30. # either expressed or implied, of the Project.
  31. from alarmdecoder.event import event
  32. from twisted.internet import reactor
  33. from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
  34. from twisted.protocols import basic
  35. from twisted.test import proto_helpers
  36. from twisted.trial import unittest
  37. import alarmdecoder
  38. import mock
  39. import twisted.internet.serialport
  40. __all__ = [ 'AlarmDecoderProtocol', 'adtwist' ]
  41. class AlarmDecoderProtocol(basic.LineReceiver):
  42. '''This is a twisted protocol for AlarmDecoder.
  43. To use this class, instantiate the class. Then you must pass it to
  44. AlarmDecoder as it's device and pass it to the transport. Once both
  45. calls have been made, only a reference to the AlarmDecoder instance
  46. should be kept.
  47. There is a helper function adtwist that does this work with a SerialPort
  48. transport.
  49. '''
  50. # Protocol Stuff
  51. delimiter = b'\r\n'
  52. dropLine = True
  53. def lineReceived(self, line):
  54. if self.dropLine:
  55. self.dropLine = False
  56. return
  57. # On real hardware, a prompt (possibly from the C and V
  58. # commands) will be inserted in the middle of a message
  59. # (and apparently flush the remaining line), so detect
  60. # when we get a prompt, and there was more data, and
  61. # ignore the line
  62. if line[-3:] == b'\n!>' and len(line) > 3:
  63. return
  64. self.on_read(data=line)
  65. # AD Device Stuff
  66. on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*")
  67. on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*")
  68. on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** def callback(device, data)*")
  69. on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*")
  70. def open(self, baudrate=None, no_reader_thread=None):
  71. # We don't have anything to do on open. We might want to
  72. # possibly do the transport connection here, or verify that
  73. # we have a transport.
  74. self.on_open()
  75. return self
  76. def write(self, data):
  77. self.transport.write(data)
  78. self.on_write(data=data)
  79. def close(self):
  80. self.on_close()
  81. def adtwist(serdev, *args, **kwargs):
  82. '''Create an AlarmDecoder instance using the twisted SerialPort transport.
  83. The arguments that are passed to this function are passed to SerialPort
  84. allowing the setting of SerialPort's parameters.
  85. open will have already been called.
  86. '''
  87. adp = AlarmDecoderProtocol()
  88. ad = alarmdecoder.AlarmDecoder(adp)
  89. twisted.internet.serialport.SerialPort(adp, serdev, reactor, *args, **kwargs)
  90. ad.open()
  91. return ad
  92. class TestADProtocol(unittest.TestCase):
  93. @staticmethod
  94. def getTimeout():
  95. return .2
  96. def setUp(self):
  97. self.adp = AlarmDecoderProtocol()
  98. self.ad = alarmdecoder.AlarmDecoder(self.adp)
  99. self.tr = proto_helpers.StringTransport()
  100. self.adp.makeConnection(self.tr)
  101. openmock = mock.MagicMock()
  102. self.adp.on_open += openmock
  103. self.ad.open()
  104. openmock.assert_called_once_with(self.adp)
  105. self.assertEqual(self.tr.value(), b'C\rV\r')
  106. self.tr.clear()
  107. self.adp.dataReceived(b'VZ;RF;ZX;RE;AU;3X;CG;DD;MF;LR;KE;MK;CB\r\n')
  108. self.adp.dataReceived(b'!CONFIG>ADDRESS=18&CONFIGBITS=ff00&LRR=N&EXP=NNNNN&REL=NNNN&MASK=ffffffff&DEDUPLICATE=N\r\n')
  109. self.adp.dataReceived(b'!VER:ffffffff,V2.2a.6,TX;RX;SM;VZ;RF;ZX;RE;AU;3X;CG;DD;MF;LR;KE;MK;CB\r\n')
  110. def test_middleprompt(self):
  111. '''Test that we don't create an error when a prompt appears
  112. in the middle of the line which can happen at start up.'''
  113. self.adp.dataReceived(b'[0000000110000000----],0f\n!>\r\n')
  114. @mock.patch('alarmdecoder.AlarmDecoder.open')
  115. @mock.patch('twisted.internet.serialport.SerialPort')
  116. def test_adtwist(self, spmock, openmock):
  117. dev = 'somedev'
  118. origkwargs = { 'baudrate': 123 }
  119. ret = adtwist(dev, **origkwargs)
  120. self.assertIsInstance(ret, alarmdecoder.AlarmDecoder)
  121. args, kwargs = spmock.call_args
  122. self.assertIsInstance(args[0], AlarmDecoderProtocol)
  123. self.assertEqual(args[1], dev)
  124. self.assertEqual(kwargs, origkwargs)
  125. openmock.assert_called_once()
  126. def test_close(self):
  127. closemock = mock.MagicMock()
  128. self.adp.on_close += closemock
  129. self.ad.close()
  130. closemock.assert_called_once_with(self.adp)
  131. def test_adprot(self):
  132. alarmfun = mock.MagicMock()
  133. ad = self.ad
  134. adp = self.adp
  135. #print(repr(self.tr.value()))
  136. self.assertEqual(ad.version_number, 'V2.2a.6')
  137. msgmock = mock.MagicMock()
  138. ad.on_message += msgmock
  139. data = b'[0000000111000100----],006,[f7000007100600202a020000000000],"FIRE 06 "\r\n'
  140. msgdata = data[:-2]
  141. if False: # pragma: no cover
  142. # This'd be nice, but the Message object doesn't have a working equality operator
  143. from alarmdecoder.messages import Message
  144. dmsg = Message(msgdata)
  145. readmock = mock.MagicMock()
  146. readmockad = mock.MagicMock()
  147. adp.on_read += readmock
  148. ad.on_read += readmockad
  149. adp.dataReceived(data)
  150. readmock.assert_called_once_with(adp, data=msgdata)
  151. readmockad.assert_called_once_with(ad, data=msgdata)
  152. msgmock.assert_called_once()
  153. msg = msgmock.call_args[1]['message']
  154. self.assertTrue(msg.ac_power)
  155. self.assertEqual(msg.text, 'FIRE 06 ')
  156. msgmock.reset_mock()
  157. adp.dataReceived(b'[0000000110000000----],010,[f70000071010000028020000000000],"FAULT 10 "\r\n')
  158. msgmock.assert_called_once()
  159. msg = msgmock.call_args[1]['message']
  160. self.assertEqual(msg.text, 'FAULT 10 ')
  161. writemock = mock.MagicMock()
  162. adp.on_write += writemock
  163. ad.send(b'5')
  164. self.assertEqual(self.tr.value(), b'5')
  165. writemock.assert_called_once_with(adp, data=b'5')