0, 'updated_at' => null, 'last' => [ 'oil_change' => null, 'front_tyre_change' => null, 'rear_tyre_change' => null, 'shaft_oil_change' => null, 'brake_pads' => null, 'registration' => null, ], 'latest_event' => null, ]; } function load_state(string $file): array { if (!is_file($file)) return default_state(); $s = json_decode(file_get_contents($file), true); return is_array($s) ? array_replace_recursive(default_state(), $s) : default_state(); } function save_state(string $file, array $state): void { file_put_contents($file, json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX); } function append_event(string $file, array $event): void { file_put_contents($file, json_encode($event, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX); } function recent_events(string $file, int $limit = 20): array { if (!is_file($file)) return []; $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $lines = array_slice($lines, -$limit); $events = []; foreach (array_reverse($lines) as $line) { $e = json_decode($line, true); if (is_array($e)) $events[] = $e; } return $events; } function km_since(array $state, string $key): ?int { $last = $state['last'][$key] ?? null; if (!is_array($last) || !isset($last['km'])) return null; return max(0, (int)$state['odometer'] - (int)$last['km']); } function ownership_text(array $config): string { $date = $config['purchase_date'] ?? null; if (!$date) return ''; try { $start = new DateTime($date); } catch (Throwable $e) { return ''; } $now = new DateTime('today'); $diff = $start->diff($now); $parts = []; if ($diff->y) $parts[] = $diff->y . 'y'; if ($diff->m) $parts[] = $diff->m . 'm'; if (!$parts || $diff->d) $parts[] = $diff->d . 'd'; return implode(' ', $parts); } function eu_date(?string $date): string { if (!$date) return ''; try { return (new DateTime($date))->setTimezone(new DateTimeZone('Europe/Copenhagen'))->format('d.m.Y'); } catch (Throwable $e) { return $date; } } function mqtt_pub(array $config, string $topic, string $payload, bool $retain = true): void { $host = $config['mqtt_host']; $port = (int)($config['mqtt_port'] ?? 1883); $cmd = 'mosquitto_pub -h ' . escapeshellarg($host) . ' -p ' . escapeshellarg((string)$port) . ' -t ' . escapeshellarg($topic) . ' -m ' . escapeshellarg($payload) . ($retain ? ' -r' : ''); @shell_exec($cmd . ' 2>/dev/null'); } function publish_discovery(array $config): void { $p = trim($config['topic_prefix'], '/'); $dev = ['identifiers' => ['fjr_tracker'], 'name' => $config['bike_name'], 'manufacturer' => 'Yamaha', 'model' => 'FJR']; $sensors = [ 'odometer' => ['name' => 'Odometer', 'unit' => 'km', 'topic' => "$p/odometer"], 'km_since_bought' => ['name' => 'Km since bought', 'unit' => 'km', 'topic' => "$p/km_since_bought"], 'oil_last_km' => ['name' => 'Oil last change km', 'unit' => 'km', 'topic' => "$p/oil/last_change_km"], 'oil_last_date' => ['name' => 'Oil last change date', 'unit' => '', 'topic' => "$p/oil/last_change_date"], 'oil_km_since_change' => ['name' => 'Oil km since change', 'unit' => 'km', 'topic' => "$p/oil/km_since_change"], 'front_tyre_last_km' => ['name' => 'Front tyre last change km', 'unit' => 'km', 'topic' => "$p/tyres/front/last_change_km"], 'front_tyre_last_date' => ['name' => 'Front tyre last change date', 'unit' => '', 'topic' => "$p/tyres/front/last_change_date"], 'front_tyre_km_since_change' => ['name' => 'Front tyre km since change', 'unit' => 'km', 'topic' => "$p/tyres/front/km_since_change"], 'rear_tyre_last_km' => ['name' => 'Rear tyre last change km', 'unit' => 'km', 'topic' => "$p/tyres/rear/last_change_km"], 'rear_tyre_last_date' => ['name' => 'Rear tyre last change date', 'unit' => '', 'topic' => "$p/tyres/rear/last_change_date"], 'rear_tyre_km_since_change' => ['name' => 'Rear tyre km since change', 'unit' => 'km', 'topic' => "$p/tyres/rear/km_since_change"], 'shaft_oil_last_km' => ['name' => 'Drive shaft oil last change km', 'unit' => 'km', 'topic' => "$p/shaft_oil/last_change_km"], 'shaft_oil_last_date' => ['name' => 'Drive shaft oil last change date', 'unit' => '', 'topic' => "$p/shaft_oil/last_change_date"], 'shaft_oil_km_since_change' => ['name' => 'Drive shaft oil km since change', 'unit' => 'km', 'topic' => "$p/shaft_oil/km_since_change"], 'brake_pads_last_km' => ['name' => 'Brake pads last change km', 'unit' => 'km', 'topic' => "$p/brakes/last_pads_km"], 'brake_pads_last_date' => ['name' => 'Brake pads last change date', 'unit' => '', 'topic' => "$p/brakes/last_pads_date"], 'brake_pads_km_since_change' => ['name' => 'Brake pads km since change', 'unit' => 'km', 'topic' => "$p/brakes/km_since_pads"], ]; foreach ($sensors as $id => $sensor) { $payload = ['name' => $sensor['name'], 'state_topic' => $sensor['topic'], 'unique_id' => "fjr_$id", 'device' => $dev]; if ($sensor['unit'] !== '') $payload['unit_of_measurement'] = $sensor['unit']; mqtt_pub($config, "homeassistant/sensor/fjr_$id/config", json_encode($payload, JSON_UNESCAPED_SLASHES)); } } function publish_service_state(array $config, string $baseTopic, array $state, string $key, string $lastKmTopic, string $lastDateTopic, string $sinceTopic): void { $p = trim($config['topic_prefix'], '/'); $last = $state['last'][$key] ?? null; if (is_array($last) && isset($last['km'])) { mqtt_pub($config, "$p/$lastKmTopic", (string)(int)$last['km']); mqtt_pub($config, "$p/$lastDateTopic", eu_date($last['time'] ?? '')); $v = km_since($state, $key); mqtt_pub($config, "$p/$sinceTopic", $v === null ? '' : (string)$v); } else { mqtt_pub($config, "$p/$lastKmTopic", 'unknown'); mqtt_pub($config, "$p/$lastDateTopic", 'unknown'); mqtt_pub($config, "$p/$sinceTopic", 'unknown'); } } function publish_state(array $config, array $state): void { $p = trim($config['topic_prefix'], '/'); publish_discovery($config); $purchaseKm = (int)($config['purchase_km'] ?? 0); $kmOwned = max(0, (int)$state['odometer'] - $purchaseKm); mqtt_pub($config, "$p/odometer", (string)(int)$state['odometer']); mqtt_pub($config, "$p/km_since_bought", (string)$kmOwned); publish_service_state($config, $p, $state, 'oil_change', 'oil/last_change_km', 'oil/last_change_date', 'oil/km_since_change'); publish_service_state($config, $p, $state, 'front_tyre_change', 'tyres/front/last_change_km', 'tyres/front/last_change_date', 'tyres/front/km_since_change'); publish_service_state($config, $p, $state, 'rear_tyre_change', 'tyres/rear/last_change_km', 'tyres/rear/last_change_date', 'tyres/rear/km_since_change'); publish_service_state($config, $p, $state, 'shaft_oil_change', 'shaft_oil/last_change_km', 'shaft_oil/last_change_date', 'shaft_oil/km_since_change'); publish_service_state($config, $p, $state, 'brake_pads', 'brakes/last_pads_km', 'brakes/last_pads_date', 'brakes/km_since_pads'); mqtt_pub($config, "$p/state", json_encode($state, JSON_UNESCAPED_SLASHES)); } $state = load_state($stateFile); $message = ''; $types = [ 'odometer' => 'Odometer update only', 'oil_change' => 'Oil change', 'front_tyre_change' => 'Front tyre changed', 'rear_tyre_change' => 'Rear tyre changed', 'shaft_oil_change' => 'Drive shaft oil change', 'brake_pads' => 'Brake pads', 'registration' => 'Registration / insurance', 'fuel' => 'Fuel', 'note' => 'Other note', ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $km = max(0, (int)($_POST['km'] ?? 0)); $type = $_POST['type'] ?? 'odometer'; if (!isset($types[$type])) $type = 'note'; $cost = trim($_POST['cost'] ?? ''); $note = trim($_POST['note'] ?? ''); $eventDate = trim($_POST['event_date'] ?? ''); $eventTime = date('c'); if ($eventDate !== '') { $dt = DateTime::createFromFormat('Y-m-d', $eventDate) ?: DateTime::createFromFormat('Y-m-d\TH:i', $eventDate); if ($dt instanceof DateTime) $eventTime = $dt->format('c'); } if ($km >= (int)$state['odometer']) $state['odometer'] = $km; $event = [ 'time' => $eventTime, 'type' => $type, 'label' => $types[$type], 'km' => $km, 'cost' => $cost === '' ? null : $cost, 'note' => $note, ]; if (array_key_exists($type, $state['last'])) { $prev = $state['last'][$type] ?? null; if (!is_array($prev) || $km >= (int)($prev['km'] ?? 0)) $state['last'][$type] = $event; } $event['summary'] = $types[$type] . ' @ ' . $km . ' km' . ($note ? ' - ' . $note : ''); $state['latest_event'] = $event; $state['updated_at'] = date('c'); save_state($stateFile, $state); append_event($eventsFile, $event); publish_state($config, $state); $message = 'Saved and published to MQTT.'; } $events = recent_events($eventsFile); function h($s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function event_class(?string $type): string { return match ($type) { 'odometer' => 'event-odometer', 'oil_change' => 'event-oil', 'front_tyre_change', 'rear_tyre_change' => 'event-tyre', 'shaft_oil_change' => 'event-shaft', 'brake_pads' => 'event-brakes', 'registration' => 'event-registration', 'fuel' => 'event-fuel', default => 'event-note', }; } ?> My <?=h($config['bike_name'])?>
'๐Ÿ›ข๏ธ', 'front_tyre_change' => 'โ—‰', 'rear_tyre_change' => 'โ—‰', 'shaft_oil_change' => 'โš™๏ธ', 'brake_pads' => '๐Ÿ›‘', 'registration' => '๐Ÿ“„', ]; $statCards = []; foreach ([ 'oil_change'=>'Oil', 'front_tyre_change'=>'Front tyre', 'rear_tyre_change'=>'Rear tyre', 'shaft_oil_change'=>'Drive shaft oil', 'brake_pads'=>'Brake pads', 'registration'=>'Registration'] as $key=>$label) { $v = km_since($state, $key); $last = $state['last'][$key]['km'] ?? null; $statCards[$key] = ['icon'=>$icons[$key], 'label'=>$label, 'value'=>$v===null ? 'โ€”' : number_format($v), 'suffix'=>$v===null ? '' : 'km', 'last'=>'last: ' . ($last===null ? 'never' : number_format((int)$last).' km')]; } ?>
๐Ÿ๏ธ Current odometer
km
Yamaha FJR ยท Reg:
๐Ÿ“ Since bought
km since
๐Ÿ“… Owned for
bought @ km


Add update

Recent history

No history yet.
km