commit 1ac87c37889679908e1bf0108aad4bbc043de9aa Author: marijo Date: Wed Jun 10 11:21:02 2026 +0200 Initial FJR webpage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c317064 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +AGENTS.md diff --git a/config.php b/config.php new file mode 100644 index 0000000..b702c0b --- /dev/null +++ b/config.php @@ -0,0 +1,12 @@ + 'Yamaha FJR', + 'production_year' => '2018', + 'registration_number' => 'DW83395', + 'purchase_date' => '2024-03-02', + 'purchase_km' => 48250, + 'topic_prefix' => 'fjr', + 'mqtt_host' => '192.168.0.203', + 'mqtt_port' => 1883, + 'data_dir' => __DIR__ . '/data', +]; diff --git a/data/events.jsonl b/data/events.jsonl new file mode 100644 index 0000000..7685670 --- /dev/null +++ b/data/events.jsonl @@ -0,0 +1,32 @@ +{"time": "2024-03-02T12:00:00+00:00", "type": "note", "label": "Other note", "km": 48250, "cost": null, "note": "picked up from dealership!", "summary": "Other note @ 48250 km - picked up from dealership!"} +{"time": "2024-03-04T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 48500, "cost": null, "note": "Trip: 250 km; Total since pickup: 250 km", "summary": "Odometer update only @ 48500 km - Trip: 250 km; Total since pickup: 250 km"} +{"time": "2024-03-25T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 48900, "cost": null, "note": "Trip: 400 km; Total since pickup: 650 km", "summary": "Odometer update only @ 48900 km - Trip: 400 km; Total since pickup: 650 km"} +{"time": "2024-04-06T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 49427, "cost": null, "note": "Trip: 527 km; Total since pickup: 1177 km", "summary": "Odometer update only @ 49427 km - Trip: 527 km; Total since pickup: 1177 km"} +{"time": "2024-04-07T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 49584, "cost": null, "note": "Trip: 157 km; Total since pickup: 1334 km", "summary": "Odometer update only @ 49584 km - Trip: 157 km; Total since pickup: 1334 km"} +{"time": "2024-04-27T12:00:00+00:00", "type": "note", "label": "Other note", "km": 50286, "cost": null, "note": "Egeskov Castle — Trip: 702 km; Total since pickup: 2036 km", "summary": "Other note @ 50286 km - Egeskov Castle — Trip: 702 km; Total since pickup: 2036 km"} +{"time": "2024-04-28T12:00:00+00:00", "type": "note", "label": "Other note", "km": 50556, "cost": null, "note": "autobahn 220km/h (actually 202) — Trip: 270 km; Total since pickup: 2306 km", "summary": "Other note @ 50556 km - autobahn 220km/h (actually 202) — Trip: 270 km; Total since pickup: 2306 km"} +{"time": "2024-05-09T12:00:00+00:00", "type": "note", "label": "Other note", "km": 51133, "cost": null, "note": "Prison Ink — Trip: 577 km; Total since pickup: 2883 km", "summary": "Other note @ 51133 km - Prison Ink — Trip: 577 km; Total since pickup: 2883 km"} +{"time": "2024-05-18T12:00:00+00:00", "type": "note", "label": "Other note", "km": 51415, "cost": null, "note": "Ribe, Esbjerg — Trip: 282 km; Total since pickup: 3165 km", "summary": "Other note @ 51415 km - Ribe, Esbjerg — Trip: 282 km; Total since pickup: 3165 km"} +{"time": "2024-05-19T12:00:00+00:00", "type": "note", "label": "Other note", "km": 51875, "cost": null, "note": "fckin Århus — Trip: 460 km; Total since pickup: 3625 km", "summary": "Other note @ 51875 km - fckin Århus — Trip: 460 km; Total since pickup: 3625 km"} +{"time": "2024-05-23T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 52105, "cost": null, "note": "Trip: 230 km; Total since pickup: 3855 km", "summary": "Odometer update only @ 52105 km - Trip: 230 km; Total since pickup: 3855 km"} +{"time": "2024-06-08T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 52883, "cost": null, "note": "Trip: 778 km; Total since pickup: 4633 km", "summary": "Odometer update only @ 52883 km - Trip: 778 km; Total since pickup: 4633 km"} +{"time": "2024-06-15T12:00:00+00:00", "type": "note", "label": "Other note", "km": 53605, "cost": null, "note": "Metallica — Trip: 722 km; Total since pickup: 5355 km", "summary": "Other note @ 53605 km - Metallica — Trip: 722 km; Total since pickup: 5355 km"} +{"time": "2024-06-23T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 53861, "cost": null, "note": "Trip: 256 km; Total since pickup: 5611 km", "summary": "Odometer update only @ 53861 km - Trip: 256 km; Total since pickup: 5611 km"} +{"time": "2024-07-25T12:00:00+00:00", "type": "oil_change", "label": "Oil change", "km": 56712, "cost": null, "note": "Oil & filter change — Trip: 2851 km; Total since pickup: 8462 km", "summary": "Oil change @ 56712 km - Oil & filter change — Trip: 2851 km; Total since pickup: 8462 km"} +{"time": "2024-07-28T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 58376, "cost": null, "note": "Trip: 1664 km; Total since pickup: 10126 km", "summary": "Odometer update only @ 58376 km - Trip: 1664 km; Total since pickup: 10126 km"} +{"time": "2024-08-02T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 58565, "cost": null, "note": "Trip: 189 km; Total since pickup: 10315 km", "summary": "Odometer update only @ 58565 km - Trip: 189 km; Total since pickup: 10315 km"} +{"time": "2024-08-18T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 59639, "cost": null, "note": "Trip: 1074 km; Total since pickup: 11389 km", "summary": "Odometer update only @ 59639 km - Trip: 1074 km; Total since pickup: 11389 km"} +{"time": "2024-08-24T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 60067, "cost": null, "note": "Trip: 428 km; Total since pickup: 11817 km", "summary": "Odometer update only @ 60067 km - Trip: 428 km; Total since pickup: 11817 km"} +{"time": "2025-04-23T12:00:00+00:00", "type": "front_tyre_change", "label": "Front tyre changed", "km": 64183, "cost": null, "note": "Tires changed — Trip: 4116 km; Total since pickup: 15933 km", "summary": "Front tyre changed @ 64183 km - Tires changed — Trip: 4116 km; Total since pickup: 15933 km"} +{"time": "2025-04-23T12:00:00+00:00", "type": "rear_tyre_change", "label": "Rear tyre changed", "km": 64183, "cost": null, "note": "Tires changed — Trip: 4116 km; Total since pickup: 15933 km", "summary": "Rear tyre changed @ 64183 km - Tires changed — Trip: 4116 km; Total since pickup: 15933 km"} +{"time": "2025-04-26T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 64745, "cost": null, "note": "Trip: 562 km; Total since pickup: 16495 km", "summary": "Odometer update only @ 64745 km - Trip: 562 km; Total since pickup: 16495 km"} +{"time": "2025-06-05T12:00:00+00:00", "type": "oil_change", "label": "Oil change", "km": 66226, "cost": null, "note": "Oil & Filter & AirFilter changed — Trip: 1481 km; Total since pickup: 17976 km", "summary": "Oil change @ 66226 km - Oil & Filter & AirFilter changed — Trip: 1481 km; Total since pickup: 17976 km"} +{"time": "2025-07-05T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 70099, "cost": null, "note": "Trip: 3873 km; Total since pickup: 21849 km", "summary": "Odometer update only @ 70099 km - Trip: 3873 km; Total since pickup: 21849 km"} +{"time": "2025-07-12T12:00:00+00:00", "type": "odometer", "label": "Odometer update only", "km": 70196, "cost": null, "note": "Trip: 97 km; Total since pickup: 21946 km", "summary": "Odometer update only @ 70196 km - Trip: 97 km; Total since pickup: 21946 km"} +{"time": "2025-07-28T12:00:00+00:00", "type": "note", "label": "Other note", "km": 73964, "cost": null, "note": "London — Trip: 3768 km; Total since pickup: 25714 km", "summary": "Other note @ 73964 km - London — Trip: 3768 km; Total since pickup: 25714 km"} +{"time": "2024-02-23T12:00:00+01:00", "type": "registration", "label": "Registration / insurance", "km": 48250, "cost": null, "note": "Registration date", "summary": "Registration / insurance @ 48250 km - Registration date"} +{"time":"2026-04-23T12:53:38+02:00","type":"odometer","label":"Odometer update only","km":79458,"cost":null,"note":"","summary":"Odometer update only @ 79458 km"} +{"time":"2026-05-09T15:29:40+02:00","type":"oil_change","label":"Oil change","km":79964,"cost":null,"note":"","summary":"Oil change @ 79964 km"} +{"time":"2026-05-25T13:46:37+02:00","type":"odometer","label":"Odometer update only","km":80264,"cost":null,"note":"Pinsedag","summary":"Odometer update only @ 80264 km - Pinsedag"} +{"time":"2026-05-30T21:44:56+02:00","type":"odometer","label":"Odometer update only","km":80801,"cost":null,"note":"S\u00f8ndervig Strand","summary":"Odometer update only @ 80801 km - S\u00f8ndervig Strand"} +{"time":"2026-06-03T17:50:01+02:00","type":"odometer","label":"Odometer update only","km":81570,"cost":null,"note":"Iron Maiden Hannover","summary":"Odometer update only @ 81570 km - Iron Maiden Hannover"} diff --git a/data/state.json b/data/state.json new file mode 100644 index 0000000..1d19fce --- /dev/null +++ b/data/state.json @@ -0,0 +1,52 @@ +{ + "odometer": 81570, + "updated_at": "2026-06-03T17:50:01+02:00", + "last": { + "oil_change": { + "time": "2026-05-09T15:29:40+02:00", + "type": "oil_change", + "label": "Oil change", + "km": 79964, + "cost": null, + "note": "" + }, + "front_tyre_change": { + "time": "2025-04-23T12:00:00+00:00", + "type": "front_tyre_change", + "label": "Front tyre changed", + "km": 64183, + "cost": null, + "note": "Tires changed \u2014 Trip: 4116 km; Total since pickup: 15933 km", + "summary": "Front tyre changed @ 64183 km - Tires changed \u2014 Trip: 4116 km; Total since pickup: 15933 km" + }, + "rear_tyre_change": { + "time": "2025-04-23T12:00:00+00:00", + "type": "rear_tyre_change", + "label": "Rear tyre changed", + "km": 64183, + "cost": null, + "note": "Tires changed \u2014 Trip: 4116 km; Total since pickup: 15933 km", + "summary": "Rear tyre changed @ 64183 km - Tires changed \u2014 Trip: 4116 km; Total since pickup: 15933 km" + }, + "shaft_oil_change": null, + "brake_pads": null, + "registration": { + "time": "2024-02-23T12:00:00+01:00", + "type": "registration", + "label": "Registration / insurance", + "km": 48250, + "cost": null, + "note": "Registration date", + "summary": "Registration / insurance @ 48250 km - Registration date" + } + }, + "latest_event": { + "time": "2026-06-03T17:50:01+02:00", + "type": "odometer", + "label": "Odometer update only", + "km": 81570, + "cost": null, + "note": "Iron Maiden Hannover", + "summary": "Odometer update only @ 81570 km - Iron Maiden Hannover" + } +} \ No newline at end of file diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..ca88352 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + FJR + + diff --git a/img/fjr.jpg b/img/fjr.jpg new file mode 100644 index 0000000..b11d382 Binary files /dev/null and b/img/fjr.jpg differ diff --git a/img/fjr.png b/img/fjr.png new file mode 100644 index 0000000..ea73233 Binary files /dev/null and b/img/fjr.png differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..cb9c238 --- /dev/null +++ b/index.php @@ -0,0 +1,245 @@ + 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'); } +?> + +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

DateTypeKmNote
+