Upload files to "biometric"
Signed-off-by: nestict <developer@nestict.com>
This commit is contained in:
7
biometric/__init__.py
Normal file
7
biometric/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
biometric app
|
||||||
|
|
||||||
|
This app contains modules for handling biometric devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import settings
|
||||||
16
biometric/admin.py
Normal file
16
biometric/admin.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Register models with the Django admin site.
|
||||||
|
|
||||||
|
This section of code registers the BiometricDevices and BiometricEmployees models
|
||||||
|
with the Django admin site, allowing them to be managed via the admin interface.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import BiometricDevices, BiometricEmployees, COSECAttendanceArguments
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
admin.site.register(BiometricDevices)
|
||||||
|
admin.site.register(BiometricEmployees)
|
||||||
|
admin.site.register(COSECAttendanceArguments)
|
||||||
172
biometric/anviz.py
Normal file
172
biometric/anviz.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
CrossChexCloudAPI module for Anviz Biometric Integration
|
||||||
|
|
||||||
|
This module provides a wrapper for interacting with the CrossChex Cloud API to manage
|
||||||
|
authentication, attendance data retrieval, and token handling. It allows for secure
|
||||||
|
communication with the API, including fetching and validating tokens, and retrieving
|
||||||
|
attendance records .
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class CrossChexCloudAPI:
|
||||||
|
"""
|
||||||
|
CrossChexCloudAPI: A class to interact with the CrossChex Cloud API for attendance data
|
||||||
|
and token management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_url, api_key, api_secret, anviz_request_id):
|
||||||
|
"""
|
||||||
|
Initializes the CrossChexCloudAPI object with necessary parameters, such as API URL,
|
||||||
|
credentials (API key and secret), and request ID for the connection.
|
||||||
|
"""
|
||||||
|
self.api_url = api_url
|
||||||
|
self.api_key = api_key
|
||||||
|
self.api_secret = api_secret
|
||||||
|
self.anviz_request_id = anviz_request_id
|
||||||
|
self.token = None
|
||||||
|
self.expires = None
|
||||||
|
self.auth_error = {
|
||||||
|
"header": {"nameSpace": "System", "name": "Exception"},
|
||||||
|
"payload": {"type": "AUTH_ERROR", "message": "AUTH_ERROR"},
|
||||||
|
}
|
||||||
|
self.expires_error = {
|
||||||
|
"header": {"nameSpace": "System", "name": "Exception"},
|
||||||
|
"payload": {"type": "TOKEN_EXPIRES", "message": "TOKEN_EXPIRES"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_timestamp(self):
|
||||||
|
"""
|
||||||
|
Generates a UTC timestamp in ISO 8601 format.
|
||||||
|
"""
|
||||||
|
return datetime.utcnow().isoformat() + "Z"
|
||||||
|
|
||||||
|
def _post(self, data):
|
||||||
|
"""
|
||||||
|
Sends a POST request with the given data to the API and handles the response, including
|
||||||
|
automatic token renewal in case of expiration or authentication error.
|
||||||
|
"""
|
||||||
|
response = requests.post(self.api_url, json=data, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if "payload" in response_data:
|
||||||
|
if response_data["payload"] == self.expires_error:
|
||||||
|
self.get_token()
|
||||||
|
response = requests.post(self.api_url, json=data, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
elif response_data["payload"] == self.auth_error:
|
||||||
|
raise Exception("Authentication error: API key or secret is incorrect.")
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def _is_token_expired(self):
|
||||||
|
"""Check if the token is expired."""
|
||||||
|
if self.expires:
|
||||||
|
expires_datetime = datetime.fromisoformat(self.expires)
|
||||||
|
# Remove timezone info to make it offset-naive
|
||||||
|
expires_datetime = expires_datetime.replace(tzinfo=None)
|
||||||
|
return datetime.utcnow() > expires_datetime
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
"""Fetch a new token if expired or not present, and store it in the database."""
|
||||||
|
if self.token is None or self._is_token_expired():
|
||||||
|
data = {
|
||||||
|
"header": {
|
||||||
|
"nameSpace": "authorize.token",
|
||||||
|
"nameAction": "token",
|
||||||
|
"version": "1.0",
|
||||||
|
"requestId": self.anviz_request_id,
|
||||||
|
"timestamp": self._get_timestamp(),
|
||||||
|
},
|
||||||
|
"payload": {"api_key": self.api_key, "api_secret": self.api_secret},
|
||||||
|
}
|
||||||
|
response = self._post(data)
|
||||||
|
self.token = response.get("payload").get("token")
|
||||||
|
self.expires = response.get("payload").get("expires")
|
||||||
|
|
||||||
|
return self.token, self.expires
|
||||||
|
|
||||||
|
def test_connection(self):
|
||||||
|
"""Test connection and fetch the token and expiry."""
|
||||||
|
token, expires = self.get_token()
|
||||||
|
return {"token": token, "expires": expires}
|
||||||
|
|
||||||
|
def get_attendance_payload(
|
||||||
|
self, begin_time, end_time, order, page, per_page, token
|
||||||
|
):
|
||||||
|
"""Constructs the payload for retrieving attendance records."""
|
||||||
|
current_utc_time = datetime.utcnow()
|
||||||
|
begin_time = begin_time or current_utc_time.replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
|
end_time = end_time or current_utc_time
|
||||||
|
begin_time_str = begin_time.isoformat() + "+00:00"
|
||||||
|
end_time_str = end_time.isoformat() + "+00:00"
|
||||||
|
return {
|
||||||
|
"header": {
|
||||||
|
"nameSpace": "attendance.record",
|
||||||
|
"nameAction": "getrecord",
|
||||||
|
"version": "1.0",
|
||||||
|
"requestId": self.anviz_request_id,
|
||||||
|
"timestamp": self._get_timestamp(),
|
||||||
|
},
|
||||||
|
"authorize": {
|
||||||
|
"type": "token",
|
||||||
|
"token": token,
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"begin_time": begin_time_str,
|
||||||
|
"end_time": end_time_str,
|
||||||
|
"order": order,
|
||||||
|
"page": page,
|
||||||
|
"per_page": per_page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_attendance_records(
|
||||||
|
self,
|
||||||
|
begin_time=None,
|
||||||
|
end_time=None,
|
||||||
|
order="asc",
|
||||||
|
page=1,
|
||||||
|
per_page=100,
|
||||||
|
token=None,
|
||||||
|
):
|
||||||
|
"""Get attendance records, optimizing token usage and handling pagination."""
|
||||||
|
all_records = []
|
||||||
|
token = token or self.get_token()[0]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
payload_data = self.get_attendance_payload(
|
||||||
|
begin_time=begin_time,
|
||||||
|
end_time=end_time,
|
||||||
|
order=order,
|
||||||
|
page=str(page),
|
||||||
|
per_page=str(per_page),
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
response = self._post(payload_data)
|
||||||
|
|
||||||
|
# Safe extraction to avoid KeyError
|
||||||
|
payload = response.get("payload", {}) if isinstance(response, dict) else {}
|
||||||
|
records = payload.get("list", [])
|
||||||
|
|
||||||
|
all_records.extend(records)
|
||||||
|
|
||||||
|
page_count = payload.get("pageCount", 0)
|
||||||
|
if page >= page_count or not records:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"token": self.token,
|
||||||
|
"expires": self.expires,
|
||||||
|
"list": all_records,
|
||||||
|
"count": len(all_records),
|
||||||
|
}
|
||||||
34
biometric/apps.py
Normal file
34
biometric/apps.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Django application configuration for the biometric app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricConfig(AppConfig):
|
||||||
|
"""
|
||||||
|
This class defines the configuration for the biometric Django app. It sets the
|
||||||
|
default auto field to use a BigAutoField for model primary keys.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
default_auto_field (str): The default auto field to use for model primary keys.
|
||||||
|
name (str): The name of the Django app, which is 'biometric'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "biometric"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from horilla.horilla_settings import APPS
|
||||||
|
from horilla.urls import urlpatterns
|
||||||
|
|
||||||
|
APPS.append("biometric")
|
||||||
|
urlpatterns.append(
|
||||||
|
path("biometric/", include("biometric.urls")),
|
||||||
|
)
|
||||||
|
|
||||||
|
from biometric import sidebar
|
||||||
|
|
||||||
|
super().ready()
|
||||||
35
biometric/context_processors.py
Normal file
35
biometric/context_processors.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Utility functions related to biometric attendance.
|
||||||
|
|
||||||
|
This file contains utility functions related to biometric attendance,
|
||||||
|
including a function to check if the biometric system is installed.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
biometric_is_installed(request): Checks if the biometric system is installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from base.models import BiometricAttendance
|
||||||
|
|
||||||
|
|
||||||
|
def biometric_is_installed(_request):
|
||||||
|
"""
|
||||||
|
Check if the biometric system is installed.
|
||||||
|
|
||||||
|
This function checks if the biometric system is installed by querying the
|
||||||
|
BiometricAttendance model. If no BiometricAttendance object exists, it
|
||||||
|
creates one with 'is_installed' set to False.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The HTTP request object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing a single key-value pair indicating whether
|
||||||
|
the biometric system is installed. The key is 'is_installed', and the value
|
||||||
|
is a boolean indicating the installation status.
|
||||||
|
"""
|
||||||
|
instance = BiometricAttendance.objects.first()
|
||||||
|
if not instance:
|
||||||
|
BiometricAttendance.objects.create(is_installed=False)
|
||||||
|
instance = BiometricAttendance.objects.first()
|
||||||
|
is_installed = instance.is_installed
|
||||||
|
return {"is_installed": is_installed}
|
||||||
744
biometric/cosec.py
Normal file
744
biometric/cosec.py
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
"""
|
||||||
|
This module provides a Python interface to interact with a COSEC biometric device.
|
||||||
|
It allows users to perform various operations such as configuring device settings,
|
||||||
|
managing users, retrieving attendance events, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
cosec_api_response_codes = {
|
||||||
|
"0": "Successful",
|
||||||
|
"1": "Failed - Invalid Login Credentials",
|
||||||
|
"2": "Date and time manual set failed",
|
||||||
|
"3": "Invalid Date/Time",
|
||||||
|
"4": "Maximum users are already configured.",
|
||||||
|
"5": "Image size is too big.",
|
||||||
|
"6": "Image format not supported",
|
||||||
|
"7": "Card 1 and card 2 are identical",
|
||||||
|
"8": "Card ID exists",
|
||||||
|
"9": "Finger print template/ Palm template/ Face template\
|
||||||
|
already exists/ Face Image already exists",
|
||||||
|
"10": "No Record Found",
|
||||||
|
"11": "Template size/ format mismatch",
|
||||||
|
"12": "FP Memory full",
|
||||||
|
"13": "User id not found",
|
||||||
|
"14": "Credential limit reached",
|
||||||
|
"15": "Reader mismatch/ Reader not configured",
|
||||||
|
"16": "Device Busy",
|
||||||
|
"17": "Internal process error ",
|
||||||
|
"18": "PIN already exists",
|
||||||
|
"19": "Credential not found",
|
||||||
|
"20": "Memory Card Not Found",
|
||||||
|
"21": "Reference User ID exists",
|
||||||
|
"22": "Wrong Selection",
|
||||||
|
"23": "Palm template mode mismatch",
|
||||||
|
"24": "Feature not enabled in the configuration",
|
||||||
|
"25": "Message already exists for same user for same date",
|
||||||
|
"26": "Invalid smart card format/Parameters not applicable as per card type defined.",
|
||||||
|
"27": "Time Out",
|
||||||
|
"28": "Read/Write failed",
|
||||||
|
"29": "Wrong Card Type",
|
||||||
|
"30": "key mismatch",
|
||||||
|
"31": "invalid card",
|
||||||
|
"32": "Scan failed",
|
||||||
|
"33": "Invalid value",
|
||||||
|
"34": "Credential does not match",
|
||||||
|
"35": "Failure",
|
||||||
|
"36": "Face Not Detected",
|
||||||
|
"37": "User Conflict",
|
||||||
|
"38": "Enroll Conflict",
|
||||||
|
"39": "Face Mask Detected",
|
||||||
|
"40": "Full Face Not Visible",
|
||||||
|
"41": "Face Not Straight",
|
||||||
|
}
|
||||||
|
|
||||||
|
true_false_arguments = [
|
||||||
|
"enroll-on-device",
|
||||||
|
"week-day0",
|
||||||
|
"week-day1",
|
||||||
|
"week-day2",
|
||||||
|
"week-day3",
|
||||||
|
"week-day4",
|
||||||
|
"week-day5",
|
||||||
|
"week-day6",
|
||||||
|
"alarm",
|
||||||
|
"tamper-alarm",
|
||||||
|
"auto-alarm-ack",
|
||||||
|
"thresh-temp-exceeded",
|
||||||
|
"allow-exit-when-locked",
|
||||||
|
"auto-relock",
|
||||||
|
"asc-active",
|
||||||
|
"door-sense-active",
|
||||||
|
"exit-switch",
|
||||||
|
"aux-output-enable",
|
||||||
|
"greeting-msg-enable",
|
||||||
|
"buzzer-mute",
|
||||||
|
"enable",
|
||||||
|
"enable-signal-wait",
|
||||||
|
"read-csn",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class COSECBiometric:
|
||||||
|
"""
|
||||||
|
A Python interface to interact with a COSEC biometric device.
|
||||||
|
|
||||||
|
This class provides methods for configuring device settings, managing users,
|
||||||
|
retrieving attendance events, and other operations.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
1. Instantiate the COSECBiometric class with the required parameters:
|
||||||
|
IP address, port, username, and password.
|
||||||
|
2. Use the provided methods to perform specific actions on the biometric device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, machine_ip, port, username, password, timeout=60):
|
||||||
|
"""
|
||||||
|
Initialize the COSECBiometric object with the specified parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
machine_ip (str): The IP address of the COSEC biometric device.
|
||||||
|
port (int): The port number of the COSEC biometric device.
|
||||||
|
username (str): The username for accessing the biometric device.
|
||||||
|
password (str): The password for accessing the biometric device.
|
||||||
|
timeout (int, optional): The timeout for HTTP requests (default is 60 seconds).
|
||||||
|
"""
|
||||||
|
self.__ip = machine_ip
|
||||||
|
self.__port = port
|
||||||
|
self.__timeout = timeout
|
||||||
|
self.__username = username
|
||||||
|
self.__password = password
|
||||||
|
self.__header = {"Authorization": self.__generate_auth_header()}
|
||||||
|
self.__base_url = f"http://{self.__ip}/device.cgi"
|
||||||
|
self.__user_fields = []
|
||||||
|
|
||||||
|
def __generate_auth_header(self):
|
||||||
|
"""
|
||||||
|
Generate the Authorization header for making authenticated requests to the COSEC
|
||||||
|
biometric device.
|
||||||
|
|
||||||
|
This method creates an Authorization header using the provided username and
|
||||||
|
password, encoded in Base64 format as per the Basic authentication scheme.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The Authorization header value in the format 'Basic <base64_encoded_credentials>'.
|
||||||
|
"""
|
||||||
|
credentials = f"{self.__username}:{self.__password}".encode()
|
||||||
|
return "Basic " + b64encode(credentials).decode()
|
||||||
|
|
||||||
|
def __send_request(self, url):
|
||||||
|
"""
|
||||||
|
This method sends an HTTP GET request to the specified URL with the
|
||||||
|
appropriate headers, and then parses the response to handle different scenarios
|
||||||
|
such as timeouts, access errors, unsupported content types, and valid responses.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Some Device API uses HTTPDigestAuth for authentication
|
||||||
|
# response = requests.get(
|
||||||
|
# url + "&format=xml",
|
||||||
|
# timeout=self.__timeout,
|
||||||
|
# auth=HTTPDigestAuth(self.__username, self.__password)
|
||||||
|
# )
|
||||||
|
response = requests.get(
|
||||||
|
url + "&format=xml", headers=self.__header, timeout=self.__timeout
|
||||||
|
)
|
||||||
|
return self.__parse_response(response)
|
||||||
|
except requests.Timeout:
|
||||||
|
return {"Timeout": "Request Timeout"}
|
||||||
|
|
||||||
|
def __parse_response(self, response):
|
||||||
|
"""
|
||||||
|
Parse the response received from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method parses the HTTP response received from the COSEC biometric device,
|
||||||
|
handles different scenarios such as HTTP status codes, content types,
|
||||||
|
and response formats, and extracts relevant data from the response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response (requests.Response): The HTTP response object received from
|
||||||
|
the device.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary representing the parsed response. If the response status
|
||||||
|
code is 200 (OK), and the content type is XML, the dictionary may contain
|
||||||
|
response data. If there is an error or unsupported content, the dictionary
|
||||||
|
will contain an appropriate error message.
|
||||||
|
"""
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {"Error": "Access Error"}
|
||||||
|
if response.headers.get("Content-Type") != "text/xml":
|
||||||
|
return {"Error": "Unsupported content"}
|
||||||
|
|
||||||
|
response_data = response.content.decode("utf-8")
|
||||||
|
root = ET.fromstring(response_data)
|
||||||
|
text_content = root.text.strip()
|
||||||
|
if not text_content:
|
||||||
|
events = root.findall("Events")
|
||||||
|
if events:
|
||||||
|
parsed_response = []
|
||||||
|
for event in root.findall("Events"):
|
||||||
|
event_dict = {}
|
||||||
|
for elem in event:
|
||||||
|
event_dict[elem.tag] = elem.text
|
||||||
|
if event_dict.get("event-id") == "101":
|
||||||
|
parsed_response.append(event_dict)
|
||||||
|
else:
|
||||||
|
parsed_response = {elem.tag: elem.text for elem in root}
|
||||||
|
if parsed_response.get("Response-Code"):
|
||||||
|
message = cosec_api_response_codes.get(
|
||||||
|
parsed_response["Response-Code"]
|
||||||
|
)
|
||||||
|
parsed_response["message"] = message
|
||||||
|
else:
|
||||||
|
parsed_response = {}
|
||||||
|
parsed_response["error"] = text_content
|
||||||
|
return parsed_response
|
||||||
|
|
||||||
|
def __authenticate_arguments(self, url, kwargs):
|
||||||
|
"""
|
||||||
|
Authenticate and validate the arguments before sending a request to the COSEC
|
||||||
|
biometric device.
|
||||||
|
|
||||||
|
This method verifies that the provided arguments are supported by the
|
||||||
|
specified URL endpoint and raises a ValueError if any unsupported arguments
|
||||||
|
are found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL endpoint for the request.
|
||||||
|
kwargs (dict): A dictionary containing the arguments to be authenticated.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any of the provided arguments are not supported by the specified
|
||||||
|
URL endpoint.
|
||||||
|
"""
|
||||||
|
if url == "special-function":
|
||||||
|
url = f"{self.__base_url}/{url}?action=get&sp-fn-index=1"
|
||||||
|
elif url == "smart-card-format":
|
||||||
|
url = f"{self.__base_url}/{url}?action=get&card-type=1&index=1"
|
||||||
|
else:
|
||||||
|
url = f"{self.__base_url}/{url}?action=get"
|
||||||
|
response = self.__send_request(url)
|
||||||
|
supported_args = response.keys()
|
||||||
|
unsupported_args = [arg for arg in kwargs.keys() if arg not in supported_args]
|
||||||
|
|
||||||
|
if unsupported_args:
|
||||||
|
unsupported_args_str = ", ".join(unsupported_args)
|
||||||
|
raise ValueError(
|
||||||
|
f"The following argument(s) are not supported\
|
||||||
|
: {unsupported_args_str}. Supported arguments are: {', '.join(supported_args)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def basic_config(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve basic settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve basic settings of
|
||||||
|
the COSEC biometric device, such as device name, IP settings, time settings,
|
||||||
|
etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/device-basic-config?action={action}"
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("device-basic-config", kwargs)
|
||||||
|
url += "&" + "&".join([f"{key}={value}" for key, value in kwargs.items()])
|
||||||
|
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def finger_reader_parameter_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve parameters related to the finger reader of the COSEC
|
||||||
|
biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve parameters related to
|
||||||
|
the finger reader of the COSEC biometric device, such as sensitivity, timeout,
|
||||||
|
template format, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/finger-parameter?action={action}"
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("finger-parameter", kwargs)
|
||||||
|
url += "&" + "&".join([f"{key}={value}" for key, value in kwargs.items()])
|
||||||
|
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def enrollment_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve enrollment options of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve enrollment options
|
||||||
|
of the COSEC biometric device, such as enabling or disabling self-enrollment,
|
||||||
|
setting enrollment timeout, template format, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/enroll-options?action={action}"
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("enroll-options", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def access_settings_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve access settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve access settings of the
|
||||||
|
COSEC biometric device, such as door access control, alarm settings,
|
||||||
|
exit switches, etc
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/access-setting?action={action}"
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("access-setting", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def alarm_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve alarm settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve alarm settings of
|
||||||
|
the COSEC biometric device, such as enabling or disabling alarms, setting
|
||||||
|
alarm thresholds, configuring alarm acknowledgements, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = f"{self.__base_url}/alarm?action={action}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("alarm", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def date_and_time_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve date and time settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve date and time settings
|
||||||
|
of the COSEC biometric device, such as setting the current date and time,
|
||||||
|
configuring time zones, enabling daylight saving time, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/date-time?action={action}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("date-time", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def door_features_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve door features settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve door features settings
|
||||||
|
of the COSEC biometric device, such as enabling or disabling door senses,
|
||||||
|
setting door open durations, configuring auxiliary outputs, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/door-feature?action={action}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("door-feature", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def system_timer_configuration(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve system timer settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve system timer settings
|
||||||
|
of the COSEC biometric device, such as setting the system idle timeout,
|
||||||
|
configuring system heartbeat intervals, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/system-timer?action={action}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("system-timer", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def special_function_configuration(self, action="get", sp_fn_index="1", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure special functions on the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows configuring special functions on the COSEC biometric device,
|
||||||
|
such as enabling or disabling
|
||||||
|
specific functionalities
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/special-function?action={action}&sp-fn-index={sp_fn_index}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("special-function", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def wiegand_interface(self, action="get", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve Wiegand interface settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve Wiegand interface settings
|
||||||
|
of the COSEC biometric device,such as setting up Wiegand card readers, configuring
|
||||||
|
Wiegand data formats, etc.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/wiegand-interface?action={action}"
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("wiegand-interface", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def smart_card_format(self, action="get", card_type="1", index="1", **kwargs):
|
||||||
|
"""
|
||||||
|
Configure or retrieve smart card format settings of the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows the user to configure or retrieve smart card format settings
|
||||||
|
of the COSEC biometric device,
|
||||||
|
such as setting up card types and their corresponding formats.
|
||||||
|
"""
|
||||||
|
url = (
|
||||||
|
f"{self.__base_url}/smart-card-format?action={action}"
|
||||||
|
f"&card-type={card_type}&index={index}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
self.__authenticate_arguments("smart-card-format", kwargs)
|
||||||
|
url += "&" + "&".join(
|
||||||
|
[
|
||||||
|
f"{key}={int(value) if key in true_false_arguments else value}"
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def get_cosec_user(self, user_id):
|
||||||
|
"""
|
||||||
|
Retrieve user information from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method retrieves user information, such as user details,
|
||||||
|
credentials, and access rights, from the COSEC biometric device based
|
||||||
|
on the provided user ID.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/users?action=get&user-id={user_id}"
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def check_user_url_arguments(self, user_id, url_arguments):
|
||||||
|
"""
|
||||||
|
Check if the provided URL arguments are supported for the COSEC
|
||||||
|
biometric device's user configuration.
|
||||||
|
|
||||||
|
This method verifies if the provided URL arguments are supported
|
||||||
|
for configuring or retrieving user settings on the COSEC biometric
|
||||||
|
device. It compares the provided arguments with the supported fields
|
||||||
|
retrieved from the device for user configuration.
|
||||||
|
"""
|
||||||
|
user = self.get_cosec_user(user_id)
|
||||||
|
if not user.get("Response-Code"):
|
||||||
|
self.__user_fields = list(user.keys())
|
||||||
|
|
||||||
|
else:
|
||||||
|
ref_user_id = 1
|
||||||
|
code = "13"
|
||||||
|
while code != "0":
|
||||||
|
url = (
|
||||||
|
f"{self.__base_url}/users?action=set&user-id={user_id}"
|
||||||
|
f"&ref-user-id={ref_user_id}&format=xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
url, headers=self.__header, timeout=self.__timeout
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_data = response.content.decode("utf-8")
|
||||||
|
root = ET.fromstring(response_data)
|
||||||
|
code = root.find("Response-Code").text
|
||||||
|
if code == "0":
|
||||||
|
user = self.get_cosec_user(user_id)
|
||||||
|
self.__user_fields = list(user.keys())
|
||||||
|
self.delete_cosec_user(user_id)
|
||||||
|
break
|
||||||
|
ref_user_id += 1
|
||||||
|
fields = ""
|
||||||
|
for arg in url_arguments:
|
||||||
|
if arg.split("=")[0] not in self.__user_fields:
|
||||||
|
fields += arg.split("=")[0] + " , "
|
||||||
|
if fields:
|
||||||
|
raise ValueError(
|
||||||
|
f"{fields} argument is not support on this biometric device API"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_cosec_user(
|
||||||
|
self,
|
||||||
|
user_id,
|
||||||
|
ref_user_id,
|
||||||
|
name=None,
|
||||||
|
user_active=None,
|
||||||
|
vip=None,
|
||||||
|
validity_enable=None,
|
||||||
|
validity_time_hh=None,
|
||||||
|
validity_time_mm=None,
|
||||||
|
validity_date_dd=None,
|
||||||
|
validity_date_mm=None,
|
||||||
|
validity_date_yyyy=None,
|
||||||
|
user_pin=None,
|
||||||
|
card1=None,
|
||||||
|
card2=None,
|
||||||
|
by_pass_finger=None,
|
||||||
|
dob_enable=None,
|
||||||
|
dob_dd=None,
|
||||||
|
dob_mm=None,
|
||||||
|
dob_yyyy=None,
|
||||||
|
by_pass_palm=None,
|
||||||
|
user_group=None,
|
||||||
|
self_enrollment_enable=None,
|
||||||
|
enable_fr=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set or update user information on the COSEC biometric device.
|
||||||
|
|
||||||
|
This method allows setting or updating user information on the COSEC
|
||||||
|
biometric device, including user details, access rights, credentials,
|
||||||
|
validity periods, etc.
|
||||||
|
"""
|
||||||
|
if not user_id or not ref_user_id:
|
||||||
|
raise ValueError(
|
||||||
|
"Both user_id and ref_id are mandatory for create & edit a user"
|
||||||
|
)
|
||||||
|
# user_id : Mandatory To set or retrieve the alphanumeric user ID for the selected user.
|
||||||
|
# Note: If a set request is sent against an existing user ID, then configuration for this
|
||||||
|
# user will be updated with the new values.
|
||||||
|
# ref_user_id : Mandatory for the set action.Maximum 8 digits.To select the numeric user
|
||||||
|
# ID on which the specified operation is to be done.
|
||||||
|
url_arguments = []
|
||||||
|
url = f"{self.__base_url}/users?action=set"
|
||||||
|
url_arguments.append(f"user-id={user_id}")
|
||||||
|
url_arguments.append(f"ref-user-id={ref_user_id}")
|
||||||
|
|
||||||
|
if name:
|
||||||
|
# Truncate name if it exceeds 15 characters
|
||||||
|
truncated_name = name[:15] if len(name) > 15 else name
|
||||||
|
url_arguments.append(f"name={truncated_name}")
|
||||||
|
|
||||||
|
if user_active is not None:
|
||||||
|
# To activate or deactivate a user.
|
||||||
|
if user_active not in [True, False]:
|
||||||
|
raise ValueError("user_active must be either True, False, or None")
|
||||||
|
url_arguments.append(f"user-active={int(user_active)}")
|
||||||
|
|
||||||
|
if vip is not None:
|
||||||
|
# To define a user as VIP.
|
||||||
|
# Note: A VIP user is a user with the special privilege to access a particular door.
|
||||||
|
if vip not in [True, False]:
|
||||||
|
raise ValueError("vip must be either True, False, or None")
|
||||||
|
url_arguments.append(f"vip={int(vip)}")
|
||||||
|
|
||||||
|
if validity_enable is not None:
|
||||||
|
# To enable/disable the user validity.
|
||||||
|
if validity_enable not in [True, False]:
|
||||||
|
raise ValueError("validity_enable must be either True, False, or None")
|
||||||
|
url_arguments.append(f"validity-enable={int(validity_enable)}")
|
||||||
|
|
||||||
|
if validity_date_dd and validity_date_mm and validity_date_yyyy:
|
||||||
|
# To define the end date for user validity.Valid Values : validity_date_dd = 1-31 &
|
||||||
|
# validity_date_mm = 1-12 validity_date_yyyy = based on device model
|
||||||
|
url_arguments.append(f"validity-date-dd={validity_date_dd}")
|
||||||
|
url_arguments.append(f"validity-date-mm={validity_date_mm}")
|
||||||
|
url_arguments.append(f"validity-date-yyyy={validity_date_yyyy}")
|
||||||
|
|
||||||
|
if validity_time_hh and validity_time_mm:
|
||||||
|
# To define the end time for user validity.Valid Values : validity_time_hh = 00-23
|
||||||
|
# & validity_time_mm = 00-59
|
||||||
|
url_arguments.append(f"validity-time-hh={validity_time_hh}")
|
||||||
|
url_arguments.append(f"validity-time-mm={validity_time_mm}")
|
||||||
|
|
||||||
|
if user_pin:
|
||||||
|
# 1 to 6 Digits . To set the user PIN or get the event from user PIN.
|
||||||
|
# Note: The user-pin can be set to a blank value.
|
||||||
|
url_arguments.append(f"user-pin={user_pin}")
|
||||||
|
|
||||||
|
if by_pass_finger is not None:
|
||||||
|
# To enable/disable the bypass finger option.
|
||||||
|
url_arguments.append(f"by-pass-finger={by_pass_finger}")
|
||||||
|
|
||||||
|
if by_pass_palm is not None:
|
||||||
|
# To enable/disable the bypass palm option.
|
||||||
|
if by_pass_palm not in [True, False]:
|
||||||
|
raise ValueError("by_pass_palm must be either True, False, or None")
|
||||||
|
url_arguments.append(f"by-pass-palm={int(by_pass_palm)}")
|
||||||
|
|
||||||
|
if card1:
|
||||||
|
# Values : 64 Bits (8 bytes) (max value - 18446744073709551615).
|
||||||
|
# Defines the value of access card 1 and 2.
|
||||||
|
url_arguments.append(f"card1={card1}")
|
||||||
|
if card2:
|
||||||
|
url_arguments.append(f"card2={card2}")
|
||||||
|
|
||||||
|
if dob_enable is not None:
|
||||||
|
# To enable/disable the display of a birthday message.
|
||||||
|
if dob_enable not in [True, False]:
|
||||||
|
raise ValueError("dob_enable must be either True, False, or None")
|
||||||
|
url_arguments.append(f"dob-enable={int(dob_enable)}")
|
||||||
|
|
||||||
|
if dob_dd and dob_mm and dob_yyyy:
|
||||||
|
# To set or delete the date of birth for a user Valid Values :
|
||||||
|
# dob_dd = 1-31 & dob_mm = 1-12 dob_yyyy = 1990-2037
|
||||||
|
url_arguments.append(f"dob-dd={dob_dd}")
|
||||||
|
url_arguments.append(f"dob-mm={dob_mm}")
|
||||||
|
url_arguments.append(f"dob-yyyy={dob_yyyy}")
|
||||||
|
|
||||||
|
if user_group:
|
||||||
|
# To set the user group number.
|
||||||
|
# Note: A user can be assigned to any user group ranging from 1 to 999.
|
||||||
|
# User group number can be set/update via “Set” action.
|
||||||
|
# To remove a user from an assigned user group, user group should be set to 0.
|
||||||
|
url_arguments.append(f"user-group={user_group}")
|
||||||
|
|
||||||
|
if self_enrollment_enable is not None:
|
||||||
|
# To enable/disable self-enrollment for user
|
||||||
|
if self_enrollment_enable not in [True, False]:
|
||||||
|
raise ValueError(
|
||||||
|
"self_enrollment_enable must be either True, False, or None"
|
||||||
|
)
|
||||||
|
url_arguments.append(
|
||||||
|
f"self-enrollment-enable={int(self_enrollment_enable)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if enable_fr is not None:
|
||||||
|
# To enable/disable face recognition for a user
|
||||||
|
if enable_fr not in [True, False]:
|
||||||
|
raise ValueError("enable_fr must be either True, False, or None")
|
||||||
|
url_arguments.append(f"enable-fr={int(enable_fr)}")
|
||||||
|
self.check_user_url_arguments(user_id, url_arguments)
|
||||||
|
url += "&" + "&".join(url_arguments)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def delete_cosec_user(self, user_id):
|
||||||
|
"""
|
||||||
|
Delete a user from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method deletes a user with the specified user ID from the COSEC
|
||||||
|
biometric device.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/users?action=delete&user-id={user_id}"
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def enable_user_face_recognition(self, user_id, enable_fr=True):
|
||||||
|
"""
|
||||||
|
Enable or disable face recognition for a user in cosec biometric device.
|
||||||
|
"""
|
||||||
|
url = (
|
||||||
|
f"{self.__base_url}/users?action=set&user-id={user_id}"
|
||||||
|
f"&enable-fr={int(enable_fr)}&format=xml"
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def get_user_credential(self, user_id, credential_type=1, finger_index=1):
|
||||||
|
"""
|
||||||
|
Retrieve the credential of a user from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method retrieves the credential of a user, such as fingerprint,
|
||||||
|
card, palm template, face template, or face image, from the COSEC biometric
|
||||||
|
device based on the provided user ID
|
||||||
|
and credential type.
|
||||||
|
"""
|
||||||
|
# type values: 1 = Finger , 2 = Card , 3 = Palm , 4 = Palm template with
|
||||||
|
# guide mode , 5 = Face Template , 6 = Face Image
|
||||||
|
if not isinstance(credential_type, int) or not isinstance(finger_index, int):
|
||||||
|
raise ValueError("type and finger_index arguments value must be integers")
|
||||||
|
|
||||||
|
if credential_type < 1 or credential_type > 6:
|
||||||
|
raise ValueError("Type must be between 1 and 6")
|
||||||
|
|
||||||
|
if finger_index < 1 or finger_index > 10:
|
||||||
|
raise ValueError("Finger index must be between 1 and 10")
|
||||||
|
|
||||||
|
url = (
|
||||||
|
f"{self.__base_url}/credential?action=get&type={credential_type}"
|
||||||
|
f"&user-id={user_id}&finger-index={finger_index}"
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def get_user_credential_count(self, user_id):
|
||||||
|
"""
|
||||||
|
Retrieve the credential of a user from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method retrieves the count of credentials of a user, such as fingerprint,
|
||||||
|
card, palm template, face template, or face image, from the COSEC biometric
|
||||||
|
device based on the provided user ID
|
||||||
|
and credential type.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/command?action=getcount&user-id={user_id}"
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def delete_cosec_user_credential(self, user_id, credential_type):
|
||||||
|
"""
|
||||||
|
Delete a specific type of credential associated
|
||||||
|
with a user from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method deletes a specific type of credential
|
||||||
|
associated with a user, such as fingerprint,
|
||||||
|
card, palm template, face template, or face image,
|
||||||
|
from the COSEC biometric device.
|
||||||
|
"""
|
||||||
|
# type values: 0 = All , 1 = Finger , 2 = Card , 3 = Palm , 4 = Palm template
|
||||||
|
# with guide mode , 5 = Face Template , 6 = Face Image
|
||||||
|
# type= 5 and 6 are applicable only for ARGO FACE.
|
||||||
|
if type < 0 or type > 6:
|
||||||
|
raise ValueError("Type must be between 0 and 6")
|
||||||
|
url = f"{self.__base_url}/credential?action=delete&user-id={user_id}&type={credential_type}"
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def get_user_count(self):
|
||||||
|
"""
|
||||||
|
Retrieve the total number of users configured on the COSEC biometric device.
|
||||||
|
|
||||||
|
This method retrieves the total number of users configured on the COSEC biometric device.
|
||||||
|
"""
|
||||||
|
url = f"{self.__base_url}/command?action=getusercount"
|
||||||
|
return self.__send_request(url)
|
||||||
|
|
||||||
|
def get_attendance_events(self, roll_over_count=0, seq_num=1, no_of_events=100):
|
||||||
|
"""
|
||||||
|
Retrieve attendance events from the COSEC biometric device.
|
||||||
|
|
||||||
|
This method retrieves attendance events, such as punch-in and punch-out records,
|
||||||
|
from the COSEC biometric device.
|
||||||
|
"""
|
||||||
|
url = (
|
||||||
|
f"{self.__base_url}/events?action=getevent&roll-over-count={roll_over_count}"
|
||||||
|
f"&seq-number={seq_num}&no-of-events={no_of_events}"
|
||||||
|
)
|
||||||
|
return self.__send_request(url)
|
||||||
521
biometric/dahua.py
Normal file
521
biometric/dahua.py
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
"""
|
||||||
|
DahuaAPI module for interacting with Dahua biometric and access control devices.
|
||||||
|
|
||||||
|
This module provides a set of methods for managing and configuring Dahua devices,
|
||||||
|
including retrieving system information, managing users, setting up network configurations,
|
||||||
|
and interacting with attendance logs. It communicates with Dahua devices via HTTP requests
|
||||||
|
and supports basic operations such as system reboot, setting time, and language configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPDigestAuth
|
||||||
|
|
||||||
|
key_map = {
|
||||||
|
"AttendanceState": "attendance_state",
|
||||||
|
"CardID": "card_id",
|
||||||
|
"CardName": "card_name",
|
||||||
|
"CardNo": "card_no",
|
||||||
|
"CardType": "card_type",
|
||||||
|
"CreateTime": "create_time",
|
||||||
|
"CreateTimeRealUTC": "create_time_real_utc",
|
||||||
|
"Door": "door",
|
||||||
|
"ErrorCode": "error_code",
|
||||||
|
"FaceIndex": "face_index",
|
||||||
|
"FacilityCode": "facility_code",
|
||||||
|
"HatColor": "hat_color",
|
||||||
|
"HatType": "hat_type",
|
||||||
|
"Mask": "mask",
|
||||||
|
"Method": "method",
|
||||||
|
"Notes": "notes",
|
||||||
|
"Password": "password",
|
||||||
|
"ReaderID": "reader_id",
|
||||||
|
"RecNo": "rec_no",
|
||||||
|
"RemainingTimes": "remaining_times",
|
||||||
|
"ReservedInt": "reserved_int",
|
||||||
|
"ReservedString": "reserved_string",
|
||||||
|
"RoomNumber": "room_number",
|
||||||
|
"Status": "status",
|
||||||
|
"Type": "type",
|
||||||
|
"URL": "url",
|
||||||
|
"UserID": "user_id",
|
||||||
|
"UserType": "user_type",
|
||||||
|
"VTONumber": "vto_number",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_logs_to_list(logs):
|
||||||
|
"""
|
||||||
|
Converts a dictionary of logs into a list of records.
|
||||||
|
|
||||||
|
This function processes a dictionary containing log data, identifies records by
|
||||||
|
the keys starting with "records[", and converts the corresponding values into
|
||||||
|
a list of dictionaries with more readable keys. It also handles timestamp fields
|
||||||
|
by converting them to `datetime` objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logs (dict): The dictionary containing the log data to be converted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of dictionaries representing individual log records, with
|
||||||
|
formatted keys and values. Each record is a dictionary with keys
|
||||||
|
mapped according to the `key_map` and timestamps converted to `datetime`.
|
||||||
|
"""
|
||||||
|
records_list = []
|
||||||
|
record_dict = defaultdict(dict)
|
||||||
|
previous_key = None
|
||||||
|
|
||||||
|
for key, value in logs.items():
|
||||||
|
if key.startswith("records["):
|
||||||
|
parts = key.split(".")
|
||||||
|
current_key = parts[0]
|
||||||
|
|
||||||
|
if previous_key and previous_key != current_key:
|
||||||
|
records_list.append(dict(record_dict))
|
||||||
|
record_dict.clear()
|
||||||
|
time_keys = ["CreateTime", "CreateTimeRealUTC"]
|
||||||
|
if parts[-1] in time_keys:
|
||||||
|
value = datetime.fromtimestamp(int(value))
|
||||||
|
|
||||||
|
record_dict[key_map.get(parts[-1])] = value
|
||||||
|
previous_key = current_key
|
||||||
|
if record_dict:
|
||||||
|
records_list.append(dict(record_dict))
|
||||||
|
|
||||||
|
return records_list
|
||||||
|
|
||||||
|
|
||||||
|
class DahuaAPI:
|
||||||
|
"""
|
||||||
|
A class for interacting with Dahua biometric and access control devices.
|
||||||
|
|
||||||
|
This class provides methods to interact with Dahua devices, including retrieving
|
||||||
|
system information, configuring device settings (network, language, general, etc.),
|
||||||
|
managing users, and processing logs related to attendance and access control.
|
||||||
|
The class communicates with the Dahua device via HTTP requests and supports
|
||||||
|
actions like enrolling users, rebooting the device, and fetching various device logs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, username: str, password: str):
|
||||||
|
# self.base_url = f"http://{ip}/cgi-bin/"
|
||||||
|
self.base_url = f"{ip}/cgi-bin/"
|
||||||
|
self.auth = HTTPDigestAuth(username, password)
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.auth = self.auth
|
||||||
|
|
||||||
|
def parse_response(self, response):
|
||||||
|
"""
|
||||||
|
Parses the response from the Dahua API request.
|
||||||
|
|
||||||
|
This method processes the HTTP response from the Dahua device API. It decodes
|
||||||
|
the content of the response, checks the status code, and returns a structured
|
||||||
|
result. If the response is successful (status code 200), it attempts to parse
|
||||||
|
the content as a dictionary of key-value pairs. If the response is an error,
|
||||||
|
it returns a relevant error message
|
||||||
|
"""
|
||||||
|
content = response.content.decode("utf-8").strip()
|
||||||
|
status_code = response.status_code
|
||||||
|
|
||||||
|
if status_code == 200:
|
||||||
|
if "\r\n" not in content and "=" not in content:
|
||||||
|
return {"result": content, "status_code": status_code}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_dict = dict(
|
||||||
|
line.split("=", 1) for line in content.split("\r\n") if "=" in line
|
||||||
|
)
|
||||||
|
content_dict["status_code"] = status_code
|
||||||
|
return content_dict
|
||||||
|
except Exception:
|
||||||
|
return {
|
||||||
|
"result": f"Invalid parameter {content}",
|
||||||
|
"status_code": status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
if status_code == 400:
|
||||||
|
return {
|
||||||
|
"result": "Error: Bad Request. Check the parameters.",
|
||||||
|
"status_code": status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"result": content, "status_code": status_code}
|
||||||
|
|
||||||
|
def _get(self, endpoint: str, params: Dict[str, Any] = None):
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
response = self.session.get(url, params=params)
|
||||||
|
return self.parse_response(response)
|
||||||
|
|
||||||
|
def _post(self, endpoint: str, data: Dict[str, Any]):
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
response = self.session.post(url, data=data)
|
||||||
|
return self.parse_response(response)
|
||||||
|
|
||||||
|
def get_system_info(self):
|
||||||
|
"""Get system information."""
|
||||||
|
endpoint = "magicBox.cgi?action=getSystemInfo"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_serial_number(self):
|
||||||
|
"""Get the device serial number."""
|
||||||
|
endpoint = "magicBox.cgi?action=getSerialNo"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_hardware_version(self):
|
||||||
|
"""Get the hardware version."""
|
||||||
|
endpoint = "magicBox.cgi?action=getHardwareVersion"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_device_type(self):
|
||||||
|
"""Get the device type."""
|
||||||
|
endpoint = "magicBox.cgi?action=getDeviceType"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_basic_config(self):
|
||||||
|
"""i"""
|
||||||
|
endpoint = "configManager.cgi?action=getConfig&name=Network"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_basic_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set basic network configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=setConfig"
|
||||||
|
return self._post(endpoint, data=params)
|
||||||
|
|
||||||
|
def get_general_config(self):
|
||||||
|
"""Get general system configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=getConfig&name=General"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_general_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set general system configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=setConfig"
|
||||||
|
return self._get(endpoint, params=params)
|
||||||
|
|
||||||
|
def get_system_time(self):
|
||||||
|
"""Get the current system time."""
|
||||||
|
endpoint = "global.cgi?action=getCurrentTime"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_system_time(self, date: str, time: str):
|
||||||
|
"""
|
||||||
|
Set the system time.
|
||||||
|
Date format : YYYY-MM-DD
|
||||||
|
Time format : HH-MM-SS
|
||||||
|
Combines date and time with %20 between them.
|
||||||
|
%20 character used for add space
|
||||||
|
"""
|
||||||
|
# Validate the date format (YYYY-MM-DD)
|
||||||
|
date_pattern = r"^\d{4}-\d{2}-\d{2}$"
|
||||||
|
if not re.match(date_pattern, date):
|
||||||
|
raise ValueError("Invalid date format. Expected YYYY-MM-DD.")
|
||||||
|
|
||||||
|
# Validate the time format (HH:MM:SS)
|
||||||
|
time_pattern = r"^\d{2}:\d{2}:\d{2}$"
|
||||||
|
if not re.match(time_pattern, time):
|
||||||
|
raise ValueError("Invalid time format. Expected HH:MM:SS.")
|
||||||
|
|
||||||
|
device_datetime = f"{date}%20{time}"
|
||||||
|
endpoint = f"global.cgi?action=setCurrentTime&time={device_datetime}"
|
||||||
|
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def reboot_device(self):
|
||||||
|
"""Reboot the device."""
|
||||||
|
endpoint = "configManager.cgi?action=reboot"
|
||||||
|
return self._post(endpoint, data={})
|
||||||
|
|
||||||
|
def shutdown_device(self):
|
||||||
|
"""Shut down the device."""
|
||||||
|
endpoint = "configManager.cgi?action=shutdown"
|
||||||
|
return self._post(endpoint, data={})
|
||||||
|
|
||||||
|
def get_language_caps(self):
|
||||||
|
"""Get supported language capabilities."""
|
||||||
|
endpoint = "magicBox.cgi?action=getLanguageCaps"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_language_config(self):
|
||||||
|
"""Get current language configuration."""
|
||||||
|
endpoint = "magicBox.cgi?action=getLanguageConfig"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_language_config(self, language: str):
|
||||||
|
"""Set the device language."""
|
||||||
|
endpoint = "magicBox.cgi?action=setLanguageConfig"
|
||||||
|
data = {"Language": language}
|
||||||
|
return self._post(endpoint, data=data)
|
||||||
|
|
||||||
|
def get_locales_config(self):
|
||||||
|
"""Get current locales configuration."""
|
||||||
|
endpoint = "magicBox.cgi?action=getLocalesConfig"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_locales_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set locales configuration."""
|
||||||
|
endpoint = "magicBox.cgi?action=setLocalesConfig"
|
||||||
|
return self._post(endpoint, data=params)
|
||||||
|
|
||||||
|
def get_group_info(self, name):
|
||||||
|
"""
|
||||||
|
Retrieves information about a specific user group.
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
raise ValueError("The 'name' parameter is required and cannot be empty.")
|
||||||
|
endpoint = f"userManager.cgi?action=getGroupInfoAll&name={name}"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_group_info_all(self):
|
||||||
|
"""
|
||||||
|
Retrieves information about all user groups.
|
||||||
|
"""
|
||||||
|
endpoint = "userManager.cgi?action=getGroupInfoAll"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def enroll_new_user(
|
||||||
|
self,
|
||||||
|
card_name: str,
|
||||||
|
card_no: str,
|
||||||
|
user_id: str,
|
||||||
|
card_status: int = 0,
|
||||||
|
card_type: int = 0,
|
||||||
|
password: str = "",
|
||||||
|
doors: list[int] = None,
|
||||||
|
time_sections: list[int] = None,
|
||||||
|
vto_position: str = "",
|
||||||
|
valid_date_start: str = "",
|
||||||
|
valid_date_end: str = "",
|
||||||
|
is_valid: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Enroll a new user with access control card details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
card_name (str): Card name, up to 32 characters.
|
||||||
|
card_no (str): Card number,Must be unique.
|
||||||
|
user_id (str): User ID,Must be unique..
|
||||||
|
card_status (int): Card status, default is 0 (Normal).
|
||||||
|
card_type (int): Card type, default is 0 (Ordinary card).
|
||||||
|
password (str): Card password (default is an empty string).
|
||||||
|
doors (list[int]): Door permissions (default is None).
|
||||||
|
time_sections (list[int]): Time sections corresponding to
|
||||||
|
door permissions (default is None).
|
||||||
|
vto_position (str): Door number linked with indoor monitor (default is an empty string).
|
||||||
|
valid_date_start (str): Start time of the validity period,
|
||||||
|
format "yyyyMMdd hhmmss" (default is an empty string).
|
||||||
|
valid_date_end (str): End time of the validity period,
|
||||||
|
format "yyyyMMdd hhmmss" (default is an empty string).
|
||||||
|
is_valid (bool): Validity of the card, default is True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response from the server.
|
||||||
|
"""
|
||||||
|
endpoint = "recordUpdater.cgi?action=insert&name=AccessControlCard"
|
||||||
|
endpoint += f"&CardName={card_name}"
|
||||||
|
endpoint += f"&CardNo={card_no}"
|
||||||
|
endpoint += f"&UserID={user_id}"
|
||||||
|
endpoint += f"&CardStatus={card_status}"
|
||||||
|
endpoint += f"&CardType={card_type}"
|
||||||
|
|
||||||
|
if password:
|
||||||
|
endpoint += f"&Password={password}"
|
||||||
|
if doors:
|
||||||
|
for index, door in enumerate(doors):
|
||||||
|
endpoint += f"&Doors[{index}]={door}"
|
||||||
|
if time_sections:
|
||||||
|
for index, section in enumerate(time_sections):
|
||||||
|
endpoint += f"&TimeSections[{index}]={section}"
|
||||||
|
if vto_position:
|
||||||
|
endpoint += f"&VTOPosition={vto_position}"
|
||||||
|
if valid_date_start:
|
||||||
|
endpoint += f"&ValidDateStart={valid_date_start}"
|
||||||
|
if valid_date_end:
|
||||||
|
endpoint += f"&ValidDateEnd={valid_date_end}"
|
||||||
|
endpoint += f"&IsValid={'true' if is_valid else 'false'}"
|
||||||
|
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_user_info_all(self):
|
||||||
|
"""
|
||||||
|
Retrieves information about all users.
|
||||||
|
"""
|
||||||
|
endpoint = "userManager.cgi?action=getUserInfoAll"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def get_user_info(self, username: str):
|
||||||
|
"""
|
||||||
|
Retrieves information about a specific user.
|
||||||
|
"""
|
||||||
|
if not username:
|
||||||
|
raise ValueError("The 'name' parameter is required and cannot be empty.")
|
||||||
|
endpoint = f"userManager.cgi?action=getUserInfo&name={username}"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def add_user(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
group: str,
|
||||||
|
sharable: bool,
|
||||||
|
reserved: bool,
|
||||||
|
memo: str = "",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Add a new user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): The username of the new user.
|
||||||
|
password (str): The password for the new user.
|
||||||
|
group (str): The group of the new user, either "admin" or "user".
|
||||||
|
sharable (bool): Whether the user can have multi-point login.
|
||||||
|
reserved (bool): Whether the user is reserved and cannot be deleted.
|
||||||
|
memo (str): An optional memo for the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response from the server.
|
||||||
|
"""
|
||||||
|
all_groups_info = self.get_group_info_all()
|
||||||
|
name_list = [
|
||||||
|
value for key, value in all_groups_info.items() if key.endswith(".Name")
|
||||||
|
]
|
||||||
|
if group not in name_list:
|
||||||
|
raise ValueError(f"Invalid group. It must be comes in {name_list}.")
|
||||||
|
|
||||||
|
endpoint = (
|
||||||
|
f"userManager.cgi?action=addUser&"
|
||||||
|
f"user.Name={username}&"
|
||||||
|
f"user.Password={password}&"
|
||||||
|
f"user.Group={group}&"
|
||||||
|
f"user.Sharable={'true' if sharable else 'false'}&"
|
||||||
|
f"user.Reserved={'true' if reserved else 'false'}&"
|
||||||
|
f"user.Memo={memo}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def delete_user(self, username: str):
|
||||||
|
"""Delete an existing user."""
|
||||||
|
endpoint = f"userManager.cgi?action=deleteUser&name={username}"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def fetch_attendance_logs(self):
|
||||||
|
"""Fetch attendance logs using the API."""
|
||||||
|
endpoint = "log.cgi?action=doFind"
|
||||||
|
params = {
|
||||||
|
"name": "AttendanceLog",
|
||||||
|
"SessionID": "session-id",
|
||||||
|
"count": 100,
|
||||||
|
"offset": 0,
|
||||||
|
}
|
||||||
|
return self._get(endpoint, params=params)
|
||||||
|
|
||||||
|
def get_logs(self, log_type: str = "SystemLog"):
|
||||||
|
"""Fetch logs from the device."""
|
||||||
|
endpoint = "log.cgi?action=doFind"
|
||||||
|
params = {
|
||||||
|
"name": log_type,
|
||||||
|
"SessionID": "session-id",
|
||||||
|
"count": 100,
|
||||||
|
"offset": 0,
|
||||||
|
}
|
||||||
|
return self._get(endpoint, params=params)
|
||||||
|
|
||||||
|
def get_record_config(self):
|
||||||
|
"""Get record configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=getConfig&name=Record"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_record_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set record configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=setConfig"
|
||||||
|
return self._post(endpoint, data=params)
|
||||||
|
|
||||||
|
def get_record_mode_config(self):
|
||||||
|
"""Get record mode configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=getConfig&name=RecordMode"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_record_mode_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set record mode configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=setConfig"
|
||||||
|
return self._post(endpoint, data=params)
|
||||||
|
|
||||||
|
def get_snapshot_config(self):
|
||||||
|
"""Get snapshot configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=getConfig&name=Snap"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
|
def set_snapshot_config(self, params: Dict[str, Any]):
|
||||||
|
"""Set snapshot configuration."""
|
||||||
|
endpoint = "configManager.cgi?action=setConfig"
|
||||||
|
return self._post(endpoint, data=params)
|
||||||
|
|
||||||
|
def get_control_card_rec(
|
||||||
|
self,
|
||||||
|
card_no: str = None,
|
||||||
|
start_time: datetime = None,
|
||||||
|
end_time: datetime = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get offline records from device.
|
||||||
|
start_time and end_time must be Python datetime objects, used to convert to Unix timestamp.
|
||||||
|
"""
|
||||||
|
endpoint = "recordFinder.cgi?action=find&name=AccessControlCardRec"
|
||||||
|
|
||||||
|
if start_time:
|
||||||
|
if isinstance(start_time, datetime):
|
||||||
|
start_time = int(start_time.timestamp())
|
||||||
|
else:
|
||||||
|
raise ValueError("start_time must be a Python datetime object")
|
||||||
|
|
||||||
|
if end_time:
|
||||||
|
if isinstance(end_time, datetime):
|
||||||
|
end_time = int(end_time.timestamp())
|
||||||
|
else:
|
||||||
|
raise ValueError("end_time must be a Python datetime object")
|
||||||
|
|
||||||
|
if card_no:
|
||||||
|
endpoint += f"&condition.CardNo={card_no}"
|
||||||
|
if start_time:
|
||||||
|
endpoint += f"&StartTime={start_time}"
|
||||||
|
if end_time:
|
||||||
|
endpoint += f"&EndTime={end_time}"
|
||||||
|
|
||||||
|
card_records = self._get(endpoint)
|
||||||
|
|
||||||
|
records = convert_logs_to_list(card_records)
|
||||||
|
logs = {
|
||||||
|
"records": records,
|
||||||
|
"found": card_records.get("found"),
|
||||||
|
"status_code": card_records.get("status_code"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
# dahua = DahuaAPI(ip="192.168.100.195", username="admin", password="User@123")
|
||||||
|
# result = dahua.get_system_time()
|
||||||
|
# result1 = dahua.get_control_card_rec(start_time="1736418923")
|
||||||
|
# result1 = dahua.get_snapshot_config()
|
||||||
|
# result1 = dahua.delete_user(username="TestUser")
|
||||||
|
# result2 = dahua.get_user_info_all()
|
||||||
|
# result = dahua.get_user_info(name="TestUser")
|
||||||
|
# result1 = dahua.add_user(
|
||||||
|
# username="TestUser",
|
||||||
|
# password="Test@!12",
|
||||||
|
# group="admin",
|
||||||
|
# sharable=True,
|
||||||
|
# reserved=False,
|
||||||
|
# memo="TestUser Group",
|
||||||
|
# )
|
||||||
|
# response = dahua.enroll_new_user(
|
||||||
|
# card_name="Nikhil Ravi", # Card name
|
||||||
|
# card_no="987564", # Card number
|
||||||
|
# user_id="CTS437", # User ID
|
||||||
|
# card_status=0, # Card status (Normal)
|
||||||
|
# card_type=0, # Card type (Ordinary card)
|
||||||
|
# password="Nikh@!12", # Password for card + password
|
||||||
|
# )
|
||||||
122
biometric/etimeoffice.py
Normal file
122
biometric/etimeoffice.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
ETimeOfficeAPI Class for interacting with the ETimeOffice API.
|
||||||
|
|
||||||
|
This module provides an interface to interact with the ETimeOffice API.
|
||||||
|
It includes methods to fetch punch data, validate dates, and convert response data
|
||||||
|
into Python datetime objects for further processing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
|
||||||
|
class ETimeOfficeAPI:
|
||||||
|
"""
|
||||||
|
A client for interacting with the ETimeOffice API to fetch punch data and related information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, username, password, base_url="https://api.etimeoffice.com/api/"):
|
||||||
|
"""
|
||||||
|
Initialize the ETimeOfficeAPI client.
|
||||||
|
"""
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.base_url = base_url.rstrip("/") + "/"
|
||||||
|
|
||||||
|
def _is_valid_date(self, date_str, with_time=True):
|
||||||
|
"""
|
||||||
|
Validate date format.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if with_time:
|
||||||
|
datetime.strptime(date_str, "%d/%m/%Y_%H:%M")
|
||||||
|
else:
|
||||||
|
datetime.strptime(date_str, "%d/%m/%Y")
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _convert_punch_dates(self, response_data):
|
||||||
|
"""
|
||||||
|
Convert punch data strings into datetime objects.
|
||||||
|
"""
|
||||||
|
if not response_data.get("Error", True):
|
||||||
|
if "PunchData" in response_data:
|
||||||
|
for punch in response_data["PunchData"]:
|
||||||
|
try:
|
||||||
|
punch["PunchDate"] = datetime.strptime(
|
||||||
|
punch["PunchDate"], "%d/%m/%Y %H:%M:%S"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if "InOutPunchData" in response_data:
|
||||||
|
for punch in response_data["InOutPunchData"]:
|
||||||
|
try:
|
||||||
|
punch["DateString"] = datetime.strptime(
|
||||||
|
punch["DateString"], "%d/%m/%Y"
|
||||||
|
).date()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
punch["INTime"] = datetime.strptime(
|
||||||
|
punch["INTime"], "%H:%M"
|
||||||
|
).time()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
punch["OUTTime"] = datetime.strptime(
|
||||||
|
punch["OUTTime"], "%H:%M"
|
||||||
|
).time()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def _fetch_data(self, endpoint, emp_code, from_date, to_date, with_time=True):
|
||||||
|
"""
|
||||||
|
Make an authenticated API request and parse punch data.
|
||||||
|
"""
|
||||||
|
if not (
|
||||||
|
self._is_valid_date(from_date, with_time)
|
||||||
|
and self._is_valid_date(to_date, with_time)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"Error": True,
|
||||||
|
"Msg": "Error: Invalid date format. Expected format: "
|
||||||
|
+ ("DD/MM/YYYY_HH:MM" if with_time else "DD/MM/YYYY"),
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{self.base_url}{endpoint}?Empcode={emp_code}&FromDate={from_date}&ToDate={to_date}"
|
||||||
|
response = requests.get(
|
||||||
|
url, auth=HTTPBasicAuth(self.username, self.password), timeout=20
|
||||||
|
)
|
||||||
|
return self._convert_punch_dates(response.json())
|
||||||
|
|
||||||
|
def download_punch_data(self, from_date, to_date, emp_code="ALL"):
|
||||||
|
"""
|
||||||
|
Download punch data with timestamps.
|
||||||
|
"""
|
||||||
|
return self._fetch_data(
|
||||||
|
"DownloadPunchData", emp_code, from_date, to_date, with_time=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def download_punch_data_mcid(self, from_date, to_date, emp_code="ALL"):
|
||||||
|
"""
|
||||||
|
Download punch data with MCID (Machine Code ID).
|
||||||
|
"""
|
||||||
|
return self._fetch_data(
|
||||||
|
"DownloadPunchDataMCID", emp_code, from_date, to_date, with_time=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def download_in_out_punch_data(self, from_date, to_date, emp_code="ALL"):
|
||||||
|
"""
|
||||||
|
Download in and out punch data (without timestamps).
|
||||||
|
"""
|
||||||
|
return self._fetch_data(
|
||||||
|
"DownloadInOutPunchData", emp_code, from_date, to_date, with_time=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# api = ETimeOfficeAPI(username={corporateid}:{usename}:{password}:true",password="")
|
||||||
|
# response = api.download_punch_data(from_date="25/03/2025_00:00",to_date="25/03/2025_12:22")
|
||||||
37
biometric/filters.py
Normal file
37
biometric/filters.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Module for defining filters related to biometric devices.
|
||||||
|
|
||||||
|
This module contains the definition of the BiometricDeviceFilter class,
|
||||||
|
which is used to filter instances of BiometricDevices
|
||||||
|
"""
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
|
||||||
|
from base.filters import FilterSet
|
||||||
|
from biometric.models import BiometricDevices
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricDeviceFilter(FilterSet):
|
||||||
|
"""
|
||||||
|
Filter class for querying biometric devices.
|
||||||
|
|
||||||
|
This class defines filters for querying instances of BiometricDevices
|
||||||
|
based on various criteria such as name, machine type, and activity status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
search = django_filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = BiometricDevices
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"machine_type",
|
||||||
|
"is_active",
|
||||||
|
"is_scheduler",
|
||||||
|
"is_live",
|
||||||
|
]
|
||||||
362
biometric/forms.py
Normal file
362
biometric/forms.py
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Module containing forms for managing biometric devices and associated data.
|
||||||
|
|
||||||
|
This module provides Django forms for creating and managing biometric devices,
|
||||||
|
employee biometric data, COSEC users, and related configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from base.forms import Form, ModelForm
|
||||||
|
from base.methods import reload_queryset
|
||||||
|
from employee.models import Employee
|
||||||
|
from horilla.horilla_middlewares import _thread_locals
|
||||||
|
from horilla_widgets.forms import default_select_option_template
|
||||||
|
|
||||||
|
from .models import BiometricDevices, BiometricEmployees
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricDeviceForm(ModelForm):
|
||||||
|
"""
|
||||||
|
Form for creating and updating biometric device configurations.
|
||||||
|
|
||||||
|
This form is used to create and update biometric device configurations.
|
||||||
|
It includes fields for specifying the device name, IP address, TCP communication port,
|
||||||
|
and other relevant settings. Additionally, it excludes fields related to scheduler
|
||||||
|
settings and device activation status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = BiometricDevices
|
||||||
|
fields = "__all__"
|
||||||
|
exclude = [
|
||||||
|
"is_scheduler",
|
||||||
|
"scheduler_duration",
|
||||||
|
"last_fetch_date",
|
||||||
|
"last_fetch_time",
|
||||||
|
"is_active",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"machine_type": forms.Select(
|
||||||
|
attrs={
|
||||||
|
"id": "machineTypeInput",
|
||||||
|
"onchange": "machineTypeChange($(this))",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"bio_password": forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"class": "oh-input oh-input--password w-100",
|
||||||
|
"type": "password",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
company_widget = self.fields["company_id"].widget
|
||||||
|
if isinstance(company_widget, forms.Select):
|
||||||
|
company_widget.option_template_name = default_select_option_template
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricDeviceSchedulerForm(ModelForm):
|
||||||
|
"""
|
||||||
|
Form for updating the scheduler duration of a biometric device.
|
||||||
|
|
||||||
|
This form is used to update the scheduler duration of a biometric
|
||||||
|
device to fetch attendance data.
|
||||||
|
It includes a field for entering the scheduler duration in the format HH:MM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = BiometricDevices
|
||||||
|
fields = ["scheduler_duration"]
|
||||||
|
labels = {
|
||||||
|
"scheduler_duration": _("Enter the duration in the format HH:MM"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBiometricAddForm(Form):
|
||||||
|
"""
|
||||||
|
Form for adding employees to a biometric device.
|
||||||
|
|
||||||
|
This form allows administrators to add employees to a biometric device
|
||||||
|
for biometric authentication. It includes a field for selecting employees from
|
||||||
|
a queryset and ensures that only active employees not already associated with
|
||||||
|
a 'zk' type biometric device are available for selection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
employee_ids = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Employee.objects.all(),
|
||||||
|
widget=forms.SelectMultiple(),
|
||||||
|
label=_("Employees"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.request = getattr(_thread_locals, "request")
|
||||||
|
self.device_id = (
|
||||||
|
self.request.resolver_match.kwargs.get("device_id", None)
|
||||||
|
if self.request.resolver_match
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
self.device = BiometricDevices.find(self.device_id)
|
||||||
|
zk_employee_ids = BiometricEmployees.objects.filter(
|
||||||
|
device_id=self.device
|
||||||
|
).values_list("employee_id", flat=True)
|
||||||
|
self.fields["employee_ids"].queryset = Employee.objects.filter(
|
||||||
|
is_active=True
|
||||||
|
).exclude(id__in=zk_employee_ids)
|
||||||
|
|
||||||
|
|
||||||
|
class CosecUserAddForm(Form):
|
||||||
|
"""
|
||||||
|
Form for adding users to a COSEC biometric device.
|
||||||
|
|
||||||
|
This form allows administrators to add multiple users to a COSEC biometric device
|
||||||
|
for biometric authentication. It includes a field for selecting users from
|
||||||
|
a queryset and ensures that only active users not already associated with
|
||||||
|
a 'cosec' type biometric device are available for selection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
employee_ids = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Employee.objects.all(),
|
||||||
|
widget=forms.SelectMultiple(),
|
||||||
|
label=_("Employees"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, device_id=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
cosec_employee_ids = BiometricEmployees.objects.filter(
|
||||||
|
device_id=device_id
|
||||||
|
).values_list("employee_id", flat=True)
|
||||||
|
self.fields["employee_ids"].queryset = Employee.objects.filter(
|
||||||
|
is_active=True
|
||||||
|
).exclude(id__in=cosec_employee_ids)
|
||||||
|
|
||||||
|
|
||||||
|
class COSECUserForm(Form):
|
||||||
|
"""
|
||||||
|
Form for adding or updating users in a COSEC biometric device.
|
||||||
|
|
||||||
|
This form allows administrators to add or update users in a COSEC biometric
|
||||||
|
device. It includes fields for specifying the user's name, whether the user
|
||||||
|
is active, whether the user is a VIP user, whether validity is enabled for
|
||||||
|
the user, the validity end date, and whether to bypass finger-based
|
||||||
|
authentication for the user. It provides validation to ensure that the
|
||||||
|
name does not exceed 15 characters and that the validity end date is
|
||||||
|
provided when validity is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = forms.CharField(
|
||||||
|
label=_("Employee Name"),
|
||||||
|
help_text=_("15 characters max."),
|
||||||
|
widget=forms.TextInput(attrs={"class": "oh-input w-100"}),
|
||||||
|
)
|
||||||
|
user_active = forms.BooleanField(
|
||||||
|
initial=False,
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(),
|
||||||
|
)
|
||||||
|
vip = forms.BooleanField(
|
||||||
|
initial=False,
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(),
|
||||||
|
)
|
||||||
|
validity_enable = forms.BooleanField(
|
||||||
|
initial=False,
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(),
|
||||||
|
)
|
||||||
|
validity_end_date = forms.DateField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.DateInput(
|
||||||
|
attrs={"type": "date", "class": "oh-input w-100 form-control"}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
by_pass_finger = forms.BooleanField(
|
||||||
|
initial=False,
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if len(cleaned_data["name"]) > 15:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Maximum 15 characters allowed for Name in COSEC Biometric Device"
|
||||||
|
)
|
||||||
|
if cleaned_data["validity_enable"]:
|
||||||
|
if cleaned_data.get("validity_end_date") is None:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"When the Validity field is enabled, a Validity End Date is required."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DahuaUserForm(Form):
|
||||||
|
"""
|
||||||
|
This form is used to map a Horilla employee to a user entry on a Dahua biometric device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CARD_STATUS_CHOICES = [
|
||||||
|
(0, "Normal"),
|
||||||
|
(1 << 0, "Reported for loss"),
|
||||||
|
(1 << 1, "Canceled"),
|
||||||
|
(1 << 2, "Frozen"),
|
||||||
|
(1 << 3, "Arrearage"),
|
||||||
|
(1 << 4, "Overdue"),
|
||||||
|
(1 << 5, "Pre-arrearage (The door still can be unlocked with a voice prompt)"),
|
||||||
|
]
|
||||||
|
CARD_TYPE_CHOICES = [
|
||||||
|
(0, "Ordinary card"),
|
||||||
|
(1, "VIP card"),
|
||||||
|
(2, "Guest card"),
|
||||||
|
(3, "Patrol card"),
|
||||||
|
(4, "Blocklist card"),
|
||||||
|
(5, "Duress card"),
|
||||||
|
]
|
||||||
|
employee = forms.ModelChoiceField(
|
||||||
|
queryset=Employee.objects.all(),
|
||||||
|
widget=forms.Select(),
|
||||||
|
label=_("Employee"),
|
||||||
|
)
|
||||||
|
card_no = forms.CharField(max_length=50, required=True, label=_("Card Number"))
|
||||||
|
user_id = forms.CharField(max_length=50, required=True, label=_("User ID"))
|
||||||
|
card_status = forms.ChoiceField(
|
||||||
|
choices=CARD_STATUS_CHOICES,
|
||||||
|
required=False,
|
||||||
|
label=_("Card Status"),
|
||||||
|
initial=0,
|
||||||
|
)
|
||||||
|
card_type = forms.ChoiceField(
|
||||||
|
choices=CARD_TYPE_CHOICES, required=False, label=_("Card Type")
|
||||||
|
)
|
||||||
|
password = forms.CharField(max_length=50, required=False, label=_("Password"))
|
||||||
|
forms.DateTimeField(
|
||||||
|
widget=forms.DateTimeInput(
|
||||||
|
attrs={"class": "oh-input w-100", "type": "datetime-local"}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
valid_date_start = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
label=_("Valid Date Start"),
|
||||||
|
widget=forms.DateTimeInput(
|
||||||
|
attrs={"class": "oh-input w-100", "type": "datetime-local"}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
valid_date_end = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
label=_("Valid Date End"),
|
||||||
|
widget=forms.DateTimeInput(
|
||||||
|
attrs={"class": "oh-input w-100", "type": "datetime-local"}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = getattr(_thread_locals, "request")
|
||||||
|
self.device_id = (
|
||||||
|
self.request.resolver_match.kwargs.get("device_id", None)
|
||||||
|
if self.request.resolver_match
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
reload_queryset(self.fields)
|
||||||
|
self.fields["employee"].widget.attrs.update(
|
||||||
|
{
|
||||||
|
"hx-include": "#dahuaBiometricUserForm",
|
||||||
|
"hx-target": "#id_user_id",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-trigger": "change",
|
||||||
|
"hx-get": "/biometric/find-employee-badge-id",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
device = None
|
||||||
|
error_fields = {}
|
||||||
|
card_no = cleaned_data.get("card_no")
|
||||||
|
user_id = cleaned_data.get("user_id")
|
||||||
|
|
||||||
|
if self.device_id:
|
||||||
|
device = BiometricDevices.find(self.device_id)
|
||||||
|
|
||||||
|
if card_no and device:
|
||||||
|
if BiometricEmployees.objects.filter(
|
||||||
|
dahua_card_no=card_no, device_id=device
|
||||||
|
).exists():
|
||||||
|
error_fields["card_no"] = _("This Card Number already exists.")
|
||||||
|
|
||||||
|
if user_id and device:
|
||||||
|
if BiometricEmployees.objects.filter(
|
||||||
|
user_id=user_id, device_id=device
|
||||||
|
).exists():
|
||||||
|
error_fields["user_id"] = _("This User ID already exists.")
|
||||||
|
|
||||||
|
if error_fields:
|
||||||
|
raise forms.ValidationError(error_fields)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class MapBioUsers(ModelForm):
|
||||||
|
"""
|
||||||
|
Form for mapping biometric users to Horilla employees.
|
||||||
|
|
||||||
|
This form is used to associate a biometric user (from a biometric device) with
|
||||||
|
an employee in the Horilla system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = BiometricEmployees
|
||||||
|
fields = ["employee_id", "user_id"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = getattr(_thread_locals, "request")
|
||||||
|
self.device_id = (
|
||||||
|
self.request.resolver_match.kwargs.get("device_id", None)
|
||||||
|
if self.request.resolver_match
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.device_id:
|
||||||
|
already_mapped_employees = BiometricEmployees.objects.filter(
|
||||||
|
device_id=self.device_id
|
||||||
|
).values_list("employee_id", flat=True)
|
||||||
|
self.fields["employee_id"].queryset = Employee.objects.exclude(
|
||||||
|
Q(id__in=already_mapped_employees) | Q(is_active=False)
|
||||||
|
)
|
||||||
|
self.fields["user_id"].required = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
user_id = cleaned_data.get("user_id")
|
||||||
|
user_id_label = self.fields["user_id"].label or "User ID"
|
||||||
|
if self.device_id and user_id:
|
||||||
|
if BiometricEmployees.objects.filter(
|
||||||
|
user_id=user_id, device_id=self.device_id
|
||||||
|
).exists():
|
||||||
|
raise forms.ValidationError(
|
||||||
|
{
|
||||||
|
"user_id": _(
|
||||||
|
"This biometric %(label)s is already mapped with an employee"
|
||||||
|
)
|
||||||
|
% {"label": user_id_label}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
302
biometric/models.py
Normal file
302
biometric/models.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
This module contains Django models for managing biometric devices
|
||||||
|
and employee attendance within a company.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MaxValueValidator
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from base.horilla_company_manager import HorillaCompanyManager
|
||||||
|
from base.models import Company
|
||||||
|
from employee.models import Employee
|
||||||
|
from horilla.models import HorillaModel
|
||||||
|
|
||||||
|
|
||||||
|
def validate_schedule_time_format(value):
|
||||||
|
"""
|
||||||
|
this method is used to validate the format of duration like fields.
|
||||||
|
"""
|
||||||
|
if len(value) > 6:
|
||||||
|
raise ValidationError(_("Invalid format, it should be HH:MM format"))
|
||||||
|
try:
|
||||||
|
hour, minute = value.split(":")
|
||||||
|
hour = int(hour)
|
||||||
|
minute = int(minute)
|
||||||
|
if len(str(hour)) > 3 or minute not in range(60):
|
||||||
|
raise ValidationError(_("Invalid time"))
|
||||||
|
if hour == 0 and minute == 0:
|
||||||
|
raise ValidationError(_("Both hour and minute cannot be zero"))
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValidationError(_("Invalid format, it should be HH:MM format")) from error
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricDevices(HorillaModel):
|
||||||
|
"""
|
||||||
|
Model: BiometricDevices
|
||||||
|
|
||||||
|
Represents a biometric device used for attendance tracking within a
|
||||||
|
company. Each device can be of different types such as ZKTeco Biometric,
|
||||||
|
Anviz Biometric, or Matrix COSEC Biometric.The model includes fields for
|
||||||
|
device details, authentication credentials, scheduling information, and
|
||||||
|
company association.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BIO_DEVICE_TYPE = [
|
||||||
|
("zk", _("ZKTeco / eSSL Biometric")),
|
||||||
|
("anviz", _("Anviz Biometric")),
|
||||||
|
("cosec", _("Matrix COSEC Biometric")),
|
||||||
|
("dahua", _("Dahua Biometric")),
|
||||||
|
("etimeoffice", _("e-Time Office")),
|
||||||
|
]
|
||||||
|
BIO_DEVICE_DIRECTION = [
|
||||||
|
("in", _("In Device")),
|
||||||
|
("out", _("Out Device")),
|
||||||
|
("alternate", _("Alternate In/Out Device")),
|
||||||
|
("system", _("System Direction(In/Out) Device")),
|
||||||
|
]
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
|
||||||
|
name = models.CharField(max_length=100, verbose_name=_("Name"))
|
||||||
|
machine_type = models.CharField(
|
||||||
|
max_length=18, choices=BIO_DEVICE_TYPE, null=True, verbose_name=_("Device Type")
|
||||||
|
)
|
||||||
|
machine_ip = models.CharField(
|
||||||
|
max_length=150, null=True, blank=True, default="", verbose_name=_("Machine IP")
|
||||||
|
)
|
||||||
|
port = models.IntegerField(null=True, blank=True, verbose_name=_("Port No"))
|
||||||
|
zk_password = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True, default="0", verbose_name=_("Password")
|
||||||
|
)
|
||||||
|
bio_username = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True, default="", verbose_name=_("Username")
|
||||||
|
)
|
||||||
|
bio_password = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True, verbose_name=_("Password")
|
||||||
|
)
|
||||||
|
anviz_request_id = models.CharField(
|
||||||
|
max_length=200, null=True, blank=True, verbose_name=_("Request ID")
|
||||||
|
)
|
||||||
|
api_url = models.CharField(
|
||||||
|
max_length=200, null=True, blank=True, verbose_name=_("API Url")
|
||||||
|
)
|
||||||
|
api_key = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True, verbose_name=_("API Key")
|
||||||
|
)
|
||||||
|
api_secret = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True, verbose_name=_("API Secret")
|
||||||
|
)
|
||||||
|
api_token = models.CharField(max_length=500, null=True, blank=True)
|
||||||
|
api_expires = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
is_live = models.BooleanField(default=False, verbose_name=_("Is Live"))
|
||||||
|
is_scheduler = models.BooleanField(default=False, verbose_name=_("Is Scheduled"))
|
||||||
|
scheduler_duration = models.CharField(
|
||||||
|
null=True,
|
||||||
|
default="00:00",
|
||||||
|
max_length=10,
|
||||||
|
validators=[validate_schedule_time_format],
|
||||||
|
)
|
||||||
|
last_fetch_date = models.DateField(null=True, blank=True)
|
||||||
|
last_fetch_time = models.TimeField(null=True, blank=True)
|
||||||
|
device_direction = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=BIO_DEVICE_DIRECTION,
|
||||||
|
default="system",
|
||||||
|
verbose_name=_("Device Direction"),
|
||||||
|
)
|
||||||
|
company_id = models.ForeignKey(
|
||||||
|
Company,
|
||||||
|
null=True,
|
||||||
|
editable=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
verbose_name=_("Company"),
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = HorillaCompanyManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} - {self.machine_type}"
|
||||||
|
|
||||||
|
def clean(self, *args, **kwargs):
|
||||||
|
super().clean(*args, **kwargs)
|
||||||
|
required_fields = {}
|
||||||
|
|
||||||
|
if self.machine_type in ("zk", "cosec", "dahua"):
|
||||||
|
if not self.machine_ip:
|
||||||
|
required_fields["machine_ip"] = _(
|
||||||
|
"The Machine IP is required for the selected biometric device."
|
||||||
|
)
|
||||||
|
if not self.port:
|
||||||
|
required_fields["port"] = _(
|
||||||
|
"The Port Number is required for the selected biometric device."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.machine_type == "zk":
|
||||||
|
if not self.zk_password:
|
||||||
|
required_fields["zk_password"] = _(
|
||||||
|
"The password is required for ZKTeco Biometric Device."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
int(self.zk_password)
|
||||||
|
except ValueError:
|
||||||
|
required_fields["zk_password"] = _(
|
||||||
|
"The password must be an integer (numeric) value for\
|
||||||
|
ZKTeco Biometric Device."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.machine_type in ("cosec", "dahua"):
|
||||||
|
if not self.bio_username:
|
||||||
|
required_fields["bio_username"] = _(
|
||||||
|
"The Username is required for the selected biometric device."
|
||||||
|
)
|
||||||
|
if not self.bio_password:
|
||||||
|
required_fields["bio_password"] = _(
|
||||||
|
"The Password is required for the selected biometric device."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.machine_type == "anviz":
|
||||||
|
if not self.anviz_request_id:
|
||||||
|
required_fields["anviz_request_id"] = _(
|
||||||
|
"The Request ID is required for the Anviz Biometric Device."
|
||||||
|
)
|
||||||
|
if not self.api_url:
|
||||||
|
required_fields["api_url"] = _(
|
||||||
|
"The API URL is required for Anviz Biometric Device."
|
||||||
|
)
|
||||||
|
if not self.api_key:
|
||||||
|
required_fields["api_key"] = _(
|
||||||
|
"The API Key is required for Anviz Biometric Device."
|
||||||
|
)
|
||||||
|
if not self.api_secret:
|
||||||
|
required_fields["api_secret"] = _(
|
||||||
|
"The API Secret is required for Anviz Biometric Device."
|
||||||
|
)
|
||||||
|
if self.anviz_request_id and self.api_key and self.api_secret:
|
||||||
|
payload = {
|
||||||
|
"header": {
|
||||||
|
"nameSpace": "authorize.token",
|
||||||
|
"nameAction": "token",
|
||||||
|
"version": "1.0",
|
||||||
|
"requestId": self.anviz_request_id,
|
||||||
|
"timestamp": "2022-10-21T07:39:07+00:00",
|
||||||
|
},
|
||||||
|
"payload": {"api_key": self.api_key, "api_secret": self.api_secret},
|
||||||
|
}
|
||||||
|
error = {
|
||||||
|
"header": {"nameSpace": "System", "name": "Exception"},
|
||||||
|
"payload": {"type": "AUTH_ERROR", "message": "AUTH_ERROR"},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
self.api_url,
|
||||||
|
json=payload,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise ValidationError(
|
||||||
|
{f"API call failed with status code {response.status_code}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
api_response = response.json()
|
||||||
|
if api_response == error:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"api_url": _(
|
||||||
|
"Authentication failed. Please check your API Url\
|
||||||
|
, API Key and API Secret."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = api_response["payload"]
|
||||||
|
api_token = payload["token"]
|
||||||
|
api_expires = payload["expires"]
|
||||||
|
self.api_token = api_token
|
||||||
|
self.api_expires = api_expires
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"api_url": _(
|
||||||
|
"Authentication failed. Please check your API Url , API Key\
|
||||||
|
and API Secret."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) from exc
|
||||||
|
if required_fields:
|
||||||
|
raise ValidationError(required_fields)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
verbose_name = _("Biometric Device")
|
||||||
|
verbose_name_plural = _("Biometric Devices")
|
||||||
|
|
||||||
|
|
||||||
|
class BiometricEmployees(models.Model):
|
||||||
|
"""
|
||||||
|
Model: BiometricEmployees
|
||||||
|
|
||||||
|
Description:
|
||||||
|
Represents the association between employees and biometric devices for
|
||||||
|
attendance tracking within a company.Each entry in this model maps an
|
||||||
|
employee to a specific biometric device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
|
||||||
|
uid = models.IntegerField(null=True, blank=True)
|
||||||
|
ref_user_id = models.IntegerField(
|
||||||
|
null=True, blank=True, validators=[MaxValueValidator(99999999)]
|
||||||
|
)
|
||||||
|
user_id = models.CharField(max_length=100, verbose_name=_("User ID"))
|
||||||
|
dahua_card_no = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
employee_id = models.ForeignKey(
|
||||||
|
Employee, on_delete=models.CASCADE, verbose_name=_("Employee")
|
||||||
|
)
|
||||||
|
device_id = models.ForeignKey(
|
||||||
|
BiometricDevices, on_delete=models.CASCADE, null=True, blank=True
|
||||||
|
)
|
||||||
|
objects = models.Manager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.employee_id} - {self.user_id} - {self.device_id}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta class to add additional options
|
||||||
|
"""
|
||||||
|
|
||||||
|
verbose_name = _("Employee in Biometric Device")
|
||||||
|
verbose_name_plural = _("Employees in Biometric Device")
|
||||||
|
|
||||||
|
|
||||||
|
class COSECAttendanceArguments(models.Model):
|
||||||
|
"""
|
||||||
|
Model: COSECAttendanceArguments
|
||||||
|
|
||||||
|
Description:
|
||||||
|
Represents arguments related to attendance fetching for COSEC biometric
|
||||||
|
devices within a company.This model stores information such as the last
|
||||||
|
fetched roll-over count and sequence number for COSEC devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
|
||||||
|
last_fetch_roll_ovr_count = models.CharField(max_length=100, null=True)
|
||||||
|
last_fetch_seq_number = models.CharField(max_length=100, null=True)
|
||||||
|
device_id = models.ForeignKey(
|
||||||
|
BiometricDevices, on_delete=models.CASCADE, null=True, blank=True
|
||||||
|
)
|
||||||
|
objects = models.Manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("COSEC Attendance Arguments")
|
||||||
|
verbose_name_plural = _("COSEC Attendance Arguments")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.device_id} - {self.last_fetch_roll_ovr_count} - {self.last_fetch_seq_number}"
|
||||||
12
biometric/settings.py
Normal file
12
biometric/settings.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
This module extends the Django settings related to templates to include a
|
||||||
|
custom context processor for biometric functionality.
|
||||||
|
It imports the `TEMPLATES` setting from `horilla.settings` and appends a
|
||||||
|
custom context processor path to it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from horilla.settings import TEMPLATES
|
||||||
|
|
||||||
|
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
|
||||||
|
"biometric.context_processors.biometric_is_installed",
|
||||||
|
)
|
||||||
43
biometric/sidebar.py
Normal file
43
biometric/sidebar.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Biometric App sidebar configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as trans
|
||||||
|
|
||||||
|
from attendance.sidebar import SUBMENUS
|
||||||
|
from base.context_processors import biometric_app_exists
|
||||||
|
from biometric.context_processors import biometric_is_installed
|
||||||
|
|
||||||
|
biometric_submenu = {
|
||||||
|
"menu": trans("Biometric Devices"),
|
||||||
|
"redirect": reverse_lazy("view-biometric-devices"),
|
||||||
|
"accessibility": "biometric.sidebar.biometric_device_accessibility",
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBMENUS.insert(1, biometric_submenu)
|
||||||
|
|
||||||
|
|
||||||
|
def biometric_device_accessibility(request, submenu, user_perms, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Determine if the biometric device submenu should be accessible to the user.
|
||||||
|
|
||||||
|
This function checks if the biometric app exists, if the user has the
|
||||||
|
necessary permissions to view biometric devices, and if the biometric
|
||||||
|
system is installed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The HTTP request object.
|
||||||
|
submenu: The submenu being accessed.
|
||||||
|
user_perms: The permissions of the user.
|
||||||
|
*args: Additional arguments.
|
||||||
|
**kwargs: Additional keyword arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the submenu should be accessible, False otherwise.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
biometric_app_exists(None).get("biometric_app_exists")
|
||||||
|
and request.user.has_perm("biometric.view_biometricdevices")
|
||||||
|
and biometric_is_installed(None)["is_installed"]
|
||||||
|
)
|
||||||
156
biometric/urls.py
Normal file
156
biometric/urls.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"""
|
||||||
|
Module: urls
|
||||||
|
|
||||||
|
Description:
|
||||||
|
This module defines URL patterns for routing HTTP requests to views
|
||||||
|
in the biometric management application.
|
||||||
|
It imports the `path` function from `django.urls` for defining URL
|
||||||
|
patterns and imports views for handling requests.
|
||||||
|
Additionally, it imports the `BiometricDevices` model for use in URL patterns
|
||||||
|
that require device IDs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
from .models import BiometricDevices
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"view-biometric-devices/",
|
||||||
|
views.biometric_devices_view,
|
||||||
|
name="view-biometric-devices",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-live-capture",
|
||||||
|
views.biometric_device_live,
|
||||||
|
name="biometric-device-live-capture",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-schedule/<uuid:device_id>/",
|
||||||
|
views.biometric_device_schedule,
|
||||||
|
name="biometric-device-schedule",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-unschedule/<uuid:device_id>/",
|
||||||
|
views.biometric_device_unschedule,
|
||||||
|
name="biometric-device-unschedule",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-test/<uuid:device_id>/",
|
||||||
|
views.biometric_device_test,
|
||||||
|
name="biometric-device-test",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-fetch-logs/<uuid:device_id>/",
|
||||||
|
views.biometric_device_fetch_logs,
|
||||||
|
name="biometric-device-fetch-logs",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-bulk-fetch-logs/",
|
||||||
|
views.biometric_device_bulk_fetch_logs,
|
||||||
|
name="biometric-device-bulk-fetch-logs",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-add",
|
||||||
|
views.biometric_device_add,
|
||||||
|
name="biometric-device-add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-edit/<uuid:device_id>/",
|
||||||
|
views.biometric_device_edit,
|
||||||
|
name="biometric-device-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-delete/<uuid:device_id>/",
|
||||||
|
views.biometric_device_delete,
|
||||||
|
name="biometric-device-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-archive/<uuid:device_id>/",
|
||||||
|
views.biometric_device_archive,
|
||||||
|
name="biometric-device-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-device-employees/<uuid:device_id>/",
|
||||||
|
views.biometric_device_employees,
|
||||||
|
name="biometric-device-employees",
|
||||||
|
kwargs={"model": BiometricDevices},
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"search-employee-in-device",
|
||||||
|
views.search_employee_device,
|
||||||
|
name="search-employee-in-device",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"find-employee-badge-id",
|
||||||
|
views.find_employee_badge_id,
|
||||||
|
name="find-employee-badge-id",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"add-biometric-user/<uuid:device_id>/",
|
||||||
|
views.add_biometric_user,
|
||||||
|
name="add-biometric-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"map-biometric-users/<uuid:device_id>/",
|
||||||
|
views.map_biometric_users,
|
||||||
|
name="map-biometric-users",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"add-dahua-biometric-user/<uuid:device_id>/",
|
||||||
|
views.add_dahua_biometric_user,
|
||||||
|
name="add-dahua-biometric-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-dahua-user/<uuid:obj_id>",
|
||||||
|
views.delete_dahua_user,
|
||||||
|
name="delete-dahua-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-dahua-user",
|
||||||
|
views.delete_dahua_user,
|
||||||
|
name="delete-dahua-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-etimeoffice-user",
|
||||||
|
views.delete_etimeoffice_user,
|
||||||
|
name="delete-etimeoffice-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-etimeoffice-user/<uuid:obj_id>",
|
||||||
|
views.delete_etimeoffice_user,
|
||||||
|
name="delete-etimeoffice-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"enable-cosec-face-recognition/<str:user_id>/<uuid:device_id>/",
|
||||||
|
views.enable_cosec_face_recognition,
|
||||||
|
name="enable-cosec-face-recognition",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"edit-cosec-user/<str:user_id>/<uuid:device_id>/",
|
||||||
|
views.edit_cosec_user,
|
||||||
|
name="edit-cosec-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-biometric-user/<int:uid>/<uuid:device_id>/",
|
||||||
|
views.delete_biometric_user,
|
||||||
|
name="delete-biometric-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete-cosec-user/<str:user_id>/<uuid:device_id>/",
|
||||||
|
views.delete_horilla_cosec_user,
|
||||||
|
name="delete-cosec-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"biometric-users-bulk-delete",
|
||||||
|
views.bio_users_bulk_delete,
|
||||||
|
name="biometric-users-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"cosec-users-bulk-delete",
|
||||||
|
views.cosec_users_bulk_delete,
|
||||||
|
name="cosec-users-bulk-delete",
|
||||||
|
),
|
||||||
|
]
|
||||||
2683
biometric/views.py
Normal file
2683
biometric/views.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user