# Raspberry Pi Serveur

# Installation sur un Raspberry Pi

### Prérequis

#### Raspberry Pi OS

Vous devrez installer la dernière version complète du système d'exploitation [Raspberry Pi OS](https://www.raspberrypi.com/software/)   
Utilisez l'application [Raspberry Pi Imager](https://www.raspberrypi.com/software/)

#### Node.js

Node.js est le framework requis pour exécuter MagicMirror.   
L'installation de Node.js est très simple ; suivez simplement le guide officiel ci-dessous :

👉🏻 [nodejs download current](https://nodejs.org/en/download/current "nodejs download current")

```bash
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"

# Download and install Node.js:
nvm install 25

# Verify the Node.js version:
node -v # Should print "v25.2.1".

# Verify npm version:
npm -v # Should print "11.6.2".

```

### Installation manuelle

1. Vérifiez si `Git` est installé sur votre machine en exécutant la commande `git` (l'aide devrait s'afficher), sinon installez-le.  
    ```shell
    sudo apt update
    sudo apt install git -y
    ```
    
    Vérifier
    
    ```
    git --version
    ```
    
    *(Cela devrait vous afficher quelque chose comme `git version 2.30.x` ou plus récent).*
2. <div class="gtx-body">Cloner le dépôt :  
    </div>```
    git clone https://github.com/MagicMirrorOrg/MagicMirror
    ```
3. <div class="gtx-body">Accédez au dépôt :  
    </div>```
    cd MagicMirror
    ```
4. <div class="gtx-body">Installez l'application : ``  
    </div>```
    node --run install-mm
    ```
5. Faites une copie du fichier de configuration d'exemple :  
    ```
    cp config/config.js.sample config/config.js
    ```
6. #### Lancer l'application :  
    
    
    ```
    node --run start
    ```

### Démarrage automatique de votre MagicMirror

**Utilisation de PM2**

---

PM2 est un gestionnaire de processus de production pour les applications Node.js, doté d'un équilibreur de charge intégré. Il permet de maintenir les applications actives en permanence, de les recharger sans interruption de service et de simplifier les tâches d'administration système courantes. Dans cet exemple, nous l'utiliserons pour exécuter un script shell.

#### Install PM2

<div class="gtx-body" id="bkmrk-installez-pm2-%C3%A0-l%27ai">Installez PM2 à l'aide de NPM :  
</div>```
npm install -g pm2
```

#### Démarrage de PM2 au boot

Pour que PM2 puisse fonctionner correctement lors du (re)démarrage de votre système d'exploitation, il doit être lancé au démarrage. Heureusement, PM2 dispose d'une fonction pratique pour cela.

```
pm2 startup
```

<p class="callout info">PM2 va maintenant vous afficher une commande à exécuter.</p>

#### Créer un script de démarrage MagicMirror²

Pour utiliser PM2 avec MagicMirror², il suffit de créer un simple script shell. Il est préférable de placer ce script en dehors du dossier MagicMirror² afin d'éviter tout problème lors d'une éventuelle mise à jour du miroir.

```bash
cd ~
nano mm.sh
```

<div class="gtx-body" id="bkmrk-ajoutez-les-lignes-s">Ajoutez les lignes suivantes :  
</div>```
cd ./MagicMirror
DISPLAY=:0 node --run start
```

Enregistrez et fermez le fichier à l'aide des commandes CTRL+O et CTRL+X. Assurez-vous ensuite que le script shell est exécutable en exécutant la commande suivante :

```
chmod +x mm.sh
```

#### Démarrez votre MagicMirror² avec PM2

<div class="gtx-body" id="bkmrk-il-vous-suffit-de-d%C3%A9">Il vous suffit de démarrer votre miroir avec la commande suivante :  
</div>```
pm2 start mm.sh
```

Votre miroir devrait maintenant démarrer et apparaître sur votre écran après quelques secondes.

#### Activer le redémarrage du script MagicMirror²

Pour garantir le redémarrage de MagicMirror² après un redémarrage, vous devez enregistrer l'état actuel de tous les scripts exécutés via PM2. Pour ce faire, exécutez la commande suivante :

```
pm2 save
```

Et voilà ! Votre MagicMirror² devrait maintenant redémarrer après le démarrage et après toute panne.

### Controlling your MagicMirror² via PM2  


Avec votre MagicMirror fonctionnant via PM2, vous disposez d'outils pratiques :

##### Redémarrage de votre MagicMirror²  


```
pm2 restart mm
```

##### Arrêter votre MagicMirror²

```
pm2 stop mm
```

<div class="gtx-body" id="bkmrk-afficher-les-logs-ma">Afficher les logs MagicMirror²</div>```
pm2 logs mm
```

##### Show the MagicMirror² process information

```
pm2 show mm
```

# Rotation de l’écran à 90°

Configurer un Raspberry Pi pour afficher l’écran en **mode portrait**, tourné de **90° vers la droite**, **sans environnement graphique** (console ou mode kiosk type MagicMirror, Immich kiosk, etc.).

La rotation se fait via le fichier de démarrage `cmdline.txt` avec le pilote KMS.

---

#### ✅ Prérequis

- Raspberry Pi OS récent (avec le pilote vidéo **KMS** activé par défaut).
- Accès au Raspberry Pi en **SSH** ou clavier/écran.
- Un écran branché sur la sortie **HDMI** du Raspberry Pi.

<p class="callout info">Dans l’exemple ci-dessous, on utilise la sortie **HDMI-A-1**, qui est la plus courante.</p>

---

#### Vérifier le nom de la sortie HDMI

Sur le Raspberry Pi, lancer :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-kmsprint-%7C-grep-conn"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`kmsprint | grep Connector`</div></div>Exemple de résultat :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-connector-0-%2833%29-hdm"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`Connector 0 (33) HDMI-A-1 (connected)`</div></div>- Le nom de la sortie HDMI à utiliser est ici : **HDMI-A-1**.
- Si tu obtiens un autre nom (par ex. `HDMI-A-2`), il faudra l’utiliser à la place.

Dans notre cas, on part sur **HDMI-A-1**.

---

#### Éditer le fichier `cmdline.txt`

Sur Raspberry Pi OS **Bookworm** (versions récentes), le fichier se trouve ici :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-sudo-nano-%2Fboot%2Ffirm"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`sudo nano /boot/firmware/cmdline.txt`</div></div>> Sur des versions plus anciennes (Bullseye et avant), le chemin peut être :  
> `/boot/cmdline.txt`

Tu vas voir **une seule ligne très longue**.  
⚠️ **Important : ne surtout pas ajouter de retour à la ligne. Toute la configuration doit rester sur UNE seule ligne.**

Ajoute un paramètre `video=...` contenant la résolution + la rotation.

Par exemple, pour un écran en **1920x1080 à 60 Hz**, tourné de **90° vers la droite** :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-...-quiet-splash-vid"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`... quiet splash video=HDMI-A-1:1920x1080M@60,rotate=90`</div></div>Conseils :

- Garde tous les autres paramètres déjà présents (ne supprime rien).
- Si un `video=...` existe déjà, remplace-le par ta version avec `rotate=90` au bon endroit.
- Ne mets pas d’espace dans `video=HDMI-A-1:1920x1080M@60,rotate=90` (tout doit rester collé après le `=`).

---

#### Sauvegarder et redémarrer

Dans `nano` :

- Sauvegarder : `Ctrl + O`, puis **Entrée**
- Quitter : `Ctrl + X`

Puis redémarrer le Raspberry Pi :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-sudo-reboot"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`sudo reboot`</div></div>Au redémarrage, l’affichage doit être :

- en **portrait**,
- tourné de **90° vers la droite** (côté droit vers le bas).

---

#### Dépannage / ajustements

##### A. L’écran ne s’affiche pas correctement

Tu peux essayer une autre résolution plus standard :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-video%3Dhdmi-a-1%3A1280x"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`video=HDMI-A-1:1280x720M@60,rotate=90`</div></div>ou

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-video%3Dhdmi-a-1%3A1024x"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`video=HDMI-A-1:1024x768M@60,rotate=90`</div></div>---

##### B. Retour à l’orientation normale

Pour revenir en mode paysage classique, il suffit de :

- enlever `,rotate=90`, ou
- supprimer entièrement le paramètre `video=HDMI-A-1:...` ajouté.

Puis redémarrer.

---

#### Résumé

- **Orientation 90° vers la droite** = `rotate=90`
- Fichier à modifier :
    
    
    - `/boot/firmware/cmdline.txt` (Bookworm)
    - ou `/boot/cmdline.txt` (anciennes versions)
- Tout doit tenir sur **une seule ligne**.
- Exemple complet :

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-console%3Dserial0%2C1152"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`console=serial0,115200 console=tty1 root=PARTUUID=xxxxx-xx rootfstype=ext4 fsck.repair=yes rootwait quiet splash video=HDMI-A-1:1920x1080M@60,rotate=90`</div></div>

# Tailscale (Tunnel VPN pour la gestion à distance)

Tailscale est un service VPN qui vous permet d'accéder à vos appareils et applications partout dans le monde, de manière sécurisée et simple. Il établit des connexions point à point chiffrées grâce au protocole open source WireGuard, ce qui signifie que seuls les appareils de votre réseau privé peuvent communiquer entre eux.

Installer Tailscale sur le cadre photo numérique me permet de m'y connecter et de le gérer en toute sécurité.

Vous pouvez l'installer facilement en exécutant la commande suivante :

```
curl -fsSL https://tailscale.com/install.sh | sh
```

Après l'installation, connectez la machine au réseau tailnet.

```
sudo tailscale up
```

Suivre les instructions données pour se connecter au compte maitre.

# Installation et configuration de vdirsyncer

Synchroniser automatiquement un calendrier iCloud (CalDAV) vers un fichier `.ics` local, exploitable par le module **calendar** de MagicMirror.

---

#### 🧩 Installation

✅ Méthode recommandée (via `apt`)

```shell
sudo apt update
sudo apt install vdirsyncer -y
```

> 💡 Cela installe la version stable packagée pour Ubuntu (ex. `0.19.2`).

**Vérification :**

```bash
vdirsyncer --version
```

**Résultat attendu :**

```postgresql
vdirsyncer, version 0.19.2
```

---

#### ⚙️ Emplacement des fichiers

**Fichier de configuration :**

```bash
~/.vdirsyncer/config
```

**Dossier de statut :**

Il sera automatiquement créé à l’emplacement :

```bash
~/.vdirsyncer/status/
```

**Fichiers `.ics` exportés :**

Répertoire cible à créer pour MagicMirror :

```shell
~/MagicMirror/modules/calendars/
```

---

#### 🧾 3. Configuration `~/.vdirsyncer/config` 

Voici une version corrigée et adaptée pour Ubuntu :

```ini
# vdirsyncer configuration for MagicMirror

[general]
status_path = "~/.vdirsyncer/status/"

# --- CALDAV Sync 1 ---
[pair iCloud_to_MagicMirror1]
a = Mirror1
b = iCloud1
collections = ["home"]
metadata = ["displayname", "color"]

[storage Mirror1]
type = singlefile
path = /home/thymon/MagicMirror/modules/calendars/%s.ics

[storage iCloud1]
type = caldav
url = https://caldav.icloud.com/
username = sebort@icloud.com
password = abcd-efgh-ijkl-mnop   # Mot de passe d'application iCloud
read_only = true
item_types = ["VEVENT"]

# --- CALDAV Sync 2 ---
[pair iCloud_to_MagicMirror2]
a = Mirror2
b = iCloud2
collections = ["145c2a93-c79a-4347-8045-86fca9c3d201"]
metadata = ["displayname", "color"]

[storage Mirror2]
type = singlefile
path = /home/thymon/MagicMirror/modules/calendars/%s.ics

[storage iCloud2]
type = caldav
url = https://caldav.icloud.com/
username = sebou@icloud.com
password = zzzz-yyyy-xxxx-wwww   # Mot de passe d'application iCloud
read_only = true
item_types = ["VEVENT"]

```

---

#### 🔐 Génération du mot de passe d’application iCloud

1. Se connecter à [https://appleid.apple.com<span class="ms-0.5 inline-block align-middle leading-none"></span>](https://appleid.apple.com)
2. Section **Sécurité** → *Mots de passe spécifiques d’application* → *Générer un mot de passe…*
3. Donner un nom (ex. *vdirsyncer*)
4. Copier le mot de passe généré (ex. `abcd-efgh-ijkl-mnop`)
5. Le coller dans la configuration (`password = …`)

> ⚠️ iCloud **refuse les mots de passe standards** pour CalDAV — ce mot de passe spécifique est obligatoire.

---

#### 🧪 Test de découverte

Découverte des calendriers disponibles :

```bash
vdirsyncer discover
```

Résultat attendu :

```bash
Discovering collections for pair iCloud_to_MagicMirror1
Found 1 collection
/home/thymon/MagicMirror/modules/calendars/home.ics
```

> Si tu vois une erreur `401 Unauthorized`, vérifie ton mot de passe d’application iCloud.

---

#### 🔁 Synchronisation manuelle

Pour lancer une synchronisation :

```bash
vdirsyncer sync
```

Le ou les fichiers `.ics` seront créés dans :

```bash
~/MagicMirror/modules/calendars/
```

---

##### ⏱️ Synchronisation automatique avec systemd.timer

Créer le dossier systemd utilisateur :

```bash
mkdir -p ~/.config/systemd/user
```

Télécharger les unités :

```bash
curl -o ~/.config/systemd/user/vdirsyncer.service https://raw.githubusercontent.com/pimutils/vdirsyncer/master/contrib/vdirsyncer.service
curl -o ~/.config/systemd/user/vdirsyncer.timer https://raw.githubusercontent.com/pimutils/vdirsyncer/master/contrib/vdirsyncer.timer
```

Modifier la fréquence (facultatif) :

```bash
nano ~/.config/systemd/user/vdirsyncer.timer
```

Par défaut :

```ini
[Timer]
OnBootSec=5m
OnUnitActiveSec=15m
```

➡️ Modifier par exemple pour une synchro toutes les heures :

```ini
OnUnitActiveSec=1h
```

Activer et démarrer :

```bash
systemctl --user daemon-reload
systemctl --user enable --now vdirsyncer.timer
```

Vérifier :

```bash
systemctl --user status vdirsyncer.timer
```

---

#### 📋 Points importants

<div class="_tableContainer_1rjym_1" id="bkmrk-%C3%89l%C3%A9ment-ancien-tuto-"><div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1"><table class="w-fit min-w-(--thread-content-width)"><thead><tr><th>Élément</th><th>Ancien tuto Raspberry</th><th>Adaptation Ubuntu</th></tr></thead><tbody><tr><td>Utilisateur</td><td>`pi`</td><td>`thymon`</td></tr><tr><td>Chemins</td><td>`/home/pi/...`</td><td>`/home/thymon/...`</td></tr><tr><td>Guillemets dans la config</td><td>`"Mirror"`</td><td>`Mirror` (sans guillemets)</td></tr><tr><td>Dates Python (`datetime.now()`)</td><td>Autorisées (ancien vdirsyncer)</td><td>❌ Non supportées</td></tr><tr><td>systemd unit</td><td>`/etc/systemd/user`</td><td>`~/.config/systemd/user`</td></tr><tr><td>Commande</td><td>`sudo vdirsyncer`</td><td>❌ Toujours sans `sudo`</td></tr><tr><td>Mot de passe iCloud</td><td>Standard</td><td>🔐 Mot de passe d’application</td></tr></tbody></table>

</div></div>

# MMM-AirQuality – Bug fetch failed / ETIMEDOUT

#### Symptômes

- MagicMirror démarre mais log PM2 affiche :
    
    
    - `TypeError: fetch failed`
    - `AggregateError [ETIMEDOUT]` dans `MMM-AirQuality/helper.js`
- `curl https://api.waqi.info/...` fonctionne

---

#### Objectif

👉 Contourner `fetch` de Node en remplaçant l’appel API par `https.request`.

---

#### Étapes rapides

1. **Aller dans le dossier du module**
    
    <div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
    </div></div><div class="overflow-y-auto p-4" dir="ltr">  
    </div></div>```shell
    cd ~/MagicMirror/modules/MMM-AirQuality
    ```
2. **Sauvegarder le helper**
    
    <div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
    </div></div><div class="overflow-y-auto p-4" dir="ltr">  
    </div></div>```
    cp helper.js helper.js.bak
    ```
3. **Éditer `helper.js`**
    
    <div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
    </div></div><div class="overflow-y-auto p-4" dir="ltr">  
    </div></div>```
    nano helper.js
    ```
4. **Remplacer tout le contenu**
    
    ```javascript
    /* MagicMirror²
     * Module: MMM-AirQuality
     *
     * By Christopher Fenner https://github.com/CFenner
     * Patched to use https.request instead of fetch (ETIMEDOUT issue).
     * MIT Licensed.
     */
    
    const https = require("https");
    const { URL } = require("url");
    
    module.exports = {
      notifications: {
        DATA: "AIR_QUALITY_DATA",
        DATA_RESPONSE: "AIR_QUALITY_DATA_RESPONSE",
      },
    
      start: function () {
        console.log("AirQuality helper started ...");
      },
    
      loadData: function (payload) {
        const self = this;
        const urlString = `https://${payload.config.apiBase}${payload.config.dataEndpoint}${payload.config.location}/?token=${payload.config.token}`;
    
        console.log(`AirQuality-Fetcher (https): ${urlString}`);
    
        let url;
        try {
          url = new URL(urlString);
        } catch (e) {
          console.error("AirQuality-Fetcher: invalid URL", e);
          self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
            payloadReturn: null,
            status: "ERROR",
            identifier: payload.identifier,
          });
          return;
        }
    
        const options = {
          hostname: url.hostname,
          port: 443,
          path: url.pathname + url.search,
          method: "GET",
          timeout: 10000, // 10s
        };
    
        const req = https.request(options, (res) => {
          let data = "";
    
          res.on("data", (chunk) => {
            data += chunk;
          });
    
          res.on("end", () => {
            try {
              const json = JSON.parse(data);
              self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
                payloadReturn: json,
                status: "OK",
                identifier: payload.identifier,
              });
            } catch (err) {
              console.error("AirQuality-Fetcher: JSON parse error", err);
              self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
                payloadReturn: null,
                status: "ERROR",
                identifier: payload.identifier,
              });
            }
          });
        });
    
        req.on("error", (err) => {
          console.error("AirQuality-Fetcher: https error", err);
          self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
            payloadReturn: null,
            status: "ERROR",
            identifier: payload.identifier,
          });
        });
    
        req.on("timeout", () => {
          console.error("AirQuality-Fetcher: request timeout");
          req.destroy();
          self.sendSocketNotification(self.notifications.DATA_RESPONSE, {
            payloadReturn: null,
            status: "TIMEOUT",
            identifier: payload.identifier,
          });
        });
    
        req.end();
      },
    
      socketNotificationReceived: function (notification, payload) {
        switch (notification) {
          case this.notifications.DATA:
            console.log(
              `AirQuality-Fetcher: Loading data of ${payload.config.location} for module ${payload.identifier}`
            );
            this.loadData(payload);
            break;
        }
      },
    };
    
    ```
5. **Redémarrer MagicMirror**
    
    <div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="sticky top-9"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
    </div></div><div class="overflow-y-auto p-4" dir="ltr"></div></div>```
    pm2 restart mm
    ```
6. **Vérifier**
    
    
    - Plus d’erreur `fetch failed / ETIMEDOUT`
    - MMM-AirQuality affiche à nouveau les données

---

#### À noter

- En cas de mise à jour du module, le patch peut être écrasé → **réappliquer `helper.js` patché**.