This repository has no description
0

Configure Feed

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

superconfig / configs / gg23 / internet-hass.nix
14 kB 392 lines
1# Home Assistant side of ./internet.nix: sensors that query VictoriaMetrics on 2# styx (the gg23 monitoring server) and an "Internet" dashboard explaining 3# why the internet is slow. Both are mounted into the container as a package 4# and a YAML dashboard (configs/hass loads /config/packages); generated as 5# JSON, which HA's YAML loader reads as well. 6{ 7 lib, 8 pkgs, 9 self, 10 ... 11}: 12let 13 inherit (import ../monitoring/default.nix) ports; 14 # IPv4: VictoriaMetrics doesn't listen on IPv6, styx.r resolves to it first. 15 vm = "http://${self.inputs.kartei.hosts.styx.nets.retiolum.ip4.addr}:${toString ports.victoriametrics}/api/v1/query"; 16 17 # booked Vodafone cable rate in Mbit/s (speedtests from acheron: ~920/50) 18 line = { 19 down = 1000; 20 up = 50; 21 }; 22 23 ping = sel: { 24 latency = ''avg(rate(smokeping_response_duration_seconds_sum{host="acheron",${sel}}[5m]) / rate(smokeping_response_duration_seconds_count{host="acheron",${sel}}[5m])) * 1000''; 25 p95 = ''histogram_quantile(0.95, sum(rate(smokeping_response_duration_seconds_bucket{host="acheron",${sel}}[5m])) by (le)) * 1000''; 26 loss = ''100 * (1 - sum(rate(smokeping_response_duration_seconds_count{host="acheron",${sel}}[5m])) / sum(rate(smokeping_requests_total{host="acheron",${sel}}[5m])))''; 27 }; 28 internet = ping ''target_name=~"cloudflare|google"''; 29 cmts = ping ''target_name="cmts"''; 30 station = ping ''target_name="station"''; 31 wan = dir: ''rate(host_network_${dir}_bytes_total{host="acheron",device="et0"}[2m]) * 8 / 1e6''; 32 docsis = ''{host="acheron",channel_type="SC-QAM"}''; 33 34 # id -> sensor.internet_<id> 35 sensors = { 36 latency = { 37 name = "Latency"; 38 query = internet.latency; 39 unit = "ms"; 40 icon = "mdi:timer-outline"; 41 }; 42 latency_p95 = { 43 name = "Latency p95"; 44 query = internet.p95; 45 unit = "ms"; 46 icon = "mdi:timer-alert-outline"; 47 }; 48 loss = { 49 name = "Packet loss"; 50 query = internet.loss; 51 unit = "%"; 52 icon = "mdi:lan-disconnect"; 53 }; 54 cmts_latency = { 55 name = "Cable latency"; 56 query = cmts.latency; 57 unit = "ms"; 58 icon = "mdi:timer-outline"; 59 }; 60 cmts_loss = { 61 name = "Cable packet loss"; 62 query = cmts.loss; 63 unit = "%"; 64 icon = "mdi:lan-disconnect"; 65 }; 66 station_latency = { 67 name = "Station latency"; 68 query = station.latency; 69 unit = "ms"; 70 icon = "mdi:router-network"; 71 }; 72 station_loss = { 73 name = "Station packet loss"; 74 query = station.loss; 75 unit = "%"; 76 icon = "mdi:router-network"; 77 }; 78 download = { 79 name = "Download"; 80 query = wan "receive"; 81 unit = "Mbit/s"; 82 device_class = "data_rate"; 83 icon = "mdi:download"; 84 }; 85 upload = { 86 name = "Upload"; 87 query = wan "transmit"; 88 unit = "Mbit/s"; 89 device_class = "data_rate"; 90 icon = "mdi:upload"; 91 }; 92 # hourly, ./internet-speedtest.nix; last result within 2h 93 speedtest_download = { 94 name = "Speedtest download"; 95 query = ''last_over_time(speedtest_download_bits_per_second{host="acheron"}[2h]) / 1e6''; 96 unit = "Mbit/s"; 97 device_class = "data_rate"; 98 icon = "mdi:speedometer"; 99 }; 100 speedtest_upload = { 101 name = "Speedtest upload"; 102 query = ''last_over_time(speedtest_upload_bits_per_second{host="acheron"}[2h]) / 1e6''; 103 unit = "Mbit/s"; 104 device_class = "data_rate"; 105 icon = "mdi:speedometer"; 106 }; 107 speedtest_latency = { 108 name = "Speedtest latency"; 109 query = ''last_over_time(speedtest_latency_seconds{host="acheron"}[2h]) * 1000''; 110 unit = "ms"; 111 icon = "mdi:timer-outline"; 112 }; 113 ds_snr_min = { 114 name = "Downstream SNR min"; 115 query = ''min(vodafone_station_docsis_snr_db{direction="downstream",host="acheron"})''; 116 unit = "dB"; 117 icon = "mdi:signal"; 118 }; 119 ds_power_min = { 120 name = "Downstream power min"; 121 query = ''min(vodafone_station_docsis_power_dbmv{direction="downstream",${lib.removePrefix "{" docsis})''; 122 unit = "dBmV"; 123 icon = "mdi:arrow-down-bold"; 124 }; 125 ds_power_max = { 126 name = "Downstream power max"; 127 query = ''max(vodafone_station_docsis_power_dbmv{direction="downstream",${lib.removePrefix "{" docsis})''; 128 unit = "dBmV"; 129 icon = "mdi:arrow-down-bold"; 130 }; 131 us_power_max = { 132 name = "Upstream power max"; 133 query = ''max(vodafone_station_docsis_power_dbmv{direction="upstream",${lib.removePrefix "{" docsis})''; 134 unit = "dBmV"; 135 icon = "mdi:arrow-up-bold"; 136 }; 137 unlocked = { 138 name = "Unlocked channels"; 139 query = ''count(vodafone_station_docsis_locked{host="acheron"} == 0) or vector(0)''; 140 icon = "mdi:lock-open-alert"; 141 }; 142 docsis_online = { 143 name = "DOCSIS online"; 144 query = ''max(vodafone_station_docsis_online{host="acheron"})''; 145 icon = "mdi:cable-data"; 146 }; 147 station_up = { 148 name = "Station scrape"; 149 query = ''max(vodafone_station_up{host="acheron"})''; 150 icon = "mdi:router-wireless"; 151 }; 152 station_uptime = { 153 name = "Station uptime"; 154 query = ''max(vodafone_station_uptime_seconds{host="acheron"})''; 155 unit = "s"; 156 device_class = "duration"; 157 icon = "mdi:restart"; 158 }; 159 }; 160 161 restEntry = id: s: { 162 resource = vm; 163 params.query = s.query; 164 scan_interval = 60; 165 timeout = 20; 166 sensor = [ 167 ( 168 { 169 name = "Internet ${s.name}"; 170 unique_id = "gg23_internet_${id}"; 171 # no series yet (e.g. rate() right after a restart): unavailable 172 availability = "{{ value_json is defined and value_json.data.result | length > 0 }}"; 173 value_template = "{{ (value_json.data.result[0].value[1] | float) | round(2) }}"; 174 state_class = "measurement"; 175 inherit (s) icon; 176 } 177 // lib.optionalAttrs (s ? unit) { unit_of_measurement = s.unit; } 178 // lib.optionalAttrs (s ? device_class) { inherit (s) device_class; } 179 ) 180 ]; 181 }; 182 183 # HA derives the entity id from the name ("Internet Latency p95" -> 184 # sensor.internet_latency_p95); unique_id doesn't influence it. 185 e = 186 id: 187 "sensor.internet_" + lib.toLower (lib.replaceStrings [ " " ] [ "_" ] (sensors.${id}.name or id)); 188 189 # Each check: the problem it names, in the order things are upstream of 190 # each other (LAN cable -> Station -> cable segment -> backbone). Thresholds 191 # for DOCSIS levels follow the usual Vodafone cable level tables. 192 diagnosis = '' 193 {%- set n = namespace(p=[]) -%} 194 {%- if states('${e "station_up"}') in ['0.0', '0'] -%} 195 {%- set n.p = n.p + ['Station web UI not scrapeable'] -%} 196 {%- endif -%} 197 {%- if states('${e "docsis_online"}') in ['0.0', '0'] -%} 198 {%- set n.p = n.p + ['Cable modem not DOCSIS online'] -%} 199 {%- endif -%} 200 {%- if states('${e "station_loss"}') | float(0) > 1 -%} 201 {%- set n.p = n.p + ['Loss acheron->Station ' ~ states('${e "station_loss"}') ~ '% (LAN cable/port)'] -%} 202 {%- endif -%} 203 {%- if states('${e "unlocked"}') | float(0) > 0 -%} 204 {%- set n.p = n.p + [states('${e "unlocked"}') | int ~ ' DOCSIS channels unlocked'] -%} 205 {%- endif -%} 206 {%- if states('${e "ds_snr_min"}') | float(99) < 33 -%} 207 {%- set n.p = n.p + ['Downstream SNR low (' ~ states('${e "ds_snr_min"}') ~ ' dB)'] -%} 208 {%- endif -%} 209 {%- if states('${e "ds_power_min"}') | float(0) < -4 or states('${e "ds_power_max"}') | float(0) > 13 -%} 210 {%- set n.p = n.p + ['Downstream level out of range (' ~ states('${e "ds_power_min"}') ~ '..' ~ states('${e "ds_power_max"}') ~ ' dBmV)'] -%} 211 {%- endif -%} 212 {%- if states('${e "us_power_max"}') | float(0) > 51 -%} 213 {%- set n.p = n.p + ['Upstream level high (' ~ states('${e "us_power_max"}') ~ ' dBmV): return path attenuated/noisy'] -%} 214 {%- endif -%} 215 {%- if states('${e "cmts_loss"}') | float(0) > 1 -%} 216 {%- set n.p = n.p + ['Loss on the cable segment ' ~ states('${e "cmts_loss"}') ~ '%'] -%} 217 {%- endif -%} 218 {%- if states('${e "cmts_latency"}') | float(0) > 30 -%} 219 {%- set n.p = n.p + ['Cable segment slow (' ~ states('${e "cmts_latency"}') ~ ' ms to CMTS): node congested'] -%} 220 {%- endif -%} 221 {%- if states('${e "loss"}') | float(0) > 1 and states('${e "cmts_loss"}') | float(0) <= 1 -%} 222 {%- set n.p = n.p + ['Loss behind the CMTS ' ~ states('${e "loss"}') ~ '% (Vodafone backbone)'] -%} 223 {%- endif -%} 224 {#- the hourly speedtest saturates the line itself: skip the 5 min window after a result -#} 225 {%- set st = states['${e "speedtest_download"}'] -%} 226 {%- set after_speedtest = st is not none and (now() - st.last_changed).total_seconds() < 420 -%} 227 {%- if states('${e "latency_p95"}') | float(0) > 60 and not after_speedtest -%} 228 {%- set n.p = n.p + ['Latency spikes (p95 ' ~ states('${e "latency_p95"}') ~ ' ms) at ' ~ states('${e "download"}') ~ '/' ~ states('${e "upload"}') ~ ' Mbit/s: bufferbloat or congestion'] -%} 229 {%- endif -%} 230 {%- if states('${e "station_uptime"}') | float(99999) < 3600 -%} 231 {%- set n.p = n.p + ['Station rebooted in the last hour'] -%} 232 {%- endif -%} 233 {%- if states('${e "speedtest_download"}') | float(9999) < 0.3 * ${toString line.down} -%} 234 {%- set n.p = n.p + ['Speedtest only ' ~ states('${e "speedtest_download"}') ~ ' of ${toString line.down} Mbit/s down'] -%} 235 {%- endif -%} 236 {%- if states('${e "speedtest_upload"}') | float(9999) < 0.3 * ${toString line.up} -%} 237 {%- set n.p = n.p + ['Speedtest only ' ~ states('${e "speedtest_upload"}') ~ ' of ${toString line.up} Mbit/s up'] -%} 238 {%- endif -%} 239 ''; 240 241 package = { 242 rest = lib.mapAttrsToList restEntry sensors; 243 template = [ 244 { 245 sensor = [ 246 { 247 name = "Internet diagnosis"; 248 unique_id = "gg23_internet_diagnosis"; 249 icon = "mdi:stethoscope"; 250 # the problem itself when there is one, else the count 251 state = "${diagnosis}{{ 'OK' if not n.p else n.p[0] if n.p | count == 1 else n.p | count ~ ' problems' }}"; 252 attributes.problems = "${diagnosis}{{ ('- ' ~ n.p | join('\\n- ')) if n.p else 'nothing found' }}"; 253 } 254 ]; 255 } 256 ]; 257 lovelace.dashboards.internet-gg23 = { 258 mode = "yaml"; 259 title = "Internet"; 260 icon = "mdi:wan"; 261 show_in_sidebar = true; 262 filename = "dashboards/internet.yaml"; 263 }; 264 }; 265 266 graph = title: hours: ids: { 267 type = "history-graph"; 268 inherit title; 269 hours_to_show = hours; 270 entities = map e ids; 271 }; 272 273 dashboard = { 274 title = "Internet"; 275 views = [ 276 { 277 title = "Internet"; 278 path = "internet"; 279 type = "sections"; 280 max_columns = 3; 281 sections = [ 282 { 283 type = "grid"; 284 cards = [ 285 { 286 type = "markdown"; 287 title = "Why does it suck?"; 288 content = '' 289 **{{ states('${e "diagnosis"}') }}** 290 291 {{ state_attr('${e "diagnosis"}', 'problems') }} 292 293 Path: acheron → Station (LAN) → CMTS (cable) → internet. 294 Loss/latency appearing first at a hop points at that segment. 295 ''; 296 } 297 { 298 type = "entities"; 299 title = "Now (5 min)"; 300 entities = map e [ 301 "download" 302 "upload" 303 "latency" 304 "latency_p95" 305 "loss" 306 "cmts_latency" 307 "cmts_loss" 308 "station_latency" 309 "station_loss" 310 ]; 311 } 312 ]; 313 } 314 { 315 type = "grid"; 316 cards = [ 317 (graph "Latency" 24 [ 318 "latency" 319 "latency_p95" 320 "cmts_latency" 321 "station_latency" 322 ]) 323 (graph "Packet loss" 24 [ 324 "loss" 325 "cmts_loss" 326 "station_loss" 327 ]) 328 (graph "Throughput (WAN)" 24 [ 329 "download" 330 "upload" 331 ]) 332 { 333 type = "entities"; 334 title = "Speedtest (hourly, from acheron)"; 335 entities = map e [ 336 "speedtest_download" 337 "speedtest_upload" 338 "speedtest_latency" 339 ]; 340 } 341 (graph "Speedtest" 168 [ 342 "speedtest_download" 343 "speedtest_upload" 344 ]) 345 ]; 346 } 347 { 348 type = "grid"; 349 cards = [ 350 { 351 type = "entities"; 352 title = "Cable modem"; 353 entities = map e [ 354 "docsis_online" 355 "unlocked" 356 "ds_snr_min" 357 "ds_power_min" 358 "ds_power_max" 359 "us_power_max" 360 "station_uptime" 361 "station_up" 362 ]; 363 } 364 (graph "DOCSIS levels" 168 [ 365 "ds_snr_min" 366 "ds_power_min" 367 "ds_power_max" 368 "us_power_max" 369 ]) 370 { 371 type = "markdown"; 372 content = '' 373 Good ranges: downstream -4..13 dBmV, SNR ≥ 33 dB (256QAM), 374 upstream 35..51 dBmV. Per channel and longer history: 375 Grafana "Internet" on styx.r:${toString ports.grafana}. 376 ''; 377 } 378 ]; 379 } 380 ]; 381 } 382 ]; 383 }; 384 385 json = name: value: pkgs.writeText name (builtins.toJSON value); 386in 387{ 388 virtualisation.oci-containers.containers.homeassistant.volumes = [ 389 "${json "internet.yaml" package}:/config/packages/internet.yaml:ro" 390 "${json "internet-dashboard.yaml" dashboard}:/config/dashboards/internet.yaml:ro" 391 ]; 392}