pyzkaccess

Library and CLI tool for working with ZKTeco ZKAccess C3-100/200/400 controllers

View the Project on GitHub bdragon300/pyzkaccess

PyZKAccess

PyZKAccess is a library for working with ZKTeco ZKAccess C3-100/200/400 access controllers.

API reference

Summary

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.

Features

Here are the controllers we’re taking about:

C3-100 C3-200 C3-400
alt text alt text alt text

NOTE: API pyzkaccess>=0.2 is incompatible with API pyzkaccess==0.1.

Installing

Requirements:

PULL SDK

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.

*nix

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)

Windows

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.

Usage

Quick start

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()

Working with a device

Use as context manager

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)

Find a device in a local network and connect to it

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 model is C3-400. Here is how to use another device model

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)

Set current datetime

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()

Change ip settings

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'

Relays

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

Readers

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

Aux inputs

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

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))

Device data tables

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.

Model objects

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'}

Saving changes in objects

model.save() method

The 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() method

Just 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:

“Upsert” (update/insert) operation means that if such record already exists then it will get updated, otherwise it will get inserted.

Deleting objects

Deleting object is similar to saving.

model.delete() method

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.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() method

Just 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:

queryset.delete_all() method

This 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()

Making queries

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.

Building a query

Filtering

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.

Only new records

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.

Query only listed fields

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.

Data table size

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).

Retrieving results

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!')

len()

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.

bool()

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.

Command-line interface

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

Author

Igor Derkach, gosha753951@gmail.com