You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
551 lines
15 KiB
551 lines
15 KiB
#! /usr/bin/env python |
|
# -*- coding: utf-8 -*- |
|
# |
|
# Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org> |
|
# Licensed under GPL v3 or later |
|
|
|
from __future__ import print_function |
|
import freitagslib.network as net |
|
from freitagslib.commands import BuyCommand, DepositCommand |
|
from freitagslib.encoding import asciify |
|
|
|
import colorama |
|
from colorama import Fore, Style |
|
|
|
|
|
import sys |
|
from decimal import Decimal |
|
import os |
|
import time |
|
import urllib2 |
|
import select |
|
|
|
from display import Display |
|
from thread import start_new_thread, allocate_lock |
|
import time |
|
|
|
myDisplay=None |
|
|
|
COLOR_HINT = Fore.YELLOW + Style.BRIGHT |
|
COLOR_ERROR = Fore.RED |
|
COLOR_DEPOSIT = Fore.GREEN + Style.BRIGHT |
|
COLOR_TO_GO = Fore.MAGENTA + Style.BRIGHT |
|
COLOR_WITHDRAW = Fore.RED + Style.BRIGHT |
|
|
|
COLOR_SOME = Fore.WHITE + Style.BRIGHT |
|
COLOR_MUCH = Fore.YELLOW + Style.BRIGHT |
|
|
|
COLOR_RESET = Style.RESET_ALL |
|
|
|
display_fifo = None |
|
|
|
scroll_line1 = None |
|
scroll_line2 = None |
|
|
|
|
|
offset_line1 = 0 |
|
offset_line2 = 0 |
|
|
|
brightness = 5 |
|
screensaver = 0 |
|
|
|
SCREENSAVER_DIM = 2* 20 |
|
SCREENSAVER_TIMEOUT = 2* 120 |
|
SCREENSAVER_OFF = 2* 10*60 |
|
|
|
lock = allocate_lock() |
|
|
|
|
|
def clear(): |
|
os.system('clear') |
|
|
|
|
|
TO_GO_ONLY = 2 |
|
TO_GO_ALL = 1 |
|
TO_GO_NONE = 0 |
|
TO_GO_PREV = -1 |
|
|
|
CODES = { |
|
'UNDO':('undo',), |
|
'COMMIT':('commit',), |
|
|
|
'TO GO ALL':('to_go', TO_GO_ALL), |
|
'TO GO NONE':('to_go', TO_GO_NONE), |
|
'TO GO PREV':('to_go', TO_GO_PREV), |
|
'TO GO ONLY':('to_go', TO_GO_ONLY), |
|
'TO GO ONLZ':('to_go', TO_GO_ONLY), # Workaround for German keyboard layouts |
|
|
|
'DEPOSIT 0.01':('deposit', Decimal('0.01')), |
|
'DEPOSIT 0.05':('deposit', Decimal('0.05')), |
|
'DEPOSIT 0.10':('deposit', Decimal('0.10')), |
|
'DEPOSIT 0.50':('deposit', Decimal('0.50')), |
|
'DEPOSIT 1.00':('deposit', Decimal('1.00')), |
|
'DEPOSIT 5.00':('deposit', Decimal('5.00')), |
|
'DEPOSIT 10.00':('deposit', Decimal('10.00')), |
|
'DEPOSIT 50.00':('deposit', Decimal('50.00')), |
|
} |
|
|
|
|
|
def delay(what, seconds): |
|
for i in xrange(seconds, 0, -1): |
|
if i < seconds: |
|
sys.stdout.write('\r') |
|
sys.stdout.write('%s in %d Sekunden... ' % (what, i)) |
|
sys.stdout.flush() |
|
time.sleep(1) |
|
|
|
|
|
def warn_balance(): |
|
print('Kontostand im Minus, bitte Geld aufladen.') |
|
|
|
|
|
def error_page(error_message, hint_message=None): |
|
clear() |
|
print(COLOR_ERROR + error_message + COLOR_RESET) |
|
print() |
|
delay_seconds = 3 |
|
if hint_message is not None: |
|
print(COLOR_HINT + hint_message + COLOR_RESET) |
|
print() |
|
delay_seconds += 3 |
|
delay('Weiter', delay_seconds) |
|
|
|
|
|
def item_info_page(item): |
|
indent = 4 * ' ' |
|
|
|
clear() |
|
print('Diese Ware heißt') |
|
print() |
|
print(indent + COLOR_SOME + item.name + COLOR_RESET) |
|
print() |
|
print('und kostet') |
|
print() |
|
if item.deposit > 0: |
|
myDisplay.display_screen("PREISINFO","%s: %4.2f Euro (%4.2f Euro Pfand)" % (asciify(item.name), item.price, item.deposit)) |
|
print(indent + '%s%4.2f Euro%s + %4.2f Euro Pfand = %s%4.2f Euro%s .' \ |
|
% (COLOR_SOME, item.price, COLOR_RESET, item.deposit, |
|
COLOR_MUCH, item.price + item.deposit, COLOR_RESET)) |
|
else: |
|
myDisplay.display_screen("PREISINFO","%s: %4.2f Euro" % (asciify(item.name), item.price)) |
|
print(indent + '%s%4.2f Euro%s .' \ |
|
% (COLOR_MUCH, item.price, COLOR_RESET)) |
|
print() |
|
print() |
|
print(COLOR_MUCH + 'Zum Kaufen bitte einloggen.' + COLOR_RESET) |
|
print() |
|
|
|
delay('Weiter', 6) |
|
|
|
|
|
class Status: |
|
def __init__(self): |
|
self._reset() |
|
self.item_cache = dict() |
|
|
|
def _reset(self): |
|
self.auth_blob = None |
|
self.login_name = None |
|
self.balance = None |
|
self.transfers = None |
|
|
|
def dump(self): |
|
|
|
def sign(amount, plus='+'): |
|
return '-' if amount < 0 else plus |
|
|
|
def color(amount): |
|
return COLOR_WITHDRAW if amount < 0 else COLOR_DEPOSIT |
|
|
|
def show_total(balance, plus=' '): |
|
print('%3s %-40s %s%c %6.2f Euro%s' \ |
|
% ('', '', color(balance), sign(balance, plus), |
|
abs(balance), COLOR_RESET)) |
|
|
|
def shorten(text, length): |
|
if len(text) <= length: |
|
return text |
|
else: |
|
return text[:length - 3] + '...' |
|
|
|
def show_item(position, diff, label, color): |
|
print('%2d) %-40s %s%c %6.2f Euro%s' \ |
|
% (position, shorten(label, 40), color, sign(diff), |
|
abs(diff), COLOR_RESET)) |
|
|
|
def show_bar(): |
|
print('%3s %-40s %13s' % ('', '', '=============')) |
|
|
|
if self.logged_in(): |
|
print('Eingeloggt als: %s%s%s' % (COLOR_SOME, self.login_name, COLOR_RESET)) |
|
print() |
|
myDisplay.cmd_clear() |
|
myDisplay.write('Hallo %-14s' % (self.login_name[:13]+"!") ) |
|
if self.transfers: |
|
initial_command, initial_balance = self.transfers[0] |
|
|
|
print('Geplante Änderungen:') |
|
show_total(initial_balance) |
|
i = 1 |
|
for command, dummy in self.transfers: |
|
if isinstance(command, BuyCommand): |
|
if (command.includes_commodity()): |
|
show_item(i, -command.commodity_value(), command.commodity_label(), COLOR_WITHDRAW) |
|
i += 1 |
|
if (command.includes_deposit()): |
|
show_item(i, -command.deposit_value(), command.deposit_label(), COLOR_TO_GO) |
|
i += 1 |
|
else: |
|
show_item(i, command.difference(), command.label(), COLOR_DEPOSIT) |
|
i += 1 |
|
show_bar() |
|
|
|
if isinstance(command, BuyCommand): |
|
mycmd = 0; |
|
if (command.includes_commodity()): |
|
mycmd+=1; |
|
if (command.includes_deposit()): |
|
mycmd+=2; |
|
if (mycmd==1): |
|
mylabel=command.item_name() |
|
if (mycmd==2): |
|
mylabel="Pfand "+command.commodity_label()[:9] |
|
if (mycmd==3): |
|
mylabel=("%-13s" % (command.commodity_label()[:13]))+"+P" |
|
myDisplay.cmd_home() |
|
myDisplay.write('%-15s %4.2f' % (asciify(mylabel)[:15],abs(command.difference()))); |
|
myDisplay.cmd_home() |
|
myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance)); |
|
else: |
|
myDisplay.cmd_home() |
|
myDisplay.write('%-15s %4.2f' % (command.label()[:15],abs(command.difference()))); |
|
myDisplay.cmd_home() |
|
myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance)); |
|
|
|
if len(self.transfers) > 1: |
|
show_total(self.balance - initial_balance, plus='+') |
|
show_bar() |
|
show_total(self.balance) |
|
|
|
if self.balance < 0: |
|
warn_balance() |
|
myDisplay.cmd_home() |
|
myDisplay.write('\nKonto: %5.2f!' % (self.balance) ) |
|
print() |
|
|
|
print(COLOR_MUCH + 'Committen nicht vergessen.' + COLOR_RESET) |
|
else: |
|
print('Kontostand beträgt: %s%.2f Euro%s' % (COLOR_MUCH, self.balance, COLOR_RESET)) |
|
myDisplay.display_screen("KONTOSTAND","%s: %.2f Euro" % (self.login_name,self.balance)) |
|
if self.balance < 0: |
|
print() |
|
warn_balance() |
|
else: |
|
print(COLOR_MUCH + 'Bitte einloggen.' + COLOR_RESET) |
|
print() |
|
print('Scanne dazu deine ID-Karte mit dem Barcode-Leser.') |
|
myDisplay.display_screen("LOGIN","Bitte scanne Deinen Login-Token! *** ") |
|
print() |
|
|
|
def logged_in(self): |
|
return self.auth_blob is not None |
|
|
|
def login(self, auth_blob): |
|
assert(not self.logged_in()) |
|
|
|
user_name = net.get_user_name_from_auth_blob(auth_blob) |
|
balance = net.get_balance(user_name) |
|
|
|
self.auth_blob = auth_blob |
|
self.login_name = user_name |
|
self.balance = balance |
|
self.transfers = list() |
|
|
|
def logout(self): |
|
# Must not fail if not logged in |
|
self._reset() |
|
|
|
def commit(self): |
|
assert(self.logged_in()) |
|
|
|
def compress_deposit_commands(): |
|
if not self.transfers: |
|
return |
|
|
|
dummy, initial_balance = self.transfers[0] |
|
balance_before = initial_balance |
|
compressed_deposit = DepositCommand(Decimal('0')) |
|
others = list() |
|
for (command, dummy) in list(self.transfers): |
|
if isinstance(command, DepositCommand): |
|
compressed_deposit.add(command) |
|
else: |
|
others.append((command, balance_before)) |
|
balance_before += command.difference() |
|
if compressed_deposit.difference() != 0: |
|
others.append((compressed_deposit, balance_before)) |
|
self.transfers = others |
|
|
|
def process_buy_commands_combined(): |
|
if not self.transfers: |
|
return |
|
|
|
# Compress BuyCommands, use a single bulkbuy |
|
dummy, initial_balance = self.transfers[0] |
|
balance_before = initial_balance |
|
buy_commands = list() |
|
non_buy_commands = list() |
|
total_buy_diff = 0 |
|
|
|
for command, dummy in self.transfers: |
|
if isinstance(command, BuyCommand): |
|
buy_commands.append(command) |
|
else: |
|
balance_before += command.difference() |
|
non_buy_commands.append((command, balance_before)) |
|
|
|
try: |
|
net.bulk_buy(buy_commands, self.login_name) |
|
except urllib2.HTTPError as e: |
|
myDisplay.display_screen("Server error",'Server Error: %s' % str(e)) |
|
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e)) |
|
else: |
|
self.transfers = non_buy_commands |
|
|
|
def process_commands(): |
|
for (command, balance_backup) in list(self.transfers): |
|
try: |
|
command.run(self.login_name) |
|
except urllib2.HTTPError as e: |
|
myDisplay.display_screen("Server error",'Server Error: %s' % str(e)) |
|
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e)) |
|
break |
|
else: |
|
self.transfers.pop(0) |
|
|
|
def finish(): |
|
if not self.transfers: |
|
# Show final balance for some time |
|
clear() |
|
self.dump() |
|
delay('Logout', 3) |
|
|
|
self.logout() |
|
|
|
compress_deposit_commands() |
|
process_buy_commands_combined() |
|
process_commands() |
|
finish() |
|
|
|
def find(self, barcode): |
|
try: |
|
return self.item_cache[barcode] |
|
except KeyError: |
|
item = net.get_item(barcode) |
|
self.item_cache[barcode] = item |
|
return item |
|
|
|
def buy(self, item): |
|
assert(self.logged_in()) |
|
log_entry = (BuyCommand(item), self.balance) |
|
self.transfers.append(log_entry) |
|
self.balance = self.balance - item.price |
|
|
|
def deposit(self, amount): |
|
assert(self.logged_in()) |
|
log_entry = (DepositCommand(amount), self.balance) |
|
self.transfers.append(log_entry) |
|
self.balance = self.balance + amount |
|
|
|
def to_go(self, scope): |
|
_PRODUCT_FIRST = 'FEHLER: Bitte zuerst den betreffenden Artikel scannen.' |
|
if not self.transfers: |
|
error_page(_PRODUCT_FIRST) |
|
return |
|
|
|
if scope == TO_GO_ALL: |
|
""" |
|
Makes all BuyCommands with deposit > 0 include deposit |
|
...and updates future balance accordingly. |
|
""" |
|
dummy, initial_balance = self.transfers[0] |
|
balance_before = initial_balance |
|
|
|
for command, dummy in self.transfers: |
|
if isinstance(command, BuyCommand) \ |
|
and not command.includes_deposit() \ |
|
and command.deposit_value() > 0: |
|
command.include_deposit(True) |
|
balance_before += command.difference() |
|
|
|
self.balance = balance_before |
|
elif scope == TO_GO_NONE: |
|
""" |
|
Makes all BuyCommands that include commodity |
|
not include deposit any more |
|
...and updates future balance accordingly. |
|
""" |
|
first_command, initial_balance = self.transfers[0] |
|
balance_before = initial_balance |
|
|
|
for command, dummy in self.transfers: |
|
if isinstance(command, BuyCommand) \ |
|
and command.includes_commodity(): |
|
command.include_deposit(False) |
|
balance_before += command.difference() |
|
|
|
self.balance = balance_before |
|
elif scope == TO_GO_PREV: |
|
""" |
|
Makes the last BuyCommand include deposit |
|
...and updates future balance accordingly. |
|
""" |
|
prev, balance_backup = self.transfers[-1] |
|
if not isinstance(prev, BuyCommand): |
|
error_page(_PRODUCT_FIRST) |
|
return |
|
if prev.includes_deposit(): |
|
myDisplay.cmd_clear() |
|
myDisplay.write('FEHLER: schon Pfand %20s' % prev.item_name()[:20]) |
|
error_page('FEHLER: Pfand für Produkt "%s" bereits aktiviert' % prev.item_name()) |
|
return |
|
if prev.deposit_value() <= 0: |
|
myDisplay.cmd_clear() |
|
myDisplay.write('FEHLER: Pfandfrei! %20s' % prev.item_name()[:20]) |
|
error_page('FEHLER: Produkt "%s" hat kein Pfand' % prev.item_name()) |
|
return |
|
before = prev.difference() |
|
prev.include_deposit(True) |
|
after = prev.difference() |
|
self.balance += (after - before) |
|
elif scope == TO_GO_ONLY: |
|
""" |
|
Makes all BuyCommand that include commodity |
|
be deposit only |
|
...and updates future balance accordingly. |
|
""" |
|
dummy, initial_balance = self.transfers[0] |
|
balance_before = initial_balance |
|
|
|
for command, dummy in self.transfers: |
|
if isinstance(command, BuyCommand) \ |
|
and command.deposit_value() > 0 \ |
|
and command.includes_commodity(): |
|
command.include_commodity(False) |
|
command.include_deposit(True) |
|
balance_before += command.difference() |
|
|
|
self.balance = balance_before |
|
|
|
def undo(self): |
|
assert(self.logged_in()) |
|
if self.transfers: |
|
dummy, balance_backup = self.transfers[-1] |
|
self.transfers.pop() |
|
self.balance = balance_backup |
|
else: |
|
error_page('FEHLER: Nichts da, was ich rückgängig machen könnte.') |
|
|
|
|
|
def print_prompt(): |
|
sys.stdout.write(">>> ") |
|
sys.stdout.flush() |
|
|
|
|
|
def handle(line, status): |
|
myDisplay.setIdlemessage("Mir ist langweilig!") |
|
if line == 'exit': |
|
clear() |
|
myDisplay.cmd_clear() |
|
myDisplay.terminate() |
|
sys.exit(0) |
|
|
|
if status.logged_in(): |
|
if line in CODES: |
|
call = CODES[line] |
|
method = call[0] |
|
params = call[1:] |
|
getattr(status, method)(*params) |
|
else: |
|
try: |
|
item = status.find(line) |
|
except urllib2.HTTPError as e: |
|
if e.code == 404: # URL not found == item not found with REST |
|
myDisplay.display_screen("FEHLER","Code ist unbekannt: '%s'" % ( line[:23])) |
|
error_page('FEHLER: Aktion oder Ware "%s" nicht bekannt' % line) |
|
else: |
|
myDisplay.display_screen("Server error",'%20s' % str(e)[:20]) |
|
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e)) |
|
else: |
|
status.buy(item) |
|
else: |
|
try: |
|
status.login(line) |
|
myDisplay.setIdlemessage(" Comitten nicht vergessen! ***") |
|
except urllib2.HTTPError as e: |
|
if e.code == 404: # URL not found == user unknown |
|
# Try same code as a product |
|
item = None |
|
try: |
|
item = status.find(line) |
|
except urllib2.HTTPError as e: |
|
pass |
|
|
|
if item is None: |
|
myDisplay.display_screen("FEHLER","Nutzer ist unbekannt: '%s' *** " % line) |
|
|
|
error_page('FEHLER: Produkt oder Nutzer "%s" nicht bekannt' % line, |
|
hint_message='Ist in der WebApp unter "Einstellungen" ' \ |
|
'für Ihren Account Plugin "BarcodePlugin" ' \ |
|
'als erlaubt markiert?') |
|
else: |
|
item_info_page(item) |
|
|
|
else: |
|
myDisplay.display_screen("FEHLER",'Server Error %20s' % str(e)[:20]) |
|
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e)) |
|
except urllib2.URLError as e: |
|
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e)) |
|
|
|
|
|
def read_line(f, timeout, timeout_func): |
|
ready_to_read, _, _ = select.select([f], [], [], timeout) |
|
if ready_to_read: |
|
return f.readline().rstrip() |
|
else: |
|
timeout_func() |
|
return '' |
|
|
|
|
|
def main(): |
|
colorama.init() |
|
status = Status() |
|
global scroll_line1,scroll_line2 |
|
global myDisplay |
|
|
|
myDisplay = Display("/dev/ttyUSB0") |
|
myDisplay.start() |
|
myDisplay.cmd_reset() |
|
myDisplay.cmd_cursor_show(False) |
|
|
|
myDisplay.display_screen("Bitte Geduld","Initialisierung... ") |
|
while True: |
|
clear() |
|
status.dump() |
|
print_prompt() |
|
line = read_line(sys.stdin, timeout=3*60.0, timeout_func=status.logout) |
|
if line: |
|
myDisplay.cmd_clear() |
|
myDisplay.write('RFID/Barcode:\n%20s' % line[:20]) |
|
handle(line, status) |
|
|
|
|
|
if __name__ == '__main__': |
|
try: |
|
main() |
|
except KeyboardInterrupt: |
|
myDisplay.terminate() |
|
myDisplay.cmd_reset() |
|
myDisplay.cmd_cursor_show(False) |
|
pass |
|
|
|
|