Library and CLI tool for working with ZKTeco ZKAccess C3-100/200/400 controllers
PyZKAccess is a library for working with ZKTeco ZKAccess C3-100/200/400 access controllers.
The ZKTeco PULL SDK is used as machinery. So the code is intended to be executed in Windows environment. *nix are also supported by Wine.
Here are the controllers we’re taking about:
C3-100 | C3-200 | C3-400 |
---|---|---|
NOTE: API pyzkaccess>=0.2
is incompatible with API pyzkaccess==0.1
.
Requirements:
Download a current version of PULL SDK and extract the archive: ZKTeco software downloads.
A downloaded archive contains 32-bit pl*.dll
DLL files and documentation.
First, you will need to set up 32-bit Wine environment (commands below are for Debian/Ubuntu):
apt-get install wine
Next, install the last Python version for Windows:
wine python-3.8.5.exe
Make sure you checked the box “Add executable to PATH variable”. Next, install the library:
wine pip install pyzkaccess
Finally, copy pl*.dll
files from SDK archive to system directory in Wine. Usually it is
/home/user/.wine/drive_c/windows/SysWOW64
(or/home/user/.wine/drive_c/windows/system32
on older Windows versions). Registration via the regsvr32
is not needed.
(Sometimes Wine doesn’t see dll files even if you have copied them right. In this case you may
run wine explorer.exe
and move these files using it)
Install the last Python version for Windows. Next, open command window and install library from pip:
pip install pyzkaccess
Finally, copy pl*.dll
files from SDK archive to system directory. Usually it is
C:\Windows\SysWOW64
(or C:\Windows\System32
on older Windows versions). Registration via the
regsvr32
is not needed.
The default factory ip of C3 devices is 192.168.1.201
.
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)
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. A relay can be accessed by 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 board. Readers can be accessed by 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
Like for a reader, the main operation for aux input is to read events. The number of aux input is also denoted on board. Aux inputs can be accessed by 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 accessible through .events
property. C3 controller keeps
maximum 30 last unread events. Events start to register just after making connection to a device.
Event log should be refreshed manually using refresh()
method. Due to restriction of
maximum 30 entries described above, you should call refresh()
periodically in order to avoid
losing new events.
Another way to obtain events is poll()
method which awaits new log entries by doing
periodical refresh and returns new events if any.
Event log is available in several places. For ZKAccess
object it keeps all events occured on
a device. Readers, doors, aut inputs also give access to events which are related to this reader.
Under the hood these properties use the same event list which keeps all device events, but
each one apply its own filter to this list.
zk.events # Event log with all events occured on a device
zk.events.refresh() # Get unread events from device
zk.events.poll() # Wait until any event will occur
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 an 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
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))
ZK devices have several tables in non-volatile data storage where a device keeps transaction history, input/output events, and where user can manage users, ACLs, local time settings.
pyzkaccess
provides interface to access, making queries and modify these tables. A table record
is presented as a python object with properties – a model. Query objects helps to build a query
and to iterate over the results. Anyway, if you ever worked with any of popular ORM
(Object-related mapping), such interface could be pretty familiar to you.
Device keeps all data in tables as strings, despite which type a particular field value has.
Model provides a convenient way to work with data depening on actual type. For example,
transaction.Time_second
field contains an integer with encoded datetime, but appropriate
Transaction.time
allows to work with that value using usual datetime
objects.
The following sections describe how to work with table records, make queries and update the data.
pyzkaccess
contains pre-defined models that represent built-in tables in ZK devices.
Model 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 data (with string values) that are sent to a device on saving
an object. Use raw_data
property for it:
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'}
model.save()
methodThe save()
method is used to save changes in object. Manually created objects
(unlike retrieved from a query, see below) must know which connection to use, so also set it
by with_zk()
:
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()
Processing the records obtained from table is pretty simple:
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()
Any changes will not be actually saved in a record until save()
will be called.
queryset.upsert()
methodJust saves particular records without considering QuerySet
state. Returns nothing:
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 accepts the following values:
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'}, {...}])
“Upsert” (update/insert) operation means that if such record already exists then it will get updated, otherwise it will get inserted.
Deleting object is similar to saving.
model.delete()
methodManually created objects (unlike retrieved from a query, see below) must know which connection
to use, so also set it by with_zk()
:
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()
For queries:
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.delete()
queryset.delete()
methodJust deletes particular records without considering QuerySet
state. Returns nothing.
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 accepts the following:
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'}, {...}])
queryset.delete_all()
methodThis method deletes records that matched to a QuerySet
object. If results was not fetched yet,
fetches them first. Returns nothing.
The following example deletes all transactions related to card “123456”:
zk.table('User').where(card='123456').delete_all()
The QuerySet
class is intended to build a query to a data table, execute it and obtain results.
Its operations are limited to the ZK PULL SDK capabilities. QuerySet
supports filtering (only
for equal condition), limiting to unread records, to the certain fields, slicing results.
QuerySet
uses lazy loading, which means that it will not make a query and fetch the results
until you started to iterate over it or to get element by index.
The common approach to work with a QuerySet is:
records = zk.table('Model').limit1(parameters).limit2(parameters)[index_or_slice]
for record in records:
...
QuerySet
is binded with a particular Model class, where queries are targeted to. New
QuerySet
is created by ZKAccess.table(model)
function. As model
parameter you can pass
model’s name, or it’s class, or it’s object.
The following examples works identically, they return empty QuerySet
object binded with
User
model:
from pyzkaccess.tables import User
qs = zk.table('User')
qs = zk.table(User)
qs = zk.table(User(pin='1', password='123'))
Some QuerySet
methods return new object, the others not. Let’s see what you can do with QuerySets.
where()
method returns a new QuerySet
containing records that match the given filter parameters.
You can only use equality operation due to PULL SDK restriction. Several fields are AND’ed.
Filters in repeated where()
calls are also AND’ed with fields from previous calls. If this
field has already set in previous call, it will be replaced with new 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)
Resulting query conditions will be group == '4' AND super_authorize == True
.
unread()
method returns a new QuerySet
containing records that was not read yet.
All data tables on ZK device has a pointer which is set to the last record on each read query. If no records have been inserted to a table since last read, the “unread” query will return nothing.
only_fields()
method returns a new QuerySet
containing records with only specified fields.
Other fields will be set to None. Fields in repeated only_fields()
calls will be added to fields
from previous calls.
As a field name you can pass either a field name or model field object.
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)
Resulting records will have only pin
and password
field values, retrieved from table,
other fields will remain None.
count()
method returns total records count in records. Calling this method leads to a special
SDK call. Pay attention that this method does not consider conditions, it just returns total
records count in a table. It is an analogue of SQL query SELECT COUNT(*) FROM table;
.
If you want to know count of records contained in QuerySet
, use len(qs)
.
QuerySet
supports iterator protocol and also slicing, indexing. Some examples should help to
understand the approach:
# 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!')
When you apply len()
on a QuerySet
object, all matched results will be fetched from a device
and put to the cache. Unlike count()
method, the len()
returns actual results size.
When you apply bool()
on a QuerySet
object, all matched results will be fetched from a device
and put to the cache. If there is any record was returned, returns True
, or False
otherwise.
CLI interface uses command/subcommand chain approach. Typical CLI usage is:
Commands for a connected device:
$ pyzkaccess connect <ip> <subcommand|group> [parameters] [<subcommand> [parameters] ...]
Commands not related to a particular device:
$ pyzkaccess <command> [parameters]
By default, all input data is consumed from stdin, and all output is printed to stdout. You can
specify a file instead by setting --file
parameter.
CLI gives access to most of PyZKAccess features. Also, it is supported the ascii tables in console or CSV format.
Every command, group and subcommand has its own help contents, just type them and append
--help
at the end. For example, here is the help for connect
command:
$ pyzkaccess connect --help
Or for where
subcommand of table
subcommand:
$ pyzkaccess connect 192.168.1.201 table User where --help
Igor Derkach, gosha753951@gmail.com