188 lines
7.9 KiB
Python
188 lines
7.9 KiB
Python
from django.db import models
|
|
from django.db.models import ExpressionWrapper, F, Func, Q
|
|
from django.db.models.functions import Mod
|
|
|
|
|
|
class Door(models.Model):
|
|
name = models.CharField(max_length=64, unique=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class HIDEventQuerySet(models.QuerySet):
|
|
def with_is_red(self):
|
|
"""Based on `function isRedEvent` from /html/hid-global.js on a HID EDGE EVO Solo"""
|
|
return self.annotate(
|
|
is_red=ExpressionWrapper(
|
|
Q(
|
|
event_type__in=[
|
|
HIDEvent.EventType.DENIED_ACCESS_CARD_NOT_FOUND,
|
|
HIDEvent.EventType.DENIED_ACCESS_ACCESS_PIN_NOT_FOUND,
|
|
HIDEvent.EventType.DENIED_ACCESS_SCHEDULE,
|
|
HIDEvent.EventType.DENIED_ACCESS_WRONG_PIN,
|
|
HIDEvent.EventType.DENIED_ACCESS_CARD_EXPIRED,
|
|
HIDEvent.EventType.DENIED_ACCESS_PIN_LOCKOUT,
|
|
HIDEvent.EventType.DENIED_ACCESS_UNASSIGNED_CARD,
|
|
HIDEvent.EventType.DENIED_ACCESS_PIN_EXPIRED,
|
|
HIDEvent.EventType.DOOR_FORCED_ALARM,
|
|
HIDEvent.EventType.DOOR_HELD_ALARM,
|
|
HIDEvent.EventType.TAMPER_SWITCH_ALARM,
|
|
HIDEvent.EventType.AC_FAILURE,
|
|
HIDEvent.EventType.BATTERY_FAILURE,
|
|
]
|
|
),
|
|
output_field=models.BooleanField(),
|
|
)
|
|
)
|
|
|
|
def with_decoded_card_number(self):
|
|
# TODO: CONV and BIT_COUNT are MySQL/MariaDB specific
|
|
class Conv(Func):
|
|
function = "CONV"
|
|
arity = 3
|
|
# This is technically not true, but fine for my purposes
|
|
output_field = models.IntegerField()
|
|
|
|
class BitCount(Func):
|
|
function = "BIT_COUNT"
|
|
arity = 1
|
|
|
|
return (
|
|
self.alias(card_number=Conv(F("raw_card_number"), 16, 10))
|
|
.alias(more_than_26_bits=F("card_number").bitrightshift(26))
|
|
.annotate(card_is_26_bit=Q(more_than_26_bits=0))
|
|
.alias(
|
|
parity_a=Mod(
|
|
BitCount(F("card_number").bitrightshift(1).bitand(0xFFF)), 2
|
|
),
|
|
parity_b=Mod(
|
|
BitCount(F("card_number").bitrightshift(13).bitand(0xFFF)), 2
|
|
),
|
|
)
|
|
.annotate(
|
|
card_is_valid_26_bit=~Q(parity_a=F("card_number").bitand(1))
|
|
& Q(parity_b=F("card_number").bitrightshift(25).bitand(1))
|
|
)
|
|
.annotate(
|
|
card_number_26_bit=F("card_number").bitrightshift(1).bitand(0xFFFF),
|
|
card_facility_code_26_bit=F("card_number")
|
|
.bitrightshift(17)
|
|
.bitand(0xFF),
|
|
)
|
|
)
|
|
|
|
|
|
class HIDEvent(models.Model):
|
|
objects = HIDEventQuerySet.as_manager()
|
|
|
|
class EventType(models.IntegerChoices):
|
|
DENIED_ACCESS_CARD_NOT_FOUND = 1022, "Denied Access: Card Not Found"
|
|
DENIED_ACCESS_ACCESS_PIN_NOT_FOUND = 1023, "Denied Access Access: PIN Not Found"
|
|
GRANTED_ACCESS = 2020, "Granted Access"
|
|
GRANTED_ACCESS_EXTENDED_TIME = 2021, "Granted Access: Extended Time"
|
|
DENIED_ACCESS_SCHEDULE = 2024, "Denied Access: Schedule"
|
|
DENIED_ACCESS_WRONG_PIN = 2029, "Denied Access: Wrong PIN"
|
|
DENIED_ACCESS_CARD_EXPIRED = 2036, "Denied Access: Card Expired"
|
|
DENIED_ACCESS_PIN_LOCKOUT = 2042, "Denied Access: PIN Lockout"
|
|
DENIED_ACCESS_UNASSIGNED_CARD = 2043, "Denied Access: Unassigned Card"
|
|
DENIED_ACCESS_UNASSIGNED_ACCESS_PIN = (
|
|
2044,
|
|
"Denied Access: Unassigned Access PIN",
|
|
)
|
|
DENIED_ACCESS_PIN_EXPIRED = 2046, "Denied Access: PIN Expired"
|
|
ALARM_ACKNOWLEDGED = 4034, "Alarm Acknowledged"
|
|
DOOR_LOCKED_SCHEDULED = 4035, "Door Locked: Scheduled"
|
|
DOOR_UNLOCKED_SCHEDULED = 4036, "Door Unlocked: Scheduled"
|
|
DOOR_FORCED_ALARM = 4041, "Door Forced Alarm"
|
|
DOOR_HELD_ALARM = 4042, "Door Held Alarm"
|
|
TAMPER_SWITCH_ALARM = 4043, "Tamper Switch Alarm"
|
|
AC_FAILURE = 4044, "AC Failure"
|
|
BATTERY_FAILURE = 4045, "Battery Failure"
|
|
REX_SWITCH_ALARM = 4051, "REX Switch Alarm"
|
|
TIME_SET_TO = 7020, "Time Set To"
|
|
GRANTED_ACCESS_MANUAL = 12031, "Granted Access: Manual"
|
|
DOOR_UNLOCKED = 12032, "Door Unlocked"
|
|
DOOR_LOCKED = 12033, "Door Locked"
|
|
|
|
door = models.ForeignKey(Door, on_delete=models.CASCADE)
|
|
timestamp = models.DateTimeField()
|
|
event_type = models.IntegerField(db_column="eventType", choices=EventType.choices)
|
|
reader_address = models.IntegerField(db_column="readerAddress")
|
|
cardholder_id = models.IntegerField(blank=True, null=True, db_column="cardholderID")
|
|
command_status = models.BooleanField(
|
|
blank=True, null=True, db_column="commandStatus"
|
|
)
|
|
forename = models.TextField(blank=True, null=True)
|
|
surname = models.TextField(blank=True, null=True)
|
|
io_state = models.BooleanField(blank=True, null=True, db_column="ioState")
|
|
new_time = models.DateTimeField(blank=True, null=True, db_column="newTime")
|
|
old_time = models.DateTimeField(blank=True, null=True, db_column="oldTime")
|
|
raw_card_number = models.CharField(
|
|
max_length=8, blank=True, null=True, db_column="rawCardNumber"
|
|
)
|
|
|
|
@property
|
|
def description(self):
|
|
"""
|
|
Based on `Global.localeStrings` from /html/en_EN/en_EN.js
|
|
and `function eventDataHandler` from /html/modules/hid-dashboard.js
|
|
on a HID EDGE EVO Solo
|
|
"""
|
|
|
|
name = f"{self.forename} {self.surname}"
|
|
direction = "IN" if self.reader_address == 0 else "OUT"
|
|
|
|
event_types = {
|
|
1022: f"Denied Access, {direction} Card Not Found {self.raw_card_number}",
|
|
1023: f"Denied Access, {direction} Access PIN Not Found {self.raw_card_number}",
|
|
2020: f"Granted Access, {direction} {name}",
|
|
2021: f"Granted Access, {direction} Extended Time {name}",
|
|
2024: f"Denied Access, {direction} Schedule {name}",
|
|
2029: f"Denied Access, {direction} Wrong PIN {name}",
|
|
2036: f"Denied Access, {direction} Card Expired {name}",
|
|
2042: f"Denied Access, {direction} PIN Lockout {name}",
|
|
2043: f"Denied Access, {direction} Unassigned Card {self.raw_card_number}",
|
|
2044: f"Denied Access, {direction} Unassigned Access PIN {self.raw_card_number}",
|
|
2046: f"Denied Access - PIN Expired {name}",
|
|
4034: "Alarm Acknowledged",
|
|
4035: "Door Locked-Scheduled",
|
|
4036: "Door Unlocked-Scheduled",
|
|
4041: "Door Forced Alarm",
|
|
4042: "Door Held Alarm",
|
|
4043: "Tamper Switch Alarm",
|
|
4044: "AC Failure",
|
|
4045: "Battery Failure",
|
|
4051: "REX Switch Alarm",
|
|
7020: f"Time Set to: {self.new_time}",
|
|
12031: f"Granted Access, {direction} Manual",
|
|
12032: "Door Unlocked",
|
|
12033: "Door Locked",
|
|
}
|
|
|
|
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
|
|
|
|
def __str__(self):
|
|
return f"{self.door.name} {self.timestamp} - {self.description}"
|
|
|
|
def decoded_card_number(self) -> str:
|
|
"""Requires annotations from `with_decoded_card_number`"""
|
|
if self.raw_card_number is None:
|
|
return None
|
|
elif self.card_is_26_bit:
|
|
if self.card_is_valid_26_bit:
|
|
return f"{self.card_facility_code_26_bit} - {self.card_number_26_bit}"
|
|
else:
|
|
return "Invalid"
|
|
else:
|
|
return "Not 26 bit card"
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["door", "timestamp", "event_type"], name="unique_hidevent"
|
|
)
|
|
]
|
|
db_table = "hidevent"
|
|
ordering = ("-timestamp",)
|