Library and CLI tool for working with ZKTeco ZKAccess C3-100/200/400 controllers
$# PyZKAccess
PyZKAccess is a library for working with ZKTeco ZKAccess C3-100/200/400 access controllers.
This package allows you to work with ZKTeco ZKAccess C3-100/200/400 access controllers.
This library is Windows-only, but it can be used on *nix systems with Wine. It built on top of the ZKTeco PULL SDK.
This package, once installed, may be used as library or command-line interface. It’s also distributed as a portable Windows executable, created by PyInstaller with built-in 32-bit Python interpreter.
Here are the controllers we work with:
C3-100 | C3-200 | C3-400 |
---|---|---|
NOTE: the default factory IP address of C3 devices is 192.168.1.201
.
The quickest way is using our portable executable.
Download it, open up a terminal window and run it to setup the environment:
pyzkaccess setup
It will make a quick compatibility check of your system and suggest you to install PULL SDK from the official ZKTeco site.
All set! Now let’s find out what ZKAccess devices are available on the local network:
$ pyzkaccess search_devices
+---------------+-------------------+--------+---------------------+--------------------------+
| ip | mac | model | serial_number | version |
+---------------+-------------------+--------+---------------------+--------------------------+
| 192.168.1.201 | 00:17:61:C3:BA:55 | C3-400 | DGD9190010050345332 | AC Ver 4.3.4 Apr 28 2017 |
+---------------+-------------------+--------+---------------------+--------------------------+
Let’s enumerate all users registered on a device:
$ pyzkaccess connect 192.168.1.201 table User
+----------+------------+-------+----------+-----+------------+-----------------+
| card | end_time | group | password | pin | start_time | super_authorize |
+----------+------------+-------+----------+-----+------------+-----------------+
| 16268812 | 2020-12-01 | 2 | 123456 | 1 | 2020-06-01 | 1 |
| 16268813 | | 3 | 123451 | 3 | | 0 |
+----------+------------+-------+----------+-----+------------+-----------------+
For more usage examples, please see the usage section.
The quickest way is to use portable pyzkaccess.exe.
It contains the full pyzkaccess
package with built-in Python and necessary libraries.
Download it and run the pyzkaccess.exe setup
to install PULL SDK.
Finally, check the installation by running the pyzkaccess.exe search_devices
command.
pyzkaccess
requires the 32-bit Python version for Windows.
Make sure you ticked the checkbox “Add executable to PATH variable” in Python installer.
Open up the command window and install this library from pip:
pip install pyzkaccess
Next, install a current version of PULL SDK – just run the pyzkaccess setup
command and follow the instructions
As alternative, you can install the SDK manually:
- download the PULL SDK from ZKTeco software downloads
- extract the archive
- copy
pl*.dll
files to the 32-bit system directory. Usually it’sC:\Windows\SysWOW64
(orC:\Windows\System32
on older 32-bit Windows versions).
Finally, check the installation by running the pyzkaccess search_devices
command.
First, you will need to set up 32-bit Wine environment (commands below are relevant for Debian/Ubuntu):
apt-get install wine wine32
The quickest way is to use portable pyzkaccess.exe.
It contains the full pyzkaccess
package with built-in Python and necessary libraries.
Download it and run the wine pyzkaccess.exe setup
to install PULL SDK.
Finally, check the installation by running the wine pyzkaccess.exe search_devices
command.
This package requires the 32-bit Python version for Windows.
Open up the terminal and install Python:
wine python-3.8.5.exe`
Make sure you ticked the checkbox “Add executable to PATH variable in Python installer.”
Next, install this library from pip:
pip install pyzkaccess
Next, install a current version of PULL SDK – just run the pyzkaccess setup
command and follow the instructions
As alternative, you can install the SDK manually:
- download the PULL SDK manually from ZKTeco software downloads
- extract the archive
- copy
pl*.dll
files to the 32-bit system directory. Usually it’s/home/user/.wine/drive_c/windows/SysWOW64
(or/home/user/.wine/drive_c/windows/system32
if the 32-bit Windows version selected in config).Sometimes Wine doesn’t see SDK *.dll files has been copied. In this case you may run
wine explorer.exe
and copy them in its window
Command line interface consists of commands and subcommands, grouped in tree-like hierarchy.
The command tree is as follows:
setup
search_devices
change_ip
connect <IP>
├ cancel_alarm
├ download_file <file_name>
├ upload_file <file_name>
├ read_raw <table_name>
├ write_raw <table_name>
├ restart
├ table <table_name>
│ ├ where --field=<value>, ...
│ │ └ ...recursive...
│ ├ unread
│ ├ upsert
│ ├ delete
│ ├ delete_all
│ └ count
├ aux_inputs
│ ├ select <index|range>
│ │ └ ...recursive...
│ └ events
│ └ *events* commands...
├ events
│ ├ only --field=<value>, ...
│ │ └...recursive...
│ └ poll
├ parameters
│ ├ list
│ └ set --parameter=<value>, ...
├ readers
│ ├ select <index|range>
│ │ └ ...recursive...
│ └ events
│ └ *events* commands...
├ relays
│ ├ select <index|range>
│ │ └ ...recursive...
│ └ switch_on
└ doors
├ select <index|range>
│ └ ...recursive...
├ aux_inputs <index|range>
│ └ *aux_input* commands...
├ events
│ └ *events* commands...
├ parameters
│ └ *parameters* commands...
├ readers
│ └ *readers* commands...
└ relays
└ *relays* commands...
Commands and parameters in command line are followed by each other according to the tree structure.
For example, to poll the events of the first reader of the first door:
$ pyzkaccess connect 192.168.0.201 doors select 1 readers select 1 events poll
Every command has its own help message. To get help for a command, just append --help
at the end.
For example, the top-level help:
$ pyzkaccess --help
Or for a subcommand:
$ pyzkaccess connect 192.168.1.201 doors select 1 readers --help
The connect
command makes a connection to the device. It requires the IP address of the device as the first argument.
pyzkaccess connect 192.168.1.201
However you might need to pass the whole connection string, for example, if a device requires the password. But this
should not get revealed in the command line arguments by security reasons.
In this case, you can pass the options to the connect
command via the environment variables:
PYZKACCESS_CONNECT_IP
or PYZKACCESS_CONNECT_CONNSTR
– IPv4 address or the whole connection string like this
PYZKACCESS_CONNECT_CONNSTR="protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=123456"
PYZKACCESS_CONNECT_MODEL
– device model. Possible values are: ZK100
, ZK200
, ZK400
Let’s look how to use this package in your code.
from pyzkaccess import ZKAccess
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
zk = ZKAccess(connstr=connstr)
print('Device SN:', zk.parameters.serial_number, 'IP:', zk.parameters.ip_address)
# Turn on relays in "lock" group for 5 seconds
zk.relays.lock.switch_on(5)
# Wait for any card will appear on reader of Door 1
card = None
while not card:
for door1_event in zk.doors[0].events.poll(timeout=60):
print(door1_event)
if door1_event.card and door1_event.card != '0':
print('Got card #', door1_event.card)
card = door1_event.card
# Switch on both relays on door 1
zk.doors[0].relays.switch_on(5)
# After that restart a device
zk.restart()
zk.disconnect()
from pyzkaccess import ZKAccess
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
with ZKAccess(connstr=connstr) as zk:
print(zk.parameters.ip_address)
from pyzkaccess import ZKAccess
found = ZKAccess.search_devices('192.168.1.255')
print(len(found), 'devices found')
if found:
# Pick the first device
device = found[0]
with ZKAccess(device=device) as zk:
print(zk.parameters.ip_address)
Default device model is ZK400
(C3-400).
from pyzkaccess import ZKAccess, ZK200
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
with ZKAccess(connstr=connstr, device_model=ZK200) as zk:
print(zk.parameters.ip_address)
from pyzkaccess import ZKAccess
from datetime import datetime
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
with ZKAccess(connstr=connstr) as zk:
zk.parameters.datetime = datetime.now()
from pyzkaccess import ZKAccess
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
with ZKAccess(connstr=connstr) as zk:
zk.parameters.gateway_ip_address = '172.31.255.254'
zk.parameters.netmask = '255.240.0.0'
zk.parameters.ip_address = '172.17.10.2'
The main operation we can do with a relay is to switch on it for a given count of seconds (0..255). Relay number corresponds to its number on board starting from aux relay group.
Relays can be addressed in different ways:
zk.relays.switch_on(5) # All relays
zk.relays[0].switch_on(5) # By index
zk.relays[1:3].switch_on(5) # By range
zk.doors[0].relays[0].switch_on(5) # By number of door which it belongs to
zk.relays.aux.switch_on(5) # By group
zk.relays.aux[1].switch_on(5) # By index in group
zk.relays.by_mask([1, 0, 1, 0, 0, 0, 0, 0]).switch_on(5) # By mask
The main operation we can do with a reader is to read its events. Number of reader is denoted on the board. Readers can be addressed in different ways:
zk.readers.events.refresh() # All readers
zk.readers[0].events.refresh() # By index
zk.readers[1:3].events.refresh() # By range
zk.readers[1:3].events.poll() # Await events for readers 2 and 3
zk.doors[0].reader.events.refresh() # By number of door which it belongs to
The main operation for aux input is to read events. The number of aux input is also denoted on the board. Aux inputs can be addressed in different ways:
zk.aux_inputs.events.refresh() # All aux inputs
zk.aux_inputs[0].events.refresh() # By index
zk.aux_inputs[1:3].events.refresh() # By range
zk.aux_inputs[1:3].events.poll() # Await events for aux inputs 2 and 3
zk.doors[0].aux_input.events.refresh() # By number of door which it belongs to
Events are the way to monitor the device in real time. It stores an events log in its RAM, keeping only the last 30 records. Events start to register just after making a connection to a device.
To get events without losses, you better to call the EventLog.refresh()
method periodically depending on the pace
the new events are expected to occur. It requests a device for unread events and stores them in the EventLog object.
There is also a convenience method EventLog.poll()
that peridically calls refresh()
and returns new events if any
(or if timeout is reached).
You can make a query to events related to a certain object (reader, door, aux input) or to all events on a device. Under the hood it just applies a filter to the event log.
zk.events # Access to event log
zk.events.refresh() # Get the unread events from a device
zk.events.poll() # Wait until any event will occur (or timeout will be reached)
zk.door.events # Event log for all doors (exluding auto open door by time, for instance)
zk.aux_inputs.events # Event log related to aux inputs only
zk.readers.events # Event log related to readers only
#
# More complex examples
#
# Wait until a some event will occur on Door 1 reader
zk.door[0].reader.events.poll()
# Wait until unregistered card (event_type==27) with given number will appear on Door 1 reader. Or exit by timeout.
zk.door[0].reader.events.only(card='123456', event_type=27).poll()
# Take all records from log with given card which was occur after 2010-10-11 14:28:04
zk.events.only(card='123456').after_time(datetime(2010, 10, 11, 14, 28, 4))
Device parameters are the way to manipulate the device settings. There are two groups of parameters on the device.
The first group is device parameters, which are related to the device itself. They are:
Name | Type | Flags |
---|---|---|
serial_number | str | read-only |
lock_count | int | read-only |
reader_count | int | read-only |
aux_in_count | int | read-only |
aux_out_count | int | read-only |
communication_password | str | |
ip_address | str | |
netmask | str | |
gateway_ip_address | str | |
rs232_baud_rate | int | |
watchdog_enabled | bool | |
door4_to_door2 | bool | |
backup_hour | int | |
reboot | bool | write-only |
reader_direction | str | |
display_daylight_saving | bool | |
enable_daylight_saving | bool | |
daylight_saving_mode | int | |
fingerprint_version | int | read-only |
anti_passback_rule | int | |
interlock | int | |
spring_daylight_time_mode1 | DaylightSavingMomentMode1 | |
fall_daylight_time_mode1 | DaylightSavingMomentMode1 | |
spring_daylight_time_mode2 | DaylightSavingMomentMode2 | |
fall_daylight_time_mode2 | DaylightSavingMomentMode2 | |
datetime | datetime |
The following code show how to get or set a parameter value, and how to get a description about each one:
from pyzkaccess import ZKAccess
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
zk = ZKAccess(connstr=connstr)
print(zk.parameters.ip_address) # Get a value
zk.parameters.ip_address = "192.168.1.2" # Set a value
print(zk.parameters.ip_address.__doc__) # Get description
The second group is door parameters, which are related to the door settings. They are:
Name | Type | Flags |
---|---|---|
duress_password | str | |
emergency_password | str | |
lock_on_close | bool | |
sensor_type | SensorType | |
lock_driver_time | int | |
magnet_alarm_duration | int | |
verify_mode | VerifyMode | |
multi_card_open | bool | |
first_card_open | bool | |
active_time_tz | int | |
open_time_tz | int | |
punch_interval | int | |
cancel_open_day | int |
The following code show how to get or set a door parameter value, and how to get a description about each one:
from pyzkaccess import ZKAccess
connstr = 'protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd='
zk = ZKAccess(connstr=connstr)
print(zk.doors[0].parameters.verify_mode) # Get a parameter value of the first door
zk.doors[0].parameters.verify_mode = 1 # Set a parameter value of the first door
print(zk.doors[0].parameters.verify_mode.__doc__) # Get a parameter description
ZK devices have a database inside stored in the persistent memory. This database contains the read-only tables (transaction history, input/output events) and writable tables (users, ACLs, timezone settings). You can make a query to one table at a time, no joins or something similar are supported.
pyzkaccess
provides the interface for making queries to these tables. Every table is presented as a model class
with the same fields. A QuerySet class helps build a query and iterate over the results. Basically, for those who
worked with ORM packages (Object-related mapping), such approach could be familiar.
The table data is stored on device in string format, sometimes additionally encoded. However the pyzkaccess
knows what the actual type every field should have and how to encode/decode it. Every model, that “wraps” a particular
device table, provides (and accepts when you write data to device) the decoded value in right Python type.
For example, the transaction.Time_second
field contains an integer with encoded datetime, but appropriate
Transaction.time
exposes the datetime
objects.
The following sections describe how to work with table records, make queries and update the data.
Models are the following:
User
- device users, the card number information tableUserAuthorize
- user privilege listHoliday
- holiday settingsTimezone
- (very detailed) timezone settingsTransaction
- access control transaction historyFirstCard
- first card settingsMultiCard
- multi-card settingsInOutFun
- input/output function settingsTemplateV10
- SDK docs doesn’t give a clue what this table forModel class represents whole data table, whereas a model object is a particular record in this table.
In order to create an object, instantiate it with parameters.
from pyzkaccess.tables import User
my_user = User(card='123456', pin='123', password='555', super_authorize=True)
# ...code...
To get whole data as dict, use .dict
property:
from pyzkaccess.tables import User
my_user = User(card='123456', pin='123', password='555', super_authorize=True)
print(my_user.dict)
# {'card': '123456',
# 'pin': '123',
# 'password': '555',
# 'group': None,
# 'start_time': None,
# 'end_time': None,
# 'super_authorize': True}
Sometimes you may want to get the raw string data (i.e. how it stores on device) that are sent to a device on saving
an object. Use raw_data
property for that:
from pyzkaccess.tables import User
my_user = User(card='123456', pin='123', password='555', super_authorize=True)
print(my_user.raw_data)
# {'SuperAuthorize': '1', 'Password': '555', 'Pin': '123', 'CardNo': '123456'}
The QuerySet
class is intended to build a query to a data table, execute it and obtain results. QuerySet
supports
filtering (only equality is supported), limiting to unread records, to the certain fields, slicing the results.
All query restrictions and features are driven by PULL SDK and are the results of its limitations.
QuerySet
uses lazy loading, which means it will not make a real request to device until you actually begin to
read its results.
To make a query, call the zk.table(model)
method. The returned object will be an empty QuerySet
bound with a
particular model class.
If you would read such queryset as-is, you’ll simply get all records from the table:
from pyzkaccess import ZKAccess
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
records = zk.table('User')
for record in records:
print(record) # prints all users from the table
To apply filters, use the where()
method.
from pyzkaccess import ZKAccess
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
records = zk.table('User').where(group='4', super_authorize=True)
for record in records:
print(record) # prints all superusers in group="4"
There are also available only_fields()
, unread()
methods. count()
method returns the total records count
in a table without considering all filters. There are also available bulk write operations like upsert()
, delete()
,
delete_all()
(see below).
Besides the iteration, you can use slicing, indexing, len()
and bool()
functions.
records = zk.table('Model').condition(parameters).condition(parameters)
print("Result size:", len(records))
if records:
print("Query result is not empty")
print("First record:", records[0])
if len(records) > 3:
print("First 3 records:", records[:3])
QuerySet.upsert()
updates (if found) or inserts a record or records (if not).
from pyzkaccess import ZKAccess
from pyzkaccess.tables import User
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
my_user = User(card='123456', pin='123', password='555', super_authorize=True)
zk.table(User).upsert(my_user)
upsert()
function can receive the data in different formats:
zk.table(User).upsert(User(card='123456', pin='123'))
zk.table(User).upsert({'card': '123456', 'pin': '123'})
zk.table(User).upsert([User(card='123456', pin='123'), User(...)])
zk.table(User).upsert([{'card': '123456', 'pin': '123'}, {...}])
The save()
method writes a record to the device. You may need also to pass a ZKAccess
object by calling the
with_zk()
method:
from pyzkaccess import ZKAccess
from pyzkaccess.tables import User
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
my_user = User(card='123456', pin='123', password='555', super_authorize=True).with_zk(zk)
my_user.save()
from pyzkaccess import ZKAccess
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
for record in zk.table('User'):
record.group = '3'
record.save()
QuerySet.delete()
deletes a record or records from a table.
from pyzkaccess import ZKAccess
from pyzkaccess.tables import User
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
my_user = User(card='123456', pin='123', password='555', super_authorize=True)
zk.table(User).delete(my_user)
delete()
function can receive the data in different formats:
zk.table(User).delete(User(card='123456', pin='123'))
zk.table(User).delete({'card': '123456', 'pin': '123'})
zk.table(User).delete([User(card='123456', pin='123'), User(...)])
zk.table(User).delete([{'card': '123456', 'pin': '123'}, {...}])
from pyzkaccess import ZKAccess
from pyzkaccess.tables import User
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
first_user = zk.table(User)[0]
first_user.delete()
You can also call the delete()
method on a bare model object, you also need to pass a ZKAccess
object by
calling the with_zk()
method:
from pyzkaccess import ZKAccess
from pyzkaccess.tables import User
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
my_user = User(card='123456', pin='123', password='555', super_authorize=True).with_zk(zk)
my_user.delete()
This method deletes records that matched to a QuerySet
object.
The following example deletes all transactions related to card “123456”:
zk.table('User').where(card='123456').delete_all()
QuerySet.where()
method applies a filter to a query. Only equality operation is supported (due to PULL SDK
restrictions). Repeated calls to where()
are AND’ed. Several fields in one call are also AND’ed. Repeated appearance
of the same field in different calls will replace the previous value.
The following examples will produce identical queries:
qs = zk.table('User').where(group='4', super_authorize=True)
qs = zk.table('User').where(group='4').where(super_authorize=True)
qs = zk.table('User').where(group='111').where(group='4', super_authorize=True)
Roughly speaking, the result will be the group == '4' AND super_authorize == True
.
The ZK device stores a pointer to the last read record in each table. Once a table is read, the pointer is moved to the last record. We use this to track the unread records.
QuerySet.unread()
method returns a new QuerySet
containing only the records that has not been read yet since
the last query.
only_fields()
method returns a new QuerySet
containing the records with only selected fields.
Other fields will be set to None. Repeated only_fields()
call appends the fields selected in the previous
only_fields()
calls.
The following examples will produce identical queries:
from pyzkaccess.tables import User
qs = zk.table('User').only_fields('pin', 'password')
qs = zk.table('User').only_fields(User.pin, User.password)
qs = zk.table('User').only_fields('pin').only_fields('password')
qs = zk.table('User').only_fields(User.pin).only_fields(User.password)
Every record in the result will have only pin
and password
values, other fields will remain None.
count()
method returns the total records count in table, ignoring all filters. This method is backed by a special
PULL SDK call.
If you want to know count of records contained in QuerySet
, use len(qs)
.
from pyzkaccess import ZKAccess
zk = ZKAccess('protocol=TCP,ipaddress=192.168.1.201,port=4370,timeout=4000,passwd=')
qs = zk.table('User').where(group='4').where(super_authorize=True)
print('Superusers in group 4:', list(qs), 'Total users count:', qs.count())
QuerySet
supports iterator protocol, slicing, indexing:
# Print superusers
superusers = zk.table('User').where(super_authorize=True)
print('Superusers are:')
for i in superusers:
print('Card:', i.card, '; Group:', i.group, '; From/to:', i.start_time, '/', i.end_time)
# Print cards from first 3 unread transactions for a given type and door
txns = zk.table('Transaction').where(event_type=0, door=1).unread()[:3]
cards = ', '.join(txn.card for txn in txns)
print('First card numbers:', cards)
# Print the first transaction
qs = zk.table('Transaction')
if qs.count() > 0:
print('The first transaction:', qs[0])
else:
print('Transaction table is empty!')
Igor Derkach, gosha753951@gmail.com