This commit is contained in:
Arti Zirk
2020-11-03 21:18:35 +02:00
commit d2de4e13d7
9 changed files with 440 additions and 0 deletions

114
kdoorpi/__init__.py Normal file
View File

@@ -0,0 +1,114 @@
import logging
import threading
import requests
import time
from functools import partial
from json.decoder import JSONDecodeError
try:
from hashlib import scrypt
UID_HASH = partial(scrypt, n=16384, r=8, p=1)
except ImportError:
# Python 3.5 needs pip install scrypt
from scrypt import hash as scrypt
UID_HASH = partial(scrypt, N=16384, r=8, p=1)
from .wiegand import Decoder
logging.basicConfig(level=logging.DEBUG)
def hash_uid(uid: str, salt: str) -> str:
return UID_HASH(bytes.fromhex(uid), salt=salt.encode()).hex()
class Main:
def __init__(self,
door,
api_allowed,
api_longpoll,
api_swipe,
api_key,
uid_hash):
self.door = door
self.api_allowed = api_allowed
self.api_longpoll = api_longpoll
self.api_swipe = api_swipe
self.uid_hash = uid_hash
self.uids = {}
self.force_sync_now = threading.Event()
self.session = requests.Session()
self.session.headers.update({"KEY": api_key})
self.sync_cards()
logging.info("Running")
self.wiegand = Decoder(self.wiegand_callback)
self.notify_thread = threading.Thread(target=self.listen_notification, daemon=True)
self.notify_thread.start()
self.auto_sync_loop()
def sync_cards(self):
logging.info("Downloading users list")
r = self.session.get(self.api_allowed)
try:
allowed_uids = r.json()["allowed_uids"]
except JSONDecodeError as e:
logging.exception("Failed to decode allowed uids json")
return
uids = set()
for token in allowed_uids:
uids.add(token["token"]["uid_hash"].strip())
self.uids = uids
def wiegand_callback(self, bits, value):
#print("bits", bits, "value", value)
uid_h = hash_uid(value, self.uid_hash)
logging.debug("hash %s", uid_h)
if uid_h in self.uids:
logging.info("Opening door for UID hash trail %s", uid_h[-10:])
self.wiegand.open_door()
success = True
else:
logging.info("Access card not in allow list, hash trail %s", uid_h[-10:])
success = False
data = {
"uid": value,
"door": self.door,
"success": success,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
print(data)
requests.post(self.api_swipe, data=data)
def listen_notification(self):
while 1:
try:
r = self.session.get(self.api_longpoll,
headers={"Connection": "close"},
timeout=60, stream=True)
for line in r.iter_lines(1, decode_unicode=True):
if not line.strip():
continue
logging.debug("Got notification: %s", line)
if self.door in line:
logging.info("Opening door from notify")
self.wiegand.open_door()
self.force_sync_now.set()
except (requests.Timeout,
requests.ConnectionError) as e:
logging.debug("notification timeout")
time.sleep(0.1)
def auto_sync_loop(self):
while 1:
try:
self.force_sync_now.wait(60*10) # == 10min
self.force_sync_now.clear()
self.sync_cards()
except KeyboardInterrupt as e:
self.wiegand.cancel()
break

30
kdoorpi/__main__.py Normal file
View File

@@ -0,0 +1,30 @@
import os
from . import Main
"""
if "eesuks" in buf:
door = "front"
elif "valis" in buf:
door = "ground"
elif "taga" in buf:
door = "back"
else:
door = "unknown"
"""
door = os.environ["KDOORPI_DOOR"]
api_allowed = os.environ["KDOORPI_API_ALLOWED"]
api_longpoll = os.environ["KDOORPI_API_LONGPOLL"]
api_swipe = os.environ["KDOORPI_API_SWIPE"]
api_key = os.environ["KDOORPI_API_KEY"]
uid_salt = os.environ["KDOORPI_UID_SALT"]
if __name__ == "__main__":
Main(door,
api_allowed,
api_longpoll,
api_swipe,
api_key,
uid_salt)

150
kdoorpi/wiegand.py Normal file
View File

@@ -0,0 +1,150 @@
#!/bin/python
import logging
from time import sleep
try:
import pigpio
except Exception:
pigpio = False
class Decoder:
"""
A class to read Wiegand codes of an arbitrary length.
"""
def __init__(self, callback, bit_timeout=5):
"""
Instantiate with the pi, gpio for 0 (green wire), the gpio for 1
(white wire), the callback function, and the bit timeout in
milliseconds which indicates the end of a code.
The callback is passed the code length in bits and the hex value.
"""
if pigpio:
self.pi = pigpio.pi()
else:
self.pi = False
self.gpio_0 = 17 #settings.WIEGAND[0]
self.gpio_1 = 18 #settings.WIEGAND[1]
self.door_pin = 21 # from settings.py
self.button_pin = 13 # from settings.py
self.callback = callback
self.bit_timeout = bit_timeout
self.items = []
self.in_code = False
if self.pi:
self.pi.set_mode(self.gpio_0, pigpio.INPUT)
self.pi.set_mode(self.gpio_1, pigpio.INPUT)
self.pi.set_mode(self.door_pin, pigpio.OUTPUT)
self.pi.set_mode(self.button_pin, pigpio.INPUT)
self.pi.set_pull_up_down(self.gpio_0, pigpio.PUD_UP)
self.pi.set_pull_up_down(self.gpio_1, pigpio.PUD_UP)
self.pi.set_pull_up_down(self.button_pin, pigpio.PUD_UP)
self.cb_0 = self.pi.callback(self.gpio_0, pigpio.FALLING_EDGE, self._cb)
self.cb_1 = self.pi.callback(self.gpio_1, pigpio.FALLING_EDGE, self._cb)
self.button_cb_h = self.pi.callback(self.button_pin, pigpio.FALLING_EDGE, self._cb)
def cut_empty(self, item):
if item[0:8] == "00000000":
return self.cut_empty(item[8:])
else:
return item
def get_hex(self):
try:
items = self.items
if len(self.items) == 26:
items = self.items[1:-1]
elif len(self.items) == 64:
items = self.cut_empty(self.items)
bits = []
for i in range(len(items), 0, -8):
bits.append(int(items[i - 8:i], 2))
return (" ".join(map(lambda a: "%-0.2X" % ((a + 256) % 256), bits))).rstrip()
except ValueError:
logging.error("Wiegand convert error: bin to hex convertion ended with ValeError. raw: " + str(self.items))
return False
except Exception as e:
logging.error("Wiegand convert error: (raw: " + str(self.items) + ") " + str(e))
return False
def _cb(self, gpio_pin, level, tick):
"""
Accumulate bits until both gpios 0 and 1 timeout.
"""
try:
if level < pigpio.TIMEOUT:
if self.in_code:
self.bits += 1
else:
logging.debug("Wiegand data transfer start")
self.bits = 1
self.items = ""
self.in_code = True
self.code_timeout = 0
self.pi.set_watchdog(self.gpio_0, self.bit_timeout)
self.pi.set_watchdog(self.gpio_1, self.bit_timeout)
if gpio_pin == self.gpio_0:
self.code_timeout &= 2 # clear gpio 0 timeout
self.items += "1"
else:
self.code_timeout &= 1 # clear gpio 1 timeout
self.items += "0"
else:
if self.in_code:
if gpio_pin == self.gpio_0:
self.code_timeout |= 1 # timeout gpio 0
else:
self.code_timeout |= 2 # timeout gpio 1
if self.code_timeout == 3: # both gpios timed out
self.pi.set_watchdog(self.gpio_0, 0)
self.pi.set_watchdog(self.gpio_1, 0)
self.in_code = False
if self.bits >= 26:
hex = self.get_hex()
if hex:
self.callback(self.bits, hex)
else:
logging.error("Wiegand receive error: Expected at least 26 got %i bits. raw: %s" %(self.bits, self.items))
except Exception as e:
logging.error("Wiegand callback error: " + str(e))
def button_cb(self, gpio_pin, level, tick):
print("button: gpio_pin:{}, level:{}, tick:{}".format(gpio_pin, level, tick))
def open_door(self):
self.pi.write(self.door_pin, 1)
sleep(3)
self.pi.write(self.door_pin, 0)
def cancel(self):
"""
Cancel the Wiegand decoder.
"""
self.cb_0.cancel()
self.cb_1.cancel()
self.button_cb_h.cancel()
self.pi.stop()