PageSourceSearch

https://mdworld.nl/_app/immutable/chunks/2025-08-08-asset-tracking.4880da54.js

js mdworld.nl collected 2026-10-03 23:22:05 UTC 35,897 bytes, 43 lines download raw bytes

1import{S as Fr,i as Ur,s as Qr,k as o,q as n,a as c,l as s,m as i,r as p,h as t,c as f,n as m,b as r,F as a,G as ji}from"./index.f054af2f.js";function Kr(Pr){let I,oo,Qt,j,so,Kt,z,io,Vt,S,lo,M,ro,no,Wt,y,D,po,N,co,fo,uo,Xe,mo,ho,Ye,vo,jt,g,yo,zt,J,Eo,gt,$,To,Jt,X,_o,$t,L,Y,Ze,ko,wo,bo,Z,et,Io,So,Xt,ee,Lo,Yt,te,Po,Zt,ae,Oo,ea,P,Ao,q,Co,Ro,ta,oe,xo,aa,se,Ho,oa,G,Or=`<code class="language-yaml"><span class="token comment"># Port mappings in docker-compose.yml</span>
2<span class="token comment"># Standard HTTPS for web interface</span>
39743<span class="token punctuation">:</span><span class="token number">443</span>  <span class="token comment"># Instead of 443:443 (Caddy already uses 443)</span>
4<span class="token comment"># MQTT over TLS for device communication  </span>
58883<span class="token punctuation">:</span><span class="token number">8883</span>
6<span class="token comment"># Let's Encrypt certificate renewal</span>
78012<span class="token punctuation">:</span><span class="token number">80</span> <span class="token comment"># Forwarded via Caddy</span></code>`,sa,ie,Mo,ia,le,Do,la,re,No,ra,ne,qo,na,pe,Go,pa,B,Ar=`<code class="language-undefined"># Snippet from Caddyfile
8
9# Redirect port 80 without httpS to openremote HaProxy, so SSL renewal will work
10http://my-openremote-server.example.com &#123;
11        reverse_proxy 192.168.0.3:8012
12&#125;</code>`,ca,O,Bo,tt,Fo,Uo,fa,F,Cr=`<code class="language-bash"><span class="token shebang important">#!/bin/bash</span>
13
14<span class="token comment"># Extract the live certificate (symlink to latest)</span>
15<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>
16<span class="token function">mv</span> fullchain.pem fullchain1.pem
17
18<span class="token comment">
18# Reverse certificate order (required by Teltonika)</span>
19<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
20<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
21<span class="token function">rm</span> cert*.pem
22
23<span class="token builtin class-name">echo</span> fullchain1.pem reversed to reversed_certchain.pem
24
25<span class="token function">rm</span> fullchain1.pem
26
27<span class="token function">mv</span> reversed_certchain.pem fullchain.pem
28
29<span class="token builtin class-name">echo</span> cleaned up files and renamed result to fullchain.pem
30
31<span class="token comment"># Removed the rest of the script that uses ssmtp to mail the certificate to myself</span></code>`,ua,d,at,Qo,Ko,ot,Vo,Wo,st,jo,zo,it,go,Jo,ma,ce,$o,da,fe,Xo,ha,ue,Yo,va,me,lt,Zo,ya,A,de,es,rt,ts,as,nt,os,Ea,he,pt,ss,Ta,h,ve,is,ct,ls,rs,U,ns,ft,ps,cs,fs,ye,us,ut,ms,ds,Ee,hs,mt,vs,_a,Te,dt,ys,ka,E,_e,Es,ht,Ts,_s,ke,ks,vt,ws,bs,we,Is,yt,Ss,wa,be,Et,Ls,ba,Ie,Se,Ps,Tt,Os,Ia,Le,_t,As,Sa,Pe,kt,Cs,La,Oe,Rs,Pa,Ae,xs,Oa,Q,Rr=`<code class="language-yaml"><span class="token key atrule">command_line</span><span class="token punctuation">:</span>
32  <span class="token punctuation">-</span> <span class="token key atrule">sensor</span><span class="token punctuation">:</span>
33      <span class="token key atrule">name</span><span class="token punctuation">:</span> fmc003
34      <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>
35      <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>
36      <span class="token key atrule">json_attributes</span><span class="token punctuation">:</span>
37        <span class="token punctuation">-</span> latitude
38        <span class="token punctuation">-</span> longitude
39        <span class="token punctuation">-</span> charge
40        <span class="token punctuation">-</span> speed
41        <span class="token punctuation">-</span> odometer
42        <span class="token punctuation">-</span> altitude
43      <span class="token key atrule">icon</span><span class="token punctuation">:</span> <span class="token string">"mdi:car"</span></code>`,Aa,Ce,Hs,Ca,v,wt,Ms,Ds,bt,Ns,qs,It,Gs,Bs,St,Fs,Ra,Re,Us,xa,xe,Qs,Ha,He,Ks,Ma,K,xr=`<code class="language-yaml"><span class="token key atrule">PROXY_LOGLEVEL</span><span class="token punctuation">:</span> <span class="token string">'info'</span></code>`,Da,Me,Vs,Na,V,Hr='<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>',qa,T,Ws,Lt,js,zs,Pt,gs,Js,Ga,De,$s,Ba,C,Xs,W,Ys,Zs,Fa,_,Ot,ei,ti,At,ai,oi,Ct,si,Ua,Ne,ii,Qa,k,Rt,li,ri,xt,ni,pi,Ht,ci,Ka,qe,fi,Va,Ge,ui,Wa,u,R,Mt,mi,di,Dt,hi,vi,yi,Be,Nt,Ei,Ti,_i,Fe,qt,ki,wi,bi,Ue,Gt,Ii,Si,Li,Qe,Bt,Pi,Oi,ja,Ke,Ai,za,Ve,Ft,Ci,ga,We,Ri;return{c(){I=o("p"),oo=n("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."),Qt=c(),j=o("p"),so=n("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."),Kt=c(),z=o("h2"),io=n("Hardware Selection: Teltonika FMC003"),Vt=c(),S=o("p"),lo=n("After researching various GPS trackers, I settled on the "),M=o("a"),ro=n("Teltonika FMC003"),no=n(", an enterprise-grade vehicle tracker. Key factors in this decision:"),Wt=c(),y=o("ul"),D=o("li"),po=n("Listed as compatible on "),N=o("a"),co=n("traccar.org"),fo=n(", where Traccar integrates well with Home Assistant."),uo=c(),Xe=o("li"),mo=n("It plugs into the OBD-I
43I port of the car for power and can read vehicle diagnostics like battery charge and odometer"),ho=c(),Ye=o("li"),vo=n("Supports 4G connectivity with standard SIM cards and has on-board GPS."),jt=c(),g=o("p"),yo=n("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"),zt=c(),J=o("h2"),Eo=n("Connectivity: KPN IoT SIM"),gt=c(),$=o("p"),To=n("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."),Jt=c(),X=o("p"),_o=n("The key here is APN selection as I found out:"),$t=c(),L=o("ul"),Y=o("li"),Ze=o("code"),ko=n("kpnthings2.m2m"),wo=n(": (the default APN) is restricted to the KPN IoT platform only"),bo=c(),Z=o("li"),et=o("code"),Io=n("internet.m2m"),So=n(": provides open internet access while still manageable through KPN’s portal"),Xt=c(),ee=o("p"),Lo=n("The latter is essential since we need the tracker to communicate with our self-hosted server over the public internet."),Yt=c(),te=o("h2"),Po=n("The OpenRemote Challenge"),Zt=c(),ae=o("p"),Oo=n("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."),ea=c(),P=o("p"),Ao=n("OpenRemote provides a "),q=o("a"),Co=n("Fleet Management tutorial"),Ro=n(" specifically for Teltonika devices, making it the logical choice."),ta=c(),oe=o("h2"),xo=n("Self-Hosting OpenRemote"),aa=c(),se=o("p"),Ho=n("Setting up OpenRemote required some network configuration due to port conflicts on my home server:"),oa=c(),G=o("pre"),sa=c(),ie=o("h3"),Mo=n("Certificate Management Automation"),ia=c(),le=o("p"),Do=n("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."),la=c(),re=o("p"),No=n("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."),ra=c(),ne=o("p"),qo=n("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."),na=c(),pe=o("p"),Go=n("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:"),pa=c(),B=o("pre"),ca=c(),O=o("p"),Bo=n("I automated the certificate extraction with a script ("),tt=o("code"),Fo=n("~/openremote/getcert.sh"),Uo=n("):"),fa=c(),F=o("pre"),ua=c(),d=o("p"),at=o("strong"),Qo=n("Critical lesson learned"),Ko=n(": Always use the "),ot=o("code"),Vo=n("live/"),Wo=n(" symlink, not the "),st=o("code"),jo=n("archive/"),zo=n(" directory. The live certificate automatically points to the latest renewal (e.g., "),it=o("code"),go=n("fullchain4.pem"),Jo=n(" after multiple renewals)."),ma=c(),ce=o("h2"),$o=n("Device Configuration with TCT"),da=c(),fe=o("p"),Xo=n("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."),ha=c(),ue=o("p"),Yo=n("Key configuration settings:"),va=c(),me=o("p"),lt=o("strong"),Zo=n("GPRS Tab:"),ya=c(),A=o("ul"),de=o("li"),es=n("APN: "),rt=o("code"),ts=n("internet.m2m"),as=c(),nt=o("li"),os=n("Enable GPRS"),Ea=c(),he=o("p"),pt=o("strong"),ss=n("Server Settings:"),Ta=c(),h=o("ul"),ve=o("li"),is=n("Domain: "),ct=o("code"),ls=n("my-openremote-server.example.com"),rs=c(),U=o("li"),ns=n("Port: "),ft=o("code"),ps=n("8883"),cs=n(" (MQTTS)"),fs=c(),ye=o("li"),us=n("Protocol: "),ut=o("code"),ms=n("MQTT"),ds=c(),Ee=o("li"),hs=n("TLS Encryption: "),mt=o("code"),vs=n("TLS/DTLS"),_a=c(),Te=o("p"),dt=o("strong"),ys=n("MQTTS Settings:"),ka=c(),E=o("ul"),_e=o("li"),Es=n("MQTT Client Type: "),ht=o("code"),Ts=n("AWS IoT Custom"),_s=c(),ke=o("li"),ks=n("Data to
43pic: "),vt=o("code"),ws=n("master/myFmc003/teltonika/%imei%/data"),bs=c(),we=o("li"),Is=n("Commands topic: "),yt=o("code"),Ss=n("master/myFmc003/teltonika/%imei%/commands"),wa=c(),be=o("p"),Et=o("strong"),Ls=n("System Tab:"),ba=c(),Ie=o("ul"),Se=o("li"),Ps=n("Data Protocol: "),Tt=o("code"),Os=n("Codec JSON"),Ia=c(),Le=o("p"),_t=o("strong"),As=n("Security Tab:"),Sa=c(),Pe=o("ul"),kt=o("li"),Cs=n("Upload the reversed certificate chain (fullchain.pem)"),La=c(),Oe=o("h2"),Rs=n("Home Assistant Integration"),Pa=c(),Ae=o("p"),xs=n("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."),Oa=c(),Q=o("pre"),Aa=c(),Ce=o("p"),Hs=n("This approach:"),Ca=c(),v=o("ol"),wt=o("li"),Ms=n("Obtains a fresh OAuth token"),Ds=c(),bt=o("li"),Ns=n("Fetches asset data from OpenRemote"),qs=c(),It=o("li"),Gs=n("Transforms the JSON to extract relevant attributes"),Bs=c(),St=o("li"),Fs=n("Is refreshed by Home Assistant every 60 seconds by default"),Ra=c(),Re=o("p"),Us=n("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."),xa=c(),xe=o("h2"),Qs=n("Debugging and Troubleshooting"),Ha=c(),He=o("p"),Ks=n("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:"),Ma=c(),K=o("pre"),Da=c(),Me=o("p"),Vs=n("This revealed the SSL handshake failure:"),Na=c(),V=o("pre"),qa=c(),T=o("p"),Ws=n("The root cause was using the wrong certificate path in my automation script - it was copying from "),Lt=o("code"),js=n("archive/fullchain1.pem"),zs=n(" instead of the "),Pt=o("code"),gs=n("live/fullchain.pem"),Js=n(" symlink."),Ga=c(),De=o("h2"),$s=n("Visualization"),Ba=c(),C=o("p"),Xs=n("Home Assistant’s "),W=o("a"),Ys=n("Ultra Vehicle Card"),Zs=n(" provides excellent visualization of the tracking data, displaying:"),Fa=c(),_=o("ul"),Ot=o("li"),ei=n("A picture of the car that changes based on the charging state"),ti=c(),At=o("li"),ai=n("Battery charge level"),oi=c(),Ct=o("li"),si=n("Odometer reading"),Ua=c(),Ne=o("p"),ii=n("Beside that other Home Assistant cards can be used to show:"),Qa=c(),k=o("ul"),Rt=o("li"),li=n("Real-time location on a map"),ri=c(),xt=o("li"),ni=n("Current speed"),pi=c(),Ht=o("li"),ci=n("Altitude"),Ka=c(),qe=o("p"),fi=n("Template sensors extract individual attributes for use in other automations and dashboards."),Va=c(),Ge=o("h2"),ui=n("Lessons Learned"),Wa=c(),u=o("ol"),R=o("li"),Mt=o("strong"),mi=n("Certificate management is critical"),di=n(": Automate extraction but always use the "),Dt=o("code"),hi=n("live/"),vi=n(" symlink"),yi=c(),Be=o("li"),Nt=o("strong"),Ei=n("OAuth2 complexity"),Ti=n(": Command-line sensors can handle authentication better than REST sensors for short-lived tokens"),_i=c(),Fe=o("li"),qt=o("strong"),ki=n("Enterprise hardware requirements"),wi=n(": Don’t underestimate power and connectivity requirements"),bi=c(),Ue=o("li"),Gt=o("strong"),Ii=n("Teltonika peculiarities"),Si=n(": Certificate order must be reversed, Windows required for configuration"),Li=c(),Qe=o("li"),Bt=o("strong"),Pi=n("Network planning"),Oi=n(": Multiple ports required, plan for certificate renewal downtime"),ja=c(),Ke=o("h2"),Ai=n("Future Improvements"),za=c(),Ve=o("ul"),Ft=o("li"),Ci=n("Investigate running TCT on an x64 Linux VM with a separate OBD-II power supply so configuration can be done outside the car."),ga=c(),We=o("p"),Ri=n("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."),this.h()},l(e){I=s(e,"P",{});var l=i(I);oo=p(l,"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."),l.forEach(t),Qt=f(e),j=s(e,"P",{});var zi=i(j);so=p(zi,"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."),zi.forEac
43h(t),Kt=f(e),z=s(e,"H2",{});var gi=i(z);io=p(gi,"Hardware Selection: Teltonika FMC003"),gi.forEach(t),Vt=f(e),S=s(e,"P",{});var Ja=i(S);lo=p(Ja,"After researching various GPS trackers, I settled on the "),M=s(Ja,"A",{href:!0,rel:!0});var Ji=i(M);ro=p(Ji,"Teltonika FMC003"),Ji.forEach(t),no=p(Ja,", an enterprise-grade vehicle tracker. Key factors in this decision:"),Ja.forEach(t),Wt=f(e),y=s(e,"UL",{});var je=i(y);D=s(je,"LI",{});var $a=i(D);po=p($a,"Listed as compatible on "),N=s($a,"A",{href:!0,rel:!0});var $i=i(N);co=p($i,"traccar.org"),$i.forEach(t),fo=p($a,", where Traccar integrates well with Home Assistant."),$a.forEach(t),uo=f(je),Xe=s(je,"LI",{});var Xi=i(Xe);mo=p(Xi,"It plugs into the OBD-II port of the car for power and can read vehicle diagnostics like battery charge and odometer"),Xi.forEach(t),ho=f(je),Ye=s(je,"LI",{});var Yi=i(Ye);vo=p(Yi,"Supports 4G connectivity with standard SIM cards and has on-board GPS."),Yi.forEach(t),je.forEach(t),jt=f(e),g=s(e,"P",{});var Zi=i(g);yo=p(Zi,"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"),Zi.forEach(t),zt=f(e),J=s(e,"H2",{});var el=i(J);Eo=p(el,"Connectivity: KPN IoT SIM"),el.forEach(t),gt=f(e),$=s(e,"P",{});var tl=i($);To=p(tl,"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."),tl.forEach(t),Jt=f(e),X=s(e,"P",{});var al=i(X);_o=p(al,"The key here is APN selection as I found out:"),al.forEach(t),$t=f(e),L=s(e,"UL",{});var Xa=i(L);Y=s(Xa,"LI",{});var xi=i(Y);Ze=s(xi,"CODE",{});var ol=i(Ze);ko=p(ol,"kpnthings2.m2m"),ol.forEach(t),wo=p(xi,": (the default APN) is restricted to the KPN IoT platform only"),xi.forEach(t),bo=f(Xa),Z=s(Xa,"LI",{});var Hi=i(Z);et=s(Hi,"CODE",{});var sl=i(et);Io=p(sl,"internet.m2m"),sl.forEach(t),So=p(Hi,": provides open internet access while still manageable through KPN’s portal"),Hi.forEach(t),Xa.forEach(t),Xt=f(e),ee=s(e,"P",{});var il=i(ee);Lo=p(il,"The latter is essential since we need the tracker to communicate with our self-hosted server over the public internet."),il.forEach(t),Yt=f(e),te=s(e,"H2",{});var ll=i(te);Po=p(ll,"The OpenRemote Challenge"),ll.forEach(t),Zt=f(e),ae=s(e,"P",{});var rl=i(ae);Oo=p(rl,"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."),rl.forEach(t),ea=f(e),P=s(e,"P",{});var Ya=i(P);Ao=p(Ya,"OpenRemote provides a "),q=s(Ya,"A",{href:!0,rel:!0});var nl=i(q);Co=p(nl,"Fleet Management tutorial"),nl.forEach(t),Ro=p(Ya," specifically for Teltonika devices, making it the logical choice."),Ya.forEach(t),ta=f(e),oe=s(e,"H2",{});var pl=i(oe);xo=p(pl,"Self-Hosting OpenRemote"),pl.forEach(t),aa=f(e),se=s(e,"P",{});var cl=i(se);Ho=p(cl,"Setting up OpenRemote required some network configuration due to port conflicts on my home server:"),cl.forEach(t),oa=f(e),G=s(e,"PRE",{class:!0});var Mr=i(G);Mr.forEach(t),sa=f(e),ie=s(e,"H3",{});var fl=i(ie);Mo=p(fl,"Certificate Management Automation"),fl.forEach(t),ia=f(e),le=s(e,"P",{});var ul=i(le);Do=p(ul,"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."),ul.forEach(t),la=f(e),re=s(e,"P",{});var ml=i(re);No=p(ml,"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."),ml.forEach(t),ra=f(e),ne=s(e,"P",{});var dl=i(ne);qo=p(dl,"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."),dl.forEach(t),na=f(e),pe=s(e,"P",{});
43var hl=i(pe);Go=p(hl,"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:"),hl.forEach(t),pa=f(e),B=s(e,"PRE",{class:!0});var Dr=i(B);Dr.forEach(t),ca=f(e),O=s(e,"P",{});var Za=i(O);Bo=p(Za,"I automated the certificate extraction with a script ("),tt=s(Za,"CODE",{});var vl=i(tt);Fo=p(vl,"~/openremote/getcert.sh"),vl.forEach(t),Uo=p(Za,"):"),Za.forEach(t),fa=f(e),F=s(e,"PRE",{class:!0});var Nr=i(F);Nr.forEach(t),ua=f(e),d=s(e,"P",{});var b=i(d);at=s(b,"STRONG",{});var yl=i(at);Qo=p(yl,"Critical lesson learned"),yl.forEach(t),Ko=p(b,": Always use the "),ot=s(b,"CODE",{});var El=i(ot);Vo=p(El,"live/"),El.forEach(t),Wo=p(b," symlink, not the "),st=s(b,"CODE",{});var Tl=i(st);jo=p(Tl,"archive/"),Tl.forEach(t),zo=p(b," directory. The live certificate automatically points to the latest renewal (e.g., "),it=s(b,"CODE",{});var _l=i(it);go=p(_l,"fullchain4.pem"),_l.forEach(t),Jo=p(b," after multiple renewals)."),b.forEach(t),ma=f(e),ce=s(e,"H2",{});var kl=i(ce);$o=p(kl,"Device Configuration with TCT"),kl.forEach(t),da=f(e),fe=s(e,"P",{});var wl=i(fe);Xo=p(wl,"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."),wl.forEach(t),ha=f(e),ue=s(e,"P",{});var bl=i(ue);Yo=p(bl,"Key configuration settings:"),bl.forEach(t),va=f(e),me=s(e,"P",{});var Il=i(me);lt=s(Il,"STRONG",{});var Sl=i(lt);Zo=p(Sl,"GPRS Tab:"),Sl.forEach(t),Il.forEach(t),ya=f(e),A=s(e,"UL",{});var eo=i(A);de=s(eo,"LI",{});var Mi=i(de);es=p(Mi,"APN: "),rt=s(Mi,"CODE",{});var Ll=i(rt);ts=p(Ll,"internet.m2m"),Ll.forEach(t),Mi.forEach(t),as=f(eo),nt=s(eo,"LI",{});var Pl=i(nt);os=p(Pl,"Enable GPRS"),Pl.forEach(t),eo.forEach(t),Ea=f(e),he=s(e,"P",{});var Ol=i(he);pt=s(Ol,"STRONG",{});var Al=i(pt);ss=p(Al,"Server Settings:"),Al.forEach(t),Ol.forEach(t),Ta=f(e),h=s(e,"UL",{});var x=i(h);ve=s(x,"LI",{});var Di=i(ve);is=p(Di,"Domain: "),ct=s(Di,"CODE",{});var Cl=i(ct);ls=p(Cl,"my-openremote-server.example.com"),Cl.forEach(t),Di.forEach(t),rs=f(x),U=s(x,"LI",{});var to=i(U);ns=p(to,"Port: "),ft=s(to,"CODE",{});var Rl=i(ft);ps=p(Rl,"8883"),Rl.forEach(t),cs=p(to," (MQTTS)"),to.forEach(t),fs=f(x),ye=s(x,"LI",{});var Ni=i(ye);us=p(Ni,"Protocol: "),ut=s(Ni,"CODE",{});var xl=i(ut);ms=p(xl,"MQTT"),xl.forEach(t),Ni.forEach(t),ds=f(x),Ee=s(x,"LI",{});var qi=i(Ee);hs=p(qi,"TLS Encryption: "),mt=s(qi,"CODE",{});var Hl=i(mt);vs=p(Hl,"TLS/DTLS"),Hl.forEach(t),qi.forEach(t),x.forEach(t),_a=f(e),Te=s(e,"P",{});var Ml=i(Te);dt=s(Ml,"STRONG",{});var Dl=i(dt);ys=p(Dl,"MQTTS Settings:"),Dl.forEach(t),Ml.forEach(t),ka=f(e),E=s(e,"UL",{});var ze=i(E);_e=s(ze,"LI",{});var Gi=i(_e);Es=p(Gi,"MQTT Client Type: "),ht=s(Gi,"CODE",{});var Nl=i(ht);Ts=p(Nl,"AWS IoT Custom"),Nl.forEach(t),Gi.forEach(t),_s=f(ze),ke=s(ze,"LI",{});var Bi=i(ke);ks=p(Bi,"Data topic: "),vt=s(Bi,"CODE",{});var ql=i(vt);ws=p(ql,"master/myFmc003/teltonika/%imei%/data"),ql.forEach(t),Bi.forEach(t),bs=f(ze),we=s(ze,"LI",{});var Fi=i(we);Is=p(Fi,"Commands topic: "),yt=s(Fi,"CODE",{});var Gl=i(yt);Ss=p(Gl,"master/myFmc003/teltonika/%imei%/commands"),Gl.forEach(t),Fi.forEach(t),ze.forEach(t),wa=f(e),be=s(e,"P",{});var Bl=i(be);Et=s(Bl,"STRONG",{});var Fl=i(Et);Ls=p(Fl,"System Tab:"),Fl.forEach(t),Bl.forEach(t),ba=f(e),Ie=s(e,"UL",{});var Ul=i(Ie);Se=s(Ul,"LI",{});var Ui=i(Se);Ps=p(Ui,"Data Protocol: "),Tt=s(Ui,"CODE",{});var Ql=i(Tt);Os=p(Ql,"Codec JSON"),Ql.forEach(t),Ui.forEach(t),Ul.forEach(t),Ia=f(e),Le=s(e,"P",{});var Kl=i(Le);_t=s(Kl,"STRONG",{});var Vl=i(_t);As=p(Vl,"Security Tab:"),Vl.forEach(t),Kl.forEach(t),Sa=f(e),Pe=s(e,"UL",{});var Wl=i(Pe);kt=s(Wl,"LI",{});var jl=i(kt);Cs=p(jl,"Upload the reversed certificate chain (fullchain.pem)"),jl.forEach(t),Wl.forEach(t),La=f(e),Oe=s(e,"H2",{});var zl=i(Oe);Rs=p(zl,"Home Assistant Integration"),zl.forEach(t),Pa=f(e),Ae=s(e,"P",{});var gl=i(Ae);xs=p(gl,"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."),gl.forEach(t),Oa=f(e),Q=s(e,"PRE",{class:!0});var qr=i(Q);qr.forEach(t),Aa=f(e),Ce=s(e,"P",{});var Jl=i(Ce);Hs=p(Jl,"This approach:"),Jl.forEach(t),Ca=f(e),v=s(e,"OL",{});var H=i(v);wt=s(H,"LI",{});var $l=i(wt);Ms=p($l,"Obtains a fresh OAuth token"),$l.forEach(t),Ds=f(H),bt=s(H,"LI",{});var Xl=i(bt);Ns=p(Xl,"Fetches asset data from OpenRemote"),Xl.forEach(t),qs=f(H),It=s(H,"LI",{});var Yl=i(It);Gs=p(Yl,"Transforms the JSON to extract relevant attributes"),Yl.forEach(t),Bs=f(H),St=s(H,"LI",{});
43var Zl=i(St);Fs=p(Zl,"Is refreshed by Home Assistant every 60 seconds by default"),Zl.forEach(t),H.forEach(t),Ra=f(e),Re=s(e,"P",{});var er=i(Re);Us=p(er,"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."),er.forEach(t),xa=f(e),xe=s(e,"H2",{});var tr=i(xe);Qs=p(tr,"Debugging and Troubleshooting"),tr.forEach(t),Ha=f(e),He=s(e,"P",{});var ar=i(He);Ks=p(ar,"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:"),ar.forEach(t),Ma=f(e),K=s(e,"PRE",{class:!0});var Gr=i(K);Gr.forEach(t),Da=f(e),Me=s(e,"P",{});var or=i(Me);Vs=p(or,"This revealed the SSL handshake failure:"),or.forEach(t),Na=f(e),V=s(e,"PRE",{class:!0});var Br=i(V);Br.forEach(t),qa=f(e),T=s(e,"P",{});var ge=i(T);Ws=p(ge,"The root cause was using the wrong certificate path in my automation script - it was copying from "),Lt=s(ge,"CODE",{});var sr=i(Lt);js=p(sr,"archive/fullchain1.pem"),sr.forEach(t),zs=p(ge," instead of the "),Pt=s(ge,"CODE",{});var ir=i(Pt);gs=p(ir,"live/fullchain.pem"),ir.forEach(t),Js=p(ge," symlink."),ge.forEach(t),Ga=f(e),De=s(e,"H2",{});var lr=i(De);$s=p(lr,"Visualization"),lr.forEach(t),Ba=f(e),C=s(e,"P",{});var ao=i(C);Xs=p(ao,"Home Assistant’s "),W=s(ao,"A",{href:!0,rel:!0});var rr=i(W);Ys=p(rr,"Ultra Vehicle Card"),rr.forEach(t),Zs=p(ao," provides excellent visualization of the tracking data, displaying:"),ao.forEach(t),Fa=f(e),_=s(e,"UL",{});var Je=i(_);Ot=s(Je,"LI",{});var nr=i(Ot);ei=p(nr,"A picture of the car that changes based on the charging state"),nr.forEach(t),ti=f(Je),At=s(Je,"LI",{});var pr=i(At);ai=p(pr,"Battery charge level"),pr.forEach(t),oi=f(Je),Ct=s(Je,"LI",{});var cr=i(Ct);si=p(cr,"Odometer reading"),cr.forEach(t),Je.forEach(t),Ua=f(e),Ne=s(e,"P",{});var fr=i(Ne);ii=p(fr,"Beside that other Home Assistant cards can be used to show:"),fr.forEach(t),Qa=f(e),k=s(e,"UL",{});var $e=i(k);Rt=s($e,"LI",{});var ur=i(Rt);li=p(ur,"Real-time location on a map"),ur.forEach(t),ri=f($e),xt=s($e,"LI",{});var mr=i(xt);ni=p(mr,"Current speed"),mr.forEach(t),pi=f($e),Ht=s($e,"LI",{});var dr=i(Ht);ci=p(dr,"Altitude"),dr.forEach(t),$e.forEach(t),Ka=f(e),qe=s(e,"P",{});var hr=i(qe);fi=p(hr,"Template sensors extract individual attributes for use in other automations and dashboards."),hr.forEach(t),Va=f(e),Ge=s(e,"H2",{});var vr=i(Ge);ui=p(vr,"Lessons Learned"),vr.forEach(t),Wa=f(e),u=s(e,"OL",{});var w=i(u);R=s(w,"LI",{});var Ut=i(R);Mt=s(Ut,"STRONG",{});var yr=i(Mt);mi=p(yr,"Certificate management is critical"),yr.forEach(t),di=p(Ut,": Automate extraction but always use the "),Dt=s(Ut,"CODE",{});var Er=i(Dt);hi=p(Er,"live/"),Er.forEach(t),vi=p(Ut," symlink"),Ut.forEach(t),yi=f(w),Be=s(w,"LI",{});var Qi=i(Be);Nt=s(Qi,"STRONG",{});var Tr=i(Nt);Ei=p(Tr,"OAuth2 complexity"),Tr.forEach(t),Ti=p(Qi,": Command-line sensors can handle authentication better than REST sensors for short-lived tokens"),Qi.forEach(t),_i=f(w),Fe=s(w,"LI",{});var Ki=i(Fe);qt=s(Ki,"STRONG",{});var _r=i(qt);ki=p(_r,"Enterprise hardware requirements"),_r.forEach(t),wi=p(Ki,": Don’t underestimate power and connectivity requirements"),Ki.forEach(t),bi=f(w),Ue=s(w,"LI",{});var Vi=i(Ue);Gt=s(Vi,"STRONG",{});var kr=i(Gt);Ii=p(kr,"Teltonika peculiarities"),kr.forEach(t),Si=p(Vi,": Certificate order must be reversed, Windows required for configuration"),Vi.forEach(t),Li=f(w),Qe=s(w,"LI",{});var Wi=i(Qe);Bt=s(Wi,"STRONG",{});var wr=i(Bt);Pi=p(wr,"Network planning"),wr.forEach(t),Oi=p(Wi,": Multiple ports required, plan for certificate renewal downtime"),Wi.forEach(t),w.forEach(t),ja=f(e),Ke=s(e,"H2",{});var br=i(Ke);Ai=p(br,"Future Improvements"),br.forEach(t),za=f(e),Ve=s(e,"UL",{});var Ir=i(Ve);Ft=s(Ir,"LI",{});var Sr=i(Ft);Ci=p(Sr,"Investigate running TCT on an x64 Linux VM with a separate OBD-II power supply so configuration can be done outside the car."),Sr.forEach(t),Ir.forEach(t),ga=f(e),We=s(e,"P",{});var Lr=i(We);Ri=p(Lr,"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."),Lr.forEach(t),this.h()},h(){m(M,"href","https://teltonika-gps.com/products/trackers/fmc003"),m(M,"rel","nofollow"),m(N,"href","https://www.traccar.org/devices/"),m(N,"rel","nofollow"),m(q,"href","https://github.com/openremote/fleet-management/wiki/Tutorial%3A-Create-your-own-Fleet-Management-System"),m(q,"rel","nofollow"),m(G,"class","language-yaml"),m(B,"class","language-undefined"),m(F,"class","language-bash"),m(Q,"class","language-yaml"),m(K,"class","language-yaml"),m(V,"class","language-undefined"),m(W,"href","https://ultravehiclecard.com"),m(W,"rel","nofollow")},m(e,l){r(e,I,l),a(I,oo),r(e,Qt,l),r(e,j,l),a(j,so),r(e,Kt,l),r(e,z,l),a(z,io),r(e,Vt,l),r(e,S,l),a(S,lo),a(S,M),a(M,ro),a(S,no),r(e,Wt,l),r(e,y,l),a(y,D),a(D,po),a(D,N),a(N,co),a(D,fo),a(y,uo),a(y,Xe),a(Xe,mo),a(y,ho),a(y,Ye),a(Ye,vo),r(e,jt,l),r(e,g,l),a(g,yo),r(e,zt,l),r(e,J,l),a(J,Eo),r(e,gt,l),r(e,$,l),a($,To),r(e,Jt,l),r(e,X,l),a(X,_o),r(e,$t,l),r(e,L,l),a(L,Y),a(Y,Ze),a(Ze,ko),a(Y,wo),a(L,bo),a(L,Z),a(Z,et),a(et,Io),a(Z,So),r(e,Xt,l),r(e,ee,l),a(ee,Lo),r(e,Yt,l),r(e,te,l),a(te,Po),r(e,Zt,l),r(e,ae,l),a(ae,Oo),r(e,ea,l),r(e,P,l),a(P,Ao),a(P,q),a(q,Co),a(P,Ro),r(e,ta,l),r(e,oe,l),a(oe,xo),r(e,aa,l),r(e,se,l),a(se,Ho),r(e,oa,l),r(e,G,l),G.innerHTML=Or,r(e,sa,l),r(e,ie,l),a(ie,Mo),r(e,ia,l),r(e,le,l),a(le,Do),r(e,la,l),r(e,re,l),a(re,No),r(e,ra,l),r(e,ne,l),a(ne,qo),r(e,na,l),r(e,pe,l),a(pe,Go),r(e,pa,l),r(e,B,l),B.innerHTML=Ar,r(e,ca,l),r(e,O,l),a(O,Bo),a(O,tt),a(tt,Fo),a(O,Uo),r(e,fa,l),r(e,F,l),F.innerHTML=Cr,r(e,ua,l),r(e,d,l),a(d,at),a(at,Qo),a(d,Ko),a(d,ot),a(ot,Vo),a(d,Wo),a(d,st),a(st,jo),a(d,zo),a(d,it),a(it,go),a(d,Jo),r(e,ma,l),r(e,ce,l),a(ce,$o),r(e,da,l),r(e,fe,l),a(fe,Xo),r(e,ha,l),r(e,ue,l),a(ue,Yo),r(e,va,l),r(e,me,l),a(me,lt),a(lt,Zo),r(e,ya,l),r(e,A,l),a(A,de),a(de,es),a(de,rt),a(rt,ts),a(A,as),a(A,nt),a(nt,os),r(e,Ea,l),r(e,he,l),a(he,pt),a(pt,ss),r(e,Ta,l),r(e,h,l),a(h,ve),a(ve,is),a(ve,ct),a(ct,ls),a(h,rs),a(h,U),a(U,ns),a(U,ft),a(ft,ps),a(U,cs),a(h,fs),a(h,ye),a(ye,us),a(ye,ut),a(ut,ms),a(h,ds),a(h,Ee),a(Ee,hs),a(Ee,mt),a(mt,vs),r(e,_a,l),r(e,Te,l),a(Te,dt),a(dt,ys),r(e,ka,l),r(e,E,l),a(E,_e),a(_e,Es),a(_e,ht),a(ht,Ts),a(E,_s),a(E,ke),a(ke,ks),a(ke,vt),a(vt,ws),a(E,bs),a(E,we),a(we,Is),a(we,yt),a(yt,Ss),r(e,wa,l),r(e,be,l),a(be,Et),a(Et,Ls),r(e,ba,l),r(e,Ie,l),a(Ie,Se),a(Se,Ps),a(Se,Tt),a(Tt,Os),r(e,Ia,l),r(e,Le,l),a(Le,_t),a(_t,As),r(e,Sa,l),r(e,Pe,l),a(Pe,kt),a(kt,Cs),r(e,La,l),r(e,Oe,l),a(Oe,Rs),r(e,Pa,l),r(e,Ae,l),a(Ae,xs),r(e,Oa,l),r(e,Q,l),Q.innerHTML=Rr,r(e,Aa,l),r(e,Ce,l),a(Ce,Hs),r(e,Ca,l),r(e,v,l),a(v,wt),a(wt,Ms),a(v,Ds),a(v,bt),a(bt,Ns),a(v,qs),a(v,It),a(It,Gs),a(v,Bs),a(v,St),a(St,Fs),r(e,Ra,l),r(e,Re,l),a(Re,Us),r(e,xa,l),r(e,xe,l),a(xe,Qs),r(e,Ha,l),r(e,He,l),a(He,Ks),r(e,Ma,l),r(e,K,l),K.innerHTML=xr,r(e,Da,l),r(e,Me,l),a(Me,Vs),r(e,Na,l),r(e,V,l),V.innerHTML=Hr,r(e,qa,l),r(e,T,l),a(T,Ws),a(T,Lt),a(Lt,js),a(T,zs),a(T,Pt),a(Pt,gs),a(T,Js),r(e,Ga,l),r(e,De,l),a(De,$s),r(e,Ba,l),r(e,C,l),a(C,Xs),a(C,W),a(W,Ys),a(C,Zs),r(e,Fa,l),r(e,_,l),a(_,Ot),a(Ot,ei),a(_,ti),a(_,At),a(At,ai),a(_,oi),a(_,Ct),a(Ct,si),r(e,Ua,l),r(e,Ne,l),a(Ne,ii),r(e,Qa,l),r(e,k,l),a(k,Rt),a(Rt,li),a(k,ri),a(k,xt),a(xt,ni),a(k,pi),a(k,Ht),a(Ht,ci),r(e,Ka,l),r(e,qe,l),a(qe,fi),r(e,Va,l),r(e,Ge,l),a(Ge,ui),r(e,Wa,l),r(e,u,l),a(u,R),a(R,Mt),a(Mt,mi),a(R,di),a(R,Dt),a(Dt,hi),a(R,vi),a(u,yi),a(u,Be),a(Be,Nt),a(Nt,Ei),a(Be,Ti),a(u,_i),a(u,Fe),a(Fe,qt),a(qt,ki),a(Fe,wi),a(u,bi),a(u,Ue),a(Ue,Gt),a(Gt,Ii),a(Ue,Si),a(u,Li),a(u,Qe),a(Qe,Bt),a(Bt,Pi),a(Qe,Oi),r(e,ja,l),r(e,Ke,l),a(Ke,Ai),r(e,za,l),r(e,Ve,l),a(Ve,Ft),a(Ft,Ci),r(e,ga,l),r(e,We,l),a(We,Ri)},p:ji,i:ji,o:ji,d(e){e&&t(I),e&&t(Qt),e&&t(j),e&&t(Kt),e&&t(z),e&&t(Vt),e&&t(S),e&&t(Wt),e&&t(y),e&&t(jt),e&&t(g),e&&t(zt),e&&t(J),e&&t(gt),e&&t($),e&&t(Jt),e&&t(X),e&&t($t),e&&t(L),e&&t(Xt),e&&t(ee),e&&t(Yt),e&&t(te),e&&t(Zt),e&&t(ae),e&&t(ea),e&&t(P),e&&t(ta),e&&t(oe),e&&t(aa),e&&t(se),e&&t(oa),e&&t(G),e&&t(sa),e&&t(ie),e&&t(ia),e&&t(le),e&&t(la),e&&t(re),e&&t(ra),e&&t(ne),e&&t(na),e&&t(pe),e&&t(pa),e&&t(B),e&&t(ca),e&&t(O),e&&t(fa),e&&t(F),e&&t(ua),e&&t(d),e&&t(ma),e&&t(ce),e&&t(d
43a),e&&t(fe),e&&t(ha),e&&t(ue),e&&t(va),e&&t(me),e&&t(ya),e&&t(A),e&&t(Ea),e&&t(he),e&&t(Ta),e&&t(h),e&&t(_a),e&&t(Te),e&&t(ka),e&&t(E),e&&t(wa),e&&t(be),e&&t(ba),e&&t(Ie),e&&t(Ia),e&&t(Le),e&&t(Sa),e&&t(Pe),e&&t(La),e&&t(Oe),e&&t(Pa),e&&t(Ae),e&&t(Oa),e&&t(Q),e&&t(Aa),e&&t(Ce),e&&t(Ca),e&&t(v),e&&t(Ra),e&&t(Re),e&&t(xa),e&&t(xe),e&&t(Ha),e&&t(He),e&&t(Ma),e&&t(K),e&&t(Da),e&&t(Me),e&&t(Na),e&&t(V),e&&t(qa),e&&t(T),e&&t(Ga),e&&t(De),e&&t(Ba),e&&t(C),e&&t(Fa),e&&t(_),e&&t(Ua),e&&t(Ne),e&&t(Qa),e&&t(k),e&&t(Ka),e&&t(qe),e&&t(Va),e&&t(Ge),e&&t(Wa),e&&t(u),e&&t(ja),e&&t(Ke),e&&t(za),e&&t(Ve),e&&t(ga),e&&t(We)}}}const Wr={title:"Enterprise Asset Tracking with Teltonika and Home Assistant",cover:"",date:"08-08-2025",category:"mypc",tags:["mypc","iot","homeassistant","fleet-management"],preview:"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...",previewHtml:"<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 ...</p>"};class jr extends Fr{constructor(I){super(),Ur(this,I,null,Kr,Qr,{})}}export{jr as default,Wr as metadata};

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.