NetHealth — v1.24 · clos / archivé
App menubar macOS qui mesure la santé réseau par interface (Wi-Fi, Ethernet, dock USB-LAN, iPhone tethering) + métriques 5G TP-Link M8550. Sujet **clos 2026-05-04** en v1.24 — la suite logique est le bonding multi-uplinks (cf. fichier BONDING_CDC dans cette même fiche, ou projet bonding séparé à venir).
NetHealth — santé réseau multi-interface dans la menu bar macOS
Petite app menu bar Python qui mesure et affiche la qualité réseau par interface (Wi-Fi, Ethernet, iPhone tethering, dock USB-LAN…) et la consommation 5G d'un routeur TP-Link M8550.
L'idée centrale : sur Mac on a souvent plusieurs liens réseau actifs en même temps, mais macOS ne montre que celui de la route par défaut. NetHealth affiche les N liens, donne un score 0-100 par lien, et marque celui qui sort le trafic. Pratique pour décider quand basculer sur la 5G ou un dock filaire.
Statut : clos 2026-05-04 en v1.24. La suite logique (bonding multi-uplinks, endpoint Pi5, Phase 2A Speedify) est documentée dans
BONDING_CDC.md.
Sommaire
- Vue d'ensemble
- Comment ça marche
- Pré-requis
- Build & installation
- Premier lancement
- Configuration
- Mise à jour
- Notes de révision
- Désinstallation
- Emplacements utiles
- Dépannage
Vue d'ensemble
Ce qui s'affiche dans le menu
NetHealth v1.24
Route active : Wi-Fi · Nostromo
★ ◉ Wi-Fi · 94% · 4ms · perte 0% · 45 Mbps↓
↪ ◎ DockCaseAxUSBToLAN · 78% · 8ms · perte 0% · 21 Mbps↓
↪ ✕ iPhone · 0% · 200ms · perte 100% · (skip: pas de route)
🚦 Sondes : standard
Budget 5G : 0 Mo/h (seuil 100 Mo/h)
Signal Wi-Fi : -54 dBm · SNR 41 dB · 1201 Mbps radio
Download (route active) : 192.0 Mbps
Upload (route active) : 90.4 Mbps
📡 TP-Link M8550 : 5G NSA · Bouygues
Réseau : 5G NSA · WAN 80.x.x.x
Live : 22.4 Mbps↓ / 1.8 Mbps↑
Data consommée : 4.2 Go
Δ depuis NetHealth : 12.3 Mo
Configurer mot de passe routeur…
Rafraîchir TP-Link
Rafraîchir maintenant
Lancer speedtest maintenant
Diagnostic…
Ouvrir le log
Quitter
Lecture en 1 seconde :
- Glyphes par formes (daltonien-safe) :
✕ ◌ ○ ◎ ◉du pire au meilleur - ★ = route active (par où sort le trafic)
- ↪ = interface UP mais en backup
- 📶 (à côté du nom) = lien mobile (iPhone tethering, Wi-Fi ≈ partage 5G)
L'icône menu bar
Cinq barres verticales (1 par interface, max 5), hauteur = score, couleur = palette viridis (bleu profond → turquoise → jaune doré). Petit triangle blanc au-dessus de la barre de la route active. Fonds gris ténus = "ça pourrait monter jusqu'ici".
L'icône Finder reprend le même langage visuel sur fond squircle navy.
Comment ça marche
Boucle de monitoring
Un thread d'arrière-plan tourne en permanence (_monitor_loop), et exécute un tick toutes les 30 s :
- Énumère les interfaces macOS via
networksetup -listallhardwareportsetifconfig. Sépare ce qui a une IPv4 routable (is_ready) du standby (iPhone branché sans IP utile, Wi-Fi associé sans bail DHCP). - Ping chaque interface (4 paquets vers Cloudflare 1.1.1.1) en bindant la source via
ping -S <ip>. - Sonde "medium" round-robin : 1 interface par cycle de 10 min, télécharge 1 MB Cloudflare via
curl --interface <dev>. EWMA pour lisser. Précédée d'un pre-check TCP 2 s socket-bound (IP_BOUND_IF=25sur macOS) qui évite de sécher 8 s de download sur une iface sans route. - TP-Link M8550 (si Keychain renseigné) : récupère via
tplinkrouterc6ules métriques radio + data consommée. Précédé d'un pre-check HTTP 2 s qui vérifie une signature M-series (headerServerlighttpd/boa, balise TP-Link, ou/cgi/getParmavec formatvar nn="…") — sans ça, la lib timeout à 30 s quand le M8550 est hors subnet, ou pire, parle à la box d'un Wi-Fi tiers qui répond aussi sur 192.168.1.1. - Speedtest Cloudflare sur la route par défaut, toutes les 5 min, uniquement si la route n'est pas mobile et qu'on n'est pas en mode économique 5G.
- Score qualité par interface, agrégé en un health 0-1 (cf. plus bas).
- Refresh icône + menu via
_call_on_main()(rumps + Cocoa main thread).
Mode économique 5G
Quand le M8550 dépasse TPLINK_BUDGET_MB_PER_HOUR (défaut 100 Mo/h) :
- Plus de speedtest auto
- Plus de medium probe sur les interfaces mobiles
Les pings cheap restent (négligeables). Évite de cramer un forfait pendant les tests.
Score qualité par interface
score = 0.40 × latence + 0.30 × perte + 0.30 × débit (+ bonus signal Wi-Fi ≤ 5%)
- Latence : 1.0 si ≤ 30 ms, 0.0 si ≥ 250 ms, linéaire au milieu.
- Perte :
1 - loss/20, 0 % → 1.0, 20 %+ → 0.0. - Débit :
min(1.0, mbps / 50). Source : EWMA medium probe en priorité (par iface), sinon speedtest Cloudflare (route active uniquement), sinon neutre 0.5. - Bonus Wi-Fi : +0.025 si RSSI ≥ -55 dBm, +0.025 si SNR ≥ 30 dB.
Court-circuits durs à 0 : si le pre-check TCP a marqué l'iface avec un de ces motifs, le score passe direct à 0 sans calcul :
pas de routeréseau injoignablehost downiface sans IPiface inutilisable
C'est ce qui produit le ✕ · 0% rapide quand un dock est branché à un switch sans uplink.
Palette daltonien-safe
Inspirée viridis : bleu profond (40, 40, 120) → turquoise (60, 170, 170) → jaune doré (240, 210, 80). Zéro rouge ni vert. Voir _interpolate_color() dans network_health.py.
Pré-requis
| Élément | Précision |
|---|---|
| macOS | 12 Monterey minimum (CoreWLAN + PyObjC récents). |
| Compte Apple Developer | Pour la signature Developer ID (99 €/an). Un compte gratuit signe seulement avec une cert « Apple Development » utilisable localement. |
| Outils Xcode | xcode-select --install (fournit codesign, security, notarytool, stapler, iconutil). |
| Python | 3.11 ou 3.12. 3.10 minimum. |
Build & installation
Cert Developer ID (une seule fois)
Xcode ne crée pas cette cert automatiquement.
- Xcode → Settings → Accounts → ton Apple ID.
- Manage Certificates… →
+→ Developer ID Application.
Vérifier :
security find-identity -v -p codesigning
Noter la chaîne complète :
Developer ID Application: Nom Organisation (ABCDE12345)
C'est la valeur de la variable DEVELOPER_ID ci-dessous.
Préparer l'environnement
cd path/to/nethealth # repo root
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
pip install py2app
Dépendances installées :
rumps— UI menu barPillow— génération dynamique des icônes PNGpyobjc-framework-CoreWLAN— API Wi-Fi native (SSID, RSSI, SNR, canal…)pyobjc-framework-CoreLocation— déclenche la permission Location Servicestplinkrouterc6u— client M8550 (lecture LTE/5G + data)speedtest-cli— fallback (le module principal est vendorisé viaspeedtest_vendor.py)py2app— bundling en.app
Build signé (recommandé)
DEVELOPER_ID="Developer ID Application: Nom Organisation (ABCDE12345)" \
INSTALL=1 \
./build.sh
Le script :
- Supprime les builds précédents (
build/,dist/,__pycache__/) - Génère l'icône Finder via
python3 generate_app_icon.pysiicon.icnsn'existe pas - Construit
dist/NetHealth.appvia py2app - Signe le bundle avec l'identité fournie + entitlements de
entitlements.plist - Copie
NetHealth.appdans/Applications/
Durée typique : 30-90 s.
Build signé + notarisé (distribution externe)
Pré-requis : un app-specific password créé sur appleid.apple.com → Sécurité.
DEVELOPER_ID="Developer ID Application: Nom Organisation (ABCDE12345)" \
APPLE_ID="email@example.com" \
TEAM_ID="ABCDE12345" \
APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx" \
NOTARIZE=1 INSTALL=1 \
./build.sh
Compter 1-5 min de plus pour la notarisation (envoi à Apple, attente, staple du ticket).
Build non signé (test local uniquement)
./build.sh
cp -R dist/NetHealth.app /Applications/
Gatekeeper refusera au 1er lancement → clic-droit sur l'icône → Ouvrir.
Premier lancement
- Ouvrir
/Applications/NetHealth.app(double-clic Finder). - Accepter la popup Location Services quand elle apparaît. Sans cette permission, macOS retourne
<redacted>pour le SSID et NetHealth ne peut pas distinguer les Wi-Fi entre eux. - L'icône (cinq barres) apparaît dans la menu bar.
Configurer le M8550 (optionnel)
Menu Configurer mot de passe routeur… → saisir le mot de passe admin du M8550. Stocké dans le Keychain macOS sous eu.mylastnight.nethealth.tplink. NetHealth essaie successivement les usernames user puis admin.
Si la popup Location n'apparaît pas
Le plus courant : macOS a mémorisé un refus précédent.
tccutil reset LocationServices eu.mylastnight.nethealth
open /Applications/NetHealth.app
Ou manuellement : Réglages système → Confidentialité & sécurité → Services de localisation → activer NetHealth.
Si la permission est refusée au runtime, le menu affiche en plus l'entrée ⚠️ Réglages Location (perm refusée)… qui ouvre directement le panneau macOS correspondant.
Démarrage automatique
Réglages système → Général → Éléments d'ouverture → + → /Applications/NetHealth.app.
L'app vit uniquement dans la menu bar (LSUIElement = True) — pas de Dock, pas de fenêtre.
Configuration
Style d'icône menu bar
network_health.py ligne ~207 :
ICON_STYLE = "gauge" # ou "radar" ou "pie"
gauge(défaut) : 5 barres verticales, hauteur = score, triangle blanc sur l'iface active.radar: cercle + rayons partant du centre, longueur = score, disque blanc sur l'iface active.pie: pie-chart historique, 1 quartier par iface.
Modifier la valeur, rebuild, l'icône change.
Seuils du score
network_health.py lignes ~196-199 :
LATENCY_GREAT_MS = 30 # ≤ 30 ms → score latence = 1.0
LATENCY_BAD_MS = 250 # ≥ 250 ms → score latence = 0.0
LOSS_BAD_PCT = 20 # 20 % → score perte = 0.0
DOWNLOAD_GREAT_MBPS = 50 # ≥ 50 Mbps → score débit = 1.0
Budget 5G
network_health.py ligne ~181 :
TPLINK_BUDGET_MB_PER_HOUR = 100 # au-delà → mode économique
Régénérer l'icône Finder
L'icône .icns est produite à partir de generate_app_icon.py. Si tu modifies les paramètres (couleurs, padding, taille du triangle), relance :
python3 generate_app_icon.py # régénère icon.icns + icon_1024.png + icon.iconset/
Puis rebuild.
Mise à jour
Tant que le bundle identifier ne change pas (eu.mylastnight.nethealth), macOS conserve les permissions accordées (Location, etc.).
cd path/to/nethealth # repo root
source .venv/bin/activate
osascript -e 'tell application "NetHealth" to quit' 2>/dev/null
DEVELOPER_ID="Developer ID Application: Nom Organisation (ABCDE12345)" \
INSTALL=1 \
./build.sh
open /Applications/NetHealth.app
Bumper VERSION dans network_health.py ET APP_VERSION dans setup.py à chaque build (utile pour distinguer dans les logs >>> NetHealth v1.24 STARTING <<<).
Notes de révision
Ne lister que les jalons. Les fixes mineurs intermédiaires (ex. 1.20 → 1.21 → 1.22) sont volontairement omis — l'historique fin est dans git log.
v1.24 — clos / archivé · 2026-05-04
- Pre-check TP-Link par signature HTTP (
_validate_m8550) : remplace le simple connect TCP par un GET qui vérifie le headerServer(lighttpd/boa), le body (tp-link), ou un fallback sur/cgi/getParmau formatvar nn="…". Règle le faux positif sur les Wi-Fi tiers où la gateway répond aussi sur192.168.1.1. - Mapping errno robuste :
_unwrap_oserrordescend la chaîne d'exceptionsrequests/urllib3jusqu'au 1erOSErroravecerrnonon-None(le wrapper extérieur étant lui-même unOSError errno=None, il fallait creuser). - Statut projet : clos. La suite logique est le bonding multi-uplinks (cf.
BONDING_CDC.md).
v1.23 · 2026-04-25
- Score qualité daltonien-safe : palette viridis + glyphes
✕◌○◎◉au lieu d'un dégradé rouge/vert. - Style d'icône configurable (
pie/gauge/radar),gaugeretenu par défaut. - Icône Finder reproductible via
generate_app_icon.py→icon.icns(plus de drag-drop manuel). - Ménage menu :
copy_ssidetrequest_locationretirés ;open_location_prefsconditionné sur l'OS. _humanize_router_errormappe les exceptionstplinkrouterc6uvers des labels courts en français.
v1.19 → v1.22 (intermédiaires omis)
Itérations de stabilisation : pre-check TCP côté TP-Link ajouté en 1.19 (le M8550 hors subnet bloquait toute la boucle pendant 30 s sur le timeout caché de la lib), puis ajustements CoreLocation, gestion du <redacted> SSID, exposition keychain pour le mot de passe routeur.
v1.16 · pre-check TCP socket-bound côté Wi-Fi
- Pre-check TCP par interface avec
IP_BOUND_IF: avant cette version, lesmedium probeséchaient encurl rc=28sur les ifaces sans route. Depuis, on tag l'iface comme injoignable proprement et on coupe court. - Mapping
curl rc → label lisible(timeout,pas de route,DNS KO, etc.).
v1.0 → v1.15 (préhistoire, pas de notes détaillées)
Mise au point progressive : énumération multi-iface via networksetup, classification (wifi / iphone / ethernet), score 0-100 (latence + perte + débit), ping cheap récurrent, medium probe round-robin, intégration tplinkrouterc6u pour le M8550, budget 5G horaire avec bascule en mode économique, popup macOS Location pour avoir le SSID en clair.
Désinstallation
osascript -e 'tell application "NetHealth" to quit' 2>/dev/null
rm -rf /Applications/NetHealth.app
rm -rf ~/Library/Logs/NetHealth
tccutil reset LocationServices eu.mylastnight.nethealth
security delete-generic-password -s eu.mylastnight.nethealth.tplink 2>/dev/null
Retirer aussi de Réglages système → Général → Éléments d'ouverture si l'auto-start était activé.
Emplacements utiles
| Chemin | Contenu |
|---|---|
/Applications/NetHealth.app |
Bundle installé. |
~/Library/Logs/NetHealth/nethealth.log |
Log principal (rotatif, 1 Mo, 3 archives). |
~/Library/Logs/NetHealth/debug.log |
Log debug brut. |
<projet>/dist/NetHealth.app |
Sortie py2app avant install. |
<projet>/icon.icns |
Icône Finder. |
<projet>/icon_1024.png |
Master 1024 généré pour preview. |
<projet>/build/ |
Artefacts intermédiaires py2app. |
Keychain : security find-generic-password -s eu.mylastnight.nethealth.tplink.
Dépannage
SSID <redacted> dans le diagnostic
Permission Location Services non accordée. Voir Premier lancement.
Tous les medium probe échouent en curl rc=28:
Tu tournes une version < 1.16. Rebuild en 1.24 — le pre-check TCP socket-bound + le mapping rc → label lisible (timeout, pas de route, DNS KO, etc.) sont absents avant.
TP-Link ConnectTimeout 30 s à chaque tick
Idem : il manque le pre-check routeur (TCP en 1.19+, HTTP-signature en 1.24+). Le M8550 hors subnet bloquait toute la boucle. Rebuild résout.
TP-Link répond mais les métriques sont vides / session HTTP cassée
Sur 1.23 et avant, le pre-check était un simple connect TCP : sur un Wi-Fi tiers où la gateway répond aussi sur 192.168.1.1, on parlait à la mauvaise box et tplinkrouterc6u enchaînait 4 endpoints HTTP qui foiraient. Depuis 1.24 le pre-check valide une signature M-series (header Server ∈ {lighttpd, boa}, ou body TP-Link, ou /cgi/getParm retournant var nn="…"). Si la signature est absente, on remonte hôte ≠ M8550 (signature absente) au lieu de tenter la session.
L'app est installée mais l'icône reste à l'ancienne version
Le .app a été remplacé pendant qu'une instance tournait. Tuer + recopier proprement :
osascript -e 'tell application "NetHealth" to quit'
rm -rf /Applications/NetHealth.app
cp -R dist/NetHealth.app /Applications/
open /Applications/NetHealth.app
Speedtest auto skippé en permanence
Vérifier dans les logs ~/Library/Logs/NetHealth/nethealth.log : si tu vois speedtest auto skipped (mobile=True…), c'est normal — la route active est mobile, NetHealth s'appuie sur la sonde medium par iface (50× moins de data consommée). Pour forcer un test : menu Lancer speedtest maintenant.
Gatekeeper refuse l'ouverture malgré la signature
codesign --verify --strict --verbose=2 /Applications/NetHealth.app
spctl --assess --type execute --verbose /Applications/NetHealth.app
Si signé Apple Development (et non Developer ID), Gatekeeper refuse hors machine de dev. Créer une cert Developer ID Application (cf. Build) et re-signer.
./build.sh plante sur la signature
Re-vérifier que DEVELOPER_ID correspond exactement à la sortie de security find-identity -v -p codesigning (espaces et parenthèses inclus, et toute la chaîne entre guillemets dans la commande shell).
py2app rate l'embarquement d'un module
Ajouter dans setup.py clé includes ou packages. Pour les modules critiques (speedtest), préférer la vendorisation : copier le .py directement dans le projet (cf. speedtest_vendor.py).
Le menu est gris sombre peu lisible
Bug rumps connu : items sans callback rendus désactivés. Résolu depuis longtemps en passant un callback no-op aux items info — vérifier que tu n'es pas sur une vieille version.
Checklist de déploiement initial
- [ ] Cert Developer ID Application créée et listée par
security find-identity -v -p codesigning - [ ]
xcode-select --installexécuté - [ ] Repo cloné,
venvcréé et activé - [ ]
pip install -r requirements.txt && pip install py2app - [ ]
python3 generate_app_icon.py(génèreicon.icns) - [ ]
DEVELOPER_ID=… INSTALL=1 ./build.shexécuté sans erreur - [ ] Bundle présent dans
/Applications/NetHealth.app - [ ] Premier lancement effectué, permission Location accordée
- [ ] Mot de passe M8550 enregistré via le menu (optionnel)
- [ ] Icône cinq barres visible dans la menu bar
- [ ] Application ajoutée aux Éléments d'ouverture si démarrage auto souhaité
Ressources
-
network_health.py
81 KB
-
speedtest_vendor.py
64 KB
-
NetHealth.zip
321 KB
-
NetHealth.app.zip
27097 KB
-
network_health.py
81 KB
-
speedtest_vendor.py
64 KB
-
NetHealth.app.zip
23891 KB