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

Overview

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
alt text alt text alt text

NOTE: the default factory IP address of C3 devices is 192.168.1.201.

Features

Quick start

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.

pyzkaccess setup

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.

Installation

Windows

Portable executable

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.

Manual installation

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’s C:\Windows\SysWOW64 (or C:\Windows\System32 on older 32-bit Windows versions).

Finally, check the installation by running the pyzkaccess search_devices command.

*nix

Prerequisites

First, you will need to set up 32-bit Wine environment (commands below are relevant for Debian/Ubuntu):

apt-get install wine wine32

Portable executable

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.

Manual installation

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

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

Connection options

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:

Library usage

Let’s look how to use this package in your code.

Quick start

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 library

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)

Using a certain device model

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)

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.

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

Readers

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

Aux inputs

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

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

Parameters

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

Data tables

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

Models are the following:

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

Reading data

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

Writing data

Upsert (update or insert)

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:

Insert one record

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

Update an existing record

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

Delete records

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:

Delete one record

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

Delete all selected records

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

Building a query

Filters

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.

Getting the unread records

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.

Select fields to retrieve

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.

Data table size

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

Retrieving results

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

Author

Igor Derkach, gosha753951@gmail.com