This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

websites.where.is.lassul.us: init

location of person.lass from home-assistant on an OSM map with a 24h trail;
neoprism polls the hass history API every 2 min into a static json,
token via clan vars (where-lassul-us/token)

+264 -1
+2 -1
2configs/dns/lassul.us.zone
··· 1 - @ 3600 IN SOA lassul.us. ns1.lassul.us. 2026083000 7200 3600 86400 3600 1 + @ 3600 IN SOA lassul.us. ns1.lassul.us. 2026090800 7200 3600 86400 3600 2 2 3 3 ;;@ 3600 IN NS ns1 4 4 @ 3600 IN NS ns1 ··· 60 60 id IN CNAME neoprism 61 61 covibe IN CNAME neoprism 62 62 omni IN CNAME neoprism 63 + where.is IN CNAME neoprism 63 64 64 65 ;; Mail 65 66 @ IN MX 3 mail
+3
machines/neoprism/config.nix
··· 79 79 # lassul.us website 80 80 ../../2configs/websites/lassulus.nix 81 81 82 + # where.is.lassul.us (location from home-assistant on a map) 83 + ../../2configs/websites/where.is.lassul.us 84 + 82 85 # realwallpaper generator 83 86 ../../2configs/realwallpaper.nix 84 87
+110
2configs/websites/where.is.lassul.us/default.nix
··· 1 + { config, pkgs, ... }: 2 + let 3 + domain = "where.is.lassul.us"; 4 + # home-assistant on styx, reached over retiolum (same as hass/proxy.nix). 5 + hass = "http://styx.r:8123"; 6 + entity = "person.lass"; 7 + stateDir = "/var/lib/where-lassul-us"; 8 + 9 + leaflet = pkgs.fetchzip { 10 + url = "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz"; 11 + hash = "sha256-zzU1O3AxsGn+r3/iL7GJr+LAjLozP0NaJ5DXnZvpllI="; 12 + }; 13 + 14 + webroot = pkgs.runCommand "${domain}-webroot" { } '' 15 + mkdir -p $out/leaflet 16 + cp ${./index.html} $out/index.html 17 + cp ${leaflet}/dist/leaflet.js ${leaflet}/dist/leaflet.css $out/leaflet/ 18 + cp -r ${leaflet}/dist/images $out/leaflet/images 19 + ''; 20 + 21 + # Pull the last 24h of ${entity} from the hass history API and boil it down 22 + # to [{t, lat, lon, acc}] for the page. hass returns every state write 23 + # (~1000/day), so consecutive identical coordinates are collapsed. 24 + fetch = pkgs.writeShellApplication { 25 + name = "where-lassul-us-fetch"; 26 + runtimeInputs = [ 27 + pkgs.curl 28 + pkgs.jq 29 + pkgs.coreutils 30 + ]; 31 + text = '' 32 + since=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) 33 + curl -fsS \ 34 + -H "Authorization: Bearer $(cat "$CREDENTIALS_DIRECTORY/token")" \ 35 + "${hass}/api/history/period/$since?filter_entity_id=${entity}&significant_changes_only=0" \ 36 + | jq -c ' 37 + .[0] 38 + | map(select(.attributes.latitude != null)) 39 + | map({ 40 + t: .last_updated, 41 + lat: .attributes.latitude, 42 + lon: .attributes.longitude, 43 + acc: .attributes.gps_accuracy, 44 + state: .state 45 + }) 46 + | reduce .[] as $p ([]; 47 + if length > 0 and .[-1].lat == $p.lat and .[-1].lon == $p.lon 48 + then .[:-1] + [$p] 49 + else . + [$p] 50 + end) 51 + | { updated: (now | todate), points: . } 52 + ' > "${stateDir}/location.json.tmp" 53 + mv "${stateDir}/location.json.tmp" "${stateDir}/location.json" 54 + ''; 55 + }; 56 + in 57 + { 58 + imports = [ ../default.nix ]; 59 + 60 + clan.core.vars.generators.where-lassul-us = { 61 + files.token = { }; 62 + prompts.token = { 63 + description = '' 64 + home-assistant long-lived access token (profile -> security -> 65 + long-lived access tokens) with read access to ${entity}. 66 + ''; 67 + type = "hidden"; 68 + persist = true; 69 + }; 70 + script = '' 71 + tr -d '\n' < "$prompts/token" > "$out/token" 72 + ''; 73 + }; 74 + 75 + # Static user rather than DynamicUser: the latter hides StateDirectory under 76 + # /var/lib/private (0700), which nginx cannot traverse. 77 + users.users.where-lassul-us = { 78 + isSystemUser = true; 79 + group = "where-lassul-us"; 80 + }; 81 + users.groups.where-lassul-us = { }; 82 + 83 + systemd.services.where-lassul-us-fetch = { 84 + description = "fetch ${entity} trail from home-assistant for ${domain}"; 85 + after = [ "network-online.target" ]; 86 + wants = [ "network-online.target" ]; 87 + startAt = "*:0/2"; 88 + serviceConfig = { 89 + Type = "oneshot"; 90 + User = "where-lassul-us"; 91 + Group = "where-lassul-us"; 92 + StateDirectory = "where-lassul-us"; 93 + StateDirectoryMode = "0755"; 94 + UMask = "0022"; 95 + LoadCredential = [ "token:${config.clan.core.vars.generators.where-lassul-us.files.token.path}" ]; 96 + ExecStart = "${fetch}/bin/where-lassul-us-fetch"; 97 + }; 98 + }; 99 + 100 + services.nginx.virtualHosts.${domain} = { 101 + enableACME = true; 102 + forceSSL = true; 103 + root = webroot; 104 + locations."= /location.json".extraConfig = '' 105 + alias ${stateDir}/location.json; 106 + default_type application/json; 107 + add_header Cache-Control "no-cache"; 108 + ''; 109 + }; 110 + }
+149
2configs/websites/where.is.lassul.us/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <title>where is lassulus?</title> 7 + <link rel="stylesheet" href="/leaflet/leaflet.css" /> 8 + <style> 9 + html, 10 + body { 11 + margin: 0; 12 + height: 100%; 13 + background: #111; 14 + } 15 + #map { 16 + position: absolute; 17 + inset: 0; 18 + } 19 + #info { 20 + position: absolute; 21 + z-index: 1000; 22 + left: 0; 23 + right: 0; 24 + bottom: 0; 25 + padding: 0.5em 1em; 26 + font: 14px/1.4 monospace; 27 + color: #eee; 28 + background: rgba(0, 0, 0, 0.7); 29 + } 30 + #info a { 31 + color: #9cf; 32 + } 33 + </style> 34 + </head> 35 + <body> 36 + <div id="map"></div> 37 + <div id="info">loading…</div> 38 + <script src="/leaflet/leaflet.js"></script> 39 + <script> 40 + (function () { 41 + var map = L.map("map", { zoomControl: true }); 42 + L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { 43 + maxZoom: 19, 44 + attribution: 45 + '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', 46 + }).addTo(map); 47 + 48 + var layer = L.layerGroup().addTo(map); 49 + var info = document.getElementById("info"); 50 + 51 + function ago(iso) { 52 + var s = Math.round((Date.now() - Date.parse(iso)) / 1000); 53 + if (s < 60) return s + "s"; 54 + if (s < 3600) return Math.round(s / 60) + "min"; 55 + if (s < 86400) return (s / 3600).toFixed(1) + "h"; 56 + return (s / 86400).toFixed(1) + "d"; 57 + } 58 + 59 + // trail fades from transparent (24h ago) to solid (now); one segment per point pair 60 + function render(data) { 61 + layer.clearLayers(); 62 + var pts = data.points; 63 + if (!pts.length) { 64 + info.textContent = "no location in the last 24h"; 65 + return; 66 + } 67 + var t0 = Date.parse(pts[0].t), 68 + t1 = Date.parse(pts[pts.length - 1].t) || t0 + 1; 69 + for (var i = 1; i < pts.length; i++) { 70 + var a = pts[i - 1], 71 + b = pts[i]; 72 + var age = (Date.parse(b.t) - t0) / (t1 - t0); 73 + L.polyline( 74 + [ 75 + [a.lat, a.lon], 76 + [b.lat, b.lon], 77 + ], 78 + { 79 + color: "#e33", 80 + weight: 4, 81 + opacity: 0.15 + 0.85 * age, 82 + }, 83 + ).addTo(layer); 84 + } 85 + var last = pts[pts.length - 1]; 86 + if (last.acc) { 87 + L.circle([last.lat, last.lon], { 88 + radius: last.acc, 89 + color: "#39f", 90 + weight: 1, 91 + fillOpacity: 0.1, 92 + }).addTo(layer); 93 + } 94 + L.marker([last.lat, last.lon]) 95 + .addTo(layer) 96 + .bindPopup( 97 + "last seen " + 98 + ago(last.t) + 99 + " ago<br>" + 100 + new Date(last.t).toLocaleString(), 101 + ); 102 + info.innerHTML = 103 + "<b>lassulus</b> is <b>" + 104 + last.state + 105 + "</b> · last seen " + 106 + ago(last.t) + 107 + " ago" + 108 + " · " + 109 + pts.length + 110 + " points in the last 24h" + 111 + ' · <a href="https://www.openstreetmap.org/?mlat=' + 112 + last.lat + 113 + "&mlon=" + 114 + last.lon + 115 + "#map=16/" + 116 + last.lat + 117 + "/" + 118 + last.lon + 119 + '">osm</a>'; 120 + return L.latLngBounds( 121 + pts.map(function (p) { 122 + return [p.lat, p.lon]; 123 + }), 124 + ); 125 + } 126 + 127 + var first = true; 128 + function refresh() { 129 + fetch("/location.json", { cache: "no-cache" }) 130 + .then(function (r) { 131 + return r.json(); 132 + }) 133 + .then(function (data) { 134 + var bounds = render(data); 135 + if (first && bounds) { 136 + map.fitBounds(bounds.pad(0.1), { maxZoom: 16 }); 137 + first = false; 138 + } 139 + }) 140 + .catch(function (e) { 141 + info.textContent = "failed to load location: " + e; 142 + }); 143 + } 144 + refresh(); 145 + setInterval(refresh, 60 * 1000); 146 + })(); 147 + </script> 148 + </body> 149 + </html>