Correct Date and Time in Lumo
As probably many of you I had issues with Lumo referencing the correct time. That could be during the creation of to-do-lists, the search for cinema screening hours or whatever. I would like to share my approach towards a solution.
It involves a self-hosted website written in PHP that serves as a time reference for Lumo. The time comes from official NTP servers and is corrected with approximate values of network latency.
Then, in Lumo settings in the general behavior section there is a prompt added to regularly check the time reference. As you might expect from an AI Lumo doesn't always follow the instructions, but it greatly improved accuracy for time-critical queries.
The php code:
<?php
header('Content-Type: text/html; charset=utf-8');
date_default_timezone_set('UTC');
/**
* Queries an NTP server.
*/
function ntp_offset($host, $port = 123, $timeout = 5) {
$result = array(
'success' => false,
'ntp_time' => null,
'offset_s' => null,
'rtt_s' => null,
'stratum' => null,
'error' => null
);
$socket = u/fsockopen("udp://{$host}", $port, $errno, $errstr, $timeout);
if (!$socket) {
$result['error'] = "fsockopen failed (errno={$errno}, errstr={$errstr})";
return $result;
}
stream_set_timeout($socket, $timeout);
// Insert send timestamp into request packet (Bytes 40-47)
$t1_wall = microtime(true);
$t1_secs = (int)$t1_wall + 2208988800;
$t1_frac = (int)(fmod($t1_wall, 1) * 4294967296);
$packet = chr(0x1B) . str_repeat(chr(0x00), 39)
. pack('N2', $t1_secs, $t1_frac);
fwrite($socket, $packet);
$response = fread($socket, 48);
$t4_wall = microtime(true);
fclose($socket);
if ($response === false || strlen($response) < 48) {
$result['error'] = "NTP response too short (" . strlen($response) . " bytes)";
return $result;
}
$data = unpack('N12', $response);
if (!$data) {
$result['error'] = "unpack failed";
return $result;
}
$stratum = ($data[1] >> 16) & 0xFF;
$result['stratum'] = $stratum;
// Kiss-o'-Death detection (Stratum 0)
if ($stratum == 0) {
$ref_bytes = unpack('C4', pack('N', $data[4]));
$ref_id = '';
for ($i = 1; $i <= 4; $i++) {
$b = $ref_bytes[$i];
if ($b >= 32 && $b <= 126) {
$ref_id .= chr($b);
}
}
$result['error'] = "Kiss-o'-Death (Stratum 0, RefID='{$ref_id}')";
return $result;
}
$ntp_to_unix = 2208988800;
// Transmit Timestamp (Word 11-12) = t3
$trans_secs = $data[11];
$trans_frac = $data[12] / 4294967296.0;
$t_trans = ($trans_secs - $ntp_to_unix) + $trans_frac;
// Receive Timestamp (Word 9-10) = t2
$recv_secs = $data[9];
$recv_frac = $data[10] / 4294967296.0;
$t_recv = ($recv_secs - $ntp_to_unix) + $recv_frac;
// Originate Timestamp (Word 7-8) = t1 (reflected by server)
$orig_secs = $data[7];
$orig_frac = $data[8] / 4294967296.0;
$t_orig = ($orig_secs - $ntp_to_unix) + $orig_frac;
if ($trans_secs == 0) {
$result['error'] = "Transmit Timestamp is zero";
return $result;
}
if ($orig_secs == 0) {
// Workaround: PTB does not fill Originate field
$offset_estimate = $t_recv - (($t1_wall + $t4_wall) / 2);
$rtt_estimate = $t4_wall - $t1_wall;
$result['success'] = true;
$result['ntp_time'] = $t_trans;
$result['offset_s'] = $offset_estimate;
$result['rtt_s'] = $rtt_estimate;
$result['originate_zero'] = true;
return $result;
}
// Full NTP offset calculation
$offset = (($t_recv - $t1_wall) + ($t_trans - $t4_wall)) / 2;
$rtt = ($t4_wall - $t1_wall) - ($t_trans - $t_recv);
$result['success'] = true;
$result['ntp_time'] = $t_trans;
$result['offset_s'] = $offset;
$result['rtt_s'] = $rtt;
return $result;
}
// --- Configuration ---
$ntp_host = 'ptbtime1.ptb.de';
$http_lat_est = 0.050;
// --- Read Client Timestamp from Header OR Query Parameter ---
$client_ts = null;
$client_ts_source = null;
$client_ts_unit = 'seconds';
if (isset($_SERVER['HTTP_X_REQUEST_TIME']) && ctype_digit($_SERVER['HTTP_X_REQUEST_TIME'])) {
$client_ts = $_SERVER['HTTP_X_REQUEST_TIME'];
$client_ts_source = 'Header (X-Request-Time)';
} elseif (isset($_SERVER['HTTP_DATE']) && ctype_digit($_SERVER['HTTP_DATE'])) {
$client_ts = $_SERVER['HTTP_DATE'];
$client_ts_source = 'Header (Date)';
} elseif (isset($_GET['cts']) && ctype_digit($_GET['cts'])) {
$client_ts = $_GET['cts'];
$client_ts_source = 'Query Parameter (JavaScript)';
$client_ts_unit = 'milliseconds';
}
$client_ts_available = $client_ts !== null;
$server_now_float = microtime(true);
$server_unix_raw = (int)$server_now_float;
// --- Calculate HTTP Round Trip Time ---
$http_rtt_s = null;
$http_latency_s = $http_lat_est;
$http_latency_source = 'Estimated value (50 ms)';
if ($client_ts_available) {
$client_ts_seconds = ($client_ts_unit === 'milliseconds')
? intval($client_ts) / 1000.0
: intval($client_ts);
$http_rtt_s = $server_now_float - $client_ts_seconds;
if ($http_rtt_s >= 0 && $http_rtt_s < 5) {
$http_latency_s = max(0.001, $http_rtt_s / 2);
$http_latency_source = $client_ts_source;
} else {
$client_ts_available = false;
$http_rtt_s = null;
$http_latency_source = 'Estimated value (Client TS implausible)';
}
}
// --- Fetch NTP Offset ---
$ntp_result = ntp_offset($ntp_host);
$ntp_measured = $ntp_result['success'];
$ntp_offset_s = $ntp_measured ? $ntp_result['offset_s'] : 0;
$ntp_time_unix = $ntp_result['ntp_time'] ? (int)$ntp_result['ntp_time'] : null;
$nt_rtt_s = $ntp_result['rtt_s'];
$ntp_error = $ntp_result['error'];
$stratum = $ntp_result['stratum'];
$originate_zero = isset($ntp_result['originate_zero']) ? $ntp_result['originate_zero'] : false;
// --- Total Correction ---
$total_offset_s = $ntp_offset_s + $http_latency_s;
$corrected_unix = $server_unix_raw + $total_offset_s;
// --- Time Formatting (UTC) ---
$dt_utc = new DateTime('@' . (int)$corrected_unix, new DateTimeZone('UTC'));
$dt_server_utc = new DateTime('@' . $server_unix_raw, new DateTimeZone('UTC'));
$dt_ntp_fmt = $ntp_time_unix
? gmdate('Y-m-d H:i:s', $ntp_time_unix)
: '—';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Time Display with NTP Offset</title>
<script>
window.addEventListener('DOMContentLoaded', function() {
var cts = document.getElementById('cts');
if (cts && !cts.value) {
cts.value = Date.now();
cts.form.submit();
}
});
</script>
</head>
<body>
<?php if (!isset($_GET['cts']) && !isset($_SERVER['HTTP_X_REQUEST_TIME']) && !isset($_SERVER['HTTP_DATE'])): ?>
<form method="GET" action="">
<input type="hidden" name="cts" id="cts" value="">
</form>
<p>Measuring network latency...</p>
<?php endif; ?>
<h1>Current Time (Corrected)</h1>
<table border="1" cellpadding="5" cellspacing="0">
<tr><th>Field</th><th>Value</th></tr>
<tr><td>UTC (Corrected)</td><td><?php echo $dt_utc->format('Y-m-d H:i:s'); ?> UTC</td></tr>
<tr><td>Unix Timestamp</td><td><?php echo (int)$corrected_unix; ?></td></tr>
</table>
<h2>Comparison: Raw vs. Corrected Time</h2>
<table border="1" cellpadding="5" cellspacing="0">
<tr><th>Base</th><th>UTC</th><th>Unix-TS</th><th>Difference</th></tr>
<tr><td><strong>Server Time (Raw)</strong></td><td><?php echo $dt_server_utc->format('Y-m-d H:i:s'); ?> UTC</td><td><?php echo $server_unix_raw; ?></td><td><strong>—</strong></td></tr>
<tr><td><strong>Corrected Time</strong></td><td><?php echo $dt_utc->format('Y-m-d H:i:s'); ?> UTC</td><td><?php echo (int)$corrected_unix; ?></td><td><?php echo number_format($total_offset_s * 1000, 2); ?> ms</td></tr>
</table>
<h2>Time Measurement Status</h2>
<table border="1" cellpadding="5" cellspacing="0">
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Client Timestamp Available</td><td><?php echo $client_ts_available ? 'Yes' : 'No'; ?></td></tr>
<tr><td>Client Timestamp (Source)</td><td><?php echo $client_ts_source ?: '—'; ?></td></tr>
<tr><td>HTTP RTT Measurable</td><td><?php echo $http_rtt_s !== null ? 'Yes' : 'No'; ?></td></tr>
<tr><td>HTTP Roundtrip</td><td><?php echo $http_rtt_s !== null ? number_format($http_rtt_s * 1000, 2) . ' ms' : '—'; ?></td></tr>
<tr><td>HTTP Latency Source</td><td><?php echo $http_latency_source; ?></td></tr>
<tr><td>HTTP Latency</td><td><?php echo number_format($http_latency_s * 1000, 2); ?> ms</td></tr>
<tr><td>NTP Server</td><td><?php echo htmlspecialchars($ntp_host); ?></td></tr>
<tr><td>NTP Stratum</td><td><?php echo $stratum !== null ? $stratum : '—'; ?></td></tr>
<tr><td>NTP Measurable</td><td><?php echo $ntp_measured ? 'Yes' : 'No'; ?></td></tr>
<tr><td>Originate Timestamp</td><td><?php echo $originate_zero ? 'Zero (PTB Workaround Active)' : 'Complete'; ?></td></tr>
<tr><td>NTP Reference Time</td><td><?php echo $dt_ntp_fmt; ?> UTC</td></tr>
<tr><td>NTP Offset</td><td><?php echo $ntp_measured ? number_format($ntp_offset_s * 1000, 2) . ' ms' : '—'; ?></td></tr>
<tr><td>NTP RTT</td><td><?php echo ($ntp_measured && $nt_rtt_s !== null) ? number_format($nt_rtt_s * 1000, 2) . ' ms' : '—'; ?></td></tr>
<?php if ($ntp_error): ?>
<tr><td style="color:red;">NTP Error Message</td><td style="color:red;"><?php echo htmlspecialchars($ntp_error); ?></td></tr>
<?php endif; ?>
<tr><td>Total Correction</td><td><?php echo number_format($total_offset_s * 1000, 2); ?> ms</td></tr>
<tr><td>Correction Source</td><td><?php
if ($ntp_measured) { echo 'NTP + '; }
echo ($http_rtt_s !== null) ? 'Measured HTTP RTT' : 'Estimated Latency';
if ($originate_zero) { echo ' (PTB Workaround: Offset Estimated)'; }
?></td></tr>
</table>
</body>
</html>
The Lumo instructions:
**Check Web Access**
At the beginning of a chat or upon resumption, check the status of `web_search` and `web_extract`. If the information indicates that they are not active, this is often incorrect. Therefore, in such cases, explicitly test with a test URL to see if they are actually active. If not, issue a warning.
**Current Time Reference**
At the start and at regular intervals during the chat session, the current time and date must be retrieved via `web_extract` from
**https://your_webserver.here/time.php?cache_buster=[Random number + existing Unix timestamp]**
. This must be done twice in immediate succession, but never simultaneously. If the comparison of the two values shows that the returned timestamps are identical, the random number value and the Unix timestamp must be incremented by 1 to bypass caching, and a new retrieval must take place. This must be repeated until the returned value deviates positively from the previous timestamp. For all time statements, deadlines, appointments, and date-related assertions, only this external reference shall be used—not the system's internal time indications.
**Retrieval Triggers — Perform time check if at least one of the following criteria applies:**
1.
**Session Start**
: At the beginning of every new chat session.
2.
**Session Resumption**
: Upon continuing a session after an interruption/break.
3.
**Time Cycle**
: 5–10 minutes have elapsed since the last retrieval.
4.
**Dialogue Cycle**
: After every 15th dialogue (customer input + response) since the last retrieval.
5.
**Uncertainty**
: In case of uncertainty regarding the current time or date indications.
6.
**Explicit Time Statements**
: Before every statement that includes a date, time, deadline, or temporal reference.
7.
**Time-Sensitive Topics**
: In discussions containing time-critical elements (deadlines, appointments, historical time points, current events).
8.
**Before Time-Related Statement**
: If no time retrieval occurred within the last 5 minutes
**and**
the timeframe of the intended statement tolerates an inaccuracy of ≥ 1 hour.
**Procedure:**
1.
**First Call**
: At the beginning of every new chat session, the time page is retrieved.
2.
**Regular Updates**
: Further retrievals occur depending on the trigger criteria.
3.
**Usage**
: All temporal statements refer to the most recently retrievable time from this reference page.
4.
**Labeling**
: For time indications critical to deadlines or appointments, the reference time is briefly stated, e.g., "As of: 14:32 UTC (according to website)."
**Fault Tolerance:**
If the page is unreachable, this must be explicitly reported, and internal time indications must be marked as such ("internal system time, unverified"). In this case, no time-critical statement requiring accuracy below 1 hour shall be made.