Skip to content

Commit c2edf36

Browse files
committed
- CM_ET/BT strictly on raw data and robust w.r.t. drop-outs and non-number readings
- better timeout handling - prevents stacking of stock updates - disable automatic stock updates during recording - avoid unnecessary re-authentication in ON (black plus) mode - lib updates
1 parent 6a1f87a commit c2edf36

78 files changed

Lines changed: 52154 additions & 51747 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/artisanlib/main.py

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,8 @@ def stateChanged(self, state:Qt.ApplicationState) -> None:
301301
aw.plus_account is not None and aw.qmc.roastUUID is not None and aw.curFile is not None):
302302
plus.sync.getUpdate(aw.qmc.roastUUID, aw.curFile) # sync the loaded profile data if any
303303

304-
if aw.schedule_window is not None and aw.plus_account is not None:
305-
# only if scheduler is active and plus connected we update the stock on app raise which triggers a scheduler redraw implicitly
304+
if aw.schedule_window is not None and aw.plus_account is not None and not aw.qmc.flagstart:
305+
# only if not recording, scheduler is active and plus connected we update the stock on app raise which triggers a scheduler redraw implicitly
306306
# NOTE the scheduler redraw is also happening if stock was not updated due to the update request time limit
307307
plus.stock.update() # stock update (frequency limited by plus/config.py:stock_cache_expiration)
308308

@@ -7281,11 +7281,13 @@ def replNone(a:'npt.NDArray[numpy.double]', nv:'npt.NDArray[numpy.int64]') -> 'n
72817281

72827282
return result
72837283

7284-
# computes the similarity between BT and backgroundBT as well as ET and backgroundET
7284+
# computes the similarity between BT and backgroundBT as well as ET and backgroundET on raw readings
72857285
# known as CM (idea by Hungary roasting company Casino Mocca)
7286-
# computes from profile DRY END as set in Phases dialog through DROP
7286+
# computes from profile DRY END temperature as set in Phases dialog through DROP
72877287
# returns None in case no similarity can be computed
72887288
# refactored to use numpy arrays.
7289+
# NOTE: the results can differ if foreground and background curves are swapped as the DRY END of the foreground profiles determines the number of readings
7290+
# to be compared which might differ from that of the background profile
72897291
def curveSimilarity(self) -> tuple[float|None, float|None]: # pylint: disable=no-self-use
72907292
try:
72917293
# if background profile is loaded and both profiles have a DROP event set
@@ -7294,27 +7296,16 @@ def curveSimilarity(self) -> tuple[float|None, float|None]: # pylint: disable=no
72947296
# _log.debug(f"curveSimilarity: {self.qmc.profile_sampling_interval=}") #pylint: disable=logging-fstring-interpolation
72957297
# _log.debug(f"curveSimilarity: {self.qmc.background_profile_sampling_interval=}") #pylint: disable=logging-fstring-interpolation
72967298

7297-
# create arrays using smoothed data if available
7298-
if len(self.qmc.stemp1) == len(self.qmc.temp1):
7299-
# take smoothed data if available
7300-
np_et = numpy.array(self.qmc.stemp1)
7301-
else:
7302-
np_et = numpy.array(self.qmc.temp1)
7303-
_log.debug('curveSimilarity: using non-smoothed ET')
7304-
if len(self.qmc.stemp2) == len(self.qmc.temp2):
7305-
# take smoothed data if available
7306-
np_bt = numpy.array(self.qmc.stemp2)
7307-
else:
7308-
np_bt = numpy.array(self.qmc.temp2)
7309-
_log.debug('curveSimilarity: using non-smoothed BT')
7299+
np_et = numpy.array(self.qmc.temp1)
7300+
np_bt = numpy.array(self.qmc.temp2)
73107301

73117302
# CM is based on the Phases Dry not marked Dry
73127303
# Find the DRY point
73137304
# create a view of the original with a stride that accesses it in reverse order
73147305
rev_np_bt = np_bt[::-1]
73157306
# Find TP or if there is not one then find the minimum temp before DROP
73167307
# Note - CHARGE is not considered
7317-
len_bt = len(self.qmc.stemp2)
7308+
len_bt = len(self.qmc.temp2)
73187309
rev_drop_idx:int = len_bt - self.qmc.timeindex[6]
73197310
BTlimit = self.qmc.phases[1]
73207311
if len(rev_np_bt[rev_drop_idx:]) == 0:
@@ -7344,25 +7335,38 @@ def curveSimilarity(self) -> tuple[float|None, float|None]: # pylint: disable=no
73447335
# these are not the smoothed background temps, which is how the old CM was done
73457336
np_etb = numpy.array(self.qmc.temp1B)
73467337
np_btb = numpy.array(self.qmc.temp2B)
7347-
np_timeB = numpy.array(self.qmc.timeB) + dropTimeDelta
7348-
7349-
# hack to work like OLD method where any temp before timeB[0] is -1
7350-
np_etb = numpy.insert(np_etb,0,-1)
7351-
np_btb = numpy.insert(np_btb,0,-1)
7352-
np_timeB = numpy.insert(np_timeB,0,np_timeB[0]-0.1)
7353-
7354-
interp_np_etb = numpy.interp(np_timex,np_timeB,np_etb)
7355-
interp_np_btb = numpy.interp(np_timex,np_timeB,np_btb)
7356-
7357-
det = numpy.sqrt(numpy.mean(numpy.square(np_et - interp_np_etb)))
7358-
dbt = numpy.sqrt(numpy.mean(numpy.square(np_bt - interp_np_btb)))
7359-
7360-
if numpy.isnan(det):
7361-
det = None
7362-
if numpy.isnan(dbt):
7363-
dbt = None
7364-
7365-
return det,dbt
7338+
np_timeB = numpy.array(self.qmc.timeB) + dropTimeDelta # shift background times such that they are aligned with foreground profile @ DROP
7339+
7340+
# we masked the -1 error values
7341+
np_etb_masked = numpy.ma.masked_equal(np_etb, -1) # type:ignore[no-untyped-call]
7342+
np_btb_masked = numpy.ma.masked_equal(np_btb, -1) # type:ignore[no-untyped-call]
7343+
np_timeB_etb_masked = numpy.ma.masked_array(np_timeB, np_etb_masked.mask) # type:ignore[no-untyped-call] # pylint:disable=no-member
7344+
np_timeB_btb_masked = numpy.ma.masked_array(np_timeB, np_btb_masked.mask) # type:ignore[no-untyped-call] # pylint:disable=no-member
7345+
# ignore the masked error values on computing the interpolation and fill (especially on the left) with -1 values
7346+
interp_np_etb = numpy.interp(np_timex,np_timeB_etb_masked.compressed(),np_etb_masked.compressed(),left=-1,right=-1) # pyright:ignore[reportUnknownArgumentType] # pylint:disable=no-member
7347+
interp_np_btb = numpy.interp(np_timex,np_timeB_btb_masked.compressed(),np_btb_masked.compressed(),left=-1,right=-1) # pyright:ignore[reportUnknownArgumentType] # pylint:disable=no-member
7348+
7349+
# at his point the background arrays interp_np_etb/interp_np_btb have the same length then their foreground counter parts np_et/np_bt
7350+
# however, all those errors may contain -1 error values and inf/nan readings. Let's mask them to be ignored in the computation.
7351+
7352+
# mask the -1 padding resulting from interpolating the background data as well as the inf/nan readings
7353+
interp_np_etb_masked = numpy.ma.masked_equal(numpy.ma.masked_invalid(interp_np_etb), -1) # type:ignore[no-untyped-call] # mask -1, inf, nan to be ignored
7354+
interp_np_btb_masked = numpy.ma.masked_equal(numpy.ma.masked_invalid(interp_np_btb), -1) # type:ignore[no-untyped-call] # mask -1, inf, nan to be ignored
7355+
7356+
# mask the -1 error values as well as the inf/nan readings
7357+
np_et_masked = numpy.ma.masked_equal(numpy.ma.masked_invalid(np_et), -1) # type:ignore[no-untyped-call] # mask -1, inf, nan to be ignored
7358+
np_bt_masked = numpy.ma.masked_equal(numpy.ma.masked_invalid(np_bt), -1) # type:ignore[no-untyped-call] # mask -1, inf, nan to be ignored
7359+
7360+
# all readings that are masked in the one or the other array are ignored and do not contribute in the following
7361+
RMSE_et = numpy.sqrt(numpy.mean(numpy.square(np_et_masked - interp_np_etb_masked))) # pyright:ignore[reportUnknownArgumentType]
7362+
RMSE_bt = numpy.sqrt(numpy.mean(numpy.square(np_bt_masked - interp_np_btb_masked))) # pyright:ignore[reportUnknownArgumentType]
7363+
7364+
if numpy.isnan(RMSE_et):
7365+
RMSE_et = None
7366+
if numpy.isnan(RMSE_bt):
7367+
RMSE_bt = None
7368+
7369+
return RMSE_et,RMSE_bt
73667370

73677371
# no DROP event registered
73687372
return None, None

src/artisanlib/roast_properties.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,13 +1677,11 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
16771677
self.stockWorker:plus.stock.Worker|None = None
16781678
try:
16791679
if self.aw.plus_account is not None:
1680-
if plus.controller.is_connected():
1681-
self.stockWorker= plus.stock.getWorker()
1682-
if self.stockWorker is not None:
1683-
self.updateStockSignalConnection = self.stockWorker.updatedSignal.connect(self.populatePlusCoffeeBlendCombos)
1684-
QTimer.singleShot(10, plus.stock.update)
1685-
else: # we are in ON mode, but not connected, we connect which triggers a stock update if successful
1686-
plus.controller.connect(interactive=False)
1680+
self.stockWorker= plus.stock.getWorker()
1681+
if self.stockWorker is not None:
1682+
self.updateStockSignalConnection = self.stockWorker.updatedSignal.connect(self.populatePlusCoffeeBlendCombos)
1683+
QTimer.singleShot(10, plus.stock.update)
1684+
16871685
except Exception as e: # pylint: disable=broad-except
16881686
_log.exception(e)
16891687
if platform.system() != 'Windows':

src/includes/Machines/Santoker/Cube_Bluetooth.aset

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ buttonactions=5, 5, 5, 0, 5, 0, 5, 0
5353
buttonactionstrings="santoker(80,1)", "santoker(81,1)", "santoker(82,1)", , "santoker(83,1)", , "santoker(84,1)",
5454
buttonvisibility=true, true, true, true, true, false, true, false
5555
extrabuttonactions=5, 0, 0
56-
extrabuttonactionstrings="santoker(85,0)", ,
57-
xextrabuttonactions=0, 0
58-
xextrabuttonactionstrings=,
56+
extrabuttonactionstrings="santoker(85,0);sleep(4);santoker(80,1)", ,
57+
xextrabuttonactions=0, 5
58+
xextrabuttonactionstrings=, "santoker(85,0);sleep(4);santoker(80,1)"
5959

6060
[Quantifiers]
6161
clusterEventsFlag=false

src/plus/blend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def __init__(self, parent:'QWidget', aw:'ApplicationWindow', inWeight:float, wei
126126

127127
# configure UI
128128
self.ui = BlendDialog.Ui_customBlendDialog()
129-
self.ui.setupUi(self) # OFF type:ignore[no-untyped-call]
129+
self.ui.setupUi(self) # type:ignore[no-untyped-call,unused-ignore]
130130
self.setWindowTitle(QApplication.translate('Form Caption','Custom Blend'))
131131
self.ui.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Apply)
132132
# hack to assign the Apply button the AcceptRole without losing default system translations

src/plus/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
#verify_ssl: Final[bool] = False
6060
verify_ssl: Final[bool] = True
6161
connect_timeout: Final[int] = 6 # in seconds
62-
read_timeout: Final[int] = 6 # in seconds
62+
read_timeout: Final[int] = 12 # in seconds
63+
read_timeout_max: Final[int] = 30 # in seconds
6364
min_passwd_len: Final[int] = 4
6465
min_login_len: Final[int] = 6
6566
compress_posts: Final[bool] = True
@@ -76,7 +77,7 @@
7677
# Cache and queue parameters
7778

7879
# Note: stock_cache_expiration should be larger than schedule_cache_expiration
79-
stock_cache_expiration: Final[int] = 30 # expiration period in seconds for full stock updates (expensive)
80+
stock_cache_expiration: Final[int] = 35 # expiration period in seconds for full stock updates (expensive)
8081
schedule_cache_expiration: Final[int] = 5 # expiration period in seconds for full stock updates only in case the schedule on the server has changed
8182

8283
queue_start_delay: Final[int] = 5 # startup time of queue in seconds

src/plus/connection.py

Lines changed: 107 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import logging
3636
import dateutil.parser
3737
import requests
38+
import requests.models
3839
import requests.exceptions
3940

4041
from plus import config, account, util
@@ -48,6 +49,22 @@
4849
1
4950
) # protects access to the session token which is manipulated only here
5051

52+
# request timeout
53+
54+
request_read_timeout_step:Final[int] = 2 # step size to decrease request_read_timeout on success in seconds
55+
request_read_timeout:int = config.read_timeout # dynamic read_timeout, updated on successful communication and timeouts
56+
57+
def getReadTimeout() -> int:
58+
return request_read_timeout
59+
def updateReadTimeoutOnSuccess() -> None:
60+
global request_read_timeout # pylint:disable=global-statement
61+
request_read_timeout = max(config.read_timeout, request_read_timeout - request_read_timeout_step)
62+
def updateReadTimeoutOnTimeout() -> None:
63+
global request_read_timeout # pylint:disable=global-statement
64+
request_read_timeout = config.read_timeout_max
65+
66+
#
67+
5168
def getToken() -> str|None:
5269
try:
5370
token_semaphore.acquire(1)
@@ -376,95 +393,111 @@ def sendData(
376393
verb: str, # POST or PUT
377394
authorized: bool = True,
378395
compress: bool = config.compress_posts,
379-
) -> Any:
396+
) -> requests.models.Response:
380397
# don't log POST data as it might contain credentials!
381398
_log.debug('sendData(%s,_data_,%s,%s)', url, verb, authorized)
382399
jsondata = json.dumps(data, indent=None, separators=(',', ':'), ensure_ascii=False).encode('utf8')
383400
_log.debug('-> size %s', len(jsondata))
384401
# _log.debug("PRINT jsondata: %s",jsondata)
385402
headers, postdata = getHeadersAndData(authorized, compress, jsondata, verb)
386-
if verb == 'POST':
387-
r = requests.post(
388-
url,
389-
headers=headers,
390-
data=postdata,
391-
verify=config.verify_ssl,
392-
timeout=(config.connect_timeout, config.read_timeout),
393-
)
394-
else:
395-
r = requests.put(
396-
url,
397-
headers=headers,
398-
data=postdata,
399-
verify=config.verify_ssl,
400-
timeout=(config.connect_timeout, config.read_timeout),
401-
)
402-
_log.debug('-> status %s, time %s', r.status_code, r.elapsed.total_seconds())
403-
if authorized and r.status_code == 401: # authorisation failed
404-
_log.debug('-> session token outdated (401)')
405-
# we re-authentify by renewing the session token and try again
406-
if authentify():
407-
time.sleep(0.3) # a little delay not to stress out the server too much
408-
headers, postdata = getHeadersAndData(
409-
authorized, compress, jsondata, verb
410-
) # recreate header with new token
411-
if verb == 'POST':
412-
r = requests.post(
413-
url,
414-
headers=headers,
415-
data=postdata,
416-
verify=config.verify_ssl,
417-
timeout=(config.connect_timeout, config.read_timeout),
418-
)
419-
else:
420-
r = requests.put(
421-
url,
422-
headers=headers,
423-
data=postdata,
424-
verify=config.verify_ssl,
425-
timeout=(config.connect_timeout, config.read_timeout),
426-
)
427-
_log.debug('-> status %s, time %s', r.status_code, r.elapsed.total_seconds())
428-
return r
403+
404+
try:
405+
if verb == 'POST':
406+
r = requests.post(
407+
url,
408+
headers=headers,
409+
data=postdata,
410+
verify=config.verify_ssl,
411+
timeout=(config.connect_timeout, getReadTimeout()),
412+
)
413+
else:
414+
r = requests.put(
415+
url,
416+
headers=headers,
417+
data=postdata,
418+
verify=config.verify_ssl,
419+
timeout=(config.connect_timeout, getReadTimeout()),
420+
)
421+
updateReadTimeoutOnSuccess()
422+
_log.debug('-> status %s, time %s', r.status_code, r.elapsed.total_seconds())
423+
if authorized and r.status_code == 401: # authorisation failed
424+
_log.debug('-> session token outdated (401)')
425+
# we re-authentify by renewing the session token and try again
426+
if authentify():
427+
time.sleep(0.3) # a little delay not to stress out the server too much
428+
headers, postdata = getHeadersAndData(
429+
authorized, compress, jsondata, verb
430+
) # recreate header with new token
431+
if verb == 'POST':
432+
r = requests.post(
433+
url,
434+
headers=headers,
435+
data=postdata,
436+
verify=config.verify_ssl,
437+
timeout=(config.connect_timeout, getReadTimeout()),
438+
)
439+
else:
440+
r = requests.put(
441+
url,
442+
headers=headers,
443+
data=postdata,
444+
verify=config.verify_ssl,
445+
timeout=(config.connect_timeout, getReadTimeout()),
446+
)
447+
updateReadTimeoutOnSuccess()
448+
_log.debug('on retry: -> status %s, time %s', r.status_code, r.elapsed.total_seconds())
449+
return r
450+
except requests.exceptions.Timeout as e:
451+
_log.error(e)
452+
updateReadTimeoutOnTimeout()
453+
raise e
429454

430455

431-
def getData(url: str, authorized: bool = True, params:dict[str,str]|None = None) -> Any:
456+
def getData(url: str, authorized: bool = True, params:dict[str,str]|None = None) -> requests.models.Response|None:
432457
_log.debug('getData(%s,%s,%s)', url, authorized, params)
433458
headers = getHeaders(authorized)
434459
params = params or {}
435460
# _log.debug("-> request headers %s",headers)
436-
r = requests.get(
437-
url,
438-
headers=headers,
439-
verify=config.verify_ssl,
440-
params=params,
441-
timeout=(config.connect_timeout, config.read_timeout),
442-
)
443-
_log.debug('-> status %s', r.status_code)
444-
# _log.debug("-> headers %s",r.headers)
445-
_log.debug('-> time %s', r.elapsed.total_seconds())
446-
if authorized and r.status_code == 401: # authorisation failed
447-
_log.debug(
448-
'-> session token outdated (404) - re-authentify'
449-
)
450-
# we re-authentify by renewing the session token and try again
451-
authentify()
452-
headers = getHeaders(authorized) # recreate header with new token
453-
r = requests.get(
461+
try:
462+
r:requests.models.Response = requests.get(
454463
url,
455464
headers=headers,
456465
verify=config.verify_ssl,
457466
params=params,
458-
timeout=(config.connect_timeout, config.read_timeout),
467+
timeout=(config.connect_timeout, getReadTimeout()),
459468
)
469+
updateReadTimeoutOnSuccess()
460470
_log.debug('-> status %s', r.status_code)
461-
# _log.debug("-> headers %s",r.headers)
462-
_log.debug(
463-
'-> time %s', r.elapsed.total_seconds()
464-
)
465-
try:
466-
_log.debug('-> size %s', len(r.content))
467-
# _log.debug("-> data %s",r.json())
468-
except Exception: # pylint: disable=broad-except
469-
pass
470-
return r
471+
# _log.debug("-> headers %s",r.headers)
472+
_log.debug('-> time %s', r.elapsed.total_seconds())
473+
if authorized and r.status_code == 401: # authorisation failed
474+
_log.debug(
475+
'-> session token outdated (404) - re-authentify'
476+
)
477+
# we re-authentify by renewing the session token and try again
478+
if authentify():
479+
time.sleep(0.3) # a little delay not to stress out the server too much
480+
headers = getHeaders(authorized) # recreate header with new token
481+
r = requests.get(
482+
url,
483+
headers=headers,
484+
verify=config.verify_ssl,
485+
params=params,
486+
timeout=(config.connect_timeout, getReadTimeout()),
487+
)
488+
updateReadTimeoutOnSuccess()
489+
_log.debug('-> status %s', r.status_code)
490+
# _log.debug("-> headers %s",r.headers)
491+
_log.debug(
492+
'on retry: -> time %s', r.elapsed.total_seconds()
493+
)
494+
try:
495+
_log.debug('-> size %s', len(r.content))
496+
# _log.debug("-> data %s",r.json())
497+
except Exception: # pylint: disable=broad-except
498+
pass
499+
return r
500+
except requests.exceptions.Timeout as e:
501+
_log.error(e)
502+
updateReadTimeoutOnTimeout()
503+
return None

0 commit comments

Comments
 (0)