From d867cacfef11010c13572da7478760f5ac145fae Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 4 Nov 2019 01:25:17 -0500 Subject: [PATCH] Switch to XML messages instead of CSV import for updating controllers --- hid/DoorController.py | 3 +- new_xml_based.py | 206 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 new_xml_based.py diff --git a/hid/DoorController.py b/hid/DoorController.py index 4011800..f03c7c4 100644 --- a/hid/DoorController.py +++ b/hid/DoorController.py @@ -49,7 +49,8 @@ class DoorController(): self.doImportRequest({"task": "importDone"}) def doXMLRequest(self, xml, prefix=b''): - if not isinstance(xml, str): xml = etree.tostring(xml) + if not isinstance(xml, bytes): + xml = etree.tostring(xml) r = requests.get( 'https://' + self.ip + '/cgi-bin/vertx_xml.cgi', params={'XML': prefix + xml}, diff --git a/new_xml_based.py b/new_xml_based.py new file mode 100644 index 0000000..068f917 --- /dev/null +++ b/new_xml_based.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +import requests +from lxml import etree + +from common import doors, getMembershipworksData, memberLevels +from hid.DoorController import E, ROOT + +class Member(): + def __init__(self, forename, surname, membershipWorksID, + middleName="", email="", phone="", + cardholderID=None, doorAccess=[], onHold=False, + credentials=[], levels=[], schedules=[]): + self.forename = forename + self.surname = surname + self.membershipWorksID = membershipWorksID + self.middleName = middleName + self.email = email + self.phone = phone + self.cardholderID = cardholderID + self.doorAccess = doorAccess + self.onHold = onHold + self.credentials = credentials + self.levels = levels + self.schedules = schedules + + def __repr__(self): + return f"""Name: {self.forename} | {self.middleName} | {self.surname} +MembershipWorks ID: {self.membershipWorksID} +Email: {self.email} +Phone: {self.phone} +Cardholder ID: {self.cardholderID} +doorAccess: {self.doorAccess} +On Hold? {self.onHold} +Credentials: {self.credentials} +Levels: {self.levels} +Schedules: {self.schedules} +""" + + @classmethod + def from_cardholder(cls, data, cardFormats): + # 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')] + levels = data.attrib.get('custom1', "").split('|') + return cls( + forename=data.get('forename', ""), + surname=data.get('surname', ""), + membershipWorksID=data.attrib.get('custom2', ""), + middleName=data.attrib.get('middleName', ""), + email=data.attrib.get('email', ""), + phone=data.attrib.get('phone', ""), + cardholderID=data.attrib['cardholderID'], + credentials=credentials, + levels=levels, + schedules=roles) + + @classmethod + def from_MembershipWorks(cls, data): + if data["Access Card Number"] != "": + credentials = [(data["Access Card Facility Code"], + data["Access Card Number"])] + else: + credentials = [] + + levels = {k: v for k, v in memberLevels.items() if data[k] == k} + doorAccess = [door for door, doorData in doors.items() + if data["Access " + doorData.access + "?"] == "Y"] + + return cls(data["First Name"], + data["Last Name"], + membershipWorksID=data["Account ID"], + email=data["Email"], + phone=data["Phone"], + doorAccess=doorAccess, + onHold=data["Account on Hold"] != "", + credentials=credentials, + levels=list(levels.keys()), + schedules=list(levels.values())) + + def attribs(self): + out = {"forename": self.forename, + "surname": self.surname, + "middleName": self.middleName, + "email": self.email, + "phone": self.phone, + "custom1": "|".join(self.levels).replace("&", "and"), + "custom2": self.membershipWorksID} + return out + + def make_roles(self, door, schedulesMap): + if door.name not in self.doorAccess or self.onHold: + return [] + + for schedule in self.schedules: + yield E.Role({"roleID": self.cardholderID, + "scheduleID": schedulesMap[schedule], + "resourceID": "0"}) + + def make_credentials(self, cardFormats): + for credential in self.credentials: + yield E.Credential( + {"formatName": credential[0], + "cardNumber": credential[1], + "formatID": cardFormats[credential[0]], + "isCard": "true", + "cardholderID": self.cardholderID}) + +def get_cardholders(door, cardFormats): + cardholders = door.doXMLRequest( + ROOT(E.Cardholders({"action": "LR", + "responseFormat": "expanded", + "recordOffset": "0", + "recordCount": "1000"}))) + + for cardholder in cardholders[0]: + yield Member.from_cardholder(cardholder, cardFormats) + +def get_cardFormats(door): + cardFormats = door.doXMLRequest( + ROOT(E.CardFormats({"action": "LR", + "responseFormat": "expanded"}))) + + return {fmt[0].attrib["value"]: fmt.attrib["formatID"] + for fmt in cardFormats[0].findall('{*}CardFormat[{*}FixedField]')} + +def get_schedules(door): + schedules = door.doXMLRequest( + ROOT(E.Schedules({"action": "LR", + "recordOffset": "0", + "recordCount": "8"}))) + return {fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] + for fmt in schedules[0]} + +def update_credentials(member, cardFormats): + return E.Credentials({"action": "AD"}, *member.make_credentials(cardFormats)) + +def update_schedules(member, door, schedulesMap): + return E.RoleSet({"action": "UD", "roleSetID": member.cardholderID}, + E.Roles(*member.make_roles(door, schedulesMap))) + +def update_door(door, members): + cardFormats = get_cardFormats(door) + cardholders = {ch.membershipWorksID: ch + for ch in get_cardholders(door, cardFormats)} + schedulesMap = get_schedules(door) + + for member in members: + # TODO: can I combine requests? + if member.membershipWorksID in cardholders: # cardholder exists, compare contents + ch = cardholders.pop(member.membershipWorksID) + member.cardholderID = ch.cardholderID + if member.attribs() != ch.attribs(): # update cardholder + print(f"- Updating profile for {member.forename} {member.surname}") + print(f" - Old: {ch.attribs()}") + print(f" - New: {member.attribs()}") + door.doXMLRequest(ROOT( + E.Cardholders( + {"action": "UD", "cardholderID": member.cardholderID}, + E.CardHolder(member.attribs())))) + if member.credentials != ch.credentials: + print(f"- Updating card for {member.forename} {member.surname}") + print(f" - {ch.credentials} -> {member.credentials}") + door.doXMLRequest(ROOT(update_credentials(member, cardFormats))) + + # TODO: might not handle people on hold correctly? + schedulesForDoor = member.schedules if door.name in member.doorAccess else [] + if schedulesForDoor != ch.schedules: + print("- Updating schedule for" + + f" {member.forename} {member.surname}:" + + f" {ch.schedules} -> {member.schedules}") + door.doXMLRequest(ROOT(update_schedules(member, door, schedulesMap))) + else: # do add + print(f"- Adding Member:") + print(member) + resp = door.doXMLRequest(ROOT( + E.Cardholders({"action": "AD"}, + E.Cardholder(member.attribs())))) + member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \ + .attrib["cardholderID"] + door.doXMLRequest(ROOT(update_credentials(member, cardFormats), + update_schedules(member, door, schedulesMap))) + + # TODO: delete cardholders that are no longer members? + +def main(): + memberData = getMembershipworksData( + ['members', 'staff', 'misc'], + "_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf") + + members = [Member.from_MembershipWorks(m) for m in memberData] + + for door in doors.values(): + print(door.name, door.ip) + update_door(door, members) + +# m = Member("test", "test", membershipWorksID="5af07954afd691b84c15a24d", +# credentials=[("A901146A-241", "20178")], +# schedules=["15:00-24:00"], +# doorAccess=["Front Door"]) +# update_door(doors["Front Door"], [m]) +main()