1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <link rel="icon" href="./favicon.png" /> 5 <meta charset="utf-8" /> 6 <meta name="viewport" content="width=device-width" /> 7 <meta http-equiv="content-security-policy" content=""> 8 <link href="./_app/immutable/assets/_layout.e41a4111.css" rel="stylesheet"> 9 <link href="./_app/immutable/assets/ArticleMeta.abc8c39f.css" rel="stylesheet"> 10 <link href="./_app/immutable/assets/2014-11-29-on-reactive.1d166b07.css" rel="stylesheet"> 11 <link href="./_app/immutable/assets/2015-01-20-fronteers-2014-css.c1f1e55f.css" rel="stylesheet"> 12 <link href="./_app/immutable/assets/2015-04-15-fronteers-2014-a-glossary.7246c081.css" rel="stylesheet"> 13 <link href="./_app/immutable/assets/Lightbox.ef707f92.css" rel="stylesheet"> 14 <link rel="modulepreload" href="./_app/immutable/entry/start.8bb53fb6.js"> 15 <link rel="modulepreload" href="./_app/immutable/chunks/index.f054af2f.js"> 16 <link rel="modulepreload" href="./_app/immutable/chunks/singletons.b8cd234c.js"> 17 <link rel="modulepreload" href="./_app/immutable/chunks/control.e7f5239e.js"> 18 <link rel="modulepreload" href="./_app/immutable/entry/app.74683ed1.js"> 19 <link rel="modulepreload" href="./_app/immutable/chunks/preload-helper.41c905a7.js"> 20 <link rel="modulepreload" href="./_app/immutable/entry/_layout.svelte.a0264d24.js"> 21 <link rel="modulepreload" href="./_app/immutable/entry/_layout.ts.3ea2b760.js"> 22 <link rel="modulepreload" href="./_app/immutable/chunks/_layout.6a830c33.js"> 23 <link rel="modulepreload" href="./_app/immutable/entry/_slug_-page.svelte.d76af047.js"> 24 <link rel="modulepreload" href="./_app/immutable/chunks/ArticleMeta.ae13e69e.js"> 25 <link rel="modulepreload" href="./_app/immutable/chunks/MetaTags.c7dd54e2.js"> 26 <link rel="modulepreload" href="./_app/immutable/entry/_slug_-page.ts.5b0c2a42.js"> 27 <link rel="modulepreload" href="./_app/immutable/chunks/_page.b11bff7c.js"> 28 <link rel="modulepreload" href="./_app/immutable/chunks/index.2defaa64.js"><title>Enterprise Asset Tracking with Teltonika and Home Assistant</title><!-- HEAD_svelte-1g8izgd_START --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="true"><link href="https://fonts.googleapis.com/css2?family=Playfair+Display&family=Roboto:wght@400;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-Xi8rHCmBmhbuyyhbI88391ZKP2dmfnOl4rT9ZfRI7mLTdk1wblIUnrIq35nqwEvC" crossorigin="anonymous"><!-- HEAD_svelte-1g8izgd_END --><!-- HEAD_svelte-1h7w4y4_START --><meta name="robots" content="index,follow"><meta name="googlebot" content="index,follow"><meta name="description" content="Recently I decided to set up enterprise-grade vehicle tracking for my car. What started as a simple project to monitor location turned into a comprehensive fleet management system with real-time data visualization in Home Assistant. This post covers..."><meta property="og:url" content="https://mdworld.nl//asset-tracking"> 29 30 31 32 <meta property="og:title" content="Enterprise Asset Tracking with Teltonika and Home Assistant"> 33 34 <meta property="og:description" content="Recently I decided to set up enterprise-grade vehicle tracking for my car. What started as a simple project to monitor location turned into a comprehensive fleet management system with real-time data visualization in Home Assistant. This post covers..."> 35 36 <meta property="og:image" content="https://mdworld.nl/mdworld-ross/logos/logo-48.png"> 37 <meta property="og:image:alt" content="MDWorld Logo"> 38 <meta property="og:image:width" content="48"> 39 <meta property="og:image:height" content="48"> 40 41 42 43 44 45 <meta property="og:site_name" content="https://mdworld.nl"><!-- HEAD_svelte-1h7w4y4_END --> 46 </head> 47 <body data-sveltekit-preload-data="hover"> 48 <div style="display: contents"> 49 50 51 52 53<header class="svelte-1315bri"><div class="header-container main-column svelte-1315bri"><a href="/" class="svelte-1315bri"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewB
53ox="0 0 694 586" class="svg svelte-owt51e"><title>The mdworld.nl logo</title><defs><linearGradient id="b"><stop offset="0" stop-color="#1b561b"></stop><stop offset="1" stop-color="#267626"></stop></linearGradient><linearGradient id="c" x1="351.86" x2="350.941" y1="657.606" y2="508.861" gradientTransform="matrix(3.73431 0 0 3.73431 -969.841 -1888.906)" gradientUnits="userSpaceOnUse" xlink:href="#b"></linearGradient></defs><path fill="url(#c)" stroke="#232323" stroke-width="17.403" d="M163.79 173.12l-35.343 180.715L577.23 16.413l102.027 546.81-343.357-259.901 71.452-57.516 135.87 106.028-36.01-187.382-494.13 400.105L119.111 17.08l222.792 176.046-74.586 56.681z"></path></svg> 54 <span>MDWorld, a webdevelopment blog</span></a> 55 56 <nav class="svelte-1315bri"><ul class="svelte-1315bri"><li><a href="/about" class="svelte-1315bri">About</a></li> 57 <li><a href="https://github.com/mdvanes/" class="svelte-1315bri"><img src="images/github.svg" alt="Github" class="svelte-1315bri"></a></li> 58 <li><a href="https://mastodon.social/@mdworld" rel="me" class="svelte-1315bri"><img src="images/mastodon.svg" alt="Mastodon" class="svelte-1315bri"></a></li> 59 <li><a href="https://www.linkedin.com/in/mdvanes/" class="svelte-1315bri"><img src="images/linkedin.svg" alt="LinkedIn" class="svelte-1315bri"></a></li></ul></nav></div> 60</header> 61 62<main class="main-column"> 63 64<article><h1>Enterprise Asset Tracking with Teltonika and Home Assistant</h1> 65 <p class="meta svelte-191nhos"><span>Published: 08-08-2025</span> 66 <a href="/" class="pill svelte-191nhos">mypc</a> 67</p> 68 <p>Recently I decided to set up enterprise-grade vehicle tracking for my car. What started as a simple project to monitor location turned into a comprehensive fleet management system with real-time data visualization in Home Assistant. This post covers the complete journey from hardware selection to home automation integration.</p> 69<p>I already had a vehicle tracker based on a Marvin Arduino device with LoRa connectivity, but it had some limitiations. LoRa has no coverage outside my country, and it had no on-board GPS, so it used LoRa triangulation to determine its location which is far less accurate.</p> 70<h2>Hardware Selection: Teltonika FMC003</h2> 71<p>After researching various GPS trackers, I settled on the <a href="https://teltonika-gps.com/products/trackers/fmc003" rel="nofollow">Teltonika FMC003</a>, an enterprise-grade vehicle tracker. Key factors in this decision:</p> 72<ul><li>Listed as compatible on <a href="https://www.traccar.org/devices/" rel="nofollow">traccar.org</a>, where Traccar integrates well with Home Assistant.</li> 73<li>It plugs into the OBD-II port of the car for power and can read vehicle diagnostics like battery charge and odometer</li> 74<li>Supports 4G connectivity with standard SIM cards and has on-board GPS.</li></ul> 75<p>The device costs around â¬67-97 depending on the supplier, which is reasonable for enterprise hardware and not prohibitively expensive for hobbyists. Itâs an improvement over my </p> 76<h2>Connectivity: KPN IoT SIM</h2> 77<p>For connectivity, I used a KPN IoT SIM card. It is possible to request free trial SIMs at portal.kpnthings.com which are limited to 100MB per month and are valid for one year after activation.</p> 78<p>The key here is APN selection as I found out:</p> 79<ul><li><code>kpnthings2.m2m</code>: (the default APN) is restricted to the KPN IoT platform only</li> 80<li><code>internet.m2m</code>: provides open internet access while still manageable through KPNâs portal</li></ul> 81<p>The latter is essential since we need the tracker to communicate with our self-hosted server over the public internet.</p> 82<h2>The OpenRemote Challenge</h2> 83<p>Initially, I planned to use Traccar, but the Teltonika device configuration documentation led me to OpenRemote instead. The device supports MQTT with TLS encryption, which requires a proper certificate authority - this ruled out simple self-signed certificates.</p> 84<p>OpenRemote provides a <a href="https://github.com/openremote/fleet-management/wiki/Tutorial%3A-Create-your-own-Fleet-Management-System" rel="nofollow">
84Fleet Management tutorial</a> specifically for Teltonika devices, making it the logical choice.</p> 85<h2>Self-Hosting OpenRemote</h2> 86<p>Setting up OpenRemote required some network configuration due to port conflicts on my home server:</p> 87<pre class="language-yaml"><!-- HTML_TAG_START --><code class="language-yaml"><span class="token comment"># Port mappings in docker-compose.yml</span> 88<span class="token comment"># Standard HTTPS for web interface</span> 899743<span class="token punctuation">:</span><span class="token number">443</span> <span class="token comment"># Instead of 443:443 (Caddy already uses 443)</span> 90<span class="token comment"># MQTT over TLS for device communication </span> 918883<span class="token punctuation">:</span><span class="token number">8883</span> 92<span class="token comment"># Let's Encrypt certificate renewal</span> 938012<span class="token punctuation">:</span><span class="token number">80</span> <span class="token comment"># Forwarded via Caddy</span></code><!-- HTML_TAG_END --></pre> 94<h3>Certificate Management Automation</h3> 95<p>The biggest operational challenge is TLS certificate management. Letâs Encrypt certificates expire every 90 days, and each renewal requires updating the certificate on the physical tracking device.</p> 96<p>My current server already had port 80 in use by Pi-hole and a Caddy reverse proxy for accessing services on several subdomains that use Letâs Encrypt certificates.</p> 97<p>To realise this, I forward port 80 on my modem to port 8011 on the docker container for Caddy, but there map port 8011 to port 80. That way Letâs Encrypt certification renewal still works.</p> 98<p>However, I canât use the existing Caddy reverse proxy for OpenRemote, because it needs MQTTS and Caddy doesnât support that. OpenRemote uses itâs own proxy container with HAProxy to support that. To be able to renew certificates for this proxy, I forward unencrypted HTTP traffic from Caddy to the OpenRemote Proxy running on port 8012:</p> 99<pre class="language-undefined"><!-- HTML_TAG_START --><code class="language-undefined"># Snippet from Caddyfile 100 101# Redirect port 80 without httpS to openremote HaProxy, so SSL renewal will work 102http://my-openremote-server.example.com { 103 reverse_proxy 192.168.0.3:8012 104}</code><!-- HTML_TAG_END --></pre> 105<p>I automated the certificate extraction with a script (<code>~/openremote/getcert.sh</code>):</p> 106<pre class="language-bash"><!-- HTML_TAG_START --><code class="language-bash"><span class="token shebang important">#!/bin/bash</span> 107 108<span class="token comment"># Extract the live certificate (symlink to latest)</span> 109<span class="token function">docker</span> <span class="token function">cp</span> <span class="token parameter variable">-L</span> openremote-proxy-1:/deployment/letsencrypt/live/my-openremote-server.example.com/fullchain.pem <span class="token builtin class-name">.</span> 110<span class="token function">mv</span> fullchain.pem fullchain1.pem 111 112<span class="token comment">
112# Reverse certificate order (required by Teltonika)</span> 113<span class="token function">awk</span> <span class="token string">'/-----BEGIN CERTIFICATE-----/{x="cert"++i".pem"}; {print > x}'</span> fullchain1.pem 114<span class="token function">ls</span> cert*.pem <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-V</span> <span class="token parameter variable">-r</span> <span class="token operator">|</span> <span class="token function">xargs</span> <span class="token function">cat</span> <span class="token operator">></span> reversed_certchain.pem 115<span class="token function">rm</span> cert*.pem 116 117<span class="token builtin class-name">echo</span> fullchain1.pem reversed to reversed_certchain.pem 118 119<span class="token function">rm</span> fullchain1.pem 120 121<span class="token function">mv</span> reversed_certchain.pem fullchain.pem 122 123<span class="token builtin class-name">echo</span> cleaned up files and renamed result to fullchain.pem 124 125<span class="token comment"># Removed the rest of the script that uses ssmtp to mail the certificate to myself</span></code><!-- HTML_TAG_END --></pre> 126<p><strong>Critical lesson learned</strong>: Always use the <code>live/</code> symlink, not the <code>archive/</code> directory. The live certificate automatically points to the latest renewal (e.g., <code>fullchain4.pem</code> after multiple renewals).</p> 127<h2>Device Configuration with TCT</h2> 128<p>The Teltonika Configuration Tool (TCT) requires Windows and wonât work in ARM-based VMs like Parallels on Apple Silicon. Additionally, the device needs 30V power, so configuration must initially be done with the device connected to the vehicleâs OBD-II port. After some time, the battery in the device can be used so it doesnât have to be plugged in to the OBD-II port. But after a reboot of the device, it will first have to be connected to an OBD-II port before it starts up again.</p> 129<p>Key configuration settings:</p> 130<p><strong>GPRS Tab:</strong></p> 131<ul><li>APN: <code>internet.m2m</code></li> 132<li>Enable GPRS</li></ul> 133<p><strong>Server Settings:</strong></p> 134<ul><li>Domain: <code>my-openremote-server.example.com</code></li> 135<li>Port: <code>8883</code> (MQTTS)</li> 136<li>Protocol: <code>MQTT</code></li> 137<li>TLS Encryption: <code>TLS/DTLS</code></li></ul> 138<p><strong>MQTTS Settings:</strong></p> 139<ul><li>MQTT Client Type: <code>AWS IoT Custom</code></li> 140<li>Data topic: <code>master/myFmc003/teltonika/%imei%/data</code></li> 141<li>Commands topic: <code>master/myFmc003/teltonika/%imei%/commands</code></li></ul> 142<p><strong>System Tab:</strong></p> 143<ul><li>Data Protocol: <code>Codec JSON</code></li></ul> 144<p><strong>Security Tab:</strong></p> 145<ul><li>Upload the reversed certificate chain (fullchain.pem)</li></ul> 146<h2>Home Assistant Integration</h2> 147<p>OpenRemote provides REST APIs, but they use OAuth2 with short-lived tokens (60 seconds). Since Home Assistant doesnât natively support OAuth2 token refresh for REST sensors, I created a command-line sensor that handles authentication. I have to stress that this is not a recommended or safe way to deal with OAuth2.</p> 148<pre class="language-yaml"><!-- HTML_TAG_START --><code class="language-yaml"><span class="token key atrule">command_line</span><span class="token punctuation">:</span> 149 <span class="token punctuation">-</span> <span class="token key atrule">sensor</span><span class="token punctuation">:</span> 150 <span class="token key atrule">name</span><span class="token punctuation">:</span> fmc003 151 <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token string">"token=$(curl -d 'grant_type=client_credentials&client_id=homeassistant&client_secret=SECRET' https://my-openremote-server.example.com:9743/auth/realms/master/protocol/openid-connect/token | jq -r .access_token) && curl -L 'https://my-openremote-server.example.com:9743/api/master/asset/ASSET_ID' -H "Authorization: Bearer $token" | jq '{ coordinates: .attributes.location.value.coordinates, longitude: .attributes.location.value.coordinates[0], latitude: .attributes.location.value.coordinates[1], speed: .attributes.sp.value, charge: .attributes["57"].value, odometer: .attributes["389"].value, altitude: .attributes.alt.value }'"</span>
152 <span class="token key atrule">value_template</span><span class="token punctuation">:</span> <span class="token string">"{{ value_json.coordinates}}"</span> 153 <span class="token key atrule">json_attributes</span><span class="token punctuation">:</span> 154 <span class="token punctuation">-</span> latitude 155 <span class="token punctuation">-</span> longitude 156 <span class="token punctuation">-</span> charge 157 <span class="token punctuation">-</span> speed 158 <span class="token punctuation">-</span> odometer 159 <span class="token punctuation">-</span> altitude 160 <span class="token key atrule">icon</span><span class="token punctuation">:</span> <span class="token string">"mdi:car"</span></code><!-- HTML_TAG_END --></pre> 161<p>This approach:</p> 162<ol><li>Obtains a fresh OAuth token</li> 163<li>Fetches asset data from OpenRemote</li> 164<li>Transforms the JSON to extract relevant attributes</li> 165<li>Is refreshed by Home Assistant every 60 seconds by default</li></ol> 166<p>Note that SECRET and ASSET_ID should be replaced by values from OpenRemote. SECRET can be found under Users > create a service user with âread:assetsâ rights. The ASSET_ID is visible in the URL on the asset details page.</p> 167<h2>Debugging and Troubleshooting</h2> 168<p>After a couple of months I noted that the data in Home Assistant was no longer update. I enabled detailed logging in the OpenRemote HAProxy container:</p> 169<pre class="language-yaml"><!-- HTML_TAG_START --><code class="language-yaml"><span class="token key atrule">PROXY_LOGLEVEL</span><span class="token punctuation">:</span> <span class="token string">'info'</span></code><!-- HTML_TAG_END --></pre> 170<p>This revealed the SSL handshake failure:</p> 171<pre class="language-undefined"><!-- HTML_TAG_START --><code class="language-undefined">100.200.70.60:38736 [05/Aug/2025:08:37:12.994] mqtt/1: SSL handshake failure (error:0A000418:SSL routines::tlsv1 alert unknown ca)</code><!-- HTML_TAG_END --></pre> 172<p>The root cause was using the wrong certificate path in my automation script - it was copying from <code>archive/fullchain1.pem</code> instead of the <code>live/fullchain.pem</code> symlink.</p> 173<h2>Visualization</h2> 174<p>Home Assistantâs <a href="https://ultravehiclecard.com" rel="nofollow">Ultra Vehicle Card</a> provides excellent visualization of the tracking data, displaying:</p> 175<ul><li>A picture of the car that changes based on the charging state</li> 176<li>Battery charge level</li> 177<li>Odometer reading</li></ul> 178<p>Beside that other Home Assistant cards can be used to show:</p> 179<ul><li>Real-time location on a map</li> 180<li>Current speed</li> 181<li>Altitude</li></ul> 182<p>Template sensors extract individual attributes for use in other automations and dashboards.</p> 183<h2>Lessons Learned</h2> 184<ol><li><strong>Certificate management is critical</strong>: Automate extraction but always use the <code>live/</code> symlink</li> 185<li><strong>OAuth2 complexity</strong>: Command-line sensors can handle authentication better than REST sensors for short-lived tokens</li> 186<li><strong>Enterprise hardware requirements</strong>: Donât underestimate power and connectivity requirements</li> 187<li><strong>Teltonika peculiarities</strong>: Certificate order must be reversed, Windows required for configuration</li> 188<li><strong>Network planning</strong>: Multiple ports required, plan for certificate renewal downtime</li></ol> 189<h2>Future Improvements</h2> 190<ul><li>Investigate running TCT on an x64 Linux VM with a separate OBD-II power supply so configuration can be done outside the car.</li></ul> 191<p>This setup provides enterprise-grade vehicle tracking with full control over data and impressive integration possibilities with home automation systems. While the initial setup requires effort, the result is a robust, self-hosted fleet management solution.</p></article></main> 192 193<footer class="svelte-r5ma2i"><div class="footer-container main-column svelte-r5ma2i"><div><a class="cc svelte-r5ma2i" href="https://creativecommons.org/licenses/by-nc-sa/3.0/"><img src="images/by-nc-sa.eu.svg" alt="creative commons" height="50px"> 194 <div>2003-2025 MDWORLD</div></a></div> 195 196 <div><p>
196This site uses no tracking, analytics or cookies.</p> 197 <p>To prevent <a href="https://en.wikipedia.org/wiki/Link_rot">Link Rot</a>, URLs to articles 198 of previous versions of this blog are persistent since 2018. 199 </p> 200 <p>Hosted on <a href="https://pages.github.com">Github Pages</a>, domain at 201 <a href="https://vdx.nl">VDX.nl</a></p> 202 <a data-sveltekit-reload class="rss svelte-r5ma2i" href="https://mdworld.nl/rss.xml"><img src="images/rss.svg" alt="RSS"> RSS</a></div></div> 203</footer> 204 205 206
206<script type="application/json" data-sveltekit-fetched data-url="/api/post/asset-tracking">{"status":200,"statusText":"","headers":{},"body":"\"2025-08-08-asset-tracking\""}</script>
206 207
207<script> 208 { 209 __sveltekit_35wjxw = { 210 base: new URL(".", location).pathname.slice(0, -1), 211 env: {} 212 }; 213 214 const element = document.currentScript.parentElement; 215 216 const data = [null,null]; 217 218 Promise.all([ 219 import("./_app/immutable/entry/start.8bb53fb6.js"), 220 import("./_app/immutable/entry/app.74683ed1.js") 221 ]).then(([kit, app]) => { 222 kit.start(app, element, { 223 node_ids: [0, 4], 224 data, 225 form: null, 226 error: null 227 }); 228 }); 229 } 230 </script>
230 231 </div> 232 </body> 233</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.