diff --git a/doorUtil.py b/doorUtil.py index 67d9b0f..00b04ee 100644 --- a/doorUtil.py +++ b/doorUtil.py @@ -1,34 +1,5 @@ #!/usr/bin/env python3 -import bitstring - -import requests -import csv -from io import StringIO - -from common import * - -# Reference for H10301 card format: -# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf - - -def hexToCode(h): - b = bitstring.Bits(hex=h) - facility = b[7:15].uint - code = b[15:31].uint - return ((facility, code)) - - -def codeToHex(facility, code): - b = bitstring.pack('0b000000, uint:1, uint:8, uint:16, uint:1', 0, - facility, code, 0) - # calculate parity bits - b[6] = b[7:19].count(1) % 2 # even parity - b[31] = not (b[19:31].count(1) % 2) # odd parity - return b.hex.upper() - - -# hexToCode("01E29DA1") <-> codeToHex(241, 20176) - +from common import doors def forEachDoor(fxn): for door in doors.values(): diff --git a/hid/Credential.py b/hid/Credential.py new file mode 100644 index 0000000..70b9a3f --- /dev/null +++ b/hid/Credential.py @@ -0,0 +1,38 @@ +import bitstring + +# Reference for H10301 card format: +# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf + +class Credential: + def __init__(self, code=None, hex=None): + if code is None and hex is None: + raise TypeError("Must set either code or hex for a Credential") + elif code is not None and hex is not None: + raise TypeError("Cannot set both code and hex for a Credential") + elif code is not None: + self.bits = bitstring.pack( + '0b000000, 0b0, uint:8=facility, uint:16=number, 0b0', + facility=code[0], number=code[1]) + self.bits[6] = self.bits[7:19].count(1) % 2 # even parity + self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity + elif hex is not None: + self.bits = bitstring.Bits(hex=hex) + + def __repr__(self): + return f"Credential({self.code})" + + def __eq__(self, other): + return self.bits == other.bits + + def __hash__(self): + return self.bits.int + + @property + def code(self): + facility = self.bits[7:15].uint + code = self.bits[15:31].uint + return (facility, code) + + @property + def hex(self): + return self.bits.hex.upper() diff --git a/hid/DoorController.py b/hid/DoorController.py index 73b63dd..3b34e83 100644 --- a/hid/DoorController.py +++ b/hid/DoorController.py @@ -131,6 +131,12 @@ class DoorController(): "recordOffset": "0", "recordCount": "1000"})))[0] + def get_credentials(self): + return self.doXMLRequest( + ROOT(E.Credentials({"action": "LR", + "recordOffset": "0", + "recordCount": "1000"})))[0] + def get_lock(self): el = ROOT( E.Doors({"action": "LR", "responseFormat": "status"})) diff --git a/new_xml_based.py b/new_xml_based.py index 66abfba..d7b14db 100644 --- a/new_xml_based.py +++ b/new_xml_based.py @@ -4,12 +4,13 @@ from lxml import etree from common import doors, getMembershipworksData, memberLevels from hid.DoorController import E, ROOT +from hid.Credential import Credential class Member(): def __init__(self, forename, surname, membershipWorksID, middleName="", email="", phone="", cardholderID=None, doorAccess=[], onHold=False, - credentials=[], levels=[], schedules=[]): + credentials=set(), levels=[], schedules=[]): self.forename = forename self.surname = surname self.membershipWorksID = membershipWorksID @@ -37,15 +38,13 @@ Schedules: {self.schedules} """ @classmethod - def from_cardholder(cls, data, cardFormats): + def from_cardholder(cls, data): # TODO: maybe keep all attribs, and just add them in # from_MembershipWorks? - cardFormatsByID = {v: k for k, v in cardFormats.items()} - credentials = [ - (cardFormatsByID[c.attrib['formatID']], c.attrib['cardNumber']) - for c in data.findall('{*}Credential')] - roles = [r.attrib['scheduleName'] - for r in data.findall('{*}Role')] + credentials = set( + Credential(hex=(c.attrib['rawCardNumber'])) + for c in data.findall('{*}Credential')) + roles = [r.attrib['scheduleName'] for r in data.findall('{*}Role')] levels = data.attrib.get('custom1', "").split('|') return cls( forename=data.get('forename', ""), @@ -62,10 +61,11 @@ Schedules: {self.schedules} @classmethod def from_MembershipWorks(cls, data): if data["Access Card Number"] != "": - credentials = [(data["Access Card Facility Code"], - data["Access Card Number"])] + credentials = set([Credential( + code=(data["Access Card Facility Code"], + data["Access Card Number"]))]) else: - credentials = [] + credentials = set() levels = {k: v for k, v in memberLevels.items() if data[k] == k} doorAccess = [door for door, doorData in doors.items() @@ -107,25 +107,27 @@ Schedules: {self.schedules} return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID}, E.Roles(*roles)) - def make_credentials(self, cardFormats): - credentials = [ + def make_credentials(self, newCredentials, cardFormats): + out = [ E.Credential( - {"formatName": credential[0], - "cardNumber": credential[1], - "formatID": cardFormats[credential[0]], + {"formatName": str(credential.code[0]), + "cardNumber": str(credential.code[1]), + "formatID": cardFormats[str(credential.code[0])], "isCard": "true", "cardholderID": self.cardholderID}) - for credential in self.credentials] + for credential in newCredentials] - return E.Credentials({"action": "AD"}, *credentials) + return E.Credentials({"action": "AD"}, *out) def update_door(door, members): cardFormats = door.get_cardFormats() cardholders = {member.membershipWorksID: member - for member in [Member.from_cardholder(ch, cardFormats) + for member in [Member.from_cardholder(ch) for ch in door.get_cardholders()]} schedulesMap = door.get_scheduleMap() + allCredentials = set(Credential(hex=c.attrib['rawCardNumber']) + for c in door.get_credentials()) for member in members: # TODO: can I combine requests? @@ -144,7 +146,39 @@ def update_door(door, members): if member.credentials != ch.credentials: print(f"- Updating card for {member.forename} {member.surname}") print(f" - {ch.credentials} -> {member.credentials}") - door.doXMLRequest(ROOT(member.make_credentials(cardFormats))) + + oldCards = ch.credentials + newCards = member.credentials + + allNewCards = set(card + for m in members if m != member + for card in m.credentials) + + # cards removed, and won't be reassigned to someone else + for card in (oldCards - newCards) - allNewCards: + door.doXMLRequest(ROOT(E.Credentials( + {"action": "UD", + "rawCardNumber": card.hex, + "isCard": "true"}, + E.Credential({"cardholderID": ""})))) + + if newCards - oldCards: # cards added + for card in newCards & allNewCards: # new card exists in another member + raise Exception(f"Duplicate Card in input data! {card}") + + # card existed in door, and needs to be reassigned + for card in newCards & allCredentials: + door.doXMLRequest(ROOT(E.Credentials( + {"action": "UD", + "rawCardNumber": card.hex, + "isCard": "true"}, + E.Credential({"cardholderID": member.cardholderID})))) + + # cards that never existed, and need to be created + if newCards - allCredentials: + door.doXMLRequest(ROOT(member.make_credentials( + newCards - allCredentials, cardFormats))) + if member.schedulesForDoor(door) != ch.schedules: print("- Updating schedule for" + @@ -159,8 +193,8 @@ def update_door(door, members): E.Cardholder(member.attribs())))) member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \ .attrib["cardholderID"] - door.doXMLRequest(ROOT(member.make_credentials(cardFormats), - member.update_schedules(door, schedulesMap))) + door.doXMLRequest(ROOT(member.make_credentials(member.credentials, cardFormats), + member.make_schedules(door, schedulesMap))) # TODO: delete cardholders that are no longer members?