├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── access_controller.png ├── examples ├── add_user.py ├── open_door.py ├── remove_user.py ├── webserver.py └── windows_webservice.py ├── rfid.py ├── rfid_card_number_explanation.png ├── setup.cfg ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ### Added 6 | 7 | * Tests now contain examples from real responses from the RFID controller 8 | 9 | ### Changed 10 | 11 | * Simplified code for checking for expected response from the RFID controller 12 | * Formatted examples with isort and black 13 | 14 | ## 0.1.2 15 | 16 | Released on December 20, 2020 17 | 18 | ### Changed 19 | 20 | * More Python 3 fixes (fixed TypeError) 21 | 22 | ## 0.1.1 23 | 24 | Released on December 20, 2020 25 | 26 | ### Added 27 | 28 | * More Python 3 fixes 29 | * More tests (for add_user, remove_user, open_door) 30 | 31 | ## 0.1.0 32 | 33 | Released on December 20, 2020 34 | 35 | ### Added 36 | 37 | * Python 3 support (#1) 38 | * Basic tests 39 | 40 | ### Changed 41 | 42 | * Minor refactoring for easier testing 43 | * Fixed setup.py issues preventing successful packaging 44 | * PEP8 fixes + code formatted with black and isort 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Paul Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE rfid.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Chinese RFID Access Control Library 2 | ======================== 3 | 4 | This library allows python to control one of the most common RFID Access Control Systems sold in China. Now you can integrate an access 5 | control system with your software to do things like remove an user when they haven't paid their bill. 6 | 7 | The goal of this project is to provide the ability to automate an inexpensive, out-of-the-box RFID Access Control solution. This is 8 | especially made for businesses that rely on access control + monthly billing (hackerspaces, makerspaces, and gyms). 9 | 10 | Main Features 11 | ----- 12 | 13 | - Programmatically add and remove users to/from the access control system 14 | - Programmatically trigger the relay to open the door 15 | - Convert the 10-digit format RFID numbers to comma format or vice versa 16 | 17 | Hardware Requirement 18 | ----- 19 | This library currently only works with a single type of controller which goes by a wide variety of model numbers. The controller can 20 | be found by searching for "TCP access control" on Ebay, Aliexpress, and Amazon. It costs around $30-85 (depending on the number of 21 | doors). You can know which one to buy by looking for one that looks like this: 22 | 23 | ![alt tag](https://raw.githubusercontent.com/pawl/Chinese-RFID-Access-Control-Library/master/access_controller.png) 24 | 25 | One of the awesome things this controller has is a web interface. You can also add users, remove users, view logs, and change settings 26 | manually through that interface. Pictures of the interface are available here: http://imgur.com/a/Mw04Y 27 | 28 | RFID Card Number Explanation 29 | ----- 30 | ![alt tag](https://raw.githubusercontent.com/pawl/Chinese-RFID-Access-Control-Library/master/rfid_card_number_explanation.png) 31 | 32 | There are usually two sets of numbers on the 125kHz EM4100 RFID cards. Key fobs usually only have a single 10-digit number. 33 | 34 | The number on the right, "comma format": 35 | * The access controller's web interface only shows this number. 36 | * According to the Weigand 26 spec, this is the badge number in this format: ```<8 bit facility code as an integer>, <16 bit ID number as an integer>``` 37 | 38 | The number on the left, "10-digit format": 39 | * According to the Weigand 26 spec, this is the last 24 bits of data from the card as an integer. The last 24 bits include both the 8 bit facility code and the 16 bit ID number. 40 | * Since there are 24 bits, only 10-digit IDs within a range of 0 to 16,777,215 are possible. 41 | 42 | My usage example below shows an example of a function which converts the 10-digit format to the comma format, and vice versa. 43 | 44 | Usage 45 | ----- 46 | Install: 47 | 48 | pip install Chinese-RFID-Access-Control-Library 49 | 50 | Add user (using 10-digit format RFID number): 51 | 52 | from rfid import ten_digit_to_comma_format, RFIDClient 53 | 54 | ip_address = '192.168.1.20' # IP address of the controller 55 | controller_serial = 123106461 # serial number written on the controller 56 | client = RFIDClient(ip_address, controller_serial) 57 | 58 | badge = ten_digit_to_comma_format(11111111) # badge number needs to be converted to "comma format" 59 | 60 | client.add_user(badge, [1, 2]) # add privileges for door 1 & 2 61 | 62 | Remove user (using 10-digit format RFID number): 63 | 64 | from rfid import ten_digit_to_comma_format, RFIDClient 65 | 66 | ip_address = '192.168.1.20' # IP address of the controller 67 | controller_serial = 123106461 # serial number written on the controller 68 | client = RFIDClient(ip_address, controller_serial) 69 | 70 | badge = ten_digit_to_comma_format(11111111) # badge number needs to be converted to "comma format" 71 | 72 | client.remove_user(badge) 73 | 74 | Open door #1: 75 | 76 | from rfid import RFIDClient 77 | 78 | ip_address = '192.168.1.20' 79 | controller_serial = 123106461 80 | client = RFIDClient(ip_address, controller_serial) 81 | 82 | client.open_door(1) 83 | 84 | Running Tests 85 | ----- 86 | 87 | python setup.py test 88 | 89 | TODO 90 | ----- 91 | - Adding a name to an user without the web interface doesn't seem to be possible. Figure out a way to do this? (It might not be possible to do it without doing something hacky with the web interface.) 92 | - The controller also stores the user's 2-factor pin for when the keypad is enabled. Need to add an optional parameter to add_user for a pin. 93 | - Add a get_users method to RFIDClient that outputs a list of all the users currently in the controller. 94 | - Add a get_logs method to RFIDClient which outputs the card swipe logs. 95 | 96 | Special Thanks 97 | ----- 98 | - Thanks to Brooks Scharff for figuring out the cool stuff that this access controller could do and keeping me interested in the project. 99 | - Thanks to Dallas Makerspace for letting me implement and test it at their facility. 100 | - Thanks to Mike Metzger for his work on starting to reverse engineer Dallas Makerspace's first access control system and documenting it to show me how to do it. https://dallasmakerspace.org/wiki/ReverseEngineeringRFIDReader 101 | -------------------------------------------------------------------------------- /access_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawl/Chinese-RFID-Access-Control-Library/e5b3a0b583f0edddcefd6dd239cc1fd21e6bc0e5/access_controller.png -------------------------------------------------------------------------------- /examples/add_user.py: -------------------------------------------------------------------------------- 1 | from rfid import RFIDClient, ten_digit_to_comma_format 2 | 3 | ip_address = "192.168.1.20" 4 | controller_serial = 123106461 5 | client = RFIDClient(ip_address, controller_serial) 6 | 7 | # badge number needs to be in "comma format" 8 | badge = ten_digit_to_comma_format(11111111) 9 | 10 | client.add_user(badge, [1]) # add badge to door 1 11 | client.add_user(badge, [1, 2]) # add badge to door 1 and 2 12 | client.add_user(badge, [1, 2, 3]) # add badge to door 1, 2, and 3 13 | client.add_user(badge, [1, 2, 3, 4]) # add badge to door 1, 2, 3, and 4 14 | -------------------------------------------------------------------------------- /examples/open_door.py: -------------------------------------------------------------------------------- 1 | from rfid import RFIDClient 2 | 3 | ip_address = "192.168.1.20" 4 | controller_serial = 123106461 5 | client = RFIDClient(ip_address, controller_serial) 6 | 7 | client.open_door(1) 8 | -------------------------------------------------------------------------------- /examples/remove_user.py: -------------------------------------------------------------------------------- 1 | from rfid import RFIDClient, ten_digit_to_comma_format 2 | 3 | ip_address = "192.168.1.20" 4 | controller_serial = 123106461 5 | client = RFIDClient(ip_address, controller_serial) 6 | 7 | # badge number needs to be in "comma format" 8 | badge = ten_digit_to_comma_format(11111111) 9 | 10 | client.remove_user(badge) 11 | -------------------------------------------------------------------------------- /examples/webserver.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cherrypy 4 | 5 | from rfid import RFIDClient, ten_digit_to_comma_format 6 | 7 | ip_address = "192.168.1.20" 8 | controller_serial = 123106461 9 | client = RFIDClient(ip_address, controller_serial) 10 | 11 | 12 | class RootServer: 13 | @cherrypy.expose 14 | def index(self, apiKey=None, action=None, badge=None): 15 | if apiKey == "secret": 16 | if badge: 17 | badge = ten_digit_to_comma_format(int(badge)) 18 | if action == "remove": 19 | try: 20 | client.remove_user(badge) 21 | return "User Removed Successfully" 22 | except: 23 | return "Failed To Remove User" 24 | elif action == "add": 25 | try: 26 | client.add_user(badge, [1, 2]) 27 | return "User Added Successfully" 28 | except: 29 | return "Failed To Add User" 30 | else: 31 | return "must specify an action" 32 | else: 33 | return "no badge number entered" 34 | else: 35 | return "" # return nothing when no API key is entered 36 | 37 | 38 | if __name__ == "__main__": 39 | server_config = { 40 | "global": { 41 | "server.socket_host": "0.0.0.0", 42 | "server.socket_port": 443, 43 | "server.ssl_module": "pyopenssl", 44 | "server.ssl_certificate": "server.crt", 45 | "server.ssl_private_key": "server.key", 46 | "log.access_file": os.path.join("access.log"), 47 | } 48 | } 49 | 50 | cherrypy.quickstart(RootServer(), "/accessControlApi", server_config) 51 | -------------------------------------------------------------------------------- /examples/windows_webservice.py: -------------------------------------------------------------------------------- 1 | """ 2 | The most basic (working) CherryPy 3.1 Windows service possible. 3 | Requires Mark Hammond's pywin32 package. 4 | """ 5 | 6 | import os 7 | 8 | import cherrypy 9 | import win32service 10 | import win32serviceutil 11 | 12 | from rfid import RFIDClient, ten_digit_to_comma_format 13 | 14 | ip_address = "192.168.1.20" 15 | controller_serial = 11111111 16 | client = RFIDClient(ip_address, controller_serial) 17 | 18 | 19 | class RootServer: 20 | @cherrypy.expose 21 | def index(self, apiKey=None, action=None, badge=None): 22 | if apiKey == "secret": 23 | if badge: 24 | badge = ten_digit_to_comma_format(int(badge)) 25 | if action == "remove": 26 | try: 27 | client.remove_user(badge) 28 | return "User Removed Successfully" 29 | except: 30 | return "Failed To Remove User" 31 | elif action == "add": 32 | try: 33 | client.add_user(badge, [1, 2]) 34 | return "User Added Successfully" 35 | except: 36 | return "Failed To Add User" 37 | else: 38 | return "must specify an action" 39 | else: 40 | return "no badge number entered" 41 | else: 42 | return "" # return nothing when no API key is entered 43 | 44 | 45 | class MyService(win32serviceutil.ServiceFramework): 46 | """NT Service.""" 47 | 48 | _svc_name_ = "CherryPyService" 49 | _svc_display_name_ = "CherryPy Service" 50 | 51 | def SvcDoRun(self): 52 | cherrypy.tree.mount(RootServer(), "/accessControlApi") 53 | 54 | # in practice, you will want to specify a value for 55 | # log.error_file below or in your config file. If you 56 | # use a config file, be sure to use an absolute path to 57 | # it, as you can't be assured what path your service 58 | # will run in. 59 | cherrypy.config.update( 60 | { 61 | "global": { 62 | "server.socket_host": "0.0.0.0", 63 | "server.socket_port": 443, 64 | "server.ssl_module": "pyopenssl", 65 | "server.ssl_certificate": "server.crt", 66 | "server.ssl_private_key": "server.key", 67 | "log.access_file": os.path.join("access.log"), 68 | } 69 | } 70 | ) 71 | 72 | cherrypy.engine.start() 73 | cherrypy.engine.block() 74 | 75 | def SvcStop(self): 76 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 77 | cherrypy.engine.exit() 78 | 79 | self.ReportServiceStatus(win32service.SERVICE_STOPPED) 80 | # very important for use with py2exe 81 | # otherwise the Service Controller never knows that it is stopped ! 82 | 83 | 84 | if __name__ == "__main__": 85 | win32serviceutil.HandleCommandLine(MyService) 86 | -------------------------------------------------------------------------------- /rfid.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import socket 3 | import struct 4 | import sys 5 | 6 | 7 | def ten_digit_to_comma_format(badge): 8 | """Returns the comma-format RFID number (without the comma) from the 9 | 10-digit RFID number. 10 | 11 | Explanation: 12 | *On an EM4100/4001 spec RFID card, there will generally be two sets of 13 | numbers like this: 0015362878 234,27454 14 | *The part of the number before the comma represents the first hex byte of 15 | the "10 digit" number, and the second part is the last 2 hex bytes of the 16 | "10 digit" card number. 17 | *15362878 = EA6B3E 18 | *Splitting EA and 6B3E and converting them to decimal numbers will give 19 | you 234 and 27454 (the number with the comma on the card). 20 | *The comma is excluded in the return value because the controller does not 21 | need the comma. 22 | 23 | :param badge: 10-digit RFID card number, must be integer 24 | """ 25 | # only the last 8 digits are the ID 26 | # the 8 digits correspond to only 6 hex values, so the max is FFFFFF 27 | if badge > 16777215: 28 | raise Exception("Error: Invalid RFID Number") 29 | formatted_id = str("{0:x}".format(badge)).zfill(6) # converts to hex 30 | 31 | # splits the hex at first two and last 4, converts to dec, 32 | # then combines into string 33 | id_section_1 = str(int(formatted_id[:2], 16)).zfill(3) 34 | id_section_2 = str(int(formatted_id[-4:], 16)).zfill(5) 35 | return int(id_section_1 + id_section_2) 36 | 37 | 38 | def comma_format_to_ten_digit(badge): 39 | """Returns the 10-digit number from the comma-format RFID number (without 40 | the comma) 41 | 42 | Explanation: 43 | *On an EM4100/4001 spec RFID card, there will generally be two sets of 44 | numbers like this: 0015362878 234,27454 45 | *This function turns the number with the comma (but excluding the comma) 46 | into the 10-digit number which is generally next to it. 47 | *The part of the number before the comma represents the first hex byte of 48 | the "10 digit" number, and the second part is the last 2 hex bytes of the 49 | "10 digit" card number. 50 | **234 = EA 51 | **27454 = 6B3E 52 | **Combining EA and 6B3E and converting it to a decimal number will give you 53 | 15362878 (the first 10-digit number on the card). 54 | 55 | :param badge: comma-format RFID card number, must be integer with the comma 56 | removed 57 | """ 58 | # the 8 digits correspond to a set of two and four hex values, 59 | # so the max is the decimal version of FF and FFFF concatenated 60 | if badge > 25565535: 61 | raise Exception("Error: Invalid RFID Number") 62 | badge = str(badge).zfill(8) 63 | 64 | # splits dec at last 5 digits and everything except last 5, 65 | # converts each section to hex, then combines 66 | id_section_1 = "{0:x}".format(int(badge[:-5])).zfill(2) 67 | id_section_2 = "{0:x}".format(int(badge[-5:])).zfill(4) 68 | formatted_id = id_section_1 + id_section_2 69 | 70 | # convert combined hex string to int 71 | return int(formatted_id, 16) 72 | 73 | 74 | class RFIDClient(object): 75 | # part of the byte string replaced by the CRC, not required to be valid 76 | source_port = "0000" 77 | 78 | # these bytes form the packet that starts a transaction with the RFID controller 79 | start_transaction = ( 80 | b"\r\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 81 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 82 | ) 83 | 84 | def __init__(self, ip, serial): 85 | """ 86 | :param ip: IP address of the controller. 87 | :param serial: Serial number written on the controller, also 88 | "Device NO" on the web interface's configuration page. 89 | """ 90 | self.check_valid_ipv4_address(ip) 91 | 92 | if not isinstance(serial, int): 93 | raise TypeError("Serial must be set to an integer") 94 | 95 | # pack controller serial as little endian integer 96 | self.controller_serial = self.little_endian_hex(serial) 97 | self.s = self.connect(ip) 98 | 99 | @staticmethod 100 | def little_endian_hex(val): 101 | """Convert integer to little-endian hex string.""" 102 | endian = struct.pack(" 0: 146 | num1 = (num1 >> 1) ^ 40961 147 | else: 148 | num1 >>= 1 149 | code = num1 & 65535 # integer returned from CRC function 150 | 151 | # change hex string to list to support assignment 152 | data_list = list(data) 153 | 154 | # switch order to little endian and return unsigned short, then 155 | # replace characters in list with the CRC values 156 | endian = struct.pack("\x00\x00\x00\x01\x00\x00" 24 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x10\x00\x00=\x00\x00\x00\x01" 25 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x10\x00\x00<\x00\x00" 26 | b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x10\x00\x00;" 27 | b"\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00" 28 | b"\x00:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12" 29 | b"\x83\x00\x009\x00\x00\x00\x02\x01\xa8\xc0\x00\x00\x00\x00\x1a\x1d\xf1" 30 | b"\xba\x16\x10\x00\x008\x00\x00\x00X\xd6,\x00\x00\x00\x00\x00\x1a\x1dp\xac" 31 | b"\x10\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 32 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 33 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xff\xff\x00\x00\x00" 34 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x00\x00\x00" 35 | b"\xff\x8f\xff_\x13\x86\xff\xfb\xff?\xff\x0f\xff\x1d\x00.\x00\x00\x00\x00" 36 | b"\x00\x00\x00\x00\x00\x00\x00\x00" 37 | ) 38 | 39 | add_badge_resp_2 = b"#!\xee\xb1)\x00\x00\x00\x9dtV\x07\x00\x00\x00\x00\x01\x05\x02\x00" 40 | 41 | remove_user_resp = b'#!\xa9\x86"\x00\x00\x00\x9dtV\x07\x00\x00\x00\x00\x01\x05\x02\x00' 42 | 43 | 44 | class TestTenDigitToCommaFormat(unittest.TestCase): 45 | def test_happy_path(self): 46 | result = ten_digit_to_comma_format(2058018) 47 | self.assertEqual(result, TEST_BADGE) 48 | 49 | 50 | class TestCommaFormatToTenDigit(unittest.TestCase): 51 | def test_happy_path(self): 52 | result = comma_format_to_ten_digit(TEST_BADGE) 53 | expected_result = 2058018 54 | self.assertEqual(result, expected_result) 55 | 56 | 57 | class mock_socket(object): 58 | def __init__(self, responses): 59 | self.responses = responses 60 | 61 | def recv(self, size): 62 | return self.responses.pop() 63 | 64 | def send(self, msg): 65 | return len(msg) 66 | 67 | def close(self): 68 | return True 69 | 70 | 71 | class TestRFIDClient(unittest.TestCase): 72 | def test_invalid_ip(self): 73 | with self.assertRaises(TypeError): 74 | RFIDClient("blah") 75 | 76 | @patch("rfid.RFIDClient.connect") 77 | @patch("rfid.RFIDClient.check_valid_ipv4_address") 78 | def test_controller_serial(self, mock_connect, mock_check_valid_ipv4_address): 79 | rfid_client = RFIDClient(TEST_CONTROLLER_IP, TEST_CONTROLLER_SERIAL) 80 | expected_controller_serial_hex = "9d745607" 81 | self.assertEqual(rfid_client.controller_serial, expected_controller_serial_hex) 82 | 83 | def test_crc_16_ibm(self): 84 | test_controller_serial_hex = "9d745607" 85 | test_data = ( 86 | "2010" 87 | + RFIDClient.source_port 88 | + "2800000000000000" 89 | + test_controller_serial_hex 90 | + "00000200ffffffff" 91 | ) 92 | result = RFIDClient.crc_16_ibm(test_data) 93 | expected_result = ( 94 | b" \x10f\xf2(\x00\x00\x00\x00\x00\x00\x00\x9dtV\x07\x00\x00\x02" 95 | b"\x00\xff\xff\xff\xff" 96 | ) 97 | self.assertEqual(result, expected_result) 98 | 99 | @patch( 100 | "rfid.RFIDClient.connect", 101 | return_value=mock_socket([add_badge_resp_2, add_badge_resp_1]), 102 | ) 103 | @patch("rfid.RFIDClient.check_valid_ipv4_address") 104 | def test_add_user(self, mock_connect, mock_check_valid_ipv4_address): 105 | rfid_client = RFIDClient(TEST_CONTROLLER_IP, TEST_CONTROLLER_SERIAL) 106 | test_doors = [1, 2] 107 | rfid_client.add_user(TEST_BADGE, test_doors) 108 | 109 | @patch("rfid.RFIDClient.connect", return_value=mock_socket([remove_user_resp])) 110 | @patch("rfid.RFIDClient.check_valid_ipv4_address") 111 | def test_remove_user(self, mock_connect, mock_check_valid_ipv4_address): 112 | rfid_client = RFIDClient(TEST_CONTROLLER_IP, TEST_CONTROLLER_SERIAL) 113 | rfid_client.remove_user(TEST_BADGE) 114 | 115 | @patch("rfid.RFIDClient.connect", return_value=mock_socket([open_door_resp])) 116 | @patch("rfid.RFIDClient.check_valid_ipv4_address") 117 | def test_open_door(self, mock_connect, mock_check_valid_ipv4_address): 118 | rfid_client = RFIDClient(TEST_CONTROLLER_IP, TEST_CONTROLLER_SERIAL) 119 | rfid_client.open_door(1) 120 | --------------------------------------------------------------------------------