From abe54dc3373721f6f98c8dd94274b89c06262863 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Mon, 11 Sep 2017 21:48:11 -0700 Subject: [PATCH] Create a wrapper around alarmdecoder that makes it twisted compatible. --- Makefile | 8 +++ adtwist.py | 169 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 ++ setup.py | 35 ++++++++++ 4 files changed, 216 insertions(+) create mode 100644 Makefile create mode 100644 adtwist.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e99ceb --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +test: + . ./t/bin/activate && \ + echo adtwist.py | entr sh -c 'python -m coverage run -m unittest adtwist && coverage report -m --omit=t/\*' + +setup: + virtualenv t && \ + . ./t/bin/activate && \ + pip install -r requirements.txt diff --git a/adtwist.py b/adtwist.py new file mode 100644 index 0000000..1ee5edc --- /dev/null +++ b/adtwist.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python + +from alarmdecoder.event import event + +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, Deferred, returnValue +from twisted.protocols import basic +from twisted.test import proto_helpers +from twisted.trial import unittest + +import alarmdecoder +import mock +import twisted.internet.serialport + +__all__ = [ 'AlarmDecoderProtocol', 'adtwist' ] + +class AlarmDecoderProtocol(basic.LineReceiver): + '''This is a twisted protocol for AlarmDecoder. + + To use this class, instantiate the class. Then you must pass it to + AlarmDecoder as it's device and pass it to the transport. Once both + calls have been made, only a reference to the AlarmDecoder instance + should be kept. + + There is a helper function adtwist that does this work with a SerialPort + transport. + ''' + + # Protocol Stuff + delimiter = '\r\n' + + def lineReceived(self, line): + self.on_read(data=line) + + # AD Device Stuff + on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*") + on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*") + 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)*") + on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*") + + def open(self, baudrate=None, no_reader_thread=None): + # We don't have anything to do on open. We might want to + # possibly do the transport connection here, or verify that + # we have a transport. + + self.on_open() + return self + + def write(self, data): + self.transport.write(data) + self.on_write(data=data) + + def close(self): + self.on_close() + +def adtwist(serdev, *args, **kwargs): + '''Create an AlarmDecoder instance using the twisted SerialPort transport. + + The arguments that are passed to this function are passed to SerialPort + allowing the setting of SerialPort's parameters. + + open will have already been called. + ''' + + adp = AlarmDecoderProtocol() + ad = alarmdecoder.AlarmDecoder(adp) + + twisted.internet.serialport.SerialPort(adp, serdev, reactor, *args, **kwargs) + + ad.open() + + return ad + +class TestADProtocol(unittest.TestCase): + @staticmethod + def getTimeout(): + return .2 + + def setUp(self): + self.adp = AlarmDecoderProtocol() + self.ad = alarmdecoder.AlarmDecoder(self.adp) + self.tr = proto_helpers.StringTransport() + self.adp.makeConnection(self.tr) + + openmock = mock.MagicMock() + self.adp.on_open += openmock + + self.ad.open() + + openmock.assert_called_once_with(self.adp) + + self.assertEqual(self.tr.value(), 'C\rV\r') + self.tr.clear() + + self.adp.dataReceived('!CONFIG>ADDRESS=18&CONFIGBITS=ff00&LRR=N&EXP=NNNNN&REL=NNNN&MASK=ffffffff&DEDUPLICATE=N\r\n') + self.adp.dataReceived('!VER:ffffffff,V2.2a.6,TX;RX;SM;VZ;RF;ZX;RE;AU;3X;CG;DD;MF;LR;KE;MK;CB\r\n') + + @mock.patch('alarmdecoder.AlarmDecoder.open') + @mock.patch('twisted.internet.serialport.SerialPort') + def test_adtwist(self, spmock, openmock): + dev = 'somedev' + origkwargs = { 'baudrate': 123 } + ret = adtwist(dev, **origkwargs) + + self.assertIsInstance(ret, alarmdecoder.AlarmDecoder) + + args, kwargs = spmock.call_args + self.assertIsInstance(args[0], AlarmDecoderProtocol) + self.assertEqual(args[1], dev) + self.assertEqual(kwargs, origkwargs) + + openmock.assert_called_once() + + def test_close(self): + closemock = mock.MagicMock() + self.adp.on_close += closemock + + self.ad.close() + + closemock.assert_called_once_with(self.adp) + + def test_adprot(self): + alarmfun = mock.MagicMock() + + ad = self.ad + adp = self.adp + + #print `self.tr.value()` + self.assertEqual(ad.version_number, 'V2.2a.6') + + msgmock = mock.MagicMock() + + ad.on_message += msgmock + + data = '[0000000111000100----],006,[f7000007100600202a020000000000],"FIRE 06 "\r\n' + msgdata = data[:-2] + if False: # pragma: no cover + # This'd be nice, but the Message object doesn't have a working equality operator + from alarmdecoder.messages import Message + dmsg = Message(msgdata) + + readmock = mock.MagicMock() + readmockad = mock.MagicMock() + adp.on_read += readmock + ad.on_read += readmockad + adp.dataReceived(data) + readmock.assert_called_once_with(adp, data=msgdata) + readmockad.assert_called_once_with(ad, data=msgdata) + + msgmock.assert_called_once() + msg = msgmock.call_args[1]['message'] + self.assertTrue(msg.ac_power) + self.assertEqual(msg.text, 'FIRE 06 ') + msgmock.reset_mock() + + adp.dataReceived('[0000000110000000----],010,[f70000071010000028020000000000],"FAULT 10 "\r\n') + + msgmock.assert_called_once() + msg = msgmock.call_args[1]['message'] + self.assertEqual(msg.text, 'FAULT 10 ') + + writemock = mock.MagicMock() + + adp.on_write += writemock + + ad.send('5') + + self.assertEqual(self.tr.value(), '5') + writemock.assert_called_once_with(adp, data='5') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6e7d81a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +alarmdecoder +coverage +mock +twisted diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ba62b18 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +setup(name='adtwist', + version='1.0.0', + description='Allow use of alarmdecoded with twisted', + author='John-Mark Gurney', + author_email='jmg@funkthat.com', + url='https://github.com/jmgurney/adtwist', + py_modules=['adtwist'], + install_requires=[ + 'alarmdecoder', + 'mock', + 'twisted', + 'pyserial', + ], + tests_require=[ + 'coverage', + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Framework :: Twisted', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + #'Operating System :: MacOS :: MacOS X', + #'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Programming Language :: Python', + 'Topic :: Home Automation', + 'Topic :: Security', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + )