forked from CMS/memberPlumbing
Properly add, remove, and reassign credentials for existing members
This commit is contained in:
parent
8367c8bbc1
commit
21a9aa5b5c
31
doorUtil.py
31
doorUtil.py
@ -1,34 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import bitstring
|
from common import doors
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def forEachDoor(fxn):
|
def forEachDoor(fxn):
|
||||||
for door in doors.values():
|
for door in doors.values():
|
||||||
|
38
hid/Credential.py
Normal file
38
hid/Credential.py
Normal file
@ -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()
|
@ -131,6 +131,12 @@ class DoorController():
|
|||||||
"recordOffset": "0",
|
"recordOffset": "0",
|
||||||
"recordCount": "1000"})))[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):
|
def get_lock(self):
|
||||||
el = ROOT(
|
el = ROOT(
|
||||||
E.Doors({"action": "LR", "responseFormat": "status"}))
|
E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||||
|
@ -4,12 +4,13 @@ from lxml import etree
|
|||||||
|
|
||||||
from common import doors, getMembershipworksData, memberLevels
|
from common import doors, getMembershipworksData, memberLevels
|
||||||
from hid.DoorController import E, ROOT
|
from hid.DoorController import E, ROOT
|
||||||
|
from hid.Credential import Credential
|
||||||
|
|
||||||
class Member():
|
class Member():
|
||||||
def __init__(self, forename, surname, membershipWorksID,
|
def __init__(self, forename, surname, membershipWorksID,
|
||||||
middleName="", email="", phone="",
|
middleName="", email="", phone="",
|
||||||
cardholderID=None, doorAccess=[], onHold=False,
|
cardholderID=None, doorAccess=[], onHold=False,
|
||||||
credentials=[], levels=[], schedules=[]):
|
credentials=set(), levels=[], schedules=[]):
|
||||||
self.forename = forename
|
self.forename = forename
|
||||||
self.surname = surname
|
self.surname = surname
|
||||||
self.membershipWorksID = membershipWorksID
|
self.membershipWorksID = membershipWorksID
|
||||||
@ -37,15 +38,13 @@ Schedules: {self.schedules}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_cardholder(cls, data, cardFormats):
|
def from_cardholder(cls, data):
|
||||||
# TODO: maybe keep all attribs, and just add them in
|
# TODO: maybe keep all attribs, and just add them in
|
||||||
# from_MembershipWorks?
|
# from_MembershipWorks?
|
||||||
cardFormatsByID = {v: k for k, v in cardFormats.items()}
|
credentials = set(
|
||||||
credentials = [
|
Credential(hex=(c.attrib['rawCardNumber']))
|
||||||
(cardFormatsByID[c.attrib['formatID']], c.attrib['cardNumber'])
|
for c in data.findall('{*}Credential'))
|
||||||
for c in data.findall('{*}Credential')]
|
roles = [r.attrib['scheduleName'] for r in data.findall('{*}Role')]
|
||||||
roles = [r.attrib['scheduleName']
|
|
||||||
for r in data.findall('{*}Role')]
|
|
||||||
levels = data.attrib.get('custom1', "").split('|')
|
levels = data.attrib.get('custom1', "").split('|')
|
||||||
return cls(
|
return cls(
|
||||||
forename=data.get('forename', ""),
|
forename=data.get('forename', ""),
|
||||||
@ -62,10 +61,11 @@ Schedules: {self.schedules}
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_MembershipWorks(cls, data):
|
def from_MembershipWorks(cls, data):
|
||||||
if data["Access Card Number"] != "":
|
if data["Access Card Number"] != "":
|
||||||
credentials = [(data["Access Card Facility Code"],
|
credentials = set([Credential(
|
||||||
data["Access Card Number"])]
|
code=(data["Access Card Facility Code"],
|
||||||
|
data["Access Card Number"]))])
|
||||||
else:
|
else:
|
||||||
credentials = []
|
credentials = set()
|
||||||
|
|
||||||
levels = {k: v for k, v in memberLevels.items() if data[k] == k}
|
levels = {k: v for k, v in memberLevels.items() if data[k] == k}
|
||||||
doorAccess = [door for door, doorData in doors.items()
|
doorAccess = [door for door, doorData in doors.items()
|
||||||
@ -107,25 +107,27 @@ Schedules: {self.schedules}
|
|||||||
return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID},
|
return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID},
|
||||||
E.Roles(*roles))
|
E.Roles(*roles))
|
||||||
|
|
||||||
def make_credentials(self, cardFormats):
|
def make_credentials(self, newCredentials, cardFormats):
|
||||||
credentials = [
|
out = [
|
||||||
E.Credential(
|
E.Credential(
|
||||||
{"formatName": credential[0],
|
{"formatName": str(credential.code[0]),
|
||||||
"cardNumber": credential[1],
|
"cardNumber": str(credential.code[1]),
|
||||||
"formatID": cardFormats[credential[0]],
|
"formatID": cardFormats[str(credential.code[0])],
|
||||||
"isCard": "true",
|
"isCard": "true",
|
||||||
"cardholderID": self.cardholderID})
|
"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):
|
def update_door(door, members):
|
||||||
cardFormats = door.get_cardFormats()
|
cardFormats = door.get_cardFormats()
|
||||||
cardholders = {member.membershipWorksID: member
|
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()]}
|
for ch in door.get_cardholders()]}
|
||||||
schedulesMap = door.get_scheduleMap()
|
schedulesMap = door.get_scheduleMap()
|
||||||
|
allCredentials = set(Credential(hex=c.attrib['rawCardNumber'])
|
||||||
|
for c in door.get_credentials())
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
# TODO: can I combine requests?
|
# TODO: can I combine requests?
|
||||||
@ -144,7 +146,39 @@ def update_door(door, members):
|
|||||||
if member.credentials != ch.credentials:
|
if member.credentials != ch.credentials:
|
||||||
print(f"- Updating card for {member.forename} {member.surname}")
|
print(f"- Updating card for {member.forename} {member.surname}")
|
||||||
print(f" - {ch.credentials} -> {member.credentials}")
|
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:
|
if member.schedulesForDoor(door) != ch.schedules:
|
||||||
print("- Updating schedule for" +
|
print("- Updating schedule for" +
|
||||||
@ -159,8 +193,8 @@ def update_door(door, members):
|
|||||||
E.Cardholder(member.attribs()))))
|
E.Cardholder(member.attribs()))))
|
||||||
member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \
|
member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \
|
||||||
.attrib["cardholderID"]
|
.attrib["cardholderID"]
|
||||||
door.doXMLRequest(ROOT(member.make_credentials(cardFormats),
|
door.doXMLRequest(ROOT(member.make_credentials(member.credentials, cardFormats),
|
||||||
member.update_schedules(door, schedulesMap)))
|
member.make_schedules(door, schedulesMap)))
|
||||||
|
|
||||||
# TODO: delete cardholders that are no longer members?
|
# TODO: delete cardholders that are no longer members?
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user