diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5e39799 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Codex Agent Guide (pyzk) + +## Repo overview +- `zk/` is the library package. The main entry point is `ZK` in `zk/base.py`. +- `example/` contains minimal usage scripts. +- `test_machine.py` and `test_backup_restore.py` are hardware-facing CLIs. +- `docs/` is a Sphinx site (ReadTheDocs compatible). + +## Key modules +- `zk/base.py`: core protocol, connection handling, and device operations. +- `zk/user.py`: `User` model and packing helpers. +- `zk/finger.py`: `Finger` model and template packing helpers. +- `zk/attendance.py`: `Attendance` model. +- `zk/const.py`: protocol constants and flags. +- `zk/exception.py`: library exceptions. + +## Safety notes +- Destructive calls include `clear_data()`, `clear_attendance()`, `poweroff()`, and `restart()`. +- `unlock()` opens the door/relay on some devices. Treat it as a safety-sensitive operation. +- Prefer `disable_device()` before bulk reads/writes to avoid inconsistent data. +- Live capture (`live_capture()`) can hold device state; ensure you exit cleanly. + +## Common commands +- Run an example: `python example/get_users.py` (edit IP/port inside the script). +- Basic device probe: `python test_machine.py -a 192.168.1.201`. +- Backup/restore CLI (new): `pyzk-backup --help`. + +## Docs build +- Build Sphinx docs: `cd docs && make html`. +- Or: `sphinx-build -b html docs docs/_build/html`. + +## Hardware-dependent tests +- Any command that connects to a real device requires a reachable IP/port (default 4370). +- Use `--force-udp` if TCP is unreliable for a specific model. +- Time sync and firmware reads may differ by model/firmware version. + +## Conventions +- Keep new code Python 2/3 compatible unless explicitly dropping support. +- Avoid changing protocol details without referencing `docs/_static/Communication_protocol_manual_CMD.pdf`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5e39799 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# Codex Agent Guide (pyzk) + +## Repo overview +- `zk/` is the library package. The main entry point is `ZK` in `zk/base.py`. +- `example/` contains minimal usage scripts. +- `test_machine.py` and `test_backup_restore.py` are hardware-facing CLIs. +- `docs/` is a Sphinx site (ReadTheDocs compatible). + +## Key modules +- `zk/base.py`: core protocol, connection handling, and device operations. +- `zk/user.py`: `User` model and packing helpers. +- `zk/finger.py`: `Finger` model and template packing helpers. +- `zk/attendance.py`: `Attendance` model. +- `zk/const.py`: protocol constants and flags. +- `zk/exception.py`: library exceptions. + +## Safety notes +- Destructive calls include `clear_data()`, `clear_attendance()`, `poweroff()`, and `restart()`. +- `unlock()` opens the door/relay on some devices. Treat it as a safety-sensitive operation. +- Prefer `disable_device()` before bulk reads/writes to avoid inconsistent data. +- Live capture (`live_capture()`) can hold device state; ensure you exit cleanly. + +## Common commands +- Run an example: `python example/get_users.py` (edit IP/port inside the script). +- Basic device probe: `python test_machine.py -a 192.168.1.201`. +- Backup/restore CLI (new): `pyzk-backup --help`. + +## Docs build +- Build Sphinx docs: `cd docs && make html`. +- Or: `sphinx-build -b html docs docs/_build/html`. + +## Hardware-dependent tests +- Any command that connects to a real device requires a reachable IP/port (default 4370). +- Use `--force-udp` if TCP is unreliable for a specific model. +- Time sync and firmware reads may differ by model/firmware version. + +## Conventions +- Keep new code Python 2/3 compatible unless explicitly dropping support. +- Avoid changing protocol details without referencing `docs/_static/Communication_protocol_manual_CMD.pdf`. diff --git a/README.md b/README.md index 61ffab0..45f35cf 100644 --- a/README.md +++ b/README.md @@ -335,136 +335,28 @@ optional arguments: **Backup/Restore (Users and fingers only!!!)** *(WARNING! destructive test! do it at your own risk!)* -```sh -usage: ./test_backup_restore.py [-h] [-a ADDRESS] [-p PORT] [-T TIMEOUT] - [-P PASSWORD] [-f] [-v] [-r] - [filename] - -ZK Basic Backup/Restore Tool - -positional arguments: - filename backup filename (default [serialnumber].bak) +Use the installed CLI: -optional arguments: - -h, --help show this help message and exit - -a ADDRESS, --address ADDRESS - ZK device Address [192.168.1.201] - -p PORT, --port PORT ZK device port [4370] - -T TIMEOUT, --timeout TIMEOUT - Default [10] seconds (0: disable timeout) - -P PASSWORD, --password PASSWORD - Device code/password - -f, --force-udp Force UDP communication - -v, --verbose Print debug information - -E, --erase clean the device after writting backup! - -r, --restore Restore from backup - -c, --clear-attendance - On Restore, also clears the attendance [default keep - attendance] +```sh +pyzk-backup --help +pyzk-backup -a 192.168.1.201 +pyzk-backup -a 192.168.1.201 --restore backup.json.bak ``` +The legacy script `test_backup_restore.py` now wraps the same CLI and accepts +the same flags. + To restore on a different device, make sure to specify the `filename`. on restoring, it asks for the serial number of the destination device (to make sure it was correct, as it deletes all data) WARNING. there is no way to restore attendance data, you can keep it or clear it, but once cleared, there is no way to restore it. # Compatible devices -``` -Firmware Version : Ver 6.21 Nov 19 2008 -Platform : ZEM500 -DeviceName : U580 - -Firmware Version : Ver 6.60 Apr 9 2010 -Platform : ZEM510_TFT -DeviceName : T4-C - -Firmware Version : Ver 6.60 Dec 1 2010 -Platform : ZEM510_TFT -DeviceName : T4-C - -Firmware Version : Ver 6.60 Mar 18 2011 -Platform : ZEM600_TFT -DeviceName : iClock260 - -Platform : ZEM560_TFT -Firmware Version : Ver 6.60 Feb 4 2012 -DeviceName : - -Firmware Version : Ver 6.60 Oct 29 2012 -Platform : ZEM800_TFT -DeviceName : iFace402/ID - -Firmware Version : Ver 6.60 Mar 18 2013 -Platform : ZEM560 -DeviceName : MA300 - -Firmware Version : Ver 6.60 Dec 27 2014 -Platform : ZEM600_TFT -DeviceName : iFace800/ID - -Firmware Version : Ver 6.60 Nov 6 2017 (remote tested with correct results) -Platform : ZMM220_TFT -DeviceName : (unknown device) (broken info but at least the important data was read) - -Firmware Version : Ver 6.60 Jun 9 2017 -Platform : JZ4725_TFT -DeviceName : K20 (latest checked correctly!) - -Firmware Version : Ver 6.60 Aug 23 2014 -Platform : ZEM600_TFT -DeviceName : VF680 (face device only, but we read the user and attendance list!) - -Firmware Version : Ver 6.70 Feb 16 2017 -Platform : ZLM30_TFT -DeviceName : RSP10k1 (latest checked correctly!) - -Firmware Version : Ver 6.60 Jun 16 2015 -Platform : JZ4725_TFT -DeviceName : K14 (tested & verified working as expected.) - -Firmware Version : Ver 6.60 Jan 13 2016 -Platform : ZMM220_TFT -DeviceName : iFace702 (without voice function, test with encoding='gbk') - -Firmware Version : Ver 6.60 Apr 26 2016 -Platform : ZMM210_TFT -DeviceName : F18/ID - -Firmware Version : Ver 6.60 May 25 2018 -Platform : JZ4725_TFT -DeviceName : K40/ID -``` - - - -### Latest tested (not really confirmed) - -``` -Firmware Version : Ver 6.60 Jun 16 2015 -Platform : JZ4725_TFT -DeviceName : iClock260 - -Firmware Version : Ver 6.60 Jun 5 2015 -Platform : ZMM200_TFT -DeviceName : iClock3000/ID (Active testing! latest fix) - -Firmware Version : Ver 6.70 Jul 12 2013 -Platform : ZEM600_TFT -DeviceName : iClock880-H/ID (Active testing! latest fix) -``` - -### Not Working (needs more tests, more information) - -``` -Firmware Version : Ver 6.4.1 (build 99) (display version 2012-08-31) -Platform : -DeviceName : iClock260 (no capture data - probably similar problem as the latest TESTED) -``` - -If you have another version tested and it worked, please inform me to update this list! +See `docs/compatible_devices.rst` for the canonical list and the submission template. -# Todo +# Roadmap -* Create better documentation -* ~~Finger template downloader & uploader~~ -* HTTP Rest api -* ~~Create real time api (if possible)~~ -* and much more ... +- Create better documentation (in progress) +- Finger template downloader & uploader (in progress, via `pyzk-backup`) +- HTTP REST API (spec only) +- Real-time API (spec only) +- Expand compatible devices list and contribution guide (in progress) +- Future ideas are tracked in `ROADMAP.md` diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..53c30e3 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,22 @@ +# pyzk Roadmap + +This roadmap tracks the next steps for pyzk. It is intentionally high level +and may change as new devices or protocol details are discovered. + +## Now + +- Complete the Sphinx documentation with real usage guides. +- Publish a supported backup/restore CLI (`pyzk-backup`) and helper API. +- Expand the compatible devices list and add a contribution template. + +## Next + +- Ship an HTTP REST API server (implementation of the spec in `docs/api_design.rst`). +- Provide a real-time streaming API (SSE or WebSocket) for live events. +- Add more example scripts for common tasks (backup, sync time, unlock door). + +## Later + +- Add automated compatibility tests for known devices. +- Expand device support for newer firmware families. +- Optional plugins for exporting data to common systems (CSV, database). diff --git a/docs/api_design.rst b/docs/api_design.rst new file mode 100644 index 0000000..2c5d38c --- /dev/null +++ b/docs/api_design.rst @@ -0,0 +1,139 @@ +.. toctree:: + :caption: HTTP & Real-Time API (Spec) + :name: api_design + +############################### +HTTP & Real-Time API (Spec Only) +############################### + +This is a forward-looking design spec for a REST and real-time API built on top +of pyzk. It does **not** include an implementation yet. + +Goals +----- + +- Provide a simple HTTP interface for device operations and data extraction. +- Support real-time events via server-sent events (SSE). +- Keep device state in a managed connection pool. + +Recommended stack +----------------- + +- **FastAPI** for HTTP +- **Uvicorn** as the ASGI server +- **SSE** for live capture streaming + +Connection lifecycle +-------------------- + +The API should manage device connections with explicit connect/disconnect calls +and a short idle timeout. Each connection is keyed by ``device_id`` or +``ip:port``. + +Proposed endpoints +------------------ + +Health +^^^^^^ + +- ``GET /health`` -> ``{ "status": "ok" }`` + +Devices +^^^^^^^ + +- ``POST /devices/connect`` + +Request: + +.. code-block:: json + + { + "ip": "192.168.1.201", + "port": 4370, + "timeout": 10, + "password": 0, + "force_udp": false, + "encoding": "UTF-8" + } + +Response: + +.. code-block:: json + + { + "device_id": "192.168.1.201:4370", + "serial": "0000000001", + "fp_version": "10", + "platform": "ZEM600_TFT" + } + +- ``POST /devices/disconnect`` + +Request: + +.. code-block:: json + + { "device_id": "192.168.1.201:4370" } + +Users +^^^^^ + +- ``GET /devices/{device_id}/users`` +- ``POST /devices/{device_id}/users`` +- ``DELETE /devices/{device_id}/users/{uid}`` + +Attendance +^^^^^^^^^^ + +- ``GET /devices/{device_id}/attendance`` +- ``DELETE /devices/{device_id}/attendance`` + +Templates +^^^^^^^^^ + +- ``GET /devices/{device_id}/templates`` +- ``GET /devices/{device_id}/templates/{uid}/{fid}`` + +Real-time events (SSE) +^^^^^^^^^^^^^^^^^^^^^^ + +- ``GET /devices/{device_id}/live`` + +SSE event payload: + +.. code-block:: json + + { + "type": "attendance", + "user_id": "1001", + "timestamp": "2024-06-01T12:00:00", + "status": 0, + "punch": 0 + } + +Error model +----------- + +All errors should return: + +.. code-block:: json + + { + "error": { + "code": "DEVICE_OFFLINE", + "message": "Device did not respond" + } + } + +Security +-------- + +- Prefer network isolation and IP allowlists. +- Consider an API key or JWT for remote calls. +- The server should never expose device passwords in responses. + +Open questions +-------------- + +- How to handle multiple concurrent clients reading attendance. +- Whether to expose raw template data or require an opt-in flag. diff --git a/docs/backup_restore.rst b/docs/backup_restore.rst new file mode 100644 index 0000000..8243fcc --- /dev/null +++ b/docs/backup_restore.rst @@ -0,0 +1,62 @@ +.. toctree:: + :caption: Backup & Restore + :name: backup_restore + +################ +Backup & Restore +################ + +pyzk includes a supported CLI and helper functions to export and restore users +and fingerprint templates. + +CLI usage +--------- + +The ``pyzk-backup`` command mirrors the legacy ``test_backup_restore.py`` script +but is installed as a console entry point. + +.. code-block:: sh + + pyzk-backup -a 192.168.1.201 + pyzk-backup -a 192.168.1.201 --restore backup.json.bak + +Options +------- + +- ``-E / --erase``: erase the device after writing the backup file +- ``-r / --restore``: restore from backup file +- ``-c / --clear-attendance``: also clears attendance when restoring +- ``-g / --high-rate``: use high-rate bulk write + +Programmatic usage +------------------ + +.. code-block:: python + + from zk import ZK + from zk.backup import export_backup, save_backup, load_backup, restore_backup + + conn = ZK('192.168.1.201').connect() + + data = export_backup(conn) + save_backup(conn, 'device.json.bak', data=data) + + data = load_backup('device.json.bak') + restore_backup(conn, data, erase=True, prompt_serial=True) + +JSON schema +----------- + +The backup file is a JSON document with these fields: + +- ``version``: backup schema version (currently ``1.00jut``) +- ``serial``: device serial number +- ``fp_version``: fingerprint template version +- ``users``: list of user objects +- ``templates``: list of fingerprint templates + +Notes +----- + +- Restoring always requires a matching fingerprint template version. +- For safety, the CLI prompts for the device serial number before erasing. diff --git a/docs/compatible_devices.rst b/docs/compatible_devices.rst index 29a002d..b15200d 100644 --- a/docs/compatible_devices.rst +++ b/docs/compatible_devices.rst @@ -6,3 +6,113 @@ Compatible Devices ################## +Known working devices +--------------------- + +:: + + Firmware Version : Ver 6.21 Nov 19 2008 + Platform : ZEM500 + DeviceName : U580 + + Firmware Version : Ver 6.60 Apr 9 2010 + Platform : ZEM510_TFT + DeviceName : T4-C + + Firmware Version : Ver 6.60 Dec 1 2010 + Platform : ZEM510_TFT + DeviceName : T4-C + + Firmware Version : Ver 6.60 Mar 18 2011 + Platform : ZEM600_TFT + DeviceName : iClock260 + + Platform : ZEM560_TFT + Firmware Version : Ver 6.60 Feb 4 2012 + DeviceName : + + Firmware Version : Ver 6.60 Oct 29 2012 + Platform : ZEM800_TFT + DeviceName : iFace402/ID + + Firmware Version : Ver 6.60 Mar 18 2013 + Platform : ZEM560 + DeviceName : MA300 + + Firmware Version : Ver 6.60 Dec 27 2014 + Platform : ZEM600_TFT + DeviceName : iFace800/ID + + Firmware Version : Ver 6.60 Nov 6 2017 (remote tested with correct results) + Platform : ZMM220_TFT + DeviceName : (unknown device) (broken info but at least the important data was read) + + Firmware Version : Ver 6.60 Jun 9 2017 + Platform : JZ4725_TFT + DeviceName : K20 (latest checked correctly!) + + Firmware Version : Ver 6.60 Aug 23 2014 + Platform : ZEM600_TFT + DeviceName : VF680 (face device only, but we read the user and attendance list!) + + Firmware Version : Ver 6.70 Feb 16 2017 + Platform : ZLM30_TFT + DeviceName : RSP10k1 (latest checked correctly!) + + Firmware Version : Ver 6.60 Jun 16 2015 + Platform : JZ4725_TFT + DeviceName : K14 (tested & verified working as expected.) + + Firmware Version : Ver 6.60 Jan 13 2016 + Platform : ZMM220_TFT + DeviceName : iFace702 (without voice function, test with encoding='gbk') + + Firmware Version : Ver 6.60 Apr 26 2016 + Platform : ZMM210_TFT + DeviceName : F18/ID + + Firmware Version : Ver 6.60 May 25 2018 + Platform : JZ4725_TFT + DeviceName : K40/ID + + DeviceName : uFace 800 (ZKTeco) + Notes : connected successfully, worked with finger and face + +Latest tested (not really confirmed) +----------------------------------- + +:: + + Firmware Version : Ver 6.60 Jun 16 2015 + Platform : JZ4725_TFT + DeviceName : iClock260 + + Firmware Version : Ver 6.60 Jun 5 2015 + Platform : ZMM200_TFT + DeviceName : iClock3000/ID (Active testing! latest fix) + + Firmware Version : Ver 6.70 Jul 12 2013 + Platform : ZEM600_TFT + DeviceName : iClock880-H/ID (Active testing! latest fix) + +Not working (needs more tests) +------------------------------ + +:: + + Firmware Version : Ver 6.4.1 (build 99) (display version 2012-08-31) + Platform : + DeviceName : iClock260 (no capture data - probably similar problem as the latest TESTED) + +Submit a device +--------------- + +Please include the following: + +:: + + Firmware Version : + Platform : + DeviceName : + Test results : (what worked, what failed) + Encoding : (if needed, ex: gbk) diff --git a/docs/index.rst b/docs/index.rst index d9fe8da..8f521e2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,22 @@ topic3 topic4 topic5 + backup_restore + troubleshooting compatible_devices + api_design + +.. toctree:: + :hidden: + :maxdepth: 2 + :caption: API Reference + + zk_base + zk_user + zk_finger + zk_attendance + zk_const + zk_exception ****************** @@ -97,4 +112,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/topic1.rst b/docs/topic1.rst index 3b6b5f7..cc32af1 100644 --- a/docs/topic1.rst +++ b/docs/topic1.rst @@ -6,9 +6,57 @@ Connect / Disconnect to Machine ############################### +Create a client +--------------- + +The main entry point is the ``ZK`` class from ``zk.base``. It encapsulates +socket creation, handshake, and command framing. + +.. code-block:: python + + from zk import ZK + + zk = ZK( + '192.168.1.201', + port=4370, + timeout=10, + password=0, + force_udp=False, + ommit_ping=False, + verbose=False, + encoding='UTF-8', + ) + Connect ------- +.. code-block:: python + + conn = zk.connect() + conn.disable_device() # recommended for bulk reads/writes + Disconnect ---------- +.. code-block:: python + + conn.enable_device() + conn.disconnect() + +TCP vs UDP +---------- + +By default, pyzk uses TCP. Some devices and firmware revisions are more stable +with UDP; use ``force_udp=True`` when you see intermittent timeouts. + +Ping behavior +------------- + +If ``ommit_ping=False``, the library may attempt to ping the device before +connecting. Set it to ``True`` if ICMP is blocked in your network. + +Encoding +-------- + +If names appear garbled, pass the correct device encoding (for example ``'gbk'`` +for some iFace devices). diff --git a/docs/topic2.rst b/docs/topic2.rst index 024e8cd..df5a2b6 100644 --- a/docs/topic2.rst +++ b/docs/topic2.rst @@ -6,15 +6,63 @@ User Operation ############## -Create User ------------ +Create or update a user +----------------------- -Enroll User ------------ +.. code-block:: python -Update User ------------ + from zk import ZK, const -Delete User ------------ + conn = ZK('192.168.1.201').connect() + conn.disable_device() + conn.set_user( + uid=1, + name='Alice', + privilege=const.USER_ADMIN, + password='1234', + group_id='', + user_id='1', + card=0, + ) + conn.enable_device() + conn.disconnect() +Get users +--------- + +.. code-block:: python + + users = conn.get_users() + for user in users: + print(user.uid, user.user_id, user.name) + +Delete a user +------------- + +.. code-block:: python + + conn.delete_user(uid=1) + # or + conn.delete_user(user_id='1') + +Enroll user (remote) +-------------------- + +This triggers enrollment on the device. It may not work on some TCP devices. + +.. code-block:: python + + conn.enroll_user(uid=1, temp_id=0) + +Privileges +---------- + +``const.USER_DEFAULT`` and ``const.USER_ADMIN`` are the most common privileges. +Some devices use additional values; check ``zk/const.py`` for the full list. + +UID vs User ID +-------------- + +``uid`` is the internal device ID. ``user_id`` is your external identifier. +Some older devices expect them to match. If unsure, set ``user_id`` to +``str(uid)``. diff --git a/docs/topic3.rst b/docs/topic3.rst index f87aff8..15187e7 100644 --- a/docs/topic3.rst +++ b/docs/topic3.rst @@ -6,10 +6,40 @@ Attendance Log Operation ######################## - -Get Attendance Log +Get attendance log ------------------ -Clear Attendance Log +.. code-block:: python + + records = conn.get_attendance() + for record in records: + print(record.user_id, record.timestamp, record.status, record.punch) + +Attendance fields +----------------- + +Each record is an ``Attendance`` object with: + +- ``user_id``: external user identifier +- ``timestamp``: ``datetime`` when the punch happened +- ``status``: event status code (device specific) +- ``punch``: punch type (device specific) + +Clear attendance log -------------------- +.. code-block:: python + + conn.clear_attendance() + +Live capture +------------ + +Live capture emits attendance events in near real time. + +.. code-block:: python + + for event in conn.live_capture(): + if event is None: + continue + print(event) diff --git a/docs/topic4.rst b/docs/topic4.rst index b359c18..97c3412 100644 --- a/docs/topic4.rst +++ b/docs/topic4.rst @@ -6,18 +6,51 @@ Device Maintenance ################## -Set Time +Set time -------- +.. code-block:: python + + from datetime import datetime + conn.set_time(datetime.today()) + Poweroff -------- +.. code-block:: python + + conn.poweroff() + Restart ------- -Clear Buffer +.. code-block:: python + + conn.restart() + +Clear buffer ------------ -Clear Data +``free_data()`` clears the internal device buffer used for bulk reads. + +.. code-block:: python + + conn.free_data() + +Clear data +---------- + +**Warning:** this erases users, fingerprints, and logs. + +.. code-block:: python + + conn.clear_data() + +Unlock door ----------- +On supported devices, ``unlock`` triggers the door relay for a few seconds. + +.. code-block:: python + + conn.unlock(time=3) diff --git a/docs/topic5.rst b/docs/topic5.rst index c43c363..e176404 100644 --- a/docs/topic5.rst +++ b/docs/topic5.rst @@ -6,8 +6,33 @@ IOT ### -It is possible to make a great IOT project by using pyzk. You can improve by using some hardware component like raspberry pi, relay, LED, or another stuff. Here we will show you an example of IOT impleentation. We will guide how to develops a Door Lock system implementation. +You can build simple IOT workflows using pyzk and a small controller such as a +Raspberry Pi. A common example is triggering a door relay from attendance events. Door Lock Open -------------- +This example listens to live capture events and unlocks the door when a known +user punches in. + +.. code-block:: python + + from zk import ZK + + zk = ZK('192.168.1.201') + conn = zk.connect() + conn.disable_device() + + allowed = set(['1001', '1002']) + + for event in conn.live_capture(): + if event is None: + continue + if event.user_id in allowed: + conn.unlock(time=3) + + conn.enable_device() + conn.disconnect() + +**Warning:** the ``unlock`` operation can trigger real hardware. Use caution and +test on a controlled system first. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst new file mode 100644 index 0000000..7d5f2c4 --- /dev/null +++ b/docs/troubleshooting.rst @@ -0,0 +1,35 @@ +.. toctree:: + :caption: Troubleshooting + :name: troubleshooting + +############### +Troubleshooting +############### + +Device not reachable +-------------------- + +- Verify IP and port (default 4370). +- Ensure the device is on the same network/VLAN. +- Try ``force_udp=True`` if TCP times out. + +Ping fails +---------- + +Some networks block ICMP. Set ``ommit_ping=True`` to skip ping checks. + +Garbled names +------------- + +Set ``encoding`` to match your device language, for example ``'gbk'``. + +Slow template reads +------------------- + +Template downloads can be large. Increase ``timeout`` and avoid Wi-Fi if possible. + +Restore fails with template version mismatch +------------------------------------------- + +Fingerprint template formats vary. Restore only to devices with the same +``fp_version``. diff --git a/setup.py b/setup.py index 4479efd..1a4fe8b 100644 --- a/setup.py +++ b/setup.py @@ -19,5 +19,10 @@ 'biometrics', 'security' ], + entry_points={ + 'console_scripts': [ + 'pyzk-backup=zk.cli_backup:main', + ], + }, zip_safe=False -) \ No newline at end of file +) diff --git a/test_backup_restore.py b/test_backup_restore.py index 3858c2c..a52bb9d 100755 --- a/test_backup_restore.py +++ b/test_backup_restore.py @@ -1,159 +1,8 @@ #!/usr/bin/env python2 # # -*- coding: utf-8 -*- -import sys -import traceback -import argparse -import time -import datetime -import codecs -from builtins import input -import json +from zk.cli_backup import main -sys.path.append("zk") -from zk import ZK, const -from zk.user import User -from zk.finger import Finger -from zk.attendance import Attendance -from zk.exception import ZKErrorResponse, ZKNetworkError - -class BasicException(Exception): - pass - -conn = None - -parser = argparse.ArgumentParser(description='ZK Basic Backup/Restore Tool') -parser.add_argument('-a', '--address', - help='ZK device Address [192.168.1.201]', default='192.168.1.201') -parser.add_argument('-p', '--port', type=int, - help='ZK device port [4370]', default=4370) -parser.add_argument('-T', '--timeout', type=int, - help='Default [10] seconds (0: disable timeout)', default=10) -parser.add_argument('-P', '--password', type=int, - help='Device code/password', default=0) -parser.add_argument('-f', '--force-udp', action="store_true", - help='Force UDP communication') -parser.add_argument('-v', '--verbose', action="store_true", - help='Print debug information') -parser.add_argument('-E', '--erase', action="store_true", - help='clean the device after writting backup!') -parser.add_argument('-r', '--restore', action="store_true", - help='Restore from backup') -parser.add_argument('-c', '--clear-attendance', action="store_true", - help='On Restore, also clears the attendance [default keep attendance]') -parser.add_argument('-g', '--high-rate', action="store_true", - help='in restoration, use high-rate mode') -parser.add_argument('filename', nargs='?', - help='backup filename (default [serialnumber].bak)', default='') - -args = parser.parse_args() - -def erase_device(conn, serialnumber, clear_attendance=False): - """input serial number to corroborate.""" - print ('WARNING! the next step will erase the current device content.') - print ('Please input the serialnumber of this device [{}] to acknowledge the ERASING!'.format(serialnumber)) - new_serial = input ('Serial Number : ') - if new_serial != serialnumber: - raise BasicException('Serial number mismatch') - conn.disable_device() - print ('Erasing device...') - conn.clear_data() - if clear_attendance: - print ('Clearing attendance too!') - conn.clear_attendance() - conn.read_sizes() - print (conn) - - -zk = ZK(args.address, port=args.port, timeout=args.timeout, password=args.password, force_udp=args.force_udp, verbose=args.verbose) -try: - print('Connecting to device ...') - conn = zk.connect() - serialnumber = conn.get_serialnumber() - fp_version = conn.get_fp_version() - print ('Serial Number : {}'.format(serialnumber)) - print ('Finger Version : {}'.format(fp_version)) - filename = args.filename if args.filename else "{}.json.bak".format(serialnumber) - print ('') - if not args.restore: - print ('--- sizes & capacity ---') - conn.read_sizes() - print (conn) - print ('--- Get User ---') - inicio = time.time() - users = conn.get_users() - final = time.time() - print ('Read {} users took {:.3f}[s]'.format(len(users), final - inicio)) - if len(users) == 0: - raise BasicException("Empty user list, aborting...") - print ("Read Templates...") - inicio = time.time() - templates = conn.get_templates() - final = time.time() - print ('Read {} templates took {:.3f}[s]'.format(len(templates), final - inicio)) - #save to file! - print ('') - print ('Saving to file {} ...'.format(filename)) - output = open(filename, 'w') - data = { - 'version':'1.00jut', - 'serial': serialnumber, - 'fp_version': fp_version, - 'users': [u.__dict__ for u in users], - 'templates':[t.json_pack() for t in templates] - } - json.dump(data, output, indent=1) - output.close() - if args.erase: - erase_device(conn, serialnumber, args.clear_attendance) - else: - print ('Reading file {}'.format(filename)) - infile = open(filename, 'r') - data = json.load(infile) - infile.close() - #compare versions... - if data['version'] != '1.00jut': - raise BasicException("file with different version... aborting!") - if data['fp_version'] != fp_version: - raise BasicException("fingerprint version mismmatch {} != {} ... aborting!".format(fp_version, data['fp_version'])) - #TODO: check data consistency... - users = [User.json_unpack(u) for u in data['users']] - #print (users) - print ("INFO: ready to write {} users".format(len(users))) - templates = [Finger.json_unpack(t) for t in data['templates']] - #print (templates) - print ("INFO: ready to write {} templates".format(len(templates))) - erase_device(conn, serialnumber, args.clear_attendance) - print ('Restoring Data...') - usertemplates = [] - for u in users: - #look for Templates - temps = list(filter(lambda f: f.uid ==u.uid, templates)) - #print ("user {} has {} fingers".format(u.uid, len(temps))) - if not args.high_rate: - conn.save_user_template(u,temps) - else: - usertemplates.append([u,temps]) - if args.high_rate: - conn.HR_save_usertemplates(usertemplates) - conn.enable_device() - print ('--- final sizes & capacity ---') - conn.read_sizes() - print (conn) -except BasicException as e: - print (e) - print ('') -except Exception as e: - print ("Process terminate : {}".format(e)) - print ("Error: %s" % sys.exc_info()[0]) - print ('-'*60) - traceback.print_exc(file=sys.stdout) - print ('-'*60) -finally: - if conn: - print ('Enabling device ...') - conn.enable_device() - conn.disconnect() - print ('ok bye!') - print ('') +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/test_backup_module.py b/tests/test_backup_module.py new file mode 100644 index 0000000..d50d0ab --- /dev/null +++ b/tests/test_backup_module.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import unittest + +from zk.backup import BACKUP_VERSION, restore_backup +from zk.exception import ZKErrorResponse +from zk.finger import Finger +from zk.user import User + + +class FakeConn(object): + def __init__(self, fp_version='10', serial='ABC'): + self._fp_version = fp_version + self._serial = serial + self.saved = [] + self.hr_saved = [] + self.disabled = False + self.enabled = False + self.cleared = False + self.att_cleared = False + + def get_fp_version(self): + return self._fp_version + + def get_serialnumber(self): + return self._serial + + def disable_device(self): + self.disabled = True + + def clear_data(self): + self.cleared = True + + def clear_attendance(self): + self.att_cleared = True + + def enable_device(self): + self.enabled = True + + def save_user_template(self, user, temps): + self.saved.append((user, temps)) + + def HR_save_usertemplates(self, usertemplates): + self.hr_saved = usertemplates + + +class FingerJsonRoundTripTest(unittest.TestCase): + def test_roundtrip(self): + template = b'\x01\x02\x03\x04\x05\x06\x07\x08' + finger = Finger(1, 2, 1, template) + data = finger.json_pack() + decoded = Finger.json_unpack(data) + self.assertEqual(finger.uid, decoded.uid) + self.assertEqual(finger.fid, decoded.fid) + self.assertEqual(finger.valid, decoded.valid) + self.assertEqual(finger.template, decoded.template) + + +class BackupRestoreTest(unittest.TestCase): + def _sample_data(self): + user = User(1, 'Alice', 0, '', '', '1', 0) + finger = Finger(1, 0, 1, b'\x01\x02') + return { + 'version': BACKUP_VERSION, + 'serial': 'ABC', + 'fp_version': '10', + 'users': [user.__dict__], + 'templates': [finger.json_pack()], + } + + def test_restore_calls_save_user_template(self): + conn = FakeConn(fp_version='10', serial='ABC') + data = self._sample_data() + restore_backup(conn, data, erase=False, high_rate=False, prompt_serial=False) + self.assertEqual(len(conn.saved), 1) + self.assertEqual(len(conn.hr_saved), 0) + self.assertTrue(conn.enabled) + + def test_restore_high_rate(self): + conn = FakeConn(fp_version='10', serial='ABC') + data = self._sample_data() + restore_backup(conn, data, erase=False, high_rate=True, prompt_serial=False) + self.assertEqual(len(conn.saved), 0) + self.assertEqual(len(conn.hr_saved), 1) + + def test_fp_version_mismatch(self): + conn = FakeConn(fp_version='9', serial='ABC') + data = self._sample_data() + with self.assertRaises(ZKErrorResponse): + restore_backup(conn, data, erase=False, high_rate=False, prompt_serial=False) + + +if __name__ == '__main__': + unittest.main() diff --git a/zk/backup.py b/zk/backup.py new file mode 100644 index 0000000..b2ec1b3 --- /dev/null +++ b/zk/backup.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +import io +import json +import sys + +from .exception import ZKErrorResponse +from .user import User +from .finger import Finger + +BACKUP_VERSION = '1.00jut' + + +def _write_line(msg): + try: + sys.stdout.write(msg + '\n') + except Exception: + pass + + +def _prompt_input(prompt): + try: + return input(prompt) + except NameError: + return raw_input(prompt) + + +def _validate_data(data): + required = ['version', 'serial', 'fp_version', 'users', 'templates'] + for key in required: + if key not in data: + raise ZKErrorResponse('Backup file missing key: {}'.format(key)) + if data['version'] != BACKUP_VERSION: + raise ZKErrorResponse('Unsupported backup version: {}'.format(data['version'])) + return True + + +def export_backup(conn): + """ + Export users and templates from a connected device. + + :param conn: ZK connection + :return: dict ready to be JSON serialized + """ + serial = conn.get_serialnumber() + fp_version = conn.get_fp_version() + conn.read_sizes() + users = conn.get_users() + if len(users) == 0: + raise ZKErrorResponse('Empty user list, aborting backup') + templates = conn.get_templates() + return { + 'version': BACKUP_VERSION, + 'serial': serial, + 'fp_version': fp_version, + 'users': [u.__dict__ for u in users], + 'templates': [t.json_pack() for t in templates] + } + + +def save_backup(conn, path, data=None): + if data is None: + data = export_backup(conn) + with io.open(path, 'w', encoding='utf-8') as output: + json.dump(data, output, indent=1) + return path + + +def load_backup(path): + with io.open(path, 'r', encoding='utf-8') as infile: + data = json.load(infile) + _validate_data(data) + return data + + +def _confirm_serial(expected_serial): + _write_line('WARNING! the next step will erase the current device content.') + _write_line('Please input the serial number of this device [{}] to acknowledge the ERASING!'.format(expected_serial)) + new_serial = _prompt_input('Serial Number : ') + return new_serial == expected_serial + + +def erase_device(conn, clear_attendance=False, prompt_serial=True): + serial = conn.get_serialnumber() + if prompt_serial and not _confirm_serial(serial): + raise ZKErrorResponse('Serial number mismatch') + conn.disable_device() + conn.clear_data() + if clear_attendance: + conn.clear_attendance() + return True + + +def restore_backup(conn, data, erase=False, clear_attendance=False, high_rate=False, prompt_serial=True): + """ + Restore users and templates to a connected device. + + :param conn: ZK connection + :param data: dict from load_backup/export_backup + :param erase: clear device before restore + :param clear_attendance: clear attendance log while erasing + :param high_rate: use high-rate bulk write + :param prompt_serial: confirm serial number before erasing + """ + _validate_data(data) + fp_version = conn.get_fp_version() + if data['fp_version'] != fp_version: + raise ZKErrorResponse('Fingerprint version mismatch {} != {}'.format(fp_version, data['fp_version'])) + + users = [User.json_unpack(u) for u in data['users']] + templates = [Finger.json_unpack(t) for t in data['templates']] + + by_uid = {} + for t in templates: + by_uid.setdefault(t.uid, []).append(t) + + if erase: + erase_device(conn, clear_attendance=clear_attendance, prompt_serial=prompt_serial) + + if high_rate: + usertemplates = [] + for u in users: + usertemplates.append([u, by_uid.get(u.uid, [])]) + conn.HR_save_usertemplates(usertemplates) + else: + for u in users: + conn.save_user_template(u, by_uid.get(u.uid, [])) + + conn.enable_device() + return True diff --git a/zk/cli_backup.py b/zk/cli_backup.py new file mode 100644 index 0000000..f97bec7 --- /dev/null +++ b/zk/cli_backup.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import argparse +import sys + +from . import ZK +from .backup import erase_device, export_backup, load_backup, restore_backup, save_backup +from .exception import ZKErrorResponse, ZKNetworkError + + +class BasicException(Exception): + pass + + +def _print(msg): + try: + sys.stdout.write(msg + '\n') + except Exception: + pass + + +def main(argv=None): + parser = argparse.ArgumentParser(description='ZK Backup/Restore Tool') + parser.add_argument('-a', '--address', + help='ZK device Address [192.168.1.201]', default='192.168.1.201') + parser.add_argument('-p', '--port', type=int, + help='ZK device port [4370]', default=4370) + parser.add_argument('-T', '--timeout', type=int, + help='Default [10] seconds (0: disable timeout)', default=10) + parser.add_argument('-P', '--password', type=int, + help='Device code/password', default=0) + parser.add_argument('-f', '--force-udp', action='store_true', + help='Force UDP communication') + parser.add_argument('-v', '--verbose', action='store_true', + help='Print debug information') + parser.add_argument('-E', '--erase', action='store_true', + help='Clean the device after writing backup') + parser.add_argument('-r', '--restore', action='store_true', + help='Restore from backup') + parser.add_argument('-c', '--clear-attendance', action='store_true', + help='On restore, also clears the attendance [default keep attendance]') + parser.add_argument('-g', '--high-rate', action='store_true', + help='In restoration, use high-rate mode') + parser.add_argument('filename', nargs='?', + help='backup filename (default [serialnumber].json.bak)', default='') + + args = parser.parse_args(argv) + + zk = ZK(args.address, port=args.port, timeout=args.timeout, password=args.password, + force_udp=args.force_udp, verbose=args.verbose) + conn = None + try: + _print('Connecting to device ...') + conn = zk.connect() + serialnumber = conn.get_serialnumber() + fp_version = conn.get_fp_version() + _print('Serial Number : {}'.format(serialnumber)) + _print('Finger Version : {}'.format(fp_version)) + filename = args.filename if args.filename else '{}.json.bak'.format(serialnumber) + _print('') + + if not args.restore: + _print('--- sizes & capacity ---') + conn.read_sizes() + _print(str(conn)) + _print('--- Export Users & Templates ---') + data = export_backup(conn) + save_backup(conn, filename, data=data) + _print('Saved to file {}'.format(filename)) + if args.erase: + erase_device(conn, clear_attendance=args.clear_attendance, prompt_serial=True) + _print('Device erased after backup') + else: + _print('Reading file {}'.format(filename)) + data = load_backup(filename) + _print('Restoring Data...') + restore_backup(conn, data, erase=True, clear_attendance=args.clear_attendance, + high_rate=args.high_rate, prompt_serial=True) + conn.read_sizes() + _print('--- final sizes & capacity ---') + _print(str(conn)) + except BasicException as e: + _print(str(e)) + except (ZKErrorResponse, ZKNetworkError) as e: + _print('Error: {}'.format(e)) + except Exception as e: + _print('Process terminate : {}'.format(e)) + _print('Error: {}'.format(sys.exc_info()[0])) + finally: + if conn: + try: + _print('Enabling device ...') + conn.enable_device() + except Exception: + pass + try: + conn.disconnect() + except Exception: + pass + _print('ok bye!') + return 0 + + +if __name__ == '__main__': + sys.exit(main())