This repository has no description
0

Configure Feed

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

configs.gg23.internet: rework the Home Assistant dashboard, router naming, yes/no states

+197 -97
+197 -97
configs/gg23/internet-hass.nix
··· 31 31 wan = dir: ''rate(host_network_${dir}_bytes_total{host="acheron",device="et0"}[2m]) * 8 / 1e6''; 32 32 docsis = ''{host="acheron",channel_type="SC-QAM"}''; 33 33 34 - # id -> sensor.internet_<id> 34 + # id -> sensor entity. `name` ("Internet <name>") is what HA derives the 35 + # entity id from (see `e`), so it must not change; `label` is the display 36 + # name (homeassistant.customize friendly_name). 35 37 sensors = { 36 38 latency = { 37 39 name = "Latency"; 40 + label = "Latency to internet"; 38 41 query = internet.latency; 39 42 unit = "ms"; 40 43 icon = "mdi:timer-outline"; 41 44 }; 42 45 latency_p95 = { 43 46 name = "Latency p95"; 47 + label = "Latency spikes (p95)"; 44 48 query = internet.p95; 45 49 unit = "ms"; 46 50 icon = "mdi:timer-alert-outline"; 47 51 }; 48 52 loss = { 49 53 name = "Packet loss"; 54 + label = "Packet loss to internet"; 50 55 query = internet.loss; 51 56 unit = "%"; 52 57 icon = "mdi:lan-disconnect"; 53 58 }; 54 59 cmts_latency = { 55 60 name = "Cable latency"; 61 + label = "Latency over the cable"; 56 62 query = cmts.latency; 57 63 unit = "ms"; 58 64 icon = "mdi:timer-outline"; 59 65 }; 60 66 cmts_loss = { 61 67 name = "Cable packet loss"; 68 + label = "Packet loss over the cable"; 62 69 query = cmts.loss; 63 70 unit = "%"; 64 71 icon = "mdi:lan-disconnect"; 65 72 }; 66 73 station_latency = { 67 74 name = "Station latency"; 75 + label = "Latency to the router"; 68 76 query = station.latency; 69 77 unit = "ms"; 70 78 icon = "mdi:router-network"; 71 79 }; 72 80 station_loss = { 73 81 name = "Station packet loss"; 82 + label = "Packet loss to the router"; 74 83 query = station.loss; 75 84 unit = "%"; 76 85 icon = "mdi:router-network"; 77 86 }; 78 87 download = { 79 88 name = "Download"; 89 + label = "Download"; 80 90 query = wan "receive"; 81 91 unit = "Mbit/s"; 82 92 device_class = "data_rate"; ··· 84 94 }; 85 95 upload = { 86 96 name = "Upload"; 97 + label = "Upload"; 87 98 query = wan "transmit"; 88 99 unit = "Mbit/s"; 89 100 device_class = "data_rate"; ··· 92 103 # hourly, ./internet-speedtest.nix; last result within 2h 93 104 speedtest_download = { 94 105 name = "Speedtest download"; 106 + label = "Speedtest download"; 95 107 query = ''last_over_time(speedtest_download_bits_per_second{host="acheron"}[2h]) / 1e6''; 96 108 unit = "Mbit/s"; 97 109 device_class = "data_rate"; ··· 99 111 }; 100 112 speedtest_upload = { 101 113 name = "Speedtest upload"; 114 + label = "Speedtest upload"; 102 115 query = ''last_over_time(speedtest_upload_bits_per_second{host="acheron"}[2h]) / 1e6''; 103 116 unit = "Mbit/s"; 104 117 device_class = "data_rate"; ··· 106 119 }; 107 120 speedtest_latency = { 108 121 name = "Speedtest latency"; 122 + label = "Speedtest latency"; 109 123 query = ''last_over_time(speedtest_latency_seconds{host="acheron"}[2h]) * 1000''; 110 124 unit = "ms"; 111 125 icon = "mdi:timer-outline"; 112 126 }; 113 127 ds_snr_min = { 114 128 name = "Downstream SNR min"; 129 + label = "Downstream SNR min"; 115 130 query = ''min(vodafone_station_docsis_snr_db{direction="downstream",host="acheron"})''; 116 131 unit = "dB"; 117 132 icon = "mdi:signal"; 118 133 }; 119 134 ds_power_min = { 120 135 name = "Downstream power min"; 136 + label = "Downstream power min"; 121 137 query = ''min(vodafone_station_docsis_power_dbmv{direction="downstream",${lib.removePrefix "{" docsis})''; 122 138 unit = "dBmV"; 123 139 icon = "mdi:arrow-down-bold"; 124 140 }; 125 141 ds_power_max = { 126 142 name = "Downstream power max"; 143 + label = "Downstream power max"; 127 144 query = ''max(vodafone_station_docsis_power_dbmv{direction="downstream",${lib.removePrefix "{" docsis})''; 128 145 unit = "dBmV"; 129 146 icon = "mdi:arrow-down-bold"; 130 147 }; 131 148 us_power_max = { 132 149 name = "Upstream power max"; 150 + label = "Upstream power max"; 133 151 query = ''max(vodafone_station_docsis_power_dbmv{direction="upstream",${lib.removePrefix "{" docsis})''; 134 152 unit = "dBmV"; 135 153 icon = "mdi:arrow-up-bold"; 136 154 }; 137 155 unlocked = { 138 156 name = "Unlocked channels"; 157 + label = "DOCSIS channels not locked"; 139 158 query = ''count(vodafone_station_docsis_locked{host="acheron"} == 0) or vector(0)''; 140 159 icon = "mdi:lock-open-alert"; 160 + format = "int"; 141 161 }; 142 162 docsis_online = { 143 163 name = "DOCSIS online"; 164 + label = "Cable modem online"; 144 165 query = ''max(vodafone_station_docsis_online{host="acheron"})''; 145 166 icon = "mdi:cable-data"; 167 + format = "yesno"; 146 168 }; 147 169 station_up = { 148 170 name = "Station scrape"; 171 + label = "Router stats readable"; 149 172 query = ''max(vodafone_station_up{host="acheron"})''; 150 173 icon = "mdi:router-wireless"; 174 + format = "yesno"; 151 175 }; 152 176 station_uptime = { 153 177 name = "Station uptime"; 178 + label = "Router uptime"; 154 179 query = ''max(vodafone_station_uptime_seconds{host="acheron"})''; 155 180 unit = "s"; 156 181 device_class = "duration"; ··· 158 183 }; 159 184 }; 160 185 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 - }; 186 + restEntry = 187 + id: s: 188 + let 189 + v = "(value_json.data.result[0].value[1] | float)"; 190 + format = s.format or "float"; 191 + in 192 + { 193 + resource = vm; 194 + params.query = s.query; 195 + scan_interval = 60; 196 + timeout = 20; 197 + sensor = [ 198 + ( 199 + { 200 + name = "Internet ${s.name}"; 201 + unique_id = "gg23_internet_${id}"; 202 + # no series yet (e.g. rate() right after a restart): unavailable 203 + availability = "{{ value_json is defined and value_json.data.result | length > 0 }}"; 204 + value_template = 205 + { 206 + float = "{{ ${v} | round(2) }}"; 207 + int = "{{ ${v} | int }}"; 208 + yesno = "{{ 'yes' if ${v} > 0 else 'no' }}"; 209 + } 210 + .${format}; 211 + inherit (s) icon; 212 + } 213 + // lib.optionalAttrs (format != "yesno") { state_class = "measurement"; } 214 + // lib.optionalAttrs (s ? unit) { unit_of_measurement = s.unit; } 215 + // lib.optionalAttrs (s ? device_class) { inherit (s) device_class; } 216 + ) 217 + ]; 218 + }; 182 219 183 220 # HA derives the entity id from the name ("Internet Latency p95" -> 184 221 # sensor.internet_latency_p95); unique_id doesn't influence it. ··· 187 224 "sensor.internet_" + lib.toLower (lib.replaceStrings [ " " ] [ "_" ] (sensors.${id}.name or id)); 188 225 189 226 # Each check: the problem it names, in the order things are upstream of 190 - # each other (LAN cable -> Station -> cable segment -> backbone). Thresholds 227 + # each other (LAN cable -> router -> cable segment -> backbone). Thresholds 191 228 # for DOCSIS levels follow the usual Vodafone cable level tables. 192 229 diagnosis = '' 193 230 {%- 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'] -%} 231 + {%- if is_state('${e "station_up"}', 'no') -%} 232 + {%- set n.p = n.p + ['Router stats not readable'] -%} 196 233 {%- endif -%} 197 - {%- if states('${e "docsis_online"}') in ['0.0', '0'] -%} 234 + {%- if is_state('${e "docsis_online"}', 'no') -%} 198 235 {%- set n.p = n.p + ['Cable modem not DOCSIS online'] -%} 199 236 {%- endif -%} 200 237 {%- if states('${e "station_loss"}') | float(0) > 1 -%} 201 - {%- set n.p = n.p + ['Loss acheron->Station ' ~ states('${e "station_loss"}') ~ '% (LAN cable/port)'] -%} 238 + {%- set n.p = n.p + ['Packet loss to the router ' ~ states('${e "station_loss"}') ~ '% (LAN cable/port)'] -%} 202 239 {%- endif -%} 203 240 {%- if states('${e "unlocked"}') | float(0) > 0 -%} 204 241 {%- set n.p = n.p + [states('${e "unlocked"}') | int ~ ' DOCSIS channels unlocked'] -%} ··· 228 265 {%- 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 266 {%- endif -%} 230 267 {%- if states('${e "station_uptime"}') | float(99999) < 3600 -%} 231 - {%- set n.p = n.p + ['Station rebooted in the last hour'] -%} 268 + {%- set n.p = n.p + ['Router rebooted in the last hour'] -%} 232 269 {%- endif -%} 233 270 {%- if states('${e "speedtest_download"}') | float(9999) < 0.3 * ${toString line.down} -%} 234 271 {%- set n.p = n.p + ['Speedtest only ' ~ states('${e "speedtest_download"}') ~ ' of ${toString line.down} Mbit/s down'] -%} ··· 240 277 241 278 package = { 242 279 rest = lib.mapAttrsToList restEntry sensors; 280 + # display names; `name` stays what the entity ids were derived from 281 + homeassistant.customize = lib.mapAttrs' ( 282 + id: s: lib.nameValuePair (e id) { friendly_name = s.label; } 283 + ) sensors; 243 284 template = [ 244 285 { 245 286 sensor = [ ··· 263 304 }; 264 305 }; 265 306 266 - graph = title: hours: ids: { 307 + # history-graph; rows: [ id legend-name ] in display order 308 + graph = title: hours: rows: { 267 309 type = "history-graph"; 268 310 inherit title; 269 311 hours_to_show = hours; 270 - entities = map e ids; 312 + entities = map (r: { 313 + entity = e (builtins.elemAt r 0); 314 + name = builtins.elemAt r 1; 315 + }) rows; 271 316 }; 272 317 273 318 dashboard = { ··· 281 326 sections = [ 282 327 { 283 328 type = "grid"; 329 + column_span = 3; 330 + cards = [ 331 + # apexcharts-card is installed through HACS (not declarative) 332 + { 333 + type = "custom:apexcharts-card"; 334 + header = { 335 + show = true; 336 + title = "Traffic and speedtest"; 337 + show_states = false; 338 + }; 339 + graph_span = "3d"; 340 + grid_options.columns = "full"; 341 + apex_config.chart.height = 180; 342 + yaxis = [ 343 + { 344 + min = 0; 345 + decimals = 0; 346 + } 347 + ]; 348 + series = 349 + let 350 + traffic = id: name: color: { 351 + entity = e id; 352 + inherit name color; 353 + type = "area"; 354 + opacity = 0.35; 355 + stroke_width = 1; 356 + group_by = { 357 + func = "avg"; 358 + duration = "5min"; 359 + }; 360 + }; 361 + speedtest = id: name: color: { 362 + entity = e id; 363 + inherit name color; 364 + type = "line"; 365 + curve = "stepline"; 366 + stroke_width = 1.5; 367 + }; 368 + in 369 + [ 370 + (traffic "download" "Download" "#1e88e5") 371 + (traffic "upload" "Upload" "#fb8c00") 372 + (speedtest "speedtest_download" "Speedtest download" "#1e88e5") 373 + (speedtest "speedtest_upload" "Speedtest upload" "#fb8c00") 374 + ]; 375 + } 376 + ]; 377 + } 378 + { 379 + type = "grid"; 284 380 cards = [ 285 381 { 286 382 type = "markdown"; 287 - title = "Why does it suck?"; 383 + title = "Diagnosis"; 288 384 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. 385 + {% if is_state('${e "diagnosis"}', 'OK') %}No problems found.{% else %}{{ state_attr('${e "diagnosis"}', 'problems') }}{% endif %} 295 386 ''; 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 387 } 312 388 ]; 313 389 } ··· 315 391 type = "grid"; 316 392 cards = [ 317 393 (graph "Latency" 24 [ 318 - "latency" 319 - "latency_p95" 320 - "cmts_latency" 321 - "station_latency" 394 + [ 395 + "latency" 396 + "Latency to internet (~10 ms normal)" 397 + ] 398 + [ 399 + "latency_p95" 400 + "Latency spikes, p95 (< 60 ms)" 401 + ] 402 + [ 403 + "cmts_latency" 404 + "Latency over the cable to Vodafone (~10 ms)" 405 + ] 406 + [ 407 + "station_latency" 408 + "Latency to the router (< 5 ms)" 409 + ] 322 410 ]) 323 411 (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" 412 + [ 413 + "loss" 414 + "Packet loss to internet (should be 0)" 415 + ] 416 + [ 417 + "cmts_loss" 418 + "Packet loss over the cable (should be 0)" 419 + ] 420 + [ 421 + "station_loss" 422 + "Packet loss to the router (should be 0)" 423 + ] 344 424 ]) 345 425 ]; 346 426 } ··· 350 430 { 351 431 type = "entities"; 352 432 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" 433 + # good ranges in the names; per channel: Grafana "Internet (gg23)" 434 + entities = [ 435 + (e "docsis_online") 436 + { 437 + entity = e "unlocked"; 438 + name = "DOCSIS channels not locked (should be 0)"; 439 + } 440 + { 441 + entity = e "ds_snr_min"; 442 + name = "Downstream SNR min (≥ 33 dB)"; 443 + } 444 + { 445 + entity = e "ds_power_min"; 446 + name = "Downstream power min (≥ -4 dBmV)"; 447 + } 448 + { 449 + entity = e "ds_power_max"; 450 + name = "Downstream power max (≤ 13 dBmV)"; 451 + } 452 + { 453 + entity = e "us_power_max"; 454 + name = "Upstream power max (35–51 dBmV)"; 455 + } 456 + (e "station_uptime") 457 + (e "station_up") 362 458 ]; 363 459 } 364 460 (graph "DOCSIS levels" 168 [ 365 - "ds_snr_min" 366 - "ds_power_min" 367 - "ds_power_max" 368 - "us_power_max" 461 + [ 462 + "ds_snr_min" 463 + "Downstream SNR min" 464 + ] 465 + [ 466 + "ds_power_min" 467 + "Downstream power min" 468 + ] 469 + [ 470 + "ds_power_max" 471 + "Downstream power max" 472 + ] 473 + [ 474 + "us_power_max" 475 + "Upstream power max" 476 + ] 369 477 ]) 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 478 ]; 379 479 } 380 480 ];