PageSourceSearch

https://mdworld.nl/asset-tracking

html mdworld.nl collected 2026-10-03 23:20:22 UTC 21,129 bytes, 233 lines download raw bytes

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 &#123;
103        reverse_proxy 192.168.0.3:8012
104&#125;</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-----/&#123;x="cert"++i".pem"&#125;; &#123;print > x&#125;'</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&amp;client_id=homeassistant&amp;client_secret=SECRET' https://my-openremote-server.example.com:9743/auth/realms/master/protocol/openid-connect/token | jq -r .access_token) &amp;&amp; curl -L 'https://my-openremote-server.example.com:9743/api/master/asset/ASSET_ID' -H "Authorization: Bearer $token" | jq '&#123; 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 &#125;'"</span>
152      <span class="token key atrule">value_template</span><span class="token punctuation">:</span> <span class="token string">"&#123;&#123; value_json.coordinates&#125;&#125;"</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 &gt; 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.