#! /usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2011 Sebastian Pipping # 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 from freitagslib.settings import settings, Settings 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() (read_list, _, _) = select.select([sys.stdin], [], [], 1) if len(read_list) > 0: # input on stdin, quit delay return 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() if not settings.load("freitagskasse.conf"): sys.exit(1) 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