From a6c531c22fe58249f8c78847576b6aa96d713ff0 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Tue, 24 Jan 2023 21:21:26 -0500 Subject: [PATCH] Add doorcontrol app for managing HID door controllers Just read-only access to the event log dump for now --- cmsmanage/settings/base.py | 2 + doorcontrol/__init__.py | 0 doorcontrol/admin.py | 19 ++++ doorcontrol/apps.py | 7 ++ doorcontrol/migrations/0001_initial.py | 99 ++++++++++++++++++++ doorcontrol/migrations/__init__.py | 0 doorcontrol/models.py | 122 +++++++++++++++++++++++++ doorcontrol/routers.py | 26 ++++++ doorcontrol/tests.py | 3 + doorcontrol/views.py | 3 + 10 files changed, 281 insertions(+) create mode 100644 doorcontrol/__init__.py create mode 100644 doorcontrol/admin.py create mode 100644 doorcontrol/apps.py create mode 100644 doorcontrol/migrations/0001_initial.py create mode 100644 doorcontrol/migrations/__init__.py create mode 100644 doorcontrol/models.py create mode 100644 doorcontrol/routers.py create mode 100644 doorcontrol/tests.py create mode 100644 doorcontrol/views.py diff --git a/cmsmanage/settings/base.py b/cmsmanage/settings/base.py index 2cb8cd8..c51636e 100644 --- a/cmsmanage/settings/base.py +++ b/cmsmanage/settings/base.py @@ -39,6 +39,7 @@ INSTALLED_APPS = [ "rentals.apps.RentalsConfig", "membershipworks.apps.MembershipworksConfig", "paperwork.apps.PaperworkConfig", + "doorcontrol.apps.DoorControlConfig", ] MIDDLEWARE = [ @@ -75,6 +76,7 @@ WSGI_APPLICATION = "cmsmanage.wsgi.application" DATABASE_ROUTERS = [ "membershipworks.routers.MembershipWorksRouter", + "doorcontrol.routers.DoorControlRouter", ] # Default URL to redirect to after authentication diff --git a/doorcontrol/__init__.py b/doorcontrol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doorcontrol/admin.py b/doorcontrol/admin.py new file mode 100644 index 0000000..3b76836 --- /dev/null +++ b/doorcontrol/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin + +from .models import HIDEvent + + +@admin.register(HIDEvent) +class HIDEventAdmin(admin.ModelAdmin): + search_fields = ["description", "forename", "surname", "cardholder_id"] + list_display = ["door_name", "timestamp", "event_type", "description", "is_red"] + list_filter = ["door_name", "event_type"] + + def has_add_permission(self, request, obj=None): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False diff --git a/doorcontrol/apps.py b/doorcontrol/apps.py new file mode 100644 index 0000000..fec6b76 --- /dev/null +++ b/doorcontrol/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class DoorControlConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "doorcontrol" + verbose_name = "Door Control" diff --git a/doorcontrol/migrations/0001_initial.py b/doorcontrol/migrations/0001_initial.py new file mode 100644 index 0000000..acfb6ab --- /dev/null +++ b/doorcontrol/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 4.1.3 on 2023-01-25 02:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="HIDEvent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("door_name", models.CharField(db_column="doorName", max_length=64)), + ("timestamp", models.DateTimeField()), + ( + "event_type", + models.IntegerField( + choices=[ + (1022, "Denied Access: Card Not Found"), + (1023, "Denied Access Access: PIN Not Found"), + (2020, "Granted Access"), + (2021, "Granted Access: Extended Time"), + (2024, "Denied Access: Schedule"), + (2029, "Denied Access: Wrong PIN"), + (2036, "Denied Access: Card Expired"), + (2042, "Denied Access: PIN Lockout"), + (2043, "Denied Access: Unassigned Card"), + (2044, "Denied Access: Unassigned Access PIN"), + (2046, "Denied Access: PIN Expired"), + (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, "Time Set To"), + (12031, "Granted Access: Manual"), + (12032, "Door Unlocked"), + (12033, "Door Locked"), + ], + db_column="eventType", + ), + ), + ("reader_address", models.IntegerField(db_column="readerAddress")), + ( + "cardholder_id", + models.IntegerField( + blank=True, db_column="cardholderID", null=True + ), + ), + ( + "command_status", + models.BooleanField( + blank=True, db_column="commandStatus", null=True + ), + ), + ("forename", models.TextField(blank=True, null=True)), + ("surname", models.TextField(blank=True, null=True)), + ( + "io_state", + models.BooleanField(blank=True, db_column="ioState", null=True), + ), + ( + "new_time", + models.DateTimeField(blank=True, db_column="newTime", null=True), + ), + ( + "old_time", + models.DateTimeField(blank=True, db_column="oldTime", null=True), + ), + ( + "raw_card_number", + models.CharField( + blank=True, db_column="rawCardNumber", max_length=8, null=True + ), + ), + ], + options={ + "db_table": "hidevent", + "ordering": ("-timestamp",), + "managed": False, + }, + ), + ] diff --git a/doorcontrol/migrations/__init__.py b/doorcontrol/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doorcontrol/models.py b/doorcontrol/models.py new file mode 100644 index 0000000..935192e --- /dev/null +++ b/doorcontrol/models.py @@ -0,0 +1,122 @@ +from django.contrib import admin +from django.db import models + + +class HIDEvent(models.Model): + 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_name = models.CharField(max_length=64, db_column="doorName") + 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}") + + @admin.display(boolean=True) + def is_red(self): + """Based on `function isRedEvent` from /html/hid-global.js on a HID EDGE EVO Solo""" + return self.event_type in [ + 1022, + 1023, + 2024, + 2029, + 2036, + 2042, + 2043, + 2046, + 4041, + 4042, + 4043, + 4044, + 4045, + ] + + def __str__(self): + return f"{self.door_name} {self.timestamp} - {self.description}" + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["door_name", "timestamp", "event_type"], name="unique_hidevent" + ) + ] + managed = False + db_table = "hidevent" + ordering = ("-timestamp",) diff --git a/doorcontrol/routers.py b/doorcontrol/routers.py new file mode 100644 index 0000000..ddfc41a --- /dev/null +++ b/doorcontrol/routers.py @@ -0,0 +1,26 @@ +class DoorControlRouter: + app_label = "doorcontrol" + db = "doors" + + def db_for_read(self, model, **hints): + if model._meta.app_label == self.app_label: + return self.db + return None + + def db_for_write(self, model, **hints): + if model._meta.app_label == self.app_label: + return self.db + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if db == self.db: + return False + return None + + def allow_relation(self, obj1, obj2, **hints): + if ( + obj1._meta.app_label == self.app_label + or obj2._meta.app_label == self.app_label + ): + return True + return None diff --git a/doorcontrol/tests.py b/doorcontrol/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/doorcontrol/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/doorcontrol/views.py b/doorcontrol/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/doorcontrol/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.