<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr"><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" hreflang="fr" /><updated>2026-04-09T22:05:36+00:00</updated><id>/feed.xml</id><title type="html">LPRP.fr</title><subtitle>La Page de Rémi Peyronnet</subtitle><entry xml:lang="en"><title type="html">Compare some free web analytics and migrate from Piwik Pro to Umami</title><link href="/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami/" rel="alternate" type="text/html" title="Compare some free web analytics and migrate from Piwik Pro to Umami" /><published>2026-03-15T18:00:00+00:00</published><updated>2026-03-15T18:00:00+00:00</updated><id>/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami</id><content type="html" xml:base="/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami/"><![CDATA[<p>For several years this site used <a href="https://piwik.pro/">Piwik Pro</a> for web analytics.  The service had a convenient free plan, privacy‑friendly defaults, and a simple integration. Perfect for my low traffic website. However, the free plan has recently been discontinued, and the remaining offers are not suitable for a small personal &amp; free blog like my website. So this forced a migration to another analytics solution.</p>

<h1 id="compare-solutions">Compare solutions</h1>
<p>My needs are very basic:</p>
<ul>
  <li>free for a low traffic website</li>
  <li>privacy‑friendly analytics (GDPR-compliant)</li>
  <li>lightweight script</li>
  <li>easy integration with my Jekyll static website</li>
  <li>an API allowing to access data and reuse metrics in other tools (for instance in an Home Assistant dashboard)</li>
</ul>

<p>Several alternatives were evaluated during a few weeks to compare some results:</p>
<ul>
  <li>Umami has a <a href="https://umami.is/">cloud-hosted free plan for low traffic</a> and also an <a href="https://github.com/umami-software/umami">open source version that you can self-host for free</a></li>
  <li><a href="https://www.goatcounter.com/">GoatCounter</a> is also free for low traffic, and the origin of this tool is exactly what I am looking for</li>
  <li>As I am using Cloudflare as CDN, I have also tested the free <a href="https://www.cloudflare.com/web-analytics/">Cloudflare Web Analytics</a> ; there is also a GraphQL API, but I discovered a bit late that it was exluded from the free plan</li>
</ul>

<h2 id="main-collection-techniques-and-major-issues">Main collection techniques and major issues</h2>
<p>Web analytics has become quite complex:</p>

<ol>
  <li><strong>Website logs</strong>: some years ago, the easiest way to get data was from access logs from the web server ; you were certain at that time to get all the requests, but this method have now several problems: Content Delivery Network will cache and serve contents without soliciting your own web server, so you will miss a good part of your traffic ; also, I am using GitHub Pages, and GitHub does not make available the server logs, also you won’t be able to differentiate users/sessions. This method is fairly limited and no more usable</li>
  <li><strong>JavaScript snippet</strong> to collect data: the most common one, you include a small JavaScript snippet that will run on your page, in the browser of the user, and it will call an API to record usages ; this can record a lot of information, including personal information ; the choice of the solution you use is very important to record only the information you need, in my case without personal information, and without the need of cookies ; the drawback of this method is that it depends on the user browser settings regarding JavaScript, or ad-blockers browser extension that may filter those snippets out</li>
  <li><strong>pixel images</strong> are 1-pixel transparent image, that will trigger the record of the usage ; you can pass some basic information with the query parameters of the image ; as this is included as an image, it is less likely to be deactivated by the browser or extensions, but you will usually get less information</li>
</ol>

<p>Also, there are now a lot of crawlers and search engines to be known to filter out crawlers from usage, and find the relevant source of traffic.</p>

<p>I have activated both JavaScript snippet and pixel image for Umami and Goatcounter to be able to compare the difference between all the methods.</p>

<h2 id="test-dashboard-comparison">Test Dashboard Comparison</h2>

<p>Due to the short notice of Piwik Pro end of free plan, I have only tested the different methods on a week, but still we can see some interesting elements.
The following dashboards show the collected metrics during the evaluation period.</p>

<p><img src="/files/2026/Web%20stats%20-%20Comparaison%20lprp.jpg" alt="Analytics comparison" /></p>

<p>From those dashboards we can see:</p>
<ul>
  <li>we have large differences between all the results!</li>
  <li>Goatcounter is clearly having some troubles recording the usages, quite consistently for JavaScript snippet or pixel image ; it seems that the totals are correct, but maybe some collected usages were treated lately at the time of the test ; remember this is a non-professional solution and is totally free, so anyway, it is very good for what you pay</li>
  <li>Umami Cloud and Cloudflare display quite comparable information, with the same curve as Piwik, but counts more users than Piwik</li>
  <li>Umami Self-hosted is counting more visits, and with a curve looking a bit different ; this can be explained in different ways: the script and URL is unknown from adblockers and do not get blocked, or the self-hosted version is less accurate in crawlers filtering</li>
  <li>anyway, do not expect exact values from any web analytics engine, but use it as rough estimates and trends</li>
</ul>

<h2 id="final-choice-umami-cloud">Final Choice: Umami Cloud</h2>

<p>After testing the different solutions, I decided to go mainly with Umami Cloud. Main reasons are simple setup, privacy-friendly, lightweight, clean dashboard with everything that I need,  good API, no infrastructure to maintain, and all that for free for my low traffic website. That was also the solution with the most consistent numbers in the test period. I think I will keep the other solution for some time to see webstats differences in a longer period, and also in the case Umami Cloud have in the future the same bad idea than Piwik of removing the free plan.</p>

<h1 id="the-migration-part">The migration part</h1>

<h2 id="jekyll-data-collection">Jekyll data collection</h2>

<p>I included in my Jekyll build the required files and lines for data collection</p>
<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s1">'umami'</span><span class="w"> </span><span class="p">%}</span>
        &lt;!-- Umami cloud or self-hosted https://github.com/umami-software/umami --&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">token</span><span class="w"> </span><span class="p">%}</span>
            &lt;script defer src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">script</span><span class="w"> </span><span class="p">|</span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="s1">'https://cloud.umami.is/script.js'</span><span class="w"> </span><span class="p">}}</span>" 
                    data-website-id="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">token</span><span class="w"> </span><span class="p">}}</span>"
                    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">host</span><span class="w"> </span><span class="p">%}</span>data-host-url="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">host</span><span class="w"> </span><span class="p">}}</span>"<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
                    data-tag="page_<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">lang</span><span class="w"> </span><span class="p">}}</span>"&gt;&lt;/script&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">noscript-pixel</span><span class="w"> </span><span class="p">%}</span>
            &lt;noscript&gt;
            &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">noscript-pixel</span><span class="w"> </span><span class="p">}}</span>" loading="lazy" style="position: absolute; top: 0; right: 0; width: 4px;"&gt;
            &lt;/noscript&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">pixel</span><span class="w"> </span><span class="p">%}</span>
            &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">pixel</span><span class="w"> </span><span class="p">}}</span>" loading="lazy" style="position: absolute; top: 0; right: 0; width: 4px;"&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
</code></pre></div></div>
<p><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/privacy-consent.html">Full script for all services on my GitHub</a></p>

<h2 id="jekyll-use-of-umami-results">Jekyll use of Umami results</h2>

<p>I use top pages on the front page to display most viewed pages in the corresponding order, and in the blog view to display top ten. To make the data available, here is what I am using:</p>
<ul>
  <li>I have created a share URL for my Umami ; this enables to share a readonly view of all the data ; this URL gives a readonly UI on your data, and also enables to get a read-only token and use the allowed API with that token</li>
  <li>This URL can be shared as is, as it is readonly, but I added another layer to avoid making available the full data ; a script is run weekly as action workflow on my github repository, run a script that authenticate to Umami share URL (given as github “secret”), and save the result in a gist.</li>
  <li>The URL of the corresponding GIST in included in my jekyll website, and a javascript code fetchs the gist to display the posts or widget, the corresponding code is:
    <ul>
      <li><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/top-article-script.html">top-article-script.html</a></li>
      <li><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/top-article-widget.html">top-article-widget.html</a></li>
    </ul>
  </li>
</ul>

<h2 id="a-dashboard-in-home-assistant-with-umami-web-stats">A dashboard in Home Assistant with Umami web stats</h2>

<p>I also have created a dashboard in my Home Assistant to integrate directly Umami visitors graphs and top pages.
I have a dedicated GitHub repository <a href="https://github.com/rpeyron/homeassistant_umami">rpeyron/homeassistant_umami</a> with all the elements and a usage guide.</p>

<p><img src="/files/2026/ha_umami_cards.png" alt="" class="img-center mw80" /></p>

<p>This ends the last feature I wanted with my new Umami web analytics.</p>

<h1 id="annexes">Annexes</h1>
<h2 id="cloudflare-graphql-explorer-for-web-analytics">Cloudflare GraphQL Explorer for Web Analytics</h2>

<p>One last thing about Cloudflare, the GraphQL API is not available with the free plan, but you still can use the Cloudflare GraphQL Explorer to query your data (and see what you could get with the paid plan). Anyway, this is a good solution to be able to <strong>manually query</strong> your data.</p>

<p>You first have to login to the {Cloudflare GraphQL Explorer](https://graphql.cloudflare.com/explorer)</p>

<p>You will need to configure the identifiers you will use: <code class="language-shell highlighter-rouge"><span class="o">{</span><span class="s2">"accountTag"</span>:<span class="s2">"&lt;your account id&gt;"</span>,<span class="s2">"siteTag"</span>:<span class="s2">"&lt;your site id&gt;"</span>,<span class="s2">"dateFrom"</span>:<span class="s2">"2026-02-01T00:00:00Z"</span><span class="o">}</span></code></p>

<p>And then use the following GraphQL to get visitors number:</p>
<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="p">(</span><span class="nv">$accountTag</span><span class="p">:</span><span class="w"> </span><span class="n">string</span><span class="p">!,</span><span class="w"> </span><span class="nv">$siteTag</span><span class="p">:</span><span class="w"> </span><span class="n">string</span><span class="p">!,</span><span class="w"> </span><span class="nv">$dateFrom</span><span class="p">:</span><span class="w"> </span><span class="n">Time</span><span class="p">!)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">viewer</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">accounts</span><span class="p">(</span><span class="n">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="n">accountTag</span><span class="p">:</span><span class="w"> </span><span class="nv">$accountTag</span><span class="p">})</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">rumPageloadEventsAdaptiveGroups</span><span class="p">(</span><span class="w">
              </span><span class="n">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">datetime_geq</span><span class="p">:</span><span class="w"> </span><span class="nv">$dateFrom</span><span class="w">
                </span><span class="n">siteTag</span><span class="p">:</span><span class="w"> </span><span class="nv">$siteTag</span><span class="w">
              </span><span class="p">}</span><span class="w">
              </span><span class="n">limit</span><span class="p">:</span><span class="w"> </span><span class="mi">31</span><span class="w">
              </span><span class="n">orderBy</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="n">date_ASC</span><span class="p">]</span><span class="w">
            </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="n">dimensions</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">date</span><span class="w">
              </span><span class="p">}</span><span class="w">
              </span><span class="n">sum</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">visits</span><span class="w">
              </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    
</span></code></pre></div></div>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Web" /><category term="Website" /><category term="Jekyll" /><category term="Umami" /><category term="Piwik" /><category term="Goatcounter" /><category term="Cloudflare" /><summary type="html"><![CDATA[For several years this site used Piwik Pro for web analytics. The service had a convenient free plan, privacy‑friendly defaults, and a simple integration. Perfect for my low traffic website. However, the free plan has recently been discontinued, and the remaining offers are not suitable for a small personal &amp; free blog like my website. So this forced a migration to another analytics solution.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2026/vignette-piwik-vers-umami.jpg" /><media:content medium="image" url="/files/2026/vignette-piwik-vers-umami.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Remplacer un verrou MPM cassé en 3D avec Fusion360</title><link href="/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360/" rel="alternate" type="text/html" title="Remplacer un verrou MPM cassé en 3D avec Fusion360" /><published>2026-02-07T15:30:00+00:00</published><updated>2026-02-07T15:30:00+00:00</updated><id>/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360</id><content type="html" xml:base="/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360/"><![CDATA[<p>Après de nombreuses années de service, la partie fixe du verrou plastique d’une de mes persiennes s’est cassé en deux, sans doute suite à la fatigue du plastique soumis à de fortes températures lorsqu’il est exposé au soleil, et de fortes contraintes mécaniques lors de la fermeture. Suite à quelques recherches, il semblerait que ce soit un verrou assez courant de modèle “MPM”.</p>

<p><img src="/files/2026/MPM%20_%20Casse%20et%20imprime.jpg" alt="" class="img-center mw60" /></p>

<h1 id="tentative-à-la-colle">Tentative à la colle</h1>
<p><img src="/files/2026/MPM%20_%20Tentative%20bicarbonate.jpg" alt="" class="img-right mw30" />
Ma première tentative a été de recoller la pièce avec de la colle cyanoacrylate. J’ai tenté en même temps de consolider avec du bicarbonate collé, une technique dont on peut voir plusieurs vidéos impressionnantes.</p>

<p>C’est un échec complet à la première tentative de fermeture, la contrainte mécanique casse toute la colle instantanément :</p>

<p><img src="/files/2026/MPM%20_%20Echec%20bicarbonate.jpg" alt="" class="img-left mw40" /></p>

<h1 class="clear-float" id="modélisation-3d-avec-inkscape-et-fusion360">Modélisation 3D avec Inkscape et Fusion360</h1>
<h2 id="scan">Scan</h2>
<p>La première étape est de scanner le modèle à reproduire. La meilleure solution pour la 3D est le scan 3D, soit avec un appareil dédié (mais assez cher), soit avec de la photogrammétrie (à recommander si vous avez un iPhone avec capteur à détection de profondeur, il y a des applications dédiées). Cependant pour cet objet c’est tout à fait surdimensionné, et un simple scan 2D suffit amplement.</p>

<p>Je l’ai fait assez simplement avec mon scanner, en positionnant les pièces directement sur la vitre et en ajoutant des règles pour pouvoir garder des repères de taille qui seront très utiles lors de l’étape de calibration. J’ai scanné une pièce cassée, et une autre en bon état d’une autre persienne.</p>

<p><img src="/files/2026/MPM%20_%20SCAN1869.JPG" alt="" class="img-center mw60" /></p>

<p>C’est également possible sans scanner, en prenant en photo les pièces ; il faut prévoir de traiter l’image par la suite pour supprimer la déformation, par exemple avec Darktable et ses fonctionnalités de <a href="https://docs.darktable.org/usermanual/4.6/fr/module-reference/processing-modules/lens-correction/">correction de lentille</a>, ou de <a href="https://docs.darktable.org/usermanual/4.6/fr/module-reference/processing-modules/rotate-perspective/">correction de perspective</a>. Une alternative pour minimiser la déformation sans post-traitement consiste à prendre les objets assez loin et à la verticale, puis de zoomer ensuite sur l’image.</p>

<h2 id="vectorisation">Vectorisation</h2>
<p>Cette partie est facultative, vous pouvez importer l’image directement dans Fusion360. Mais j’ai voulu tester la vectorisation dans Inkscape et la possibilité d’utiliser le profil correspondant dans Fusion360. Ce n’est finalement pas ce que j’ai utilisé.</p>

<p>Cette photo montre les différentes étapes, de gauche à droite, en vectorisant via détection de contours, puis nettoyant les imperfections et en simplifiant un maximum sans affecter la forme globale :</p>

<p><img src="/files/2026/MPM%20_%20Vectorisation%20191505.png" alt="" class="img-center mw80" /></p>

<h2 id="modélisation-sous-fusion360">Modélisation sous Fusion360</h2>

<p>Dans Fusion360, on peut ensuite insérer l’image:</p>

<p><img src="/files/2026/MPM%20-%20Fusion360%20_%20Inserer%20SVG.png" alt="" class="img-center mw80" /></p>

<p>Puis la calibrer en utilisant le clic droit sur l’image, puis option Calibrer ; il suffit ensuite de tracer une cote et la dimension associée. Parfait pour utiliser notre règle repère.</p>

<p><img src="/files/2026/MPM%20-%20Fusion360%20_%20Calibration%20_%202025-12-27%20180135.png" alt="" class="img-center mw80" /></p>

<p>J’ai ensuite créé une esquisse au-dessus de l’image, puis créé les volumes 3D à partie de l’esquisse.</p>

<p><img src="/files/2026/MPM%20Fusion360%20_%20Esquisse%20_%202026-02-07%20195158.jpg" alt="" class="img-center mw80" /></p>

<p><img src="/files/2026/MPM%20Fusion360%20_%203D%20_%202026-02-07%20195158.jpg" alt="" class="img-center mw80" /></p>

<h2 id="impression">Impression</h2>

<p>Enfin la dernière étape est l’impression ; j’ai utilisé les réglages suivants :</p>
<ul>
  <li>utilisation d’un filament PETG pour plus de résistance mécanique et thermique (et moins cassant que le PLA), à 240°C et 80°C pour le plateau</li>
  <li>un remplissage de 50% en Cubic  (un remplissage plus important ne donne pas plus de résistance mécanique)</li>
  <li>3 épaisseurs de murs, et 4 couches en haut et en bas</li>
  <li>et bien sûr des supports</li>
</ul>

<p><img src="/files/2026/3dprint_verroumpm.gif" alt="" class="img-center" /></p>

<p>Le résultat est plutôt OK, et semble résister correctement aux tests que j’ai faits à confirmer dans le temps :</p>

<p><img src="/files/2026/MPM%20_%20Resultat%20ferme.jpg" alt="" class="img-center mw60" /></p>

<p>J’avais oublié à ce stade les coins cassés, j’ai corrigé sur le dernier modèle. Les modèles sont disponibles sous <a href="https://www.thingiverse.com/thing:7290146">Thingiverse</a> ou <a href="https://www.printables.com/model/1589582-verrou-mpm-pour-persiennes">Printables</a>.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="3D" /><category term="Verrou" /><category term="3D" /><category term="Fusion 360" /><category term="MPM" /><category term="Printables" /><category term="Thingiverse" /><summary type="html"><![CDATA[Après de nombreuses années de service, la partie fixe du verrou plastique d’une de mes persiennes s’est cassé en deux, sans doute suite à la fatigue du plastique soumis à de fortes températures lorsqu’il est exposé au soleil, et de fortes contraintes mécaniques lors de la fermeture. Suite à quelques recherches, il semblerait que ce soit un verrou assez courant de modèle “MPM”.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2026/vignette_verrou_mpm.jpg" /><media:content medium="image" url="/files/2026/vignette_verrou_mpm.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Faux Hubs USB 3.0 chinois</title><link href="/2026/01/faux-hubs-usb-30-chinois/" rel="alternate" type="text/html" title="Faux Hubs USB 3.0 chinois" /><published>2026-01-31T18:11:33+00:00</published><updated>2026-01-31T18:11:33+00:00</updated><id>/2026/01/faux-hubs-usb-30-chinois</id><content type="html" xml:base="/2026/01/faux-hubs-usb-30-chinois/"><![CDATA[<p>Par curiosité j’ai ouvert un hub USB 3.0 qui a récemment cramé suite au branchement d’un dongle Wifi, et je n’ai pas été déçu, ce qui est vendu comme un hub à quatre ports USB 3.0 est en réalité un hub USB avec 4 ports USB 2.0, dont un qui peut fonctionner en USB 3.0. Tout dans son apparence laisse à penser que c’est bien un hub USB 3.0, et pourtant si on lit attentivement l’annonce, quelques incohérences permettent d’identifier cette “subtilité”. Difficile alors de parler complètement d’arnaque, mais les formulations sont clairement a minima trompeuses.</p>

<h1 id="exemple-dune-annonce-aliexpress">Exemple d’une annonce AliExpress</h1>
<p>Il existe énormément d’annonces qui présente cette “particularité”, prenons en exemple <a href="https://fr.aliexpress.com/item/1005006064840808.html">cette annonce AliExpress</a> :</p>

<p>La présentation rapide parle d’un “Hub USB 3.0 à 4 ports haute vitesse” :</p>

<p><img src="/files/2026/AliExpress_Example_Hub_USB3_2026-02-07%20163819.png" alt="" class="img-center mw80" /></p>

<p>On voit bien une mention à la fin du titre mentionnant “4 ports USB 3.0, 2.0”, mais à ce stade on peut comprendre que le titre veuille dire que les ports sont également compatibles USB 2.0</p>

<p>La présentation détaillée donne des indications plus précises, et contradictoires !</p>

<p><img src="/files/2026/AliExpress_Example_Hub_USB3_Description_2026-02-07%20163625.png" alt="" class="img-center mw80" /></p>

<p>Une première phrase dans la description (mis en évidence par le premier rectangle en rouge) mentionne sans équivoque possible 4 ports USB 3.0. Cette partie est fausse.
Mais un deuxième passage dans la description (mis en évidence par le premier rectangle en vert) donne quant à lui l’information exacte, à savoir un seul port en USB3.0 et les autres en USB2.0.</p>

<h1 id="le-piège-des-couleurs-des-ports">Le “piège” des couleurs des ports</h1>

<p>Je me suis rendu compte à cette occasion que je faisais un peu trop confiance à la couleur des ports USB. En effet, j’avais en tête “connecteur bleu = USB 3.0”. Certain sites, comme <a href="https://www.corsair.com/fr/fr/explorer/diy-builder/storage/usb-port-colors-explained/">celui de Corsair</a>, mentionnent d’ailleurs toute une palette de couleurs de ports :</p>

<p><img src="/files/2026/Corsair_Couleurs_USB_2026-02-07%20170401.png" alt="" class="img-center mw80" /></p>

<p>Dans notre exemple, que ce soit les 4 connecteurs ou le câble, tout est bleu, impossible de se douteur que les 4 ports ne sont pas identiques :</p>

<p><img src="/files/2026/IMG_20260207_162256455_HDR.jpg" alt="" /></p>

<p>Cependant, j’ai cherché dans la norme USB, et je n’ai trouvé nulle part de norme portant sur la couleur des ports USB. Je ne sais pas si je n’ai pas bien cherché ou si ce n’est effectivement pas normé. Il est probable que ce ne soit qu’une convention, très répandue, mais que le fabricant ne soit pas réellement “non conforme” s’il ne suit pas la convention…</p>

<h1 id="la-surprise-au-démontage">La surprise au démontage</h1>

<p>Le démontage du Hub est très instructif pour comprendre comment cela fonctionne :</p>

<p><img src="/files/2026/IMG_20260125_212902786_HDR.jpg" alt="" class="img-center mw80" /></p>

<p>On voit effectivement assez clairement d’après le câblage que les 3 ports USB de gauches n’ont que 4 fils et plots (rectangle blanc), et ne peuvent donc pas physiquement aller au-delà de la compatibilité USB 2.0 (2 fils d’alimentation, et 2 fils pour les données).</p>

<p>Le premier connecteur quant à lui, comporte le bon nombre de fils, avec 2 fils provenant de la puce qui doit être un hub USB 2.0, et les 5 fils USB 3.0 relié en directe depuis le câble (rectangle bleu).</p>

<p>Je n’ai pas pu vérifier car le hub n’étant plus fonctionnel, les résultats ne seraient pas exacts, et je n’ai pas envie de cramer un port USB, mais il est probable que le firmware de la puce hub USB 2.0 soit modifié pour annoncer le premier port en USB 3.0.</p>

<h1 id="les-hubs-à-7-ou-10-ports">Les Hubs à 7 ou 10 ports</h1>

<p>La norme USB permet des hubs de 4 ports. Lorsqu’il y a plus que 4 ports, c’est qu’il y a en réalité plusieurs hubs en cascade :</p>
<ul>
  <li>7 ports : un premier hub 4 ports, dont un des ports est utilisé par un autre hub, ce qui donne 7 ports disponibles ( 4 - 1 pour le premier hub, et 4 pour le deuxième)</li>
  <li>10 ports : un premier hub 4 ports, dont deux des ports sont utilisés par deux autres hubs (4 - 2 + 2 x 4)</li>
</ul>

<p>J’ai ainsi pu constater sur un Hub USB 3.0 à 7 ports, qu’en réalisé, seuls 3 ports supportent réellement l’USB 3.0, et les 4 ports restants sont seulement en USB 2.0. Comme il fonctionne encore, je ne l’ai pas démonté, mais il est certainement constitué d’un premier Hub 3.0, dont l’un des ports est ensuite utilisé par un second hub en cascade uniquement en 2.0.  Encore une fois, de l’extérieur rien de ne permet de distinguer les ports. C’est un peu dommage, ce format 3 USB 3.0 + 4 USB 2.0 est plutôt intéressante, on a rarement besoin de plus en USB 3.0, et si c’était le cas il y aurait probablement d’autres limitations de bande passante ou d’alimentation, mais c’est dommage qu’aucune indication visuelle ne permette de comprendre la différence entre les ports, pour brancher les bons périphériques sur les bons ports.</p>

<p><img src="/files/2026/AliExpress_Exemple_Hub_7ports_2026-02-07%20175813.png" alt="" class="img-center mw80" /></p>

<h1 id="conclusion">Conclusion</h1>
<p>En conclusion, méfiez-vous des hubs marqués USB 3.0 de marque inconnue (qu’ils ne soient pas chers ou non…) et ne faites pas une confiance aveugle à la couleur des connecteurs…</p>

<p>Dans la jungle des câbles USB, faîtes également très attention aux spécifications des câbles Type C, qui peuvent être très différentes sur la compatibilité avec le transport de vidéo, la bande passante disponible, ou encore la puissance maximum supportée.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Avis Conso" /><category term="USB" /><category term="AliExpress" /><category term="Elec" /><summary type="html"><![CDATA[Par curiosité j’ai ouvert un hub USB 3.0 qui a récemment cramé suite au branchement d’un dongle Wifi, et je n’ai pas été déçu, ce qui est vendu comme un hub à quatre ports USB 3.0 est en réalité un hub USB avec 4 ports USB 2.0, dont un qui peut fonctionner en USB 3.0. Tout dans son apparence laisse à penser que c’est bien un hub USB 3.0, et pourtant si on lit attentivement l’annonce, quelques incohérences permettent d’identifier cette “subtilité”. Difficile alors de parler complètement d’arnaque, mais les formulations sont clairement a minima trompeuses.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2026/vignette_faux_usb3.jpg" /><media:content medium="image" url="/files/2026/vignette_faux_usb3.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Accès distant VNC avec Proxmox/QEMU</title><link href="/2025/09/acces-distant-vnc-avec-proxmox-qemu/" rel="alternate" type="text/html" title="Accès distant VNC avec Proxmox/QEMU" /><published>2025-09-02T19:30:00+00:00</published><updated>2025-09-02T19:30:00+00:00</updated><id>/2025/09/acces-distant-vnc-avec-proxmox-qemu</id><content type="html" xml:base="/2025/09/acces-distant-vnc-avec-proxmox-qemu/"><![CDATA[<p>Je suis en train de moderniser mon serveur personnel avec Proxmox. Si la majorité des services sont maintenant en mode web, il me reste quelques services en mode graphique. J’utilise pour ceux-là un bureau léger type LXDE, avec LUbuntu ou Debian.</p>

<h1 id="vnc-dans-proxmoxqemu">VNC dans Proxmox/QEMU</h1>

<h2 id="choix-de-la-solution-de-partage-décran">Choix de la solution de partage d’écran</h2>

<p>Il existe plusieurs solutions pour se connecter en mode graphique à une VM tournant sous Proxmox.</p>

<p>Presque tous les OS intègrent une solution d’accès à distance, que ce soit le partage de bureau à distance sous Windows, ou des serveurs VNC sous Linux (soit en partage d’une session X existante, soit en création d’un serveur X dédié). C’est la solution la plus souple et la plus fiable, car elle ne dépend pas des contraintes Proxmox/QEMU. Cependant, il faudra faire la configuration sur chaque VM concernée, avec une configuration différente pour chaque OS. Par ailleurs, cela ajoute une consommation supérieure de ressources (notamment un double serveur graphique dans le mode indépendant). Si les ressources ne sont pas un problème pour votre installation et que vous n’avez pas trop de machines à configurer, c’est sans doute le mode à privilégier. Pour ma part étant sur une configuration frugale, j’ai privilégié une autre solution.</p>

<p>Proxmox/QEMU sait également partager un dispositif graphique virtuel : c’est vu par la VM comme un écran local, mais partagé à distance par Proxmox.</p>

<p>Proxmox offre plusieurs solutions de partage : une première solution intégrée avec <code class="language-shell highlighter-rouge">noVNC</code> , accessible via le menu Console de la VM ; c’est certainement la solution la plus simple à mettre en œuvre, elle est sécurisée par votre connexion à Proxmox et utilise un client VNC web qui se connecte à la VM via une socket sur la machine Proxmox. Par ailleurs <code class="language-shell highlighter-rouge">noVNC</code> supporte nativement les extensions QEMU qui vont permettre une bonne gestion du clavier et un partage partiel du presse-papier. Mais ce n’est pas forcément la solution la plus ergonomique à utiliser au quotidien</p>

<p>Une deuxième solution est proposée via l’intégration du protocole <code class="language-shell highlighter-rouge">spice</code> qui promet du rêve, rapide, bon support du clavier, partage à distance de fichiers, de devices USB, etc. Cependant, ce protocole est très peu utilisé et le client officiel pour Windows est d’une ergonomie catastrophique, et pas mis à jour depuis 4 ans. Bref, pas une option pour moi.</p>

<p>Enfin, une intégration du protocole VNC classique ; c’est cette dernière solution que j’ai utilisée, mais elle arrive avec quelques contraintes, notamment concernant la gestion du clavier et du presse-papier.</p>

<h2 id="activation-de-vnc-dans-proxmoxqemu">Activation de VNC dans Proxmox/QEMU</h2>

<p>Par défaut, il n’est pas possible de configurer un accès VNC classique (ie sans noVNC) via l’interface de Proxmox, il va falloir ajouter les bons paramètres manuellement.</p>

<p>Dans le fichier de configuration de votre VM dans <code class="language-shell highlighter-rouge">/etc/pve/qemu-server/&lt;vmid&gt;.conf</code> ajouter la ligne</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>args: <span class="nt">-vnc</span> 0.0.0.0:2,password<span class="o">=</span>off   
</code></pre></div></div>

<p>S’il existe déjà une ligne avec <code class="language-shell highlighter-rouge">args:</code> il faut ajouter ces nouveaux paramètres à la ligne existante.</p>

<p>Cela indique d’ouvrir un accès VNC via IP, sur toutes les interfaces du serveur Proxmox (<code class="language-shell highlighter-rouge">0.0.0.0</code>) sur le deuxième port (<code class="language-shell highlighter-rouge">:2</code> soit <code class="language-shell highlighter-rouge">5902</code>) en indiquant également d’ouvrir un accès non sécurisé, sans mot de passe VNC. Ce n’est clairement pas conseillé, mais dans certains cas celà peut être utile. Il faut redémarrer la VM pour que cela prenne effet.</p>

<p>Pour pouvoir indiquer un mot de passe, c’est un peu plus compliqué, car le mot de passe ne peut pas être fourni directement dans le fichier de configuration, et doit être ajouté via l’interface QEmu monitor, ce qui n’est particulièrement pas pratique. Pour cela, je vais utiliser un script hook qui va exécuter un fichier de configuration complémentaire via QEMU Monitor ; c’est une méthode particulièrement générique qui permet d’autres usages, comme le hotplug USB.</p>

<p>Pour déclarer ce hookscript il faut ajouter dans chaque VM concernée la ligne suivante :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hookscript: <span class="nb">local</span>:snippets/hook-hotplug.sh  
</code></pre></div></div>

<p>Et créer le script dans le répertoire des snippets et le rendre exécutable : <code class="language-shell highlighter-rouge">/var/lib/vz/snippets/hook-hotplug.sh</code></p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># From Perplexity: hook script pour proxmox qui ajoute dans le qm monitor après le démarrage le contenu du fichier à .hotplug.conf à coté du fichier de configuration de la VM</span>
<span class="c"># /root/hook-hotplug.sh</span>

<span class="c"># Installation</span>
<span class="c"># - chmod +x </span>
<span class="c"># - copy dans /var/lib/vz/snippets</span>
<span class="c"># - ajouter dans la VM avec: qm set &lt;VMID&gt; --hookscript local:snippets/hook-hotplug.sh</span>

<span class="c"># Variables transmises par Proxmox à chaque appel hook</span>
<span class="nv">VMID</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">PHASE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

<span class="c"># Chemin du fichier de hotplug à appliquer si présent</span>
<span class="nv">HOTPLUG_CONF</span><span class="o">=</span><span class="s2">"/etc/pve/qemu-server/</span><span class="k">${</span><span class="nv">VMID</span><span class="k">}</span><span class="s2">.hotplug.conf"</span>

<span class="c"># Lancement après démarrage effectif</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$PHASE</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"post-start"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Lire chaque ligne et l’injecter dans le monitor QEMU de la VM</span>
    <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do</span>
        <span class="c"># On ignore les lignes vides ou commentaires (#)</span>
        <span class="o">[[</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">=</span>~ ^#.<span class="k">*</span><span class="nv">$ </span><span class="o">||</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
        <span class="c"># Injecte la commande dans qm monitor</span>
        <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> | qm monitor <span class="s2">"</span><span class="nv">$VMID</span><span class="s2">"</span>
    <span class="k">done</span> &lt; <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> /var/log/hook-hotplug.log 2&gt;&amp;1
<span class="k">fi</span>
</code></pre></div></div>

<p>Le principe de ce script va être de répondre à l’évènement <code class="language-shell highlighter-rouge">post-start</code> lancé à la fin du démarrage de la VM, de lire le fichier de configuration <code class="language-shell highlighter-rouge">&lt;vmid&gt;.hotplug.conf</code> à coté du fichier de configuration de la VM, et de l’injecter dans l’interface QEMU Monitor.</p>

<p>Pour notre cas, le fichier hotplug sera simplement:  <code class="language-shell highlighter-rouge">/etc/pve/qemu-server/&lt;vmid&gt;.hotplug.conf</code></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set_password vnc &lt;votre_mot_de_passe&gt; <span class="nt">-d</span> vnc2  
</code></pre></div></div>

<p>On indique ici de sélectionner l’interface VNC vnc2 car l’interface noVNC reste active (et c’est bien pratique). Il faut également modifier la configuration de la VM pour indiquer que maintenant on veut un mot de passe :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>args: <span class="nt">-vnc</span> 0.0.0.0:2,password<span class="o">=</span>on <span class="nt">-k</span> fr  
</code></pre></div></div>

<p>On en profite également pour ajouter la mention d’utilisation du clavier français, mais lisez bien le paragraphe suivant pour comprendre le sujet du clavier avec VNC et QEMU.</p>

<p>Au redémarrage de la VM, elle sera maintenant accessible en VNC protégé par un mot de passe sur <code class="language-shell highlighter-rouge">&lt;IP de votre Proxmox&gt;:5902</code>  et toujours en noVNC via le menu Console de votre VM.</p>

<h2 id="configuration-du-clavier">Configuration du clavier</h2>

<p>Comme souvent en informatique, essentiellement de conception US, ça marche bien avec l’anglais et ça se gâte avec les alphabets/claviers un peu plus exotiques. C’est particulièrement le cas avec le combo VNC et QEMU.</p>

<p>Pour comprendre le problème il faut comprendre comment fonctionne un clavier : le clavier envoie des codes brutes correspondant aux touches appuyées (<code class="language-shell highlighter-rouge">scan codes</code>), votre OS va ensuite convertir ces touches brutes dans les symboles correspondants, suivant la configuration clavier faite lors de l’installation. Votre OS dispose des tables de mapping pour chaque type de clavier lui permettant de savoir par exemple que la touche a correspond à un ‘a’. Il va également convertir les séquences de touches, par exemple pour un a majuscule, le clavier va envoyer successivement ‘appui sur shift’, ‘appui sur a’, ‘relachement de a’, ‘relachement de shift’, que votre OS va simplement traduire en ‘A’. Et ça se complique encore plus pour les claviers français et leur touche AltGr. Plusieurs mappings existent pour cette touche, soit via la combinaison de Ctrl et Alt, soit via une touche ISO_Level3_Shift.  Le protocole VNC est prévu pour transporter les touches traduites (par exemple ‘A’, et non pas Shift+a). Là où ça se complique, c’est que QEMU doit fournir les touches brutes à la VM, comme si un clavier physique étant branché sur la VM, et va donc devoir retraduire dans le sens inverse les touches traduites reçues par VNC en touches brutes pour la VM. Cette conversion est doublement complexe car elle dépend de la configuration clavier de l’OS local que QEMU ne connait pas, et elle nécessite les mappings inverses, et ces mappings ne sont pas bijectifs… Au final cela fait 3 mappings complexes successifs:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Touches brutes – <span class="o">[</span>OS Local] → Touche traduite → VNC → <span class="o">[</span> QEMU / Mapping inverse <span class="o">]</span> → Touches brutes → <span class="o">[</span>OS Distant] → Touche finale  
</code></pre></div></div>

<p>On peut indiquer à VNC quel mapping utiliser via l’argument <code class="language-shell highlighter-rouge"><span class="nt">-k</span> fr</code> qui va permettre de choisir le mapping adapté, et permettre un fonctionnement avec n’importe quel client VNC. Cependant il reste souvent quelques problèmes, notamment avec les touches <code class="language-shell highlighter-rouge">AltGr</code> et donc ne pas permettre l’utilisation des caractères #, |, @…   Vous pouvez observer ce qui se passe via la commande <code class="language-shell highlighter-rouge">xev</code> qui va vous montrer tous les codes reçus du clavier. En l’occurence pour moi, il y a un bug dans l’ordre des touches modificatrices et donc le résultat est mauvais.  Suivant votre usage cela peut être tolérable et vous pourrez toujours copier/coller ces symboles dans la VM. Mais souvent le partage du presse-papier (la possibilité de copier/coller entre votre ordinateur et le client VNC) ne va pas bien fonctionner non plus (soit pas du tout, soit via l’utilisation des options Proxmox, fonctionner mais avec des problèmes d’encoding)</p>

<p>Pour résoudre ces problèmes, QEMU a développé des extensions au protocole VNC, pour pouvoir envoyer directement les touches brutes avant conversion et éviter le double mapping. Malheureusement, ces extensions sont peu implémentées par les clients VNC, et notamment le client RealVNC ne le gère pas. Dans les clients compatibles on trouve noVNC et TigerVNC.</p>

<p>TigerVNC fonctionne bien et a une ergonomie correcte pour un usage quotidien. Il est possible d’enregistrer les paramètres de connexion dans un fichier de configuration <code class="language-shell highlighter-rouge">.tigervnc</code> pour pouvoir lancer le client facilement. Enfin presque tous, il n’est plus possible maintenant d’enregistrer le mot de passe VNC dans le fichier !  En palliatif, il est maintenant possible de l’ajouter en variable d’environnement <code class="language-shell highlighter-rouge">VNC_PASSWORD</code> ; vous pouvez utiliser la commande PowerShell ci-dessous pour l’enregistrer dans vos variables utilisateur :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>Environment]::SetEnvironmentVariable<span class="o">(</span><span class="s2">"VNC_PASSWORD"</span>, <span class="s2">"&lt;votre_mot_de_passe&gt;"</span>, <span class="s2">"User"</span><span class="o">)</span>  
</code></pre></div></div>

<p>Vous voilà enfin avec un serveur VNC opérationnel et un client VNC qui fonctionne bien avec clavier et presse-papier !</p>

<h1 id="connexion-hors-de-votre-réseau-local">Connexion hors de votre réseau local</h1>

<p>À présent que l’on a un accès distant, il est également utile de pouvoir l’utiliser en dehors du réseau local. J’ai un VPN configuré avec WireGuard, mais pour une raison qui m’échappe, impossible d’utiliser le client RealVNC, dont la connexion échoue 90% du temps. Une fois de plus, TigerVNC fonctionne parfaitement.</p>

<h2 id="script-de-connexion">Script de connexion</h2>

<p>N’exposez jamais directement sur internet le serveur VNC, car le protocole n’est pas suffisamment sécurisé. J’utilise deux solutions pour y accéder de l’extérieur, soit un VPN sécurisé sur mon réseau local, soit un relais SSH sécurisé.</p>

<p>Pour utiliser un VPN s’il est actif, ce script va en premier pinger l’adresse locale de destination. À noter que si votre connexion locale comporte aussi cette adresse, cette méthode ultra-simpliste ne fonctionnera pas et devra être adaptée. Si l’adresse est accessible, alors le script va réaliser une connexion directe en VNC, en passant par le VPN présent.</p>

<p>Si ce n’est pas le cas, le script va créer un tunnel SSH vers le serveur VNC destination. Pour ce faire, il vous faut un serveur ssh sur votre réseau local, exposé sur internet. et qui autorise la création de tunnels TCP.</p>

<p>Suivant le serveur que vous utilisez, cela pourra être autorisé par la configuration par défaut, ou nécessiter une modification dans <code class="language-shell highlighter-rouge">sshd/sshd-config</code>, en ajoutant à la fin <code class="language-shell highlighter-rouge">AllowTcpForwarding <span class="nb">yes</span></code>, ou si vous souhaitez limiter ce privilège à votre utilisateur :</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Match User remi  
	AllowTcpForwarding <span class="nb">yes</span>  
</code></pre></div></div>

<p>Une fois le tunnel activé, le script va ensuite lancer le viewer TigerVNC en se connectant au port local du tunnel pour ouvrir la connexion. Il faut d’une part laisser un peu de temps à SSH pour établir le tunnel avant de lancer TigerVNC, et également laisser du temps à la connexion VNC de se lancer pour maintenir le tunnel actif. C’est l’objet des commandes <code class="language-shell highlighter-rouge"><span class="nb">sleep</span></code> d’une part avant le lancement de <code class="language-shell highlighter-rouge">vncviewer</code>, et d’autre part dans le script lancé à distance par SSH. Une fois le tunnel utilisé, la connexion SSH restera active, même si la commande initiale comportant le <code class="language-shell highlighter-rouge"><span class="nb">sleep</span></code> est maintenant terminée. Et le tout se terminera proprement dès que vous fermerez la fenêtre de TigerVNC.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DESTIP</span><span class="o">=</span><span class="k">${</span><span class="nv">1</span><span class="k">:-</span><span class="s2">"192.168.1.10"</span><span class="k">}</span>
<span class="nv">DESTPORT</span><span class="o">=</span><span class="k">${</span><span class="nv">2</span><span class="k">:-</span><span class="s2">"5901"</span><span class="k">}</span>
<span class="nv">REMOTESSH</span><span class="o">=</span>user@sshserver.domain
<span class="nv">LOCALPORT</span><span class="o">=</span><span class="k">${</span><span class="nv">3</span><span class="k">:-</span><span class="s2">"</span><span class="nv">$DESTPORT</span><span class="s2">"</span><span class="k">}</span>
<span class="nv">VNCVIEWER</span><span class="o">=</span>vncviewer
<span class="c"># If problem when resizing screen</span>
<span class="c">#VNCPARAM="--RemoteResize=0 --DesktopSize=1440x950 --AutoSelect=0 --CompressLevel=9 --LowColorLevel=1 --QualityLevel=5"</span>
<span class="c"># For low connections</span>
<span class="nv">VNCPARAM</span><span class="o">=</span><span class="s2">"--RemoteResize=0 --DesktopSize=1440x950 --AutoSelect=0 --CompressLevel=9 --LowColorLevel=1 --QualityLevel=5"</span>

<span class="c"># We check DESTIP is not reacheable before using ssh (VPN support)</span>
ping <span class="nt">-w</span> 2 <span class="nt">-c</span> 2 <span class="s2">"</span><span class="nv">$DESTIP</span><span class="s2">"</span>
<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-eq</span> 0 <span class="o">]</span>
<span class="k">then</span>
  <span class="c"># Direct connection</span>
  <span class="nb">echo</span> <span class="s2">"Direct connection to VNC"</span>
  <span class="nv">$VNCVIEWER</span> <span class="nv">$VNCPARAM</span> <span class="s2">"</span><span class="nv">$DESTIP</span><span class="s2">:</span><span class="nv">$DESTPORT</span><span class="s2">"</span> &amp;
<span class="k">else</span>
  <span class="c"># Wait ssh before connecting VNC</span>
  <span class="o">(</span><span class="nb">sleep </span>5 <span class="p">;</span> <span class="nv">$VNCVIEWER</span> <span class="nv">$VNCPARAM</span> <span class="s2">"localhost:</span><span class="nv">$LOCALPORT</span><span class="s2">"</span> <span class="o">)</span> &amp;
  <span class="c"># Set up SSH port forwarding</span>
  ssh <span class="nt">-L</span> <span class="s2">"</span><span class="nv">$LOCALPORT</span><span class="s2">:</span><span class="nv">$DESTIP</span><span class="s2">:</span><span class="nv">$DESTPORT</span><span class="s2">"</span> “<span class="nv">$REMOTESSH</span>” “echo Connected, waiting <span class="k">for </span>VNC connection...  <span class="o">&amp;&amp;</span> <span class="nb">sleep </span>10<span class="s2">" &amp;
  echo "</span>Connecting...<span class="s2">"
  # Wait for SSH connection before ending terminal (optional)
  sleep 10
  echo "</span>Terminal ending, connection will be closed when closing VNC<span class="s2">"
fi
</span></code></pre></div></div>

<p>Les différentes variables sont bien sûr à adapter à votre environnement, ainsi que potentiellement les délais des <code class="language-shell highlighter-rouge"><span class="nb">sleep</span></code> s’ils ne convenaient pas. Vous pouvez aussi créer des scripts secondaires appelant ce script avec les bons paramètres si vous avez plusieurs serveurs VNC destination. N’oubliez pas d’utiliser des ports locaux différents.</p>

<h2 id="menu-de-lancement">Menu de lancement</h2>

<p>À ce stade, le script est fonctionnel, mais pas très ergonomique à utiliser. On va à présent ajouter un menu de lancement via un fichier <code class="language-shell highlighter-rouge">.desktop</code>.</p>

<p>Dans le fichier <code class="language-shell highlighter-rouge">.local/share/applications/vnc.desktop</code> :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>Desktop Entry]
<span class="nv">Name</span><span class="o">=</span>VNC
<span class="nv">Exec</span><span class="o">=</span>/home/remi/vnc.sh
<span class="nv">StartupWMClass</span><span class="o">=</span>TigerVNC Viewer
<span class="nv">Icon</span><span class="o">=</span>/home/remi/.local/share/icons/YourIcon.png
<span class="nv">Terminal</span><span class="o">=</span><span class="nb">false
</span><span class="nv">Type</span><span class="o">=</span>Application
<span class="nv">StartupNotify</span><span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p>Trouvez également une icône qui vous convienne à télécharger et placer dans votre répertoire <code class="language-shell highlighter-rouge">icons</code> (n’oubliez pas de modifier les chemins dans le fichier ci-dessus)</p>

<p>Pour lancer la mise à jour du menu, il faut utiliser la commande <code class="language-shell highlighter-rouge">update-desktop-database ~/.local/share/applications/</code> , ou simplement redémarrer (notamment sous Chromebook la seule commande update ne semble pas suffisante, au moins immédiatement)</p>

<p>L’entrée <code class="language-shell highlighter-rouge">StartupVMClass</code> permet au gestionnaire de fenêtre de faire le lien avec une fenêtre ouverte par le script. On trouve sa valeur en utilisant la commande <code class="language-shell highlighter-rouge">xprop</code> et en cliquant sur la fenêtre concernée, et en regardant la valeur de <code class="language-shell highlighter-rouge">WM_CLASS</code>. Cependant sur Chromebook, cela ne semble pas fonctionner, et la fenêtre de TigerVNC reste avec l’icône par défaut et un titre vide, sans prendre ni l’icône indiquée, ni même l’icône fournie par TigerVNC. Sans doute un petit bug sur les différentes passerelles d’intégration qui sera peut être résolu dans une prochaine version.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Proxmox" /><category term="VNC" /><category term="QEmu" /><category term="Scripts" /><summary type="html"><![CDATA[Je suis en train de moderniser mon serveur personnel avec Proxmox. Si la majorité des services sont maintenant en mode web, il me reste quelques services en mode graphique. J’utilise pour ceux-là un bureau léger type LXDE, avec LUbuntu ou Debian.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/article-proxmox-vnc.png" /><media:content medium="image" url="/files/2025/article-proxmox-vnc.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Kobo Hacks</title><link href="/2025/08/kobo-hacks/" rel="alternate" type="text/html" title="Kobo Hacks" /><published>2025-08-17T10:30:58+00:00</published><updated>2025-08-17T10:30:58+00:00</updated><id>/2025/08/kobo-hacks</id><content type="html" xml:base="/2025/08/kobo-hacks/"><![CDATA[<p>Plus de 12 ans après l’achat de ma liseuse <strong>Kobo Glo</strong>, je continue à découvrir de nouvelles fonctionnalités (voir <a href="https://www.lprp.fr/tag/kobo/">mes autres articles sur Kobo</a>). C’est assez rare de voir un constructeur maintenir sa stack logicielle pendant plus de 10 ans : <strong>bravo Kobo</strong> !<br />
Voici quelques astuces et outils pratiques pour prolonger la vie de votre liseuse.</p>

<h1 id="nickelmenu--telnet-ftp-et-plein-doptions-cachées">NickelMenu : telnet, ftp et plein d’options cachées</h1>

<p><a href="https://pgaskin.net/NickelMenu/">NickelMenu</a> (<a href="https://github.com/pgaskin/NickelMenu">GitHub</a>, <a href="https://www.mobileread.com/forums/showthread.php?t=329525">forum Mobileread</a>) est un petit utilitaire qui permet d’ajouter des entrées personnalisées au menu de la liseuse. On peut ainsi activer des fonctions non documentées comme <strong>telnet</strong> ou <strong>ftp</strong>, très utiles pour accéder directement au système de la Kobo.</p>

<p>L’installation se fait simplement en mettant le fichier <a href="https://github.com/pgaskin/NickelMenu/releases/download/v0.5.4/KoboRoot.tgz">KoboRoot.tgz</a> dans le répertoire <code class="language-shell highlighter-rouge">.kobo</code> de la carte SD.  L’installation va se faire après avoir débranché la liseuse. Il faut ensuite rebrancher sur PC pour ajouter la configuration en créant le fichier <code class="language-shell highlighter-rouge">nickelmenu.cfg</code> à la racine de la liseuse (<code class="language-shell highlighter-rouge">.adds/nm/</code>). Il y a de nombreuses options décrites dans le fichier <a href="https://github.com/pgaskin/NickelMenu/blob/master/res/doc">documentation</a>  ou il y a cet exemple très complet d’un utilisateur <a href="https://gist.github.com/t18n/bbb48d10b56f7984636ff16db1ff20df">gist t18n</a>   ; par exemple pour FTP et Telnet:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>menu_item :main :Enable FTP :cmd_spawn :quiet:ftp <span class="nt">-p</span> 2121
menu_item :main :Enable Telnet :cmd_spawn :quiet:telnetd <span class="nt">-l</span> /bin/sh
</code></pre></div></div>

<p>Une fois rechargé, ces nouvelles options apparaissent dans le menu de la Kobo.</p>
<ul>
  <li><strong>FTP</strong> : utiliser votre client favori (par exemple WinSCP).  ;  ⚠️ Astuce : il faut <strong>forcer le mode UTF-8</strong> dans WinSCP pour ne pas avoir de problème d’accents dans les noms de fichiers.</li>
  <li><strong>Login</strong> par défaut : <code class="language-shell highlighter-rouge">admin / admin</code> (source : <a href="https://www.mobileread.com/forums/showthread.php?t=362428">forum Mobileread</a>).</li>
</ul>

<h1 id="plato--alternative-légère-au-lecteur-par-défaut">Plato : alternative légère au lecteur par défaut</h1>

<p><a href="https://github.com/baskerville/plato">Plato</a> est une application alternative qui se lance depuis la Kobo.</p>

<p>Avantages :</p>
<ul>
  <li>navigation par dossiers (ce qui manque au logiciel standard Kobo),</li>
  <li>interface très légère,</li>
  <li>support des formats textuels courants, notamment PDF.</li>
</ul>

<p>L’installation est super simple, il faut <a href="https://www.mobileread.com/forums/showthread.php?t=314220">télécharger le fichier <strong>One Click Setup</strong></a>, et copier le contenu de l’archive à la racine de la carte SD. Il faut bien mettre le contenu à la racine, cela va ajouter les fichiers nécessaires dans tous les répertoires existants (notamment dans .kobo pour l’installation, dans .adds pour les ressources). Rebooter et laisser le temps à l’installation de se faire.</p>

<p>Plato s’affiche alors comme une application dans NickelMenu.</p>

<h1 id="autres-éléments-non-testés">Autres éléments (non testés)</h1>

<ul>
  <li><strong>Send to Kobo</strong> : <a href="https://send.djazz.se/">send.djazz.se</a> — envoi de documents directement par navigateur</li>
  <li><strong>KoboCloud</strong> : <a href="https://github.com/fsantini/KoboCloud">fsantini/KoboCloud</a> — synchronisation automatique avec un drive (Nextcloud, GDrive, etc.)</li>
  <li>Plein d’autres ressources sont regroupées ici : <a href="https://bricoles.du-libre.org/kobo:la_page_kobo">bricoles.du-libre.org/kobo:la_page_kobo</a></li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Après toutes ces années, la Kobo reste une petite machine <strong>moddable</strong>, pratique à bidouiller pour ceux qui aiment aller plus loin que les fonctions officielles.<br />
Entre NickelMenu pour ouvrir le système, Plato pour une lecture plus flexible, et des outils comme KoboCloud, la Kobo Glo et ses consœurs continuent à vivre bien au-delà des attentes initiales.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Kobo" /><category term="Hack" /><summary type="html"><![CDATA[Plus de 12 ans après l’achat de ma liseuse Kobo Glo, je continue à découvrir de nouvelles fonctionnalités (voir mes autres articles sur Kobo). C’est assez rare de voir un constructeur maintenir sa stack logicielle pendant plus de 10 ans : bravo Kobo ! Voici quelques astuces et outils pratiques pour prolonger la vie de votre liseuse.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/kobo_glo_nickelmenu_ftp.png" /><media:content medium="image" url="/files/2025/kobo_glo_nickelmenu_ftp.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Test de précision de mesure de puissance de prises connectées bon marché</title><link href="/2025/08/test-de-precision-de-mesure-de-puissance-de-prises-connectees-bon-marche/" rel="alternate" type="text/html" title="Test de précision de mesure de puissance de prises connectées bon marché" /><published>2025-08-16T11:00:00+00:00</published><updated>2025-08-16T11:00:00+00:00</updated><id>/2025/08/test-de-precision-de-mesure-de-puissance-de-prises-connectees-bon-marche</id><content type="html" xml:base="/2025/08/test-de-precision-de-mesure-de-puissance-de-prises-connectees-bon-marche/"><![CDATA[<p>L’idée de ce test est simple : vérifier si des prises connectées à bas prix peuvent mesurer la consommation d’un climatiseur avec la même précision qu’un <strong>wattmètre dédié</strong>. Toutes les prises ont été branchées ensemble, et la consommation intrinsèque des prises étant négligeable par rapport à celle de la climatisation.</p>

<p><img src="/files/2025/dispo_clim_conso.jpg" alt="" class="img-center mw40" /></p>

<p>Trois prises bon marché différentes ont été comparées, toutes les trois chinoises, sans marque, achetées séparément, sans doute différentes mais rien de visible. Le test s’est déroulé sur trois sessions : <strong>6,5 h</strong>, <strong>10 h</strong> et <strong>9 h</strong>.  C’est bien sûr tout à fait insuffisant pour une étude sérieuse, mais suffisant pour quelques tendances et ordres de grandeur.</p>

<p>Je souhaite également me rendre compte si cette méthode de test peut permettre de calibrer ces prises si je parviens à les passer sous Tasmota, car le changement de firmware peut nécessiter une recalibration.</p>

<h1 id="puissance-instantanée-w">Puissance instantanée (W)</h1>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Prise</th>
      <th style="text-align: right">M1</th>
      <th style="text-align: right">M2</th>
      <th style="text-align: right">M3</th>
      <th style="text-align: right">M4</th>
      <th style="text-align: right">M5</th>
      <th style="text-align: center">Variation</th>
      <th style="text-align: center">Écart</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Wattmètre (Ref)</td>
      <td style="text-align: right">694</td>
      <td style="text-align: right">680</td>
      <td style="text-align: right">690</td>
      <td style="text-align: right">690</td>
      <td style="text-align: right">690.0</td>
      <td style="text-align: center">1.0%</td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 1</td>
      <td style="text-align: right">734</td>
      <td style="text-align: right">749.3</td>
      <td style="text-align: right">766</td>
      <td style="text-align: right">770</td>
      <td style="text-align: right">770.0</td>
      <td style="text-align: center">2.4%</td>
      <td style="text-align: center"><strong>10%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 2</td>
      <td style="text-align: right">705</td>
      <td style="text-align: right">652.6</td>
      <td style="text-align: right">687</td>
      <td style="text-align: right">693</td>
      <td style="text-align: right">689.0</td>
      <td style="text-align: center">3.8%</td>
      <td style="text-align: center">&lt;1%</td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 3</td>
      <td style="text-align: right"><em>570</em></td>
      <td style="text-align: right">709</td>
      <td style="text-align: right">716</td>
      <td style="text-align: right">716</td>
      <td style="text-align: right">717.0</td>
      <td style="text-align: center"><strong>10.7%</strong></td>
      <td style="text-align: center">&lt;1%</td>
    </tr>
  </tbody>
</table>

<p>La colonne “Variation” décrit la stabilité de la mesure de la prise. La colonne “Ecart” mesure l’écart absolu à la valeur de référence.</p>

<p>La prise 2 est plutôt exacte, la prise 1 est stable, mais très en écart par rapport à la référence, contrairement à la prise 3, avec de grosses variations au début, mais un écart faible ensuite. Trois prises, trois profils différents. Il ne faut pas attendre mieux de ces prises bon marché qu’une précision de 10% (et encore une belle illustration de la différence entre précision et résolution, au dixième de Watt)</p>

<p>Notes :</p>
<ul>
  <li>A la lecture les mesures des 3 prises sont très instables</li>
  <li>Les conditions thermiques étant similaires, il est raisonnable de penser que la consommation de la climatisation était proche d’être fixe sur la période des tests (soit ~690W)</li>
  <li>Les premières mesures sont les plus en écart, puis semblent se stabiliser ensuite ; de l’auto-calibration ?</li>
  <li>La première mesure de la prise 3 n’est pas une erreur, c’est également confirmé par la consommation du jour</li>
</ul>

<h1 id="consommation-du-jour-kwh">Consommation du jour (kWh)</h1>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Prise</th>
      <th style="text-align: right">J1 (6,5h)</th>
      <th style="text-align: right">J2 (10h)</th>
      <th style="text-align: right">J3 (9h)</th>
      <th style="text-align: center">Écart</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Wattmètre (Ref)</td>
      <td style="text-align: right">4.5</td>
      <td style="text-align: right">6.9</td>
      <td style="text-align: right">6.2</td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 1</td>
      <td style="text-align: right">4.48</td>
      <td style="text-align: right">6.91</td>
      <td style="text-align: right">6.2</td>
      <td style="text-align: center">0.1%</td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 2</td>
      <td style="text-align: right">4.67</td>
      <td style="text-align: right">6.78</td>
      <td style="text-align: right">6.08</td>
      <td style="text-align: center">2.5%</td>
    </tr>
    <tr>
      <td style="text-align: left">Prise 3</td>
      <td style="text-align: right">4.14</td>
      <td style="text-align: right">7.01</td>
      <td style="text-align: right">6.29</td>
      <td style="text-align: center">3.7%</td>
    </tr>
  </tbody>
</table>

<p>Les résultats cumulés sur une journée sont bien meilleurs, avec un écart de moins de 5%, voire moins de 2% si on exclue le premier jour.</p>

<p>Les résultats sont également compatibles avec d’une part la consommation instantanée probable de 690 W et l’écart de consommation relevé par Enedis (merci Linky) :</p>

<p><img src="/files/2025/clim_conso.png" alt="" class="img-center mw60" /></p>

<h1 id="tendances">Tendances</h1>
<p>Ces prises bon marché sont très bien pour <strong>suivre la tendance</strong> de consommation sur plusieurs jours , par contre il ne faut pas attendre de mesure précise de la puissance instantanée, il faut imaginer une erreur pouvant aller jusqu’à 10%</p>

<p>En résumé :<br />
📊 <strong>Conso cumulée → OK sur toutes</strong><br />
⚡ <strong>Puissance instantanée → gros écarts selon modèle</strong></p>

<h1 id="et-finalement-elle-consomme-combien-cette-climatisation">Et finalement, elle consomme combien cette climatisation ?</h1>
<p>Le climatiseur utilisé est un KLINDO KMAC7KM-21   <em>(Marque d’import Carrefour - Je passe sur l’arnaque de Carrefour Drive qui a substitué un KMAC9KM-21  plus puissant de 20% avec ce modèle sans prévenir et sans réduction de prix ; et comme seule réponse du service client que je pouvais le ramener pour un remboursement…)</em></p>

<p>Manifestement la puissance mesurée 690 W</p>

<p>Sur le carton et sur l’étiquette, il est mentionné une puissante de 2 kW ; en retrouvant la fiche descriptive de Carrefour sur leur site, on trouve la double mention suivante :</p>
<ul>
  <li>Puissance max (en W) 2     <em>(imaginons qu’il s’agisse plutôt de 2 kW)</em></li>
  <li>Puissance (en W) 980</li>
</ul>

<p>L’écart est énorme (je serai curieux de connaître à quelles conditions correspond la puissance max), et a priori très éloigné de l’usage réel, c’est plutôt bizarre de mettre en avant commercialement la valeur la plus élevée, mais tant mieux c’est une bonne surprise pour moi de voir que ça consomme moins que ce que je craignais. Bien sûr, la performance énergétique n’en ait pas meilleure pour autant, et si le sujet vous intéresse, <a href="https://youtu.be/7CXrygsg6_4">cette vidéo youtube</a> est assez intéressante en vulgarisation de la performance énergétique des climatisations.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Avis Conso" /><category term="Conso" /><category term="Compteurs" /><summary type="html"><![CDATA[L’idée de ce test est simple : vérifier si des prises connectées à bas prix peuvent mesurer la consommation d’un climatiseur avec la même précision qu’un wattmètre dédié. Toutes les prises ont été branchées ensemble, et la consommation intrinsèque des prises étant négligeable par rapport à celle de la climatisation.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/Perplexity_conso_clim_illustration.jpg" /><media:content medium="image" url="/files/2025/Perplexity_conso_clim_illustration.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Zigbee sous Home Assistant avec la box LIDL (Silvercrest TYGWZ-01)</title><link href="/2025/06/hack-Silvercrest-TYGWZ-01/" rel="alternate" type="text/html" title="Zigbee sous Home Assistant avec la box LIDL (Silvercrest TYGWZ-01)" /><published>2025-06-15T11:03:38+00:00</published><updated>2025-06-15T11:03:38+00:00</updated><id>/2025/06/hack-Silvercrest-TYGWZ-01</id><content type="html" xml:base="/2025/06/hack-Silvercrest-TYGWZ-01/"><![CDATA[<p>À l’occasion d’une promotion chez Lidl j’ai acheté une box Zigbee Silvercrest à moins de 10 euros, un peu au pif compte tenu du faible prix, et dans l’idée d’ajouter une interface Zigbee à mon installation domotique Home Assistant.  Pas si simple, mais finalement une bonne affaire.</p>

<p>Si vous cherchez la facilité, je vous conseille une autre box ou une clé USB (quasiment au même prix). L’installation est bien sûr bien plus simple car la clé est automatiquement détectée et paramétrée par Home Assistant, mais malheureusement sur le modèle que j’ai testé, la très faible couverture la rendait inutilisable, d’où la nouvelle tentative avec cette box.</p>

<h1 id="présentation-de-la-box">Présentation de la box</h1>

<p><img src="/files/2025/SilvercrestTYGWZ-01.png" alt="" class="img-center mw60" /></p>

<p>Cette box est la plus ancienne de chez LIDL, et a priori remplacée par un modèle plus récent qui se distingue par ne pas avoir les petits trous sur son boitier et a priori <a href="https://www.domo-blog.fr/lidl-smart-home-box-domotique-zigbee-compatible-home-assistant/">directement utilisable avec Home Assistant</a>. Cette  box n’est pas Wifi, il faudra donc la connecter à votre routeur Wifi via un câble Ethernet. Elle est alimentée par USB, j’ai donc pu brancher Ethernet et USB directement sur mon routeur Wifi, ce qui est finalement assez pratique.</p>

<p>LIDL a créé tout son écosystème, avec son application, sa box, sa propre gamme de capteurs, etc. Je n’ai pas testé la gamme, mais il est mentionné sur plusieurs sites que les capteurs ne sont pas compatibles avec d’autres installations Zigbee, et j’ai pu constater quelques bugs sur l’application, qui n’arrivait pas à mettre à jour le dernier firmware de la box. Également, je n’ai jamais pu ajouter des capteurs Zigbee d’une autre marque que Silvercrest sur cette box. Bef je ne recommande pas spécialement l’écosystème Silvercrest.</p>

<h1 id="hacking-de-la-box">Hacking de la box</h1>
<h2 id="sources">Sources</h2>
<p>Différents acteurs ont réussi à trouver le moyen d’utiliser cet appareil autrement ; vous allez voir que ce n’est pas simple, bravo à eux pour avoir réussi à trouver ces éléments ! La box a évolué dans le temps, nécessitant des petites variantes dans la méthode d’exploitation. Je cite en premier toutes les références qui m’ont été utiles et sans lesquelles cet article n’existerait pas :</p>
<ul>
  <li>La <a href="https://paulbanks.org/projects/lidl-zigbee/#overview">méthode originale de Paul Banks</a>, avec de nombreux renseignements utiles sur la box, et <a href="https://zigbee.blakadder.com/Lidl_TYGWZ-01.html">cette synthèse</a></li>
  <li>Une <a href="https://github.com/parasite85/tuya_dmd2cc_gateway_hack">méthode alternative</a></li>
  <li>Des compléments dans le <a href="https://community.home-assistant.io/t/hacking-the-silvercrest-lidl-tuya-smart-home-gateway/270934/117">forum Home Assistant</a> et le <a href="https://community.openhab.org/t/hacking-the-lidl-silvercrest-zigbee-gateway-a-step-by-step-tutorial/129660">forum OpenHab</a></li>
  <li>Une <a href="https://community.home-assistant.io/t/hacking-the-silvercrest-lidl-tuya-smart-home-gateway/270934/498">méthode potentiellement plus rapide avec tftpd</a> que je n’ai pas testée</li>
</ul>

<p>Je vais décrire ici la méthode qui a marché pour moi ; cela ne sera pas forcément la même pour vous, mais vous devriez trouver la solution alternative dans les liens ci-dessus.</p>

<h2 id="branchement-usb">Branchement USB</h2>

<p>Il faut avant tout ouvrir la box, et la connecter en USB à un PC pour pouvoir récupérer l’accès. J’ai pour cela utilisé un connecteur USB FTDI en 3.3V dont j’ai simplement inséré les connecteurs dupont dans les trous du circuit imprimé et en retournant le circuit pour maintenir un peu de pression sur les contacts. Et cela a suffit, pas besoin de soudure !</p>

<p><img src="/files/2025/Silvercrest_USB_Connection.jpg" alt="" class="img-center mw80" /></p>

<p>Contrairement à ce qui est indiqué dans les articles, j’ai également branché l’alimentation du port USB directement pour plus de simplicité, en utilisant un port USB3 (un port USB2 ne délivre pas assez de puissance).</p>

<p>Il faut ensuite utiliser un terminal. Compte tenu des manipulations à faire, assurez-vous de prendre un terminal pratique à utiliser. La connexion est à 38400 bauds sut 8 bits. Pour ma part j’ai utilisé</p>
<ul>
  <li>sous Windows: <a href="https://github.com/fasteddy516/SimplySerial">simplyserial</a> (<code class="language-shell highlighter-rouge">scoop instal simplyserial</code>) : <code class="language-shell highlighter-rouge">ss <span class="nt">-com</span>:7 <span class="nt">-baud</span>:38400</code></li>
  <li>sous Linux : minicom (<code class="language-shell highlighter-rouge">apt <span class="nb">install </span>minicom</code>) : <code class="language-shell highlighter-rouge">minicom <span class="nt">-D</span> /dev/ttyUSB2 <span class="nt">-b</span> 38400 <span class="nt">-8</span></code></li>
</ul>

<h2 id="récupération-du-mot-de-passe-root">Récupération du mot de passe root</h2>

<p>Malheureusement <a href="https://paulbanks.org/projects/lidl-zigbee/root/">la méthode rapide décrite ici</a> n’a pas fonctionné pour moi. J’ai donc opté pour l’extraction complète du firmware. L’extraction est très longue (~8h), prévoyez donc un setup qui permet d’opérer sur toute la durée. J’ai adapté le script de Paul Banks pour permettre l’arrêt puis la reprise.</p>

<p><details>
    <summary>Cliquez pour le script dump.py avec reprise</summary>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Dump out flash from RTL bootloader... very slowly!
#====================================================
# Author: Paul Banks [https://paulbanks.org/]
#
</span>
<span class="kn">import</span> <span class="nn">serial</span>
<span class="kn">import</span> <span class="nn">struct</span>
<span class="kn">import</span> <span class="nn">argparse</span>
<span class="kn">import</span> <span class="nn">os</span>


<span class="k">def</span> <span class="nf">doit</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">fOut</span><span class="p">,</span> <span class="n">start_addr</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">end_addr</span><span class="o">=</span><span class="mi">16</span><span class="o">*</span><span class="mi">1024</span><span class="o">*</span><span class="mi">1024</span><span class="p">):</span>

    <span class="c1"># Get a couple of prompts for sanity
</span>    <span class="n">s</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
    <span class="n">s</span><span class="p">.</span><span class="n">read_until</span><span class="p">(</span><span class="sa">b</span><span class="s">"&lt;RealTek&gt;"</span><span class="p">)</span>
    <span class="n">s</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
    <span class="n">s</span><span class="p">.</span><span class="n">read_until</span><span class="p">(</span><span class="sa">b</span><span class="s">"&lt;RealTek&gt;"</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"Starting..."</span><span class="p">)</span>

    <span class="n">step</span> <span class="o">=</span> <span class="mh">0x100</span>
    <span class="k">assert</span><span class="p">(</span><span class="n">step</span><span class="o">%</span><span class="mi">4</span><span class="o">==</span><span class="mi">0</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">flash_addr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">start_addr</span><span class="p">,</span> <span class="n">end_addr</span><span class="p">,</span> <span class="n">step</span><span class="p">):</span>

        <span class="n">s</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s">"FLR 80000000 %X %d</span><span class="se">\n</span><span class="s">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">flash_addr</span><span class="p">,</span> <span class="n">step</span><span class="p">))</span>
        <span class="k">print</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">read_until</span><span class="p">(</span><span class="sa">b</span><span class="s">"--&gt; "</span><span class="p">))</span>
        <span class="n">s</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s">"y</span><span class="se">\r</span><span class="s">"</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">read_until</span><span class="p">(</span><span class="sa">b</span><span class="s">"&lt;RealTek&gt;"</span><span class="p">))</span>

        <span class="n">s</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s">"DW 80000000 %d</span><span class="se">\n</span><span class="s">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">step</span><span class="o">/</span><span class="mi">4</span><span class="p">))</span>

        <span class="n">data</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">read_until</span><span class="p">(</span><span class="sa">b</span><span class="s">"&lt;RealTek&gt;"</span><span class="p">).</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">).</span><span class="n">split</span><span class="p">(</span><span class="s">"</span><span class="se">\n\r</span><span class="s">"</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
            <span class="n">parts</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"</span><span class="se">\t</span><span class="s">"</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span>
                <span class="n">fOut</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">struct</span><span class="p">.</span><span class="n">pack</span><span class="p">(</span><span class="s">"&gt;I"</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="mi">16</span><span class="p">)))</span>
                <span class="n">fOut</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span><span class="o">==</span><span class="s">"__main__"</span><span class="p">:</span>

    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="s">"RTL Flash Dumper"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">"--serial-port"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span>
                        <span class="n">help</span><span class="o">=</span><span class="s">"Serial port device - e.g. /dev/ttyUSB0"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">"--output-file"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span>
                        <span class="n">help</span><span class="o">=</span><span class="s">"Path to file to save dump into"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">"--start-addr"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">"Start address"</span><span class="p">,</span>
                        <span class="n">default</span><span class="o">=</span><span class="s">"0x0"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">"--end-addr"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">"End address"</span><span class="p">,</span>
                        <span class="n">default</span><span class="o">=</span><span class="nb">hex</span><span class="p">(</span><span class="mi">16</span><span class="o">*</span><span class="mi">1024</span><span class="o">*</span><span class="mi">1024</span><span class="p">))</span>

    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>

    <span class="n">s</span> <span class="o">=</span> <span class="n">serial</span><span class="p">.</span><span class="n">Serial</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">serial_port</span><span class="p">,</span> <span class="mi">38400</span><span class="p">)</span>
    <span class="n">start_addr</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">start_addr</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="n">end_addr</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">end_addr</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>

    <span class="n">nameOut</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">output_file</span>
    <span class="n">outMode</span> <span class="o">=</span> <span class="s">"wb"</span>
    <span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">nameOut</span><span class="p">):</span>
        <span class="n">taille</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">getsize</span><span class="p">(</span><span class="n">nameOut</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Resuming "</span><span class="p">,</span> <span class="n">taille</span><span class="p">,</span>  <span class="s">" ..."</span><span class="p">)</span>
        <span class="n">start_addr</span> <span class="o">=</span> <span class="n">start_addr</span> <span class="o">+</span> <span class="n">taille</span>
        <span class="n">outMode</span> <span class="o">=</span> <span class="s">"ab"</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">nameOut</span><span class="p">,</span> <span class="n">outMode</span><span class="p">)</span> <span class="k">as</span> <span class="n">fOut</span><span class="p">:</span>
        <span class="n">doit</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">fOut</span><span class="p">,</span> <span class="n">start_addr</span><span class="p">,</span> <span class="n">end_addr</span><span class="p">)</span>


</code></pre></div>    </div>

  </details></p>

<p>Le script nécessite le module <code class="language-shell highlighter-rouge">pycryptodome</code>, à installer dans un environnement virtuel pour éviter les conflits de version.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Extraction </span>
python <span class="nt">-m</span> venv .venv 
.venv/bin/pip3 <span class="nb">install </span>pycryptodome 
.venv/bin/python3 dump.py <span class="nt">--serial-port</span> /dev/ttyUSB2 <span class="nt">--output-file</span> all.bin <span class="nt">--start-addr</span> 0x000000 <span class="nt">--end-addr</span> 0x000001000000
</code></pre></div></div>

<p>Pour que le script fonctionne, l’appareil doit être dans un mode spécial que l’on active en pressant <code class="language-shell highlighter-rouge">Esc</code> au démarrage. Pour que le port USB reste actif et pouvoir presser Esc juste après le boot, il ne faut pas débrancher/rebrancher la prise USB pour rebooter, mais seulement débrancher la pin d’alimentation et la remettre. Il vaut mieux s’entrainer quelques fois avec le terminal avant de lancer le script.</p>

<p>Ensuite, on découpe les partitions suivant les adresses que l’on voit sur la console lors du boot</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Découpage des partitions</span>
<span class="nb">cp </span>all.bin flash_bank_1.bin
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>flash_bank_1.bin <span class="nv">of</span><span class="o">=</span>boot+cfg.bin    <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">iflag</span><span class="o">=</span>skip_bytes,count_bytes    <span class="nv">skip</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x000000<span class="k">))</span>    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x020000<span class="k">))</span>    <span class="nv">status</span><span class="o">=</span>progress
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>flash_bank_1.bin <span class="nv">of</span><span class="o">=</span>linux.bin    <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">iflag</span><span class="o">=</span>skip_bytes,count_bytes    <span class="nv">skip</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x020000<span class="k">))</span>    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x1E0000<span class="k">))</span>    <span class="nv">status</span><span class="o">=</span>progress
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>flash_bank_1.bin <span class="nv">of</span><span class="o">=</span>rootfs.bin    <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">iflag</span><span class="o">=</span>skip_bytes,count_bytes    <span class="nv">skip</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x200000<span class="k">))</span>    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x200000<span class="k">))</span>    <span class="nv">status</span><span class="o">=</span>progress
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>flash_bank_1.bin <span class="nv">of</span><span class="o">=</span>tuya-label.bin    <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">iflag</span><span class="o">=</span>skip_bytes,count_bytes    <span class="nv">skip</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x400000<span class="k">))</span>    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x020000<span class="k">))</span>    <span class="nv">status</span><span class="o">=</span>progress
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>flash_bank_1.bin <span class="nv">of</span><span class="o">=</span>jffs2-fs.bin    <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">iflag</span><span class="o">=</span>skip_bytes,count_bytes    <span class="nv">skip</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>x420000<span class="k">))</span>    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span><span class="m">0</span>xBE0000<span class="k">))</span>    <span class="nv">status</span><span class="o">=</span>progress
</code></pre></div></div>

<p>Nous avons maintenant besoin de l’outil <code class="language-shell highlighter-rouge">jefferson</code> pour extraire le format de partition <code class="language-shell highlighter-rouge">jffs2</code> :</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>jefferson
jefferson <span class="nt">-d</span> jffs2 jffs2-fs.bin
<span class="nb">cd </span>jffs2/jffs2-root/config
</code></pre></div></div>

<p>Vous devriez voir dans le répertoire <code class="language-shell highlighter-rouge">jffs2-root/config</code> deux fichiers <code class="language-shell highlighter-rouge">Licence.file1</code>  et <code class="language-shell highlighter-rouge">Licence.file2</code>.  Le script <a href="https://paulbanks.org/download/files/lidl-zigbee/lidl_auskey_decode.py">lidl_auskey_decode.py</a> permet d’extraire les informations utiles (à lancer également depuis le virtualenv).</p>

<p>La sortie du script donne les clés tant cherchées:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Decrypted data
b<span class="s1">'{"auzkey":"xN1e5Uhvc____YyjXArrd2CULAQNnm___","uuid":"c03808b8f8a3____","master_mac":
"10d56144_____","sn":"HMGW2113001_____","prodtest_exit":"true"}
\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f'</span>
</code></pre></div></div>

<p>Le mot de passe root étant les 8 derniers caractères de auzkey, c’est donc ici <code class="language-shell highlighter-rouge">AQNnm___</code>   (une partie des identifiants est remplacée dans cet article par <code class="language-shell highlighter-rouge">_</code>). Gardez-le précieusement !</p>

<h2 id="installation">Installation</h2>

<p>Avec le mot de passe root, vous pouvez maintenant booter normalement (sans <code class="language-shell highlighter-rouge">Esc</code>) et vous connecter en tant que root lors de l’invite, grâce au mot de passe récupéré. Une partie des opérations va maintenant nécessiter d’utiliser l’accès ssh, il faut donc connecter votre passerelle au réseau. Bien que l’accès physique à votre passerelle ne devrait plus être nécessaire, je vous conseille cependant de garder l’accès tant que toute la configuration n’est pas finie, car il est très facile sur une erreur de ne plus pouvoir se connecter en SSH. Si votre routeur n’est pas très accessible vous pouvez également tenter le partage de connexion entre le port Ethernet de votre PC et le Wifi. Le port ssh utilisé initialement est le port 2333.</p>

<p>On peut reprendre alors les instructions <a href="https://zigbee.blakadder.com/Lidl_TYGWZ-01.html">de cet article </a>:</p>
<ul>
  <li>Le remplacement du lancement de ssh
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> /tuya/ssh_monitor.original.sh <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">cp</span> /tuya/ssh_monitor.sh /tuya/ssh_monitor.original.sh<span class="p">;</span> <span class="k">fi
</span><span class="nb">echo</span> <span class="s2">"#!/bin/sh"</span> <span class="o">&gt;</span>/tuya/ssh_monitor.sh
reboot
</code></pre></div>    </div>
  </li>
  <li>Cependant le server ssh n’était pas lancé sur ma passerelle, il a fallu l’activer avec:
    <ol>
      <li><code class="language-shell highlighter-rouge"><span class="nb">cat</span> <span class="o">&gt;</span> /tuya/ssh_enable_flag </code> + Ctrl-D</li>
      <li>ajout de <code class="language-shell highlighter-rouge">dropbear <span class="nt">-p</span> 22 <span class="nt">-K</span> 300</code> dans  ssh_monitor.sh (via vi)</li>
    </ol>
  </li>
</ul>

<p>Au reboot il devrait être maintenant possible de vous connecter via ssh. Compte tenu des capacités réduites, vous devrez sans doute ajouter des paramètres pour forcer votre client ssh à utiliser ces algorithmes obsolètes</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-p22</span> <span class="nt">-oHostKeyAlgorithms</span><span class="o">=</span>+ssh-rsa <span class="nt">-oPubkeyAcceptedAlgorithms</span><span class="o">=</span>+ssh-rsa root@&lt;ip de la passerelle&gt;
</code></pre></div></div>

<ul>
  <li>
    <p>Il faut ensuite transférer le fichier <a href="https://paulbanks.org/download/files/lidl-zigbee/serialgateway.bin">serialgateway.bin</a> sur la passerelle ; je n’ai pas réussi à faire fonctionner depuis Windows (sans doute les retours chariots), donc j’ai poursuivi depuis Linux : <code class="language-shell highlighter-rouge"><span class="nb">cat </span>serialgateway.bin | ssh <span class="nt">-x</span> <span class="nt">-oHostKeyAlgorithms</span><span class="o">=</span>+ssh-rsa <span class="nt">-oPubkeyAcceptedAlgorithms</span><span class="o">=</span>+ssh-rsa root@192.168.0.154 <span class="s2">"cat &gt;/tuya/serialgateway"</span></code></p>
  </li>
  <li>
    <p>Et finaliser l’installation avec le lancement de la gateway serie</p>
  </li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> /tuya/tuya_start.original.sh <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">cp</span> /tuya/tuya_start.sh /tuya/tuya_start.original.sh<span class="p">;</span> <span class="k">fi

</span><span class="nb">cat</span> <span class="o">&gt;</span>/tuya/tuya_start.sh <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
#!/bin/sh
/tuya/serialgateway &amp;
</span><span class="no">EOF
</span><span class="nb">chmod </span>755 /tuya/serialgateway

reboot
</code></pre></div></div>

<ul>
  <li>Vous pouvez également faire la mise à jour de version EZSP (facultatif) ; suivez la procédure <a href="https://zigbee.blakadder.com/Lidl_TYGWZ-01.html">de cet article </a>, et si nécessaire pour votre cilent ssh, modifier 3x la commande ssh dans le script pour ajouter ` -x -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa ` .</li>
</ul>

<h1 id="configurer-home-assistant">Configurer Home Assistant</h1>

<p>Vous pouvez maintenant refermer votre box et l’installer comme vous souhaitez. La configuration sous Home Assistant est assez simple :</p>
<ol>
  <li>Dans les intégrations, chercher <code class="language-shell highlighter-rouge">ZHA</code></li>
  <li>Choisissez ensuite la méthode manuelle</li>
  <li>Choisir “EZSP” comme Radio Type</li>
  <li>A l’emplacement du port série, entrer <code class="language-shell highlighter-rouge">socket://[gateway_ip]:8888</code> en remplaçant[gateway_ip] pas son adresse IP (pas de DNS).</li>
  <li>Utiliser la vitesse de 115200</li>
</ol>

<p>Et voilà ! Vous pouvez maintenant ajouter des devices <a href="https://www.home-assistant.io/integrations/zha/">Zigbee</a></p>

<h1 id="conclusion">Conclusion</h1>

<p>La couverture de la box est plutôt bonne, bien meilleure que la clé USB Zigbee pas chère que j’avais testée, et la stabilité est très bonne. Bref, un succès ! C’est la solution que j’ai gardée.</p>

<p>Il est sans doute également possible de faire d’autre choses avec cette box car on a à dispostion un environnement Linux embarqué qui offre beaucoup de possibilités,certes limitées par la faible puissance de l’appareil.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="Zigbee" /><category term="Hacking" /><summary type="html"><![CDATA[À l’occasion d’une promotion chez Lidl j’ai acheté une box Zigbee Silvercrest à moins de 10 euros, un peu au pif compte tenu du faible prix, et dans l’idée d’ajouter une interface Zigbee à mon installation domotique Home Assistant. Pas si simple, mais finalement une bonne affaire.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/SilvercrestTYGWZ-01_vignette.png" /><media:content medium="image" url="/files/2025/SilvercrestTYGWZ-01_vignette.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Intégration de statistiques Piwik Pro (Custom card et sensor command_line)</title><link href="/2025/06/home-assistant-piwik/" rel="alternate" type="text/html" title="Intégration de statistiques Piwik Pro (Custom card et sensor command_line)" /><published>2025-06-14T18:08:00+00:00</published><updated>2025-06-14T18:08:00+00:00</updated><id>/2025/06/home-assistant-piwik</id><content type="html" xml:base="/2025/06/home-assistant-piwik/"><![CDATA[<p>Home Assistant est extensible très facilement, et permet d’intégrer donc de nouvelles sources. Cet article permet de montrer comment ajouter des reports Piwik Pro (web analytics) dans un dashboard Home Assistant, et également des capteurs virtuels pour récupérer certaines valeurs de Piwik :</p>

<p><img src="/files/2025/homeassistant-piwik-cards.png" alt="" class="img-center mw80" /></p>

<h1 id="custom-cards-piwik">Custom cards Piwik</h1>
<p>Pour les cards nous allons utiliser la même méthode de récupération des données que pour l’affichage des Top Articles sur mon site web, <a href="/2024/01/top-viewed-posts-in-jekyll-with-piwik/">décrit dans cet article</a>, à savoir la récupération des données via un report public, qui expose les données via un fichier json. Je vous renvoie à l’article pour le détail de la mise en place du Custom Report sur Piwik Pro et son exposition via le bouton Share / Public link. A noter que le lien est valable seulement sur un temps limité et devra être renouvelé tous les 6 mois. À noter que le code javascript des cards est exécuté  coté navigateur, il est donc tout à fait déconseillé d’y mettre des mots de passe ou token, ce qui empêche donc l’usage de l’API.</p>

<p>Pour plus d’information et des astuces sur la mise en place des custom cards sous Home Assistant, quelques astuces sont données dans <a href="/2025/06/home-assistant-custom-card-links/">mon précédent article sur le sujet</a>.</p>

<h2 id="pour-les-graphiques-piwik">Pour les graphiques Piwik</h2>

<p>Voici le premier fichier (à mettre dans <code class="language-shell highlighter-rouge">/config/www/card-piwik.js</code>, et à déclarer avec <code class="language-shell highlighter-rouge">Tableaux de bords / Ressources / Ajouter une ressource</code>)</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">CardGraphPiwik</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">setConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">content</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">`
          &lt;ha-card&gt;
            &lt;canvas id="jsonChart" height="160"&gt;&lt;/canvas&gt;
          &lt;/ha-card&gt;
        `</span><span class="p">;</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// Charger Chart.js si pas déjà là</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">window</span><span class="p">.</span><span class="nx">Chart</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">chartJs</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">script</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">chartJs</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://cdn.jsdelivr.net/npm/chart.js</span><span class="dl">"</span><span class="p">;</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">head</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">chartJs</span><span class="p">);</span>
      <span class="k">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">chartJs</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="nx">resolve</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="c1">// Charger les données JSON</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">json_url</span><span class="p">;</span> <span class="c1">// || "/local/data.json";</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">report_url</span><span class="p">)</span>
      <span class="nx">url</span> <span class="o">=</span> <span class="nx">atob</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">report_url</span><span class="p">?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">You must defind json_url or report_url</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="nx">config</span><span class="p">;</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">refreshGraph</span><span class="p">()</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">refresh_seconds</span><span class="p">)</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">refreshGraph</span><span class="p">(),</span> <span class="nx">config</span><span class="p">.</span><span class="nx">refresh_seconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">refreshGraph</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">json</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">reportData</span> <span class="o">=</span> <span class="nx">json</span><span class="p">?.</span><span class="nx">reportData</span><span class="p">?.</span><span class="nx">data</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">reportConfig</span> <span class="o">=</span> <span class="nx">json</span><span class="p">?.</span><span class="nx">reportConfig</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">column_data2key</span> <span class="o">=</span> 
      <span class="p">({</span><span class="na">column_id</span><span class="p">:</span> <span class="nx">cid</span><span class="p">,</span> <span class="na">transformation_id</span><span class="p">:</span> <span class="nx">tid</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">cid</span> <span class="o">??</span> <span class="dl">""</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span> <span class="nx">tid</span> <span class="p">?</span> <span class="p">(</span><span class="dl">"</span><span class="s2">__</span><span class="dl">"</span><span class="o">+</span><span class="nx">tid</span><span class="p">)</span> <span class="p">:</span> <span class="dl">""</span><span class="p">)</span>

    <span class="c1">// label_key</span>
    <span class="kd">const</span> <span class="nx">label_col</span> <span class="o">=</span> <span class="nx">reportConfig</span><span class="p">.</span><span class="nx">columns</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">c</span> <span class="o">=&gt;</span> <span class="nx">c</span><span class="p">.</span><span class="nx">axis</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">x</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">label_key</span> <span class="o">=</span> <span class="nx">column_data2key</span><span class="p">(</span><span class="nx">label_col</span><span class="p">?.</span><span class="nx">column_data</span><span class="p">)</span>

    <span class="c1">// Création du graphique</span>
    <span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#jsonChart</span><span class="dl">"</span><span class="p">).</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">chart</span><span class="p">)</span> <span class="k">this</span><span class="p">.</span><span class="nx">chart</span><span class="p">.</span><span class="nx">destroy</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">chart</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">line</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">data</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">labels</span><span class="p">:</span> <span class="nx">reportData</span><span class="p">?.</span><span class="nx">map</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">[</span><span class="nx">label_key</span><span class="p">]),</span>
        <span class="na">datasets</span><span class="p">:</span> <span class="nx">reportConfig</span><span class="p">.</span><span class="nx">columns</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">c</span> <span class="o">=&gt;</span> <span class="nx">c</span><span class="p">.</span><span class="nx">axis</span> <span class="o">!=</span> <span class="dl">"</span><span class="s2">x</span><span class="dl">"</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">col</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">colKey</span> <span class="o">=</span> <span class="nx">column_data2key</span><span class="p">(</span><span class="nx">col</span><span class="p">.</span><span class="nx">column_data</span><span class="p">)</span>
          <span class="k">return</span> <span class="p">{</span>
            <span class="na">label</span><span class="p">:</span> <span class="nx">col</span><span class="p">.</span><span class="nx">column_meta</span><span class="p">?.</span><span class="nx">column_name</span><span class="p">,</span>
            <span class="na">data</span><span class="p">:</span> <span class="nx">reportData</span><span class="p">?.</span><span class="nx">map</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">[</span><span class="nx">colKey</span><span class="p">]),</span>
            <span class="na">tension</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span>
            <span class="p">...(</span><span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">?.</span><span class="nx">columns</span><span class="p">?.[</span><span class="nx">colKey</span><span class="p">])</span>
          <span class="p">}</span>
        <span class="p">})</span>
      <span class="p">},</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">responsive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
        <span class="na">plugins</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">legend</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span>
          <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="nx">reportConfig</span><span class="p">?.</span><span class="nx">name</span> <span class="p">}</span>
        <span class="p">},</span>
        <span class="na">scales</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">x</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="cm">/* title: { display: true, text: "Date" }*/</span> <span class="p">},</span>
          <span class="na">y</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="cm">/* title: { display: true, text: "Nb" },*/</span><span class="p">},</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">});</span>
  <span class="p">}</span>

  <span class="kd">set</span> <span class="nx">hass</span><span class="p">(</span><span class="nx">hass</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>

  <span class="nx">getCardSize</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="kd">static</span> <span class="nx">getStubConfig</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> 
      <span class="na">report_url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://xxxx.piwik.pro/analytics/#/sharing/xxxxxxx/</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">refresh_seconds</span><span class="p">:</span> <span class="mi">3600</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">"</span><span class="s2">card-graph-piwik</span><span class="dl">"</span><span class="p">,</span> <span class="nx">CardGraphPiwik</span><span class="p">);</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">||</span> <span class="p">[];</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">card-graph-piwik</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Card Piwik Graph</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="s2">`Custom card for Piwik Graph.
  
  You have to provide at least report_url or json_url. 
  - report_url is the URL given by Piwik when sharing a report
  - json_url is the URL of the data for the report

  You can set up the refresh rate with refresh_seconds 

  The card will get configuration from the report.
  
  You may also customize the display by setting a columns config, 
  with keys corresponding to the key of the report, having as value
  an object with key corresponding to GraphJs datasets properties`</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Cette première card fonctionne pour les reports en mode graphique. Il récupère automatiquement les bons libellés d’axes à partir des meta data Piwik et génère le graphique grâce à la bibliothèque Charts.js.</p>

<p>L’utilisation est très simple, via le code yaml suivant:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">type</span><span class="pi">:</span> <span class="s">custom:card-graph-piwik</span>
<span class="na">json_url</span><span class="pi">:</span> <span class="s">https://pp-core-p-gwc.piwik.pro/blobs/shares/xxxxxxxxxxx.json</span>
</code></pre></div></div>

<p>Vous devez soit indiquer l’URL json avec <code class="language-shell highlighter-rouge">json_url:</code> ou directement l’URL du rapport fourni par Piwik lors du bouton Share en utilisant <code class="language-shell highlighter-rouge">report_url:</code>.</p>

<p>Comme indiqué sommairement dans la documentation, vous avez également la possibilité de customiser l’affichage du graphique en utilisant l’option <code class="language-shell highlighter-rouge">columns:</code>, le nom de la série, et le paramètre Charts.js à modifier</p>

<p>Par exemple pour spécifier la couleur de la série <code class="language-shell highlighter-rouge">visitors</code> :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">columns</span><span class="pi">:</span>
  <span class="na">visitors</span><span class="pi">:</span>
    <span class="na">borderColor</span><span class="pi">:</span> <span class="s">blue</span>
</code></pre></div></div>

<p>Si vous souhaitez rafraichir les données automatiquement, vous pouvez utiliser <code class="language-shell highlighter-rouge">refresh_seconds: 600</code>  pour actualiser toutes les 5 minutes par exemple. À noter que l’actualisation est automatique lors de l’affichage du dashboard, ce paramètre est donc seulement utile si vous laissez le dashboard ouvert.</p>

<h2 id="pour-les-tableaux-piwik">Pour les tableaux Piwik</h2>

<p>Et voici la deuxième card pour les tableaux  (à mettre dans <code class="language-shell highlighter-rouge">/config/www/card-piwik-table.js</code>, et à déclarer avec <code class="language-shell highlighter-rouge">Tableaux de bords / Ressources / Ajouter une ressource</code>)</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">CardTablePiwik</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">setConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Charger les données JSON</span>
    <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">json_url</span><span class="p">;</span> <span class="c1">// || "/local/data.json";</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">report_url</span><span class="p">)</span>
      <span class="nx">url</span> <span class="o">=</span> <span class="nx">atob</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">report_url</span><span class="p">?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">You must define json_url or report_url</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">config</span><span class="p">.</span><span class="nx">columns</span><span class="p">?.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">You must define columns</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="nx">config</span><span class="p">;</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">refreshGraph</span><span class="p">()</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">refresh_seconds</span><span class="p">)</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">refreshGraph</span><span class="p">(),</span> <span class="nx">config</span><span class="p">.</span><span class="nx">refresh_seconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">refreshGraph</span><span class="p">()</span> <span class="p">{</span>

    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">json</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">reportData</span> <span class="o">=</span> <span class="nx">json</span><span class="p">?.</span><span class="nx">reportData</span><span class="p">?.</span><span class="nx">data</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">reportConfig</span> <span class="o">=</span> <span class="nx">json</span><span class="p">?.</span><span class="nx">reportConfig</span><span class="p">;</span>
    
    <span class="kd">const</span> <span class="nx">column_data2key</span> <span class="o">=</span> 
      <span class="p">({</span><span class="na">column_id</span><span class="p">:</span> <span class="nx">cid</span><span class="p">,</span> <span class="na">transformation_id</span><span class="p">:</span> <span class="nx">tid</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">cid</span> <span class="o">??</span> <span class="dl">""</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span> <span class="nx">tid</span> <span class="p">?</span> <span class="p">(</span><span class="dl">"</span><span class="s2">__</span><span class="dl">"</span><span class="o">+</span><span class="nx">tid</span><span class="p">)</span> <span class="p">:</span> <span class="dl">""</span><span class="p">)</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">`
      &lt;ha-card&gt;
        &lt;style&gt;
        td { overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 1; }
        &lt;/style&gt;
        &lt;table style="width:100%;"&gt;
         &lt;tr&gt; 
         </span><span class="p">${</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">columns</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">col</span> <span class="o">=&gt;</span> 
           <span class="s2">`&lt;th&gt;
           </span><span class="p">${</span><span class="nx">reportConfig</span><span class="p">.</span><span class="nx">columns</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">c</span> <span class="o">=&gt;</span> <span class="nx">column_data2key</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">column_data</span><span class="p">)</span> <span class="o">==</span> <span class="nx">col</span><span class="p">)?.</span><span class="nx">column_meta</span><span class="p">?.</span><span class="nx">column_name</span> <span class="o">??</span> <span class="nx">col</span><span class="p">}</span><span class="s2">
           &lt;/th&gt;`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span> <span class="p">}</span><span class="s2">
         &lt;/tr&gt;
         </span><span class="p">${</span> <span class="nx">reportData</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">max</span> <span class="o">??</span> <span class="mi">10</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span> <span class="nx">l</span> <span class="o">=&gt;</span> 
          <span class="s2">`&lt;tr&gt;
           </span><span class="p">${</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">columns</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">col</span> <span class="o">=&gt;</span> <span class="s2">`&lt;td&gt;</span><span class="p">${</span><span class="nx">l</span><span class="p">[</span><span class="nx">col</span><span class="p">]}</span><span class="s2">&lt;/td&gt;`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span> <span class="p">}</span><span class="s2">
          &lt;/tr&gt;`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span> <span class="p">}</span><span class="s2">
        &lt;/table&gt;
      &lt;/ha-card&gt;
    `</span><span class="p">;</span>

  <span class="p">}</span>

  <span class="kd">set</span> <span class="nx">hass</span><span class="p">(</span><span class="nx">hass</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>

  <span class="cm">/*
  getCardSize() {
    return 2;
  }
  */</span>

  <span class="kd">static</span> <span class="nx">getStubConfig</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> 
      <span class="na">report_url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://xxxxx.piwik.pro/analytics/#/sharing/xxxxxxx/</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">refresh_seconds</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
      <span class="na">columns</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">event_title</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">events</span><span class="dl">'</span><span class="p">],</span>
      <span class="na">max</span><span class="p">:</span> <span class="mi">10</span>
    <span class="p">};</span>
  <span class="p">}</span>

<span class="p">}</span>

<span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">"</span><span class="s2">card-table-piwik</span><span class="dl">"</span><span class="p">,</span> <span class="nx">CardTablePiwik</span><span class="p">);</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">||</span> <span class="p">[];</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">card-table-piwik</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Card Piwik Table</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="s2">`Custom card for Piwik Table.
  
  You have to provide at least report_url or json_url. 
  - report_url is the URL given by Piwik when sharing a report
  - json_url is the URL of the data for the report

  You can set up the refresh rate with refresh_seconds 

  You have to define the columns config to list the columns you want in 
  which order.`</span><span class="p">,</span>
<span class="p">});</span>

</code></pre></div></div>

<p>Pour utiliser, il faut également paramétrer la card en yaml:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">type</span><span class="pi">:</span> <span class="s">custom:card-table-piwik</span>
<span class="na">report_url</span><span class="pi">:</span>  <span class="s">https://lprp.piwik.pro/analytics/#/sharing/xxxxxx/</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">event_title</span>
  <span class="pi">-</span> <span class="s">events</span>
<span class="na">max</span><span class="pi">:</span> <span class="m">10</span>
</code></pre></div></div>

<p>Comme la card précédent, il faut également fournir soit <code class="language-shell highlighter-rouge">json_url:</code>, soit <code class="language-shell highlighter-rouge">report_url:</code>. Et vous pouvez définir également <code class="language-shell highlighter-rouge">refresh_seconds:</code>.</p>

<p>Il vous faudra déclarer en plus la liste des colonnes à utiliser dans <code class="language-shell highlighter-rouge">columns:</code> (et dans l’ordre dans lequel vous voulez l’affichage), en listant les identifiants des séries (que vous pouvez voir dans le JSON, ou déduire du libellé dans Piwik)</p>

<p>Vous pouvez enfin indiquer le nombre maximum d’entrées à afficher avec <code class="language-shell highlighter-rouge">max:</code> ; en l’absence, toutes les entrées sont affichées.</p>

<h1 id="virtual-sensors-piwik">Virtual Sensors Piwik</h1>
<p>Contrairement aux cards ci-dessus, qui affichent des données externes mais sans que les données soient importées dans Home Assistant, nous allons ici intégrer quelques données dans Home Assistant (et avoir la capacité de les historiser et de les afficher avec les cards classiques)</p>

<p>Le code étant exécuté coté serveur, nous allons pouvoir ici utiliser l’API Piwik Pro, et s’authentifier via OAuth2. La documentation de l’API semble plutôt bien fournie, et pourtant il n’est pas simple de trouver les bons paramètres pour obtenir ce que l’on veut. Des exemples de la communauté permettent de s’y retrouver.</p>

<p>Il y a la possibilité de définir des capteurs “ligne de commande”, qui appellent simplement une ligne de commande pour raffraichir la valeur du capteur. Il y a également plein d’autres options décrites <a href="https://www.home-assistant.io/integrations/command_line/">dans la documentation</a>.</p>

<p>Voici le code python utilisé pour le capteur, qui réalise l’authentification et l’appel pour récupérer les données (<code class="language-shell highlighter-rouge">/config/scripts/sensor-piwik.py</code>)</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">datetime</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">urllib.request</span>
<span class="kn">import</span> <span class="nn">urllib.parse</span>
<span class="kn">import</span> <span class="nn">json</span>

<span class="n">instance_url</span> <span class="o">=</span> <span class="s">"https://xxxxxx.piwik.pro"</span>
<span class="n">website_id</span> <span class="o">=</span> <span class="s">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>
<span class="n">client_id</span> <span class="o">=</span> <span class="s">"xxxxxxxxxxxxx"</span>
<span class="n">client_secret</span> <span class="o">=</span> <span class="s">"xxxxxxxxxxxxxx"</span>

<span class="k">def</span> <span class="nf">piwik_get_token</span><span class="p">():</span>
    <span class="n">token_url</span> <span class="o">=</span> <span class="n">instance_url</span> <span class="o">+</span> <span class="s">"/auth/token"</span>
    <span class="n">token_data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"grant_type"</span><span class="p">:</span> <span class="s">"client_credentials"</span><span class="p">,</span>
        <span class="s">"client_id"</span><span class="p">:</span> <span class="n">client_id</span><span class="p">,</span>
        <span class="s">"client_secret"</span><span class="p">:</span> <span class="n">client_secret</span>
    <span class="p">}</span>
    <span class="n">data_encoded</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">urlencode</span><span class="p">(</span><span class="n">token_data</span><span class="p">).</span><span class="n">encode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)</span>
    <span class="n">req</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">Request</span><span class="p">(</span><span class="n">token_url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data_encoded</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="s">"POST"</span><span class="p">)</span>
    <span class="n">req</span><span class="p">.</span><span class="n">add_header</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"application/x-www-form-urlencoded"</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">with</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
            <span class="n">token_result</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">read</span><span class="p">())</span>
            <span class="n">bearer_token</span> <span class="o">=</span> <span class="n">token_result</span><span class="p">[</span><span class="s">'access_token'</span><span class="p">]</span>
            <span class="k">return</span> <span class="n">bearer_token</span>
    <span class="k">except</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Error retrieving token"</span><span class="p">)</span>
        <span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>

<span class="k">def</span> <span class="nf">piwik_get_data</span><span class="p">(</span><span class="n">bearer_token</span><span class="p">,</span> <span class="n">date_from</span><span class="p">,</span> <span class="n">date_to</span><span class="p">):</span>
    <span class="n">api_url</span> <span class="o">=</span> <span class="n">instance_url</span> <span class="o">+</span> <span class="s">'/api/analytics/v1/query'</span>
    <span class="n">req_data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"date_from"</span><span class="p">:</span> <span class="n">date_from</span><span class="p">,</span>
        <span class="s">"date_to"</span><span class="p">:</span> <span class="n">date_to</span><span class="p">,</span>
        <span class="s">"website_id"</span><span class="p">:</span> <span class="n">website_id</span><span class="p">,</span>
        <span class="s">"offset"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="s">"limit"</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
        <span class="s">"columns"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span><span class="s">"column_id"</span><span class="p">:</span> <span class="s">"visitors"</span><span class="p">},</span>
            <span class="p">{</span><span class="s">"column_id"</span><span class="p">:</span> <span class="s">"page_views"</span><span class="p">}</span>
        <span class="p">]</span>
    <span class="p">}</span>
    <span class="n">data_encoded</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">req_data</span><span class="p">).</span><span class="n">encode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)</span>
    <span class="n">req_api</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">Request</span><span class="p">(</span><span class="n">api_url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data_encoded</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="s">"POST"</span><span class="p">)</span>
    <span class="n">req_api</span><span class="p">.</span><span class="n">add_header</span><span class="p">(</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="sa">f</span><span class="s">"Bearer </span><span class="si">{</span><span class="n">bearer_token</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    
    <span class="k">try</span><span class="p">:</span>
        <span class="k">with</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req_api</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
            <span class="n">api_result</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">read</span><span class="p">())</span>
            <span class="k">return</span> <span class="n">api_result</span>
    <span class="k">except</span> <span class="n">urllib</span><span class="p">.</span><span class="n">error</span><span class="p">.</span><span class="n">HTTPError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">error_body</span> <span class="o">=</span> <span class="n">e</span><span class="p">.</span><span class="n">read</span><span class="p">().</span><span class="n">decode</span><span class="p">()</span>
        <span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">"error"</span><span class="p">:</span> <span class="p">{</span><span class="s">"code"</span><span class="p">:</span> <span class="n">e</span><span class="p">.</span><span class="n">code</span><span class="p">,</span> <span class="s">"body"</span><span class="p">:</span> <span class="n">error_body</span><span class="p">}}))</span>
        <span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">"error"</span><span class="p">:</span> <span class="p">{</span><span class="s">"exception:"</span><span class="p">:</span> <span class="n">e</span><span class="p">}}))</span>
        <span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>

<span class="n">bearer_token</span> <span class="o">=</span> <span class="n">piwik_get_token</span><span class="p">()</span>

<span class="n">month_start</span> <span class="o">=</span> <span class="p">(</span><span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">().</span><span class="n">replace</span><span class="p">(</span><span class="n">day</span><span class="o">=</span><span class="mi">1</span><span class="p">)).</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y-%m-%d'</span><span class="p">)</span>
<span class="n">ago30days</span> <span class="o">=</span> <span class="p">(</span><span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">datetime</span><span class="p">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">)).</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y-%m-%d'</span><span class="p">)</span>
<span class="n">yesterday</span> <span class="o">=</span> <span class="p">(</span><span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">datetime</span><span class="p">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)).</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y-%m-%d'</span><span class="p">)</span>
<span class="n">today</span> <span class="o">=</span> <span class="p">(</span><span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">()).</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y-%m-%d'</span><span class="p">)</span>

<span class="n">match</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="s">""</span><span class="p">:</span>
    <span class="n">case</span> <span class="s">"month"</span><span class="p">:</span>
        <span class="n">date_from</span> <span class="o">=</span> <span class="n">month_start</span>
        <span class="n">date_to</span> <span class="o">=</span> <span class="n">today</span>
    <span class="n">case</span> <span class="s">"30days"</span><span class="p">:</span>
        <span class="n">date_from</span> <span class="o">=</span> <span class="n">ago30days</span>
        <span class="n">date_to</span> <span class="o">=</span> <span class="n">today</span>
    <span class="n">case</span> <span class="s">"yesterday"</span><span class="p">:</span>
        <span class="n">date_from</span> <span class="o">=</span> <span class="n">yesterday</span>
        <span class="n">date_to</span> <span class="o">=</span> <span class="n">yesterday</span>
    <span class="n">case</span> <span class="s">"today"</span><span class="p">:</span>
        <span class="n">date_from</span> <span class="o">=</span> <span class="n">today</span>
        <span class="n">date_to</span> <span class="o">=</span> <span class="n">today</span>
    <span class="n">case</span> <span class="n">_</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Invalid argument. Use 'month', '30days', 'yesterday' or 'today'."</span><span class="p">)</span>
        <span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">piwik_get_data</span><span class="p">(</span><span class="n">bearer_token</span><span class="p">,</span> <span class="n">date_from</span><span class="p">,</span> <span class="n">date_to</span><span class="p">)</span>
    <span class="n">formatted_data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"date_query"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">().</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y-%m-%d %H:%M:%S'</span><span class="p">),</span>
        <span class="s">"date_from"</span><span class="p">:</span> <span class="n">date_from</span><span class="p">,</span>
        <span class="s">"date_to"</span><span class="p">:</span> <span class="n">date_to</span><span class="p">,</span>
        <span class="s">"data"</span><span class="p">:</span>  <span class="nb">dict</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">"meta"</span><span class="p">][</span><span class="s">"columns"</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">"data"</span><span class="p">][</span><span class="mi">0</span><span class="p">]))</span>
    <span class="p">}</span>
    <span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">formatted_data</span><span class="p">))</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">"error"</span><span class="p">:</span> <span class="p">{</span><span class="s">"exception"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)},</span> <span class="s">"data"</span><span class="p">:</span> <span class="n">data</span><span class="p">}))</span>
    <span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<p>N’oubliez pas de customiser le script avec vos identifiants, à créer et récupérer dans l’interface Piwik Pro.</p>

<p>Il faut ensuite définir les capteurs dans configuration.yaml:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">command_line</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">sensor</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">LPRP.fr</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">Visiteurs</span><span class="nv"> </span><span class="s">(Hier)"</span>
    <span class="na">unique_id</span><span class="pi">:</span> <span class="s">piwik_lprp_visitors_yesterday</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">python3</span><span class="nv"> </span><span class="s">/config/scripts/sensor_matomo_visits.py</span><span class="nv"> </span><span class="s">yesterday"</span>
    <span class="na">scan_interval</span><span class="pi">:</span> <span class="m">36000</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">value_json.data.visitors</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">visiteurs"</span>
    <span class="na">json_attributes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">date_query</span>
      <span class="pi">-</span> <span class="s">date_from</span>
      <span class="pi">-</span> <span class="s">date_to</span>
<span class="pi">-</span> <span class="na">sensor</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">LPRP.fr</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">Pages</span><span class="nv"> </span><span class="s">(Hier)"</span>
    <span class="na">unique_id</span><span class="pi">:</span> <span class="s">piwik_lprp_pages_yesterday</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">python3</span><span class="nv"> </span><span class="s">/config/scripts/sensor_matomo_visits.py</span><span class="nv"> </span><span class="s">yesterday"</span>
    <span class="na">scan_interval</span><span class="pi">:</span> <span class="m">36000</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">value_json.data.page_views</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">pages"</span>
    <span class="na">json_attributes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">date_query</span>
      <span class="pi">-</span> <span class="s">date_from</span>
      <span class="pi">-</span> <span class="s">date_to</span>
</code></pre></div></div>

<p>Il faut redémarrer Home Assistant et vous pourrez utiliser vos nouveaux capteurs dans des cards, par exemple:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">type</span><span class="pi">:</span> <span class="s">glance</span>
<span class="na">entities</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.lprp_fr_visiteurs_hier</span>
    <span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:calendar-week</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Hier</span>
</code></pre></div></div>

<p>Quelques précisions:</p>
<ul>
  <li>Home Assistant date les données au moment où elles sont récupérées, donc par exemple, même si sensor.lprp_fr_visiteurs_hier concerne le nombre de visiteurs de la veille, la donnée sera enregistré le jour de la récupération ; donc si vous regardez l’historique, rappelez vous de ce petit jour de décalage</li>
  <li>Il n’est pas possible d’indiquer une heure de récupération, seulement un intervalle ; il est donc impossible d’indiquer de faire l’appel à 23h59 pour avoir les visiteurs du jour.</li>
  <li>Pour un capteur qui récupère par exemple les visiteurs du jour toutes les heures, vous aurez un historique en dents de scie, qui augmente au fur et à mesure de l journée, et remis à zéro tous les jours. L’affichage d’un historique sur cette valeur n’est pas très pertinent, et Home Assistant va convertir en min/mean/max, ce qui n’a pas beaucoup de sens. C’est pour cela que je préfère le nombre de visiteurs de la veille, qui a le mérite d’être correct quelque soit l’heure d’interrogation.</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="Custom" /><category term="Card" /><category term="Piwik" /><summary type="html"><![CDATA[Home Assistant est extensible très facilement, et permet d’intégrer donc de nouvelles sources. Cet article permet de montrer comment ajouter des reports Piwik Pro (web analytics) dans un dashboard Home Assistant, et également des capteurs virtuels pour récupérer certaines valeurs de Piwik :]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/homeassistant-piwik-cards-vignette.png" /><media:content medium="image" url="/files/2025/homeassistant-piwik-cards-vignette.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Custom card Home Assistant pour des liens web (migration homer)</title><link href="/2025/06/home-assistant-custom-card-links/" rel="alternate" type="text/html" title="Custom card Home Assistant pour des liens web (migration homer)" /><published>2025-06-14T17:48:02+00:00</published><updated>2025-06-14T17:48:02+00:00</updated><id>/2025/06/home-assistant-custom-card-links</id><content type="html" xml:base="/2025/06/home-assistant-custom-card-links/"><![CDATA[<p>J’aime bien pouvoir ajouter sur les dashboards les URLs des services que j’utilise souvent. Cet article décrit comment j’ai fait dans Home Assistant</p>

<h1 id="solutions-précédentes-sous-domoticz-et-homer">Solutions précédentes sous Domoticz et Homer</h1>

<p>Sous Domoticz j’avais déjà ajouté la fonction pour ajouter des URL dans un menu spécifique, voir <a href="/2020/12/domoticz-panasonic-remote-buttons-and-custom-urls/">cet article</a></p>

<p><img src="/files/2020/12/domoticz_externalurl_menu.png" alt="" class="img-center" /></p>

<p>Puis j’ai ensuite utilisé <a href="https://github.com/bastienwirtz/homer">Homer</a> pour un dashboard hyper léger et pratique sous Raspberry Pi Zero</p>

<p><img src="/files/2025/dashboard-homer.png" alt="" class="img-center mw60" /></p>

<p>J’ai donc cherché à faire la même chose sous Home Assistant, dans un dashboard. Cependant, je n’ai pas trouvé le moyen d’avoir un lien web avec une icône en image, et facilement.</p>

<h1 id="custom-card-sous-home-assistant">Custom Card sous Home Assistant</h1>

<p>Heureusement, il y a la possibilité d’étendre les possibilités de Home Assistant avec les <a href="https://developers.home-assistant.io/docs/frontend/custom-ui/custom-card/">Custom Cards</a> ; c’est donc ce que j’ai fait, voici ce que cela donne :</p>

<p><img src="/files/2025/homeassistant-card-links.png" alt="" class="img-center mw80" /></p>

<p>La documentation est plutôt bien faite et il y a beaucoup d’exemples sur les forums ; il faut</p>
<ul>
  <li>créer le fichier javascript dans <code class="language-shell highlighter-rouge">/config/www/card-links.js</code> (voir code ci-dessous)</li>
  <li>ajouter la ressource via <code class="language-shell highlighter-rouge">Dashboard / Resources / Add resource</code>  et ajouter <code class="language-shell highlighter-rouge">/local/card-links.js</code> (<code class="language-shell highlighter-rouge">/local/</code> correspond à <code class="language-shell highlighter-rouge">/config/www</code>)</li>
  <li>puis retourner à l’édition du dashboard et ajouter la nouvelle card</li>
</ul>

<p>Le plus difficile dans la phase de développement est de trouver comment faire prendre en compte les modifications de code à Home Assistant ; j’ai commencé par essayer ce que j’ai trouvé sur le web, à savoir recharger les yaml, redémarrer Home Assistant, forcerle rafraichissement de la page, supprimer le cache du navigateur, mais rien n’y faisait. Au final la solution que j’ai trouvée au détour d’un forum Home Assistant est plus simple et plus pratique, et consiste à ajouter un suffixe <code class="language-shell highlighter-rouge">?v<span class="o">=</span>xx</code> que l’on incrémente au fur et à mesure des essais, en cliquant sur la ressource concernée dans <code class="language-shell highlighter-rouge">Dashboard / Resources</code>.</p>

<p>Quelques détails:</p>
<ul>
  <li>taille de la card : j’ai essayé initialement d’utiliser   getCardSize() et  getGridOptions()  sans succès ; au final, simplement retirer ces fonctions a permis que la taille de la card s’adapte automatiquement à la taille utile</li>
  <li>l’idéal serait de définir le paramétrage visuel, mais c’est plus compliqué que la card elle-même, donc je n’ai pas encore investi le temps nécessaire ; inclure <code class="language-shell highlighter-rouge">getStubConfig<span class="o">()</span></code> permet relativement de s’en passer, en fournissant un exemple avec l’ensemble des champs dont il est utile de se souvenir</li>
  <li>pour aoir la card dans la liste des card proposées, il suffit de l’ajouter dans <code class="language-shell highlighter-rouge">window.customCards</code></li>
</ul>

<h1 id="exemple-de-paramétrage">Exemple de paramétrage</h1>

<p>A éditer en YAML:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">type</span><span class="pi">:</span> <span class="s">custom:content-card-links</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">AI Tools</span>
<span class="na">items</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Perplexity</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">https://www.perplexity.ai/</span>
    <span class="na">logo</span><span class="pi">:</span> <span class="s">https://www.perplexity.ai/favicon.ico</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Chat GPT</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">https://chatgpt.com/</span>
    <span class="na">logo</span><span class="pi">:</span> <span class="s">https://chatgpt.com/favicon.ico</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Mistral</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">https://chat.mistral.ai/chat</span>
    <span class="na">logo</span><span class="pi">:</span> <span class="s">https://chat.mistral.ai/favicon.ico</span>
</code></pre></div></div>

<p>Le paramètre <code class="language-shell highlighter-rouge">logo:</code> permet  d’indiquer une image. Pour utiliser une icone, il est possible de remplacer par <code class="language-shell highlighter-rouge">icon:</code>  et utiliser une icone mdi ou autre. Pour utiliser une image locale, il est possible de mettre les images dans <code class="language-shell highlighter-rouge">/config/www</code> et d’utiliser le préfixe <code class="language-shell highlighter-rouge">/local/</code> (par exemple <code class="language-shell highlighter-rouge">/local/logos/chat.png</code> pour pointer sur <code class="language-shell highlighter-rouge">/config/www/logos/chat.png</code>)</p>

<h1 id="code-source-pour-la-card-content-card-links">Code source pour la card content-card-links</h1>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * To be added in /config/www/card-links.js
 * Add with Dashboard / Resources / Add resource /  "/local/card-links.js"
 * Update by adding &amp; incrementing ?v=xx after "/local/card-links.js?v=2" &amp; refresh dashboard
 */</span>

<span class="kd">class</span> <span class="nx">ContentCardLinks</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>

  <span class="c1">// Static content, so we render in setConfig</span>
  <span class="nx">setConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Use throw to show config error </span>
    <span class="k">this</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">`
      &lt;ha-card header="</span><span class="p">${</span><span class="nx">config</span><span class="p">?.</span><span class="nx">name</span> <span class="o">??</span> <span class="dl">""</span><span class="p">}</span><span class="s2">"&gt;
      &lt;style&gt;
        ul { list-style: none; margin: 0; padding: 0; }
        li { display: flex; align-items: center; margin-bottom: 12px; }
        img, .icon { width: 28px; height: 28px; margin-right: 10px; }
        a { text-decoration: none; color: inherit; display: flex; align-items: center;}
        .card-content {}
      &lt;/style&gt;
      &lt;div class="card-content"&gt;
      &lt;ul&gt;
        </span><span class="p">${</span><span class="nx">config</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=&gt;</span> <span class="s2">`
          &lt;li&gt;
            &lt;a href="</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">url</span><span class="p">}</span><span class="s2">" target="_blank"&gt;
              </span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">logo</span> <span class="p">?</span> <span class="s2">`&lt;img src="</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">logo</span><span class="p">}</span><span class="s2">" /&gt;`</span> <span class="p">:</span> <span class="s2">`&lt;span class="icon"&gt;&lt;ha-icon icon="</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">icon</span><span class="p">}</span><span class="s2">"&gt;&lt;/ha-icon&gt;&lt;/span&gt;`</span><span class="p">}</span><span class="s2">
              &lt;span&gt;</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">&lt;/span&gt;
            &lt;/a&gt;
          &lt;/li&gt;
        `</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">''</span><span class="p">)}</span><span class="s2">
      &lt;/ul&gt;
      &lt;/div&gt;
      &lt;/ha-card&gt;
    `</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// Use if we need dynamic content, or home assistant state, but not the case here</span>
  <span class="kd">set</span> <span class="nx">hass</span><span class="p">(</span><span class="nx">hass</span><span class="p">)</span> <span class="p">{}</span>

  <span class="kd">static</span> <span class="nx">getStubConfig</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> 
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Example</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">items</span><span class="p">:</span> <span class="p">[{</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Name of item</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://target.org</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">logo</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/local/logos/logo.svg</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">icon</span><span class="p">:</span> <span class="dl">"</span><span class="s2">mdi:home-assistant</span><span class="dl">"</span>
      <span class="p">}]</span>
    <span class="p">};</span>
  <span class="p">}</span>

<span class="p">}</span>

<span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">"</span><span class="s2">content-card-links</span><span class="dl">"</span><span class="p">,</span> <span class="nx">ContentCardLinks</span><span class="p">);</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span> <span class="o">||</span> <span class="p">[];</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">customCards</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">content-card-links</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Card Links</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Custom card for links</span><span class="dl">"</span><span class="p">,</span>
<span class="p">});</span>

</code></pre></div></div>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="Card" /><category term="Custom" /><category term="Links" /><category term="Web" /><summary type="html"><![CDATA[J’aime bien pouvoir ajouter sur les dashboards les URLs des services que j’utilise souvent. Cet article décrit comment j’ai fait dans Home Assistant]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/homeassistant-card-links-vignette.png" /><media:content medium="image" url="/files/2025/homeassistant-card-links-vignette.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Migrer des données historiques entre deux instances Home Assistant</title><link href="/2025/06/home-assistant-migration-donnees/" rel="alternate" type="text/html" title="Migrer des données historiques entre deux instances Home Assistant" /><published>2025-06-14T13:49:48+00:00</published><updated>2025-06-14T13:49:48+00:00</updated><id>/2025/06/home-assistant-migration-donnees</id><content type="html" xml:base="/2025/06/home-assistant-migration-donnees/"><![CDATA[<p>La migration vers Home Assistant s’est déroulée sur environ 6 mois, et comme j’ai expérimenté différents modes de déploiement pour déterminer celui qui me corresponde le mieux, j’ai eu pendant 6 mois une version sous docker qui a collecté un certain nombre de données que je souhaitais récupérer sur ma nouvelle instance. Or il n’existe pas d’outil qui permette de merger deux bases de données Home Assistant.</p>

<p>A l’instar de ce que j’ai fait pour migrer les données de Domoticz (décrit dans  <a href="/2025/06/home-assistant-migration-donnees-domoticz/">cet article</a>), ci-dessous la méthode que j’ai utilisée pour migrer quelques capteurs d’une autre instance Home Assistant, via un export des données depuis la base SQLite et l’import dans Home Assistant via un plugin déjà disponible dans HACS, le store communautaire, et l’addon SQLWeb. Ce que je présente ci-dessous est encore très manuel, adapté pour quelques capteurs ciblés (pour ma part j’avais seulement 2 capteurs dont je voulais ne pas perdre l’historique) ; la méthode pourrait cependant être automatisée moyennant quelques efforts.</p>

<h1 id="export-et-préparation-des-données-via-sql">Export et préparation des données via SQL</h1>
<p>On va faire l’extraction des données et le formatage pour import via homeassistant-statistics directement en SQL. J’ai utilisé pour ce faire <a href="https://sqlitestudio.pl/">SQLiteStudio</a> (<code class="language-shell highlighter-rouge">scoop <span class="nb">install </span>sqlitestudio</code> avec <a href="https://scoop.sh">scoop</a>), mais n’importe quel client SQL pour SQLite fera l’affaire. Pour récupérer la base SQLite de Home Assistant, le plus simple est de créer une sauvegarde (via <code class="language-shell highlighter-rouge">Paramètres / Système / Sauvegardes</code>), puis de télécharger la sauvegarde, puis à l’intérieur du fichier tar de sauvegarde, d’ouvrir le fichier <code class="language-shell highlighter-rouge">homeassistant.tar.gz</code>, et d’extraire <code class="language-shell highlighter-rouge">data/home-assistant_v2.db</code></p>

<p>Pour trouver la liste chaque capteur à migrer en regardant dans la table <code class="language-shell highlighter-rouge">statistics_meta</code>. Il faut vérifier que les capteurs ont le même nom dans votre instance cible, à vérifier également ans la table <code class="language-shell highlighter-rouge">statistics_meta</code> via l’addon SQLite Web qu’il est possible d’installer et de lancer via Paramètres / Modules complémentaires (Home Assistant OS uniquement). S’il y a certains capteurs avec des noms différents, il faudra ajouter une conversion.</p>

<p>Exemple de consultation de la table statistics_meta depuis SQLite Web:</p>

<p><img src="/files/2025/HA_Import_Domoticz_2025-06-08%20193227.png" alt="" class="img-center mw80" /></p>

<p>Une fois ces éléments récupérés, on va extraire les données via le script SQL suivant :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> 
    <span class="k">CASE</span> 
      <span class="c1">-- Pour les capteurs dont il faut modifier l'identifiant</span>
      <span class="k">WHEN</span> <span class="n">sm</span><span class="p">.</span><span class="n">statistic_id</span> <span class="o">=</span> <span class="s1">'sensor.mi_body_composition_scale_b854_mass'</span> 
                        <span class="k">THEN</span> <span class="s1">'sensor.mi_body_composition_scale_b854_poids'</span>
      <span class="k">WHEN</span> <span class="n">sm</span><span class="p">.</span><span class="n">statistic_id</span> <span class="o">=</span> <span class="s1">'sensor.nodemcu_temperature_salon_2'</span> 
                        <span class="k">THEN</span> <span class="s1">'sensor.nodemcu_temperature_salon'</span>
      <span class="k">WHEN</span> <span class="n">sm</span><span class="p">.</span><span class="n">statistic_id</span> <span class="o">=</span> <span class="s1">'sensor.nodemcu_internal_temperature_2'</span> 
                        <span class="k">THEN</span> <span class="s1">'sensor.nodemcu_internal_temperature'</span>
      <span class="k">ELSE</span> <span class="n">sm</span><span class="p">.</span><span class="n">statistic_id</span> 
    <span class="k">END</span> <span class="k">AS</span> <span class="n">statistic_id</span><span class="p">,</span>
    <span class="n">sm</span><span class="p">.</span><span class="n">unit_of_measurement</span> <span class="k">AS</span> <span class="n">unit</span><span class="p">,</span>
    <span class="n">strftime</span><span class="p">(</span><span class="s1">'%d.%m.%Y %H:%M'</span><span class="p">,</span> <span class="nb">datetime</span><span class="p">(</span><span class="n">start_ts</span><span class="p">,</span><span class="s1">'unixepoch'</span><span class="p">))</span> <span class="k">AS</span> <span class="k">start</span><span class="p">,</span>
    <span class="n">s</span><span class="p">.</span><span class="k">min</span> <span class="k">AS</span> <span class="k">min</span><span class="p">,</span>
    <span class="n">s</span><span class="p">.</span><span class="k">max</span> <span class="k">AS</span> <span class="k">max</span><span class="p">,</span>
    <span class="c1">-- De temps en temps la valeur moyenne dépasse min/max</span>
    <span class="k">MAX</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="k">min</span><span class="p">,</span><span class="k">MIN</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="k">max</span><span class="p">,</span><span class="n">s</span><span class="p">.</span><span class="n">mean</span><span class="p">))</span> <span class="k">AS</span> <span class="n">mean</span>    
<span class="k">FROM</span> <span class="k">statistics</span> <span class="n">s</span><span class="p">,</span> <span class="n">statistics_meta</span> <span class="n">sm</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">id</span>
<span class="k">AND</span> <span class="n">statistic_id</span> <span class="k">IN</span> <span class="p">(</span>
  <span class="c1">-- La liste des capteurs à extraire</span>
  <span class="s1">'sensor.temperature_humidity_sensor_a408_temperature'</span><span class="p">,</span>
  <span class="s1">'sensor.temperature_humidity_sensor_a408_humidity'</span><span class="p">,</span>
  <span class="s1">'sensor.mi_body_composition_scale_b854_mass'</span><span class="p">,</span>
  <span class="s1">'sensor.nodemcu_temperature_salon_2'</span><span class="p">,</span>
  <span class="s1">'sensor.nodemcu_humidit_salon'</span><span class="p">,</span>
  <span class="s1">'sensor.nodemcu_internal_temperature_2'</span><span class="p">,</span>
  <span class="s1">'sensor.nodemcu_temt6000_illuminance'</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Puis sauvegarder les données dans un fichier CSV au format UTF-8.</p>

<h1 id="import-dans-home-assistant-via-homeassistant-statistics">Import dans Home Assistant via homeassistant-statistics</h1>

<p>L’import s’effectue avec <a href="https://github.com/klausj1/homeassistant-statistics">homeassistant-statistics</a> disponible depuis <a href="https://www.hacs.xyz/">HACS</a>.</p>

<p>L’installation est très simple en suivant les instructions sur leurs sites.</p>
<ul>
  <li>Pour HACS voir également <a href="https://www.lesalexiens.fr/actualites/comment-installer-hacs-home-assistant/">cet article en français illustré étape par étape</a></li>
  <li>Pour homeassistant-statistics, en plus de l’installation via HACS, il faut ajouter <code class="language-shell highlighter-rouge">import_statistics:</code>  à la fin du configuration.yaml. Si vous ne l’avez pas déjà fait, l’installation de l’addon Studio Code Server est vraiment pratique pour modifier les fichiers de configuration par la suite (pas besoin de le mettre en démarrage automatique, vous pouvez le lancer uniquement au besoin)</li>
</ul>

<p>il faut ensuite importer les fichiers CSV dans le même répertoire que le fichier <code class="language-shell highlighter-rouge">configuration.yaml</code> (vous pouvez utiliser également l’addon Studio Code Server pour importer le fichier)</p>

<p>L’utilisation se fait via l’onglet Actions dans les outils de développement de Home Assistant, en renseignant les différentes informations demandées :</p>

<p><img src="/files/2025/HA_Import_Domoticz_2025-06-08%20185437.png" alt="" class="img-center mw80" /></p>

<p>Puis cliquer sur le bouton “Exécuter l’action”, qui passe en vert en cas de succès.</p>

<p>Vous pouvez ensuite aller sur le capteur pour aller voir son historique et voir le bon import.</p>

<p>À noter:</p>
<ul>
  <li>si vous ne trouvez pas l’action import_from_file, il faut redémarrer Home Assistant après l’installation depuis HACS</li>
  <li>en cas d’erreur, vérifier l’identifiant du capteur dans Home Assistant, et l’encodage du fichier CSV</li>
  <li>pour une raison bizarre, Home Assistant peut avoir des valeurs moyennes en dehors des bornes min/max et même si c’est un écart infime, c’est refusé lors de l’import, d’ou la formule intégrée au script SQL pour ajuster aux bornes.</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="SQL" /><category term="Migration" /><summary type="html"><![CDATA[La migration vers Home Assistant s’est déroulée sur environ 6 mois, et comme j’ai expérimenté différents modes de déploiement pour déterminer celui qui me corresponde le mieux, j’ai eu pendant 6 mois une version sous docker qui a collecté un certain nombre de données que je souhaitais récupérer sur ma nouvelle instance. Or il n’existe pas d’outil qui permette de merger deux bases de données Home Assistant.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/files/2025/migration-data-home-assistant.png" /><media:content medium="image" url="/files/2025/migration-data-home-assistant.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>