# ZendureEvGuard - Guide Utilisateur

## Qu'est-ce que ZendureEvGuard ?

ZendureEvGuard est un daemon qui protege automatiquement les batteries de votre **Zendure SolarFlow 2400 AC** pendant la recharge de votre vehicule electrique (VE). Il surveille la consommation electrique de votre maison en temps reel et met en pause la decharge des batteries pendant les sessions de recharge haute puissance, puis reprend le fonctionnement normal une fois la recharge terminee.

## Prerequis

- **.NET 9 Runtime** (ou SDK pour compiler depuis les sources)
- Un **Zendure SolarFlow 2400 AC** sur votre reseau local (API HTTP locale activee)
- Un des compteurs de puissance suivants :
  - **Shelly Pro 3EM** (API HTTP locale)
  - **Smappee** (API cloud OAuth2)

## Installation

Décompressez l'archive téléchargée dans le dossier de votre choix, par exemple :

| Plateforme | Emplacement conseillé |
|------------|----------------------|
| **Windows** | `C:\ZendureEvGuard\` |
| **Linux** | `/home/ZendureEvGuard/` |
| **macOS** | `/Applications/ZendureEvGuard/` |

Renseignez ensuite `appsettings.json` (voir **Configuration** plus bas), puis lancez
l'installateur fourni dans l'archive :

| Plateforme | Commande |
|------------|----------|
| **Windows** | PowerShell **en administrateur** : `powershell -ExecutionPolicy Bypass -File .\install-windows.ps1` |
| **Linux** | `sudo sh install-linux.sh` |
| **macOS** | `sudo sh install-macos.sh` |

Le programme s'installe **depuis ce dossier** : rien n'est copié ailleurs, et le dossier que vous
avez décompressé est celui qui tourne. Relancer l'installateur met à jour l'installation et
**n'écrase jamais votre `appsettings.json`**.

Pour l'essayer sans l'installer, lancez simplement l'exécutable : `[P]` met la batterie en pause,
`[R]` la remet en service, `Ctrl+C` deux fois pour arrêter.

### Depuis les sources (développeurs)

```bash
git clone <url-du-depot>
cd ZendureEvGuard
pwsh ./check.ps1
```

## Licence & Abonnement

ZendureEvGuard utilise le modele d'abonnement **Digital3D "One for All"**. Un seul abonnement donne acces a toutes les applications de bureau Digital3D.

### Essai gratuit (30 jours)

Au premier lancement de ZendureEvGuard, un **essai gratuit de 30 jours** demarre automatiquement. Aucun compte, aucune carte bancaire, aucune inscription necessaire. Vous verrez un message du type :

```
[12:00:00] Trial mode: 30 days remaining. Subscribe at https://digital3d.com/StripeAbo
```

### Acheter un abonnement

Quand votre essai expire (ou avant), suivez ces etapes :

1. Rendez-vous sur [https://digital3d.com/StripeAbo](https://digital3d.com/StripeAbo)
2. Creez un compte ou connectez-vous
3. Completez l'achat de l'abonnement via Stripe (paiement securise)
4. Une fois l'achat effectue, allez dans votre profil sur [https://digital3d.com](https://digital3d.com)
5. Trouvez votre **Cle Secrete** (un GUID du type `a1b2c3d4-e5f6-7890-abcd-ef1234567890`) — elle est affichee dans les parametres de votre compte

### Activer votre abonnement

Ajoutez votre cle secrete dans le fichier `appsettings.json` :

```json
"Subscription": {
    "SecretKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

Puis redemarrez ZendureEvGuard. Au demarrage, vous verrez :

```
[12:00:00] Zendure EV Guard PRO — License valid until 15/04/2026

    ★ Zendure EV Guard PRO — One for All subscription
    ★ License valid until 15/04/2026
    ★ Account: votre-nom-utilisateur
```

### Comment fonctionne la verification de licence

- La licence est verifiee **une fois par jour** aupres du serveur Digital3D
- Si le serveur est injoignable (pas d'internet), l'application continue de fonctionner **hors-ligne pendant 35 jours** maximum
- La licence se **renouvelle automatiquement 7 jours** avant expiration — aucune action manuelle requise
- Si l'abonnement expire et n'est pas renouvele, l'application s'arretera apres une **periode de grace de 48 heures**
- Si les notifications sont configurees (email, Pushover ou Telegram), vous recevrez un avertissement avant l'arret de l'application

### Proteger votre cle secrete

Votre cle secrete est comme un mot de passe — au meme titre que votre mot de passe SMTP, vos
identifiants Smappee et vos jetons Pushover/Telegram. **La methode recommandee est de tous
les sortir d'`appsettings.json`** vers un fichier `.env` externe :

1. Creez un fichier `.env` dans un emplacement securise :
```
Subscription__SecretKey=a1b2c3d4-e5f6-7890-abcd-ef1234567890
Notification__Password=votre-mot-de-passe-application
Smappee__ClientSecret=votre-secret
Pushover__ApiToken=votre-jeton
```
Le nom de chaque variable reprend le chemin de la cle avec un **double underscore** a la
place du point.

2. Referencez-le dans `appsettings.json` :
```json
"SecretFile": "/chemin/vers/votre/.env"
```

3. Restreignez ses droits d'acces. Sous Linux/macOS, `appsettings.json` est cree en `644`
   (lisible par tous les comptes de la machine) — le `.env` doit etre plus strict :
```bash
chmod 600 /chemin/vers/votre/.env
```

Les valeurs du `.env` **priment** sur celles d'`appsettings.json` : une cle presente dans les
deux fichiers est prise depuis le `.env`.

### Depannage licence

| Symptome | Cause | Solution |
|----------|-------|----------|
| "Trial period expired!" | L'essai de 30 jours est termine | Achetez un abonnement sur digital3d.com/StripeAbo |
| "Subscription authentication failed" | Cle secrete invalide ou expiree | Verifiez votre cle sur digital3d.com ou renouvelez l'abonnement |
| "License expired after 2 consecutive checks" | Abonnement non renouvele + 48h de grace ecoulees | Renouvelez votre abonnement sur digital3d.com |
| L'app fonctionne mais affiche "Trial mode" apres ajout de la cle | Redemarrage necessaire | Arretez et redemarrez ZendureEvGuard |

### Mesure d'usage

ZendureEvGuard envoie a digital3d.com quelques evenements d'usage (lancement de l'application,
mise en pause/reprise de la batterie). Leur contenu se limite a des indicateurs techniques :
jamais d'adresse IP d'equipement, de chemin de fichier, de jeton ni de valeur de puissance ou
de charge. Si vous utilisez un abonnement, ces evenements sont rattaches a votre compte ;
sans abonnement, ils ne portent qu'un identifiant d'appareil. Pour desactiver cet envoi,
definissez la variable d'environnement `DIGITAL3D_TELEMETRY_DISABLED=1`.

## Configuration

Tous les parametres sont dans `appsettings.json`. Les valeurs sensibles (mots de passe, cles
API, cle d'abonnement) **devraient** etre deplacees dans un fichier `.env` externe reference
par le parametre `SecretFile` — voir « Proteger votre cle secrete » ci-dessus.

### Source de puissance

Activez **une** des deux sources. Si les deux sont activees, Shelly a la priorite pour la detection VE. Smappee fonctionne en parallele comme affichage informatif independant.

#### Shelly Pro 3EM

```json
"Shelly": {
  "Enabled": true,
  "Host": "192.168.x.x",
  "UseDigestAuth": false,
  "Username": "",
  "Password": ""
}
```

| Parametre | Description |
|-----------|-------------|
| `Host` | Adresse IP de votre Shelly Pro 3EM |
| `UseDigestAuth` | Mettre `true` si l'authentification est activee sur le Shelly |
| `Username` / `Password` | Identifiants si l'authentification est activee. Le firmware Shelly Gen2 n'accepte que l'utilisateur `admin` |

> Si le Shelly refuse les identifiants, ZendureEvGuard **ne demarre pas** : il vous le dit dans
> le journal, vous envoie une notification, et laisse la batterie tranquille. Sans mesure de
> puissance il ne peut rien detecter, et se mettre en pause par precaution sur un mot de passe
> laisserait la batterie coupee indefiniment.

#### Smappee

```json
"Smappee": {
  "Enabled": true,
  "ClientId": "votre-client-id",
  "ClientSecret": "votre-secret",
  "Username": "votre-email",
  "Password": "votre-mot-de-passe",
  "ServiceLocationId": "123456",
  "PollingIntervalSeconds": 30
}
```

| Parametre | Description |
|-----------|-------------|
| `ClientId` / `ClientSecret` | Identifiants de l'API developpeur Smappee |
| `Username` / `Password` | Identifiants de votre compte Smappee |
| `ServiceLocationId` | Trouve dans le tableau de bord Smappee (pas le numero de serie) |
| `PollingIntervalSeconds` | Frequence d'interrogation de l'API cloud Smappee (defaut : 30s) |

> Shelly et Smappee ne se cumulent pas : si les deux sont actives, **Shelly pilote la
> detection** et Smappee ne sert plus qu'a l'affichage informatif.

### Appareil Zendure

```json
"ZendureDevice": {
  "Host": "192.168.x.x",
  "Port": 80,
  "DefaultOutputLimit": 2400,
  "DefaultMinSoc": 100,
  "TimeoutSeconds": 10
}
```

| Parametre | Description |
|-----------|-------------|
| `Host` | Adresse IP de votre hub Zendure SolarFlow |
| `Port` | Port HTTP (generalement `80`) |
| `DefaultOutputLimit` | Limite de sortie de secours en watts lors de la reprise (utilisee si pas d'etat sauvegarde) |
| `DefaultMinSoc` | **Plancher de decharge** de secours lors de la reprise (echelle : valeur/10 = %, donc `100` = 10%). Utilise seulement si l'etat d'avant la pause est inconnu |
| `TimeoutSeconds` | Timeout des requetes HTTP |

> ⚠️ `DefaultMinSoc` est le **plancher de decharge** (`minSoc`), pas le plafond de charge
> (`socSet`). Une valeur proche de 1000 interdirait a la batterie de se decharger : y mettre
> votre cible de charge (par ex. `950` = 95%) rendrait vos batteries inutilisables. Le
> demarrage refuse desormais toute valeur hors de la plage 1-999.

### Detection VE

```json
"EvDetection": {
  "PowerThresholdWatts": 5000,
  "ConfirmationReadings": 3,
  "ReleaseReadings": 24,
  "PollingIntervalSeconds": 5,
  "PauseOnSourceUnreachableMinutes": 3
}
```

| Parametre | Description |
|-----------|-------------|
| `PowerThresholdWatts` | La consommation au-dessus de ce seuil declenche la detection VE |
| `ConfirmationReadings` | Nombre de lectures consecutives au-dessus du seuil pour confirmer la recharge (3 lectures x 5s = 15s) |
| `ReleaseReadings` | Nombre de lectures consecutives en-dessous du seuil pour terminer la detection (24 lectures x 5s = 120s) |
| `PollingIntervalSeconds` | Frequence de lecture de la consommation |
| `PauseOnSourceUnreachableMinutes` | Minutes de lectures impossibles avant une **pause de precaution** (defaut : 3, `0` pour desactiver) |

> **Pause de precaution** : quand le compteur devient illisible (panne reseau, Shelly
> injoignable), le service ne voit plus rien — il ne peut donc pas detecter un branchement.
> Passe ce delai, la batterie est mise en pause par precaution et vous etes prevenu. Elle
> reprend d'elle-meme des que les lectures reviennent et qu'aucune charge VE n'est en cours.

> **Conseil** : Reglez `PowerThresholdWatts` au-dessus de votre consommation domestique typique mais en-dessous de la puissance de votre chargeur VE. Pour un chargeur 7,4 kW, `5000` est un bon point de depart.

### Notifications email

```json
"Notification": {
  "Enabled": true,
  "SmtpHost": "smtp.gmail.com",
  "SmtpPort": 587,
  "UseSsl": true,
  "Username": "vous@gmail.com",
  "Password": "votre-mot-de-passe-app",
  "FromAddress": "evguard@votredomaine.com",
  "ToAddress": "vous@gmail.com",
  "RateLimitMinutes": 15
}
```

Mettez `Enabled: false` pour desactiver les notifications. Le `RateLimitMinutes` empeche le spam de notifications.

### Notifications Telegram (multi-bots)

Depuis la version 1.2.0, la section `Telegram` accepte une liste `Bots` : chaque bot a son
propre nom, son propre jeton et ses propres destinataires. Cela permet par exemple de prevenir
2 personnes via un bot "famille" et 3 autres via un bot "admin" distinct.

Chaque notification est envoyee a **tous** les destinataires de **tous** les bots configures :
la liste sert a couvrir plusieurs groupes, pas a trier les messages par type. Si l'envoi echoue
vers un destinataire, les autres sont notifies quand meme.

```json
"Telegram": {
  "Enabled": true,
  "Bots": [
    {
      "Name": "Famille",
      "BotToken": "111111111:AAbotTokenFamille",
      "ChatIds": [ "111111111", "222222222" ]
    },
    {
      "Name": "Admin",
      "BotToken": "222222222:AAbotTokenAdmin",
      "ChatIds": [ "333333333", "444444444", "555555555" ]
    }
  ],
  "RateLimitMinutes": 15
}
```

Dans cet exemple : le bot "Famille" envoie a 2 destinataires, le bot "Admin" a 3. Si l'envoi
echoue vers un destinataire (jeton invalide, chat bloque, etc.), les autres destinataires et
les autres bots recoivent quand meme leur notification — un echec isole ne bloque jamais les
suivants.

| Parametre | Description |
|-----------|-------------|
| `Bots[].Name` | Nom libre du bot, affiche dans les logs |
| `Bots[].BotToken` | Jeton du bot Telegram (obtenu via [@BotFather](https://t.me/BotFather)) |
| `Bots[].ChatIds` | Liste des identifiants de chat destinataires de ce bot |

> **Installations existantes** : l'ancienne forme mono-bot (`BotToken` + `ChatId` a la racine
> de la section, ce dernier acceptant plusieurs identifiants separes par `;`) reste
> parfaitement supportee. Elle n'est utilisee que si `Bots` est vide ou absent — aucune
> migration n'est necessaire pour continuer a fonctionner tel quel.

### Surveillance

```json
"Monitoring": {
  "HealthCheckIntervalSeconds": 60,
  "LogStatusIntervalSeconds": 300
}
```

| Parametre | Description |
|-----------|-------------|
| `HealthCheckIntervalSeconds` | Frequence de verification de la connectivite des appareils |
| `LogStatusIntervalSeconds` | Frequence d'affichage du resume d'etat dans les logs |

### Langue des messages

```json
"Messages": {
  "Language": "fr"
}
```

| Valeur | Effet |
|--------|-------------|
| `"en"` | Anglais (défaut si la clé est absente) |
| `"fr"` | Français |

Cette clé vit dans la section `Messages` (et non à la racine : à la racine, la variable
d'environnement `LANGUAGE` de Linux la remplacerait sans prévenir). Les formes régionales (`fr-FR`,
`en-GB`) sont acceptées. Une valeur inconnue n'empêche jamais le démarrage : l'anglais s'applique
et un avertissement part au journal.

**Ce qui est traduit** : les notifications (e-mail, Telegram, Pushover), le rapport de fin de
charge, les messages de fuite et de décrochage, les raisons de refus de démarrage.

**Ce qui ne l'est pas** : les **journaux (logs)** restent en anglais quelle que soit la langue —
ce sont eux qu'on lit pour diagnostiquer une panne, et un support ne peut pas travailler sur
autant de langues qu'il y a de clients. La sortie console reste également en anglais.

⚠️ En français, les sujets des e-mails changent (« Batterie Zendure - EN SERVICE » remplace
« Zendure Battery - ON »). Si vous avez créé un filtre dans votre messagerie sur le libellé
anglais, pensez à l'adapter.

### Coût indicatif de la charge

```json
"Energy": {
  "PricePerKwhCents": 28
}
```

| Paramètre | Description |
|-----------|-------------|
| `PricePerKwhCents` | Prix du kWh en **centimes d'euros** (`28` = 0,28 €/kWh). `0` désactive l'affichage du coût |

**La valeur livrée par défaut est `28`** (0,28 €/kWh), qui n'est probablement pas votre tarif :
remplacez-la par le vôtre, ou mettez `0` si vous ne voulez aucun montant.

Le rapport de fin de charge affiche alors **deux montants** : celui de l'énergie mesurée au
compteur, et celui de la part attribuée au véhicule.

⚠️ **Ces montants sont indicatifs, ce n'est pas une facture.** En particulier, si votre
installation produit du solaire, la part attribuée au véhicule est un **maximum** : lorsque la
maison exporte juste avant la charge, la consommation de référence du foyer ne peut pas être
retranchée, et la part du véhicule inclut alors ce que la maison a consommé elle-même. Le rapport
vous le signale explicitement quand le cas se présente.

Les tarifs heures creuses / heures pleines ne sont pas gérés, et la devise est l'euro.

## Utilisation

### Mode interactif

```bash
dotnet run --project src/ZendureEvGuard
```

Au demarrage, l'application :
1. Affiche une banniere
2. Teste la connectivite avec tous les appareils configures
3. Demarre la surveillance

### Controles clavier (mode interactif uniquement)

| Touche | Action |
|--------|--------|
| `P` | Mettre manuellement en pause la sortie batterie. La pause TIENT jusqu'a `R` ou l'arret : aucun automatisme ne la leve, pas meme le retour du compteur. Un rappel est ecrit dans le journal toutes les heures tant qu'elle dure, pour qu'une pause oubliee ne laisse pas la batterie hors service en silence. |
| `R` | Reprendre manuellement la sortie batterie. **Refuse tant qu'une charge VE est confirmee** : la protection remettrait la batterie en pause au cycle suivant, et le demon le dit plutot que d'envoyer un ordre qui ne tiendra pas. Debranchez le vehicule, ou attendez la fin de la charge. |
| `Ctrl+C` (x1) | Avertissement : appuyez a nouveau pour confirmer l'arret |
| `Ctrl+C` (x2) | Arret gracieux (restaure la batterie si en pause) |

### Journaux (logs)

- **Console** : sortie temps reel avec horodatage
- **Fichier** : logs journaliers dans `logs/zendure-evguard-YYYYMMDD.log` (retention 30 jours)

Les fichiers de log incluent les niveaux de severite (`INF`, `WRN`, `ERR`) pour faciliter le filtrage :

```bash
# Linux/WSL
tail -f logs/zendure-evguard-*.log
grep ERR logs/zendure-evguard-*.log
```

### Mises a jour

ZendureEvGuard **ne se met pas a jour tout seul**. Il verifie une fois par heure si une version
plus recente existe, et se contente de vous prevenir. Rien n'est telecharge, rien n'est remplace,
le service n'est jamais redemarre dans votre dos : la protection de vos batteries continue de
tourner sur la version installee tant que vous n'avez pas decide d'en changer.

Deux canaux, deux rythmes differents :

- **Les logs** : un rappel **toutes les heures**, en console et dans le fichier, tant que la
  nouvelle version n'est pas installee. Une ligne passee il y a une heure est une ligne que
  personne ne voit.
- **Les notifications** (e-mail, Telegram, Pushover) : **un seul message par version**. Vous ne
  serez pas relance pour celle-ci. A la version suivante, un nouveau message part.

Le message contient le changelog, le **lien de telechargement direct** et la page de la version.
Pour installer : arretez le service, remplacez les fichiers par ceux de l'archive en **gardant
votre `appsettings.json`**, relancez.

Pour changer ce comportement, dans `appsettings.json` :

| Cle | Effet |
|---|---|
| `Update:CheckEnabled: false` | plus aucune verification — ni log, ni notification |
| `Update:NotifyOnNewVersion: false` | garde le rappel horaire dans les logs, sans aucun message |

## Fonctionnement

```
┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ Shelly 3EM  │────>│  Detecteur       │────>│  Controleur     │
│ ou Smappee  │     │  de recharge VE  │     │  Zendure        │
│ (watts)     │     │  (machine etat)  │     │  (pause/reprise)│
└─────────────┘     └──────────────────┘     └─────────────────┘
```

1. **Lecture** de la consommation toutes les 5 secondes (configurable)
2. **Detection** de la recharge VE par hysteresis (3 confirmations pour demarrer, 24 pour liberer)
3. **Pause** batterie : `minSoc=1000` (100%) + `outputLimit=0`
4. **Reprise** batterie : restauration du `minSoc` et `outputLimit` sauvegardes
5. **Auto-reparation** : si la reprise echoue, nouvel essai automatique a chaque cycle
6. **Arret** : restauration gracieuse de la sortie batterie a la fermeture

### Pourquoi minSoc=1000 ?

Le Zendure SolarFlow en mode Smart CT (Zenki/HEMS) ecrase continuellement `outputLimit` via ses lectures CT clamp. Ecrire `minSoc=1000` (100%) est ce qui empeche la decharge dans ce mode, et les deux ecritures sont necessaires : c'est pour cela que ZendureEvGuard les repete toutes les 5 secondes au lieu de les poser une fois.

**Ce n'est pas un verrou.** Le hub n'en expose aucun. Mesure sur une charge reelle le 21 aout 2026 : malgre la pause appliquee et reappliquee, la batterie a debite jusqu'a 2 324 W dans le vehicule pendant de courts instants. ZendureEvGuard detecte ces fuites, reecrit la pause immediatement, et **vous dit combien elles ont coute** dans le rapport de fin de charge :

```
Attention : la batterie a alimente le foyer pendant 45s malgre la pause
(2 episodes, crete 2324W), pour au moins 29 Wh d'energie stockee.
Le demon l'a detecte et a reapplique la pause a chaque fois.
```

Aucun rapport de ce genre = la pause a tenu du debut a la fin.

## Depannage

| Symptome | Cause | Solution |
|----------|-------|----------|
| `UNREACHABLE` au demarrage | Appareil hors reseau ou mauvaise IP | Verifier les adresses IP dans la config |
| La batterie ne se met pas en pause | Seuil trop eleve | Baisser `PowerThresholdWatts` |
| Fausse detection VE | Seuil trop bas ou electromenager | Augmenter `PowerThresholdWatts`, augmenter `ConfirmationReadings` |
| Batterie bloquee en pause | Echec de la reprise | L'application reessaie automatiquement ; consulter les logs |
| Caracteres `?` dans la console | Probleme d'encodage Unicode | Devrait etre corrige ; mettre a jour vers la derniere version |
| Echec auth Smappee | Token expire | Verifier les identifiants ; le token se rafraichit automatiquement |
