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