cmsmanage/doorcontrol/models.py

226 lines
8.8 KiB
Python
Raw Normal View History

from datetime import datetime
from django.conf import settings
from django.db import models
from django.db.models import F, Func, Q
from django.db.models.functions import Mod
from django.utils import timezone
from .hid.DoorController import DoorController
class Door(models.Model):
name = models.CharField(max_length=64, unique=True)
ip = models.GenericIPAddressField(protocol="IPv4")
@property
def controller(self) -> DoorController:
return DoorController(
self.ip,
settings.HID_DOOR_USERNAME,
settings.HID_DOOR_PASSWORD,
)
def __str__(self):
return self.name
class HIDEventQuerySet(models.QuerySet):
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"
)
# Based on `function isRedEvent` from /html/hid-global.js on a HID EDGE EVO Solo
is_red = models.GeneratedField(
expression=Q(
event_type__in=[
EventType.DENIED_ACCESS_CARD_NOT_FOUND,
EventType.DENIED_ACCESS_ACCESS_PIN_NOT_FOUND,
EventType.DENIED_ACCESS_SCHEDULE,
EventType.DENIED_ACCESS_WRONG_PIN,
EventType.DENIED_ACCESS_CARD_EXPIRED,
EventType.DENIED_ACCESS_PIN_LOCKOUT,
EventType.DENIED_ACCESS_UNASSIGNED_CARD,
EventType.DENIED_ACCESS_PIN_EXPIRED,
EventType.DOOR_FORCED_ALARM,
EventType.DOOR_HELD_ALARM,
EventType.TAMPER_SWITCH_ALARM,
EventType.AC_FAILURE,
EventType.BATTERY_FAILURE,
]
),
output_field=models.BooleanField(),
db_persist=False,
)
@classmethod
def from_xml_attributes(cls, door: Door, attrib: dict[str:str]):
field_lookup = {
field.column: field.attname for field in HIDEvent._meta.get_fields()
}
def attr_to_bool(str):
if str is None:
return None
else:
return str == "true"
return cls(
**{
**{field_lookup[k]: v for k, v in attrib.items()},
"door": door,
# fixups for specific attributes
"timestamp": timezone.make_aware(
datetime.fromisoformat(attrib["timestamp"])
),
"io_state": attr_to_bool(attrib.get("ioState")),
"command_status": attr_to_bool(attrib.get("commandStatus")),
}
)
@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",)