Index: pykota/trunk/conf/pykota.conf.sample
===================================================================
--- pykota/trunk/conf/pykota.conf.sample (revision 3012)
+++ pykota/trunk/conf/pykota.conf.sample (revision 3025)
@@ -1143,2 +1143,25 @@
 
 
+# Sets the maximum number of seconds to wait for the printer
+# being in 'printing' mode once the job has been sent to it.
+# Once this delay is expired, PyKota will consider this job
+# will never be printed, aborts the hardware accounting
+# process, and uses the latest internal page counter value seen.
+# 
+# Increasing this value, or setting it to 0, may help with some 
+# printers which don't conform to RFC3805. Problem reported on a 
+# Samsung ML2551n gave a way for clever students to bypass
+# hardware accounting entirely by removing the paper from
+# the paper tray before the job had begun to print, then
+# waiting 60 seconds, and putting the paper back in the tray...
+#
+# IMPORTANT : always ensure that your printers' firmware is up
+# to date.
+#
+# This directive can be set either globally or on a per printer
+# basis. 
+#
+# When not set, an hardcoded value of 60 seconds is used.
+# When set to 0, PyKota will wait indefinitely until the
+# printer switches to the 'printing' status.
+noprintingmaxdelay : 60
Index: pykota/trunk/pykota/accounters/snmp.py
===================================================================
--- pykota/trunk/pykota/accounters/snmp.py (revision 2877)
+++ pykota/trunk/pykota/accounters/snmp.py (revision 3025)
@@ -22,4 +22,10 @@
 #
 #
+
+"""This module is used to extract printer's internal page counter
+and status informations using SNMP queries.
+
+The values extracted are defined at least in RFC3805 and RFC2970.
+"""
 
 ITERATIONDELAY = 1.5   # 1.5 Second
@@ -65,5 +71,37 @@
                      }  
 hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
+printerDetectedErrorStateValues = [ { 128 : 'Low Paper',
+                                       64 : 'No Paper',
+                                       32 : 'Low Toner',
+                                       16 : 'No Toner',
+                                        8 : 'Door Open',
+                                        4 : 'Jammed',
+                                        2 : 'Offline',
+                                        1 : 'Service Requested',
+                                    },
+                                    { 128 : 'Input Tray Missing',
+                                       64 : 'Output Tray Missing',
+                                       32 : 'Marker Supply Missing',
+                                       16 : 'Output Near Full',
+                                        8 : 'Output Full',
+                                        4 : 'Input Tray Empty',
+                                        2 : 'Overdue Preventive Maintainance',
+                                        1 : 'Not Assigned in RFC3805',
+                                    },
+                                  ]  
+errorConditions = [ 'No Paper',
+                    # 'No Toner',
+                    'Door Open',
+                    'Jammed',
+                    'Offline',
+                    'Service Requested',
+                    'Input Tray Missing',
+                    'Output Tray Missing',
+                    # 'Marker Supply Missing',
+                    'Output Full',
+                    'Input Tray Empty',
+                  ]
 prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
+
 class BaseHandler :
     """A class for SNMP print accounting."""
@@ -76,7 +114,14 @@
             self.community = "public"
         self.port = 161
+        self.initValues()
+        
+    def initValues(self) :    
+        """Initializes SNMP values."""
         self.printerInternalPageCounter = None
         self.printerStatus = None
         self.deviceStatus = None
+        self.printerDetectedErrorState = None
+        self.consoleDisplayBufferText = None
+        self.timebefore = time.time()   # resets timer also in case of error
         
     def retrieveSNMPValues(self) :    
@@ -84,8 +129,34 @@
         raise RuntimeError, "You have to overload this method."
         
+    def extractErrorStates(self, value) :    
+        """Returns a list of textual error states from a binary value."""
+        states = []
+        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
+            byte = ord(value[i])
+            bytedescription = printerDetectedErrorStateValues[i]
+            for (k, v) in bytedescription.items() :
+                if byte & k :
+                    states.append(v)
+        return states            
+        
+    def checkIfError(self, errorstates) :    
+        """Checks if any error state is fatal or not."""
+        for err in errorstates :
+            if err in errorConditions :
+                return True
+        return False    
+        
     def waitPrinting(self) :
         """Waits for printer status being 'printing'."""
+        try :
+            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
+        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
+            noprintingmaxdelay = NOPRINTINGMAXDELAY
+            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
+        if not noprintingmaxdelay :
+            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
+        else :    
+            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
         previousValue = self.parent.getLastPageCounter()
-        timebefore = time.time()
         firstvalue = None
         while 1:
@@ -107,5 +178,7 @@
                         self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
                         break
-                    elif (time.time() - timebefore) > NOPRINTINGMAXDELAY :
+                    elif noprintingmaxdelay \
+                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
+                         and not self.checkIfError(self.printerDetectedErrorState) :
                         # More than X seconds without the printer being in 'printing' mode
                         # We can safely assume this won't change if printer is now 'idle'
@@ -135,7 +208,8 @@
             dstatusAsString = deviceStatusValues.get(self.deviceStatus)
             idle_flag = 0
-            if (pstatusAsString == 'idle') or \
-               ((pstatusAsString == 'other') and \
-                (dstatusAsString == 'running')) :
+            if (not self.checkIfError(self.printerDetectedErrorState)) \
+               and ((pstatusAsString == 'idle') or \
+                         ((pstatusAsString == 'other') and \
+                          (dstatusAsString == 'running'))) :
                 idle_flag = 1       # Standby / Powersave is considered idle
             if idle_flag :    
@@ -173,7 +247,10 @@
                                                   tuple([int(i) for i in pageCounterOID.split('.')]), \
                                                   tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
-                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]))
+                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
+                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]), \
+                                                  tuple([int(i) for i in prtConsoleDisplayBufferTextOID.split('.')]))
             if errorIndication :                                                  
                 self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
+                self.initValues()
             elif errorStatus :    
                 self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
@@ -184,8 +261,12 @@
                 self.printerStatus = int(varBinds[1][1].prettyPrint())
                 self.deviceStatus = int(varBinds[2][1].prettyPrint())
-                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'" \
+                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
+                self.consoleDisplayBufferText = varBinds[4][1].prettyPrint()
+                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
                      % (self.printerInternalPageCounter, \
                         printerStatusValues.get(self.printerStatus), \
-                        deviceStatusValues.get(self.deviceStatus)))
+                        deviceStatusValues.get(self.deviceStatus), \
+                        self.printerDetectedErrorState, \
+                        self.consoleDisplayBufferText))
 else :
     class Handler(BaseHandler) :
@@ -199,5 +280,7 @@
             req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
                                                         (hrPrinterStatusOID, ver.Null()), \
-                                                        (hrDeviceStatusOID, ver.Null()))
+                                                        (hrDeviceStatusOID, ver.Null()), \
+                                                        (hrPrinterDetectedErrorStateOID, ver.Null()), \
+                                                        (prtConsoleDisplayBufferTextOID, ver.Null()))
             tsp = Manager()
             try :
@@ -211,5 +294,4 @@
         def handleAnswer(self, wholeMsg, notusedhere, req):
             """Decodes and handles the SNMP answer."""
-            self.parent.filter.logdebug("SNMP answer : '%s'" % repr(wholeMsg))
             ver = alpha.protoVersions[alpha.protoVersionId1]
             rsp = ver.Message()
@@ -232,8 +314,12 @@
                             self.printerStatus = self.values[1]
                             self.deviceStatus = self.values[2]
-                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'" \
+                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
+                            self.consoleDisplayBufferText = self.values[4]
+                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
                                  % (self.printerInternalPageCounter, \
                                     printerStatusValues.get(self.printerStatus), \
-                                    deviceStatusValues.get(self.deviceStatus)))
+                                    deviceStatusValues.get(self.deviceStatus), \
+                                    self.printerDetectedErrorState, \
+                                    self.consoleDisplayBufferText))
                         except IndexError :    
                             self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
Index: pykota/trunk/pykota/accounters/pjl.py
===================================================================
--- pykota/trunk/pykota/accounters/pjl.py (revision 2830)
+++ pykota/trunk/pykota/accounters/pjl.py (revision 3025)
@@ -131,4 +131,13 @@
     def waitPrinting(self) :
         """Waits for printer status being 'printing'."""
+        try :
+            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
+        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
+            noprintingmaxdelay = NOPRINTINGMAXDELAY
+            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
+        if not noprintingmaxdelay :
+            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
+        else :    
+            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
         previousValue = self.parent.getLastPageCounter()
         timebefore = time.time()
@@ -151,5 +160,5 @@
                         self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
                         break
-                    elif (time.time() - timebefore) > NOPRINTINGMAXDELAY :
+                    elif noprintingmaxdelay and ((time.time() - timebefore) > noprintingmaxdelay) :
                         # More than X seconds without the printer being in 'printing' mode
                         # We can safely assume this won't change if printer is now 'idle'
Index: pykota/trunk/pykota/config.py
===================================================================
--- pykota/trunk/pykota/config.py (revision 2909)
+++ pykota/trunk/pykota/config.py (revision 3025)
@@ -557,4 +557,20 @@
                 raise PyKotaConfigError, _("Incorrect value %s for the duplicatesdelay directive in section %s") % (str(duplicatesdelay), printername)
         
+    def getNoPrintingMaxDelay(self, printername) :          
+        """Returns the max number of seconds to wait for the printer to be in 'printing' mode."""
+        try : 
+            maxdelay = self.getPrinterOption(printername, "noprintingmaxdelay")
+        except PyKotaConfigError :    
+            return None         # tells to use hardcoded value
+        else :    
+            try :
+                maxdelay = int(maxdelay)
+                if maxdelay < 0 :
+                    raise ValueError
+            except (TypeError, ValueError) :
+                raise PyKotaConfigError, _("Incorrect value %s for the noprintingmaxdelay directive in section %s") % (str(maxdelay), printername)
+            else :    
+                return maxdelay
+        
     def getWinbindSeparator(self) :          
         """Returns the winbind separator's value if it is set, else None."""
Index: pykota/trunk/pykota/version.py
===================================================================
--- pykota/trunk/pykota/version.py (revision 3018)
+++ pykota/trunk/pykota/version.py (revision 3025)
@@ -22,5 +22,5 @@
 #
 
-__version__ = "1.25alpha12_unofficial"
+__version__ = "1.25alpha13_unofficial"
 
 __doc__ = "PyKota : a complete Printing Quota Solution for CUPS."
Index: pykota/trunk/NEWS
===================================================================
--- pykota/trunk/NEWS (revision 3018)
+++ pykota/trunk/NEWS (revision 3025)
@@ -22,4 +22,11 @@
 PyKota NEWS :
        
+    - 1.25alpha13 (2006-10-04) :
+     
+        - Introduced the 'noprintingmaxdelay' directive to workaround
+          some buggy printers when hardware accounting is used.
+          
+        - Improved the reliability of hardware accounting.  
+          
     - 1.25alpha12 (2006-09-15) :
     
