CérénIT

Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)

Vers de nouveaux horizons...

floveacérénittimeseriesbigdatahebdoiotctociodsiiiot

Je l'évoquais dans le billet "Bilan 2021 et perspectives 2022", je peux en parler maintenant officiellement : j'ai été contacté par Flovea pour piloter le projet Flowbox Interactive et mettre en place l'équipe projet associée.

Après trois mois environ de mission permettant de faire connaissance, d'auditer la solution existante, de définir une roadmap et de mettre en place l'équipe projet, mon recrutement en tant que DSI/CIO de Flovéa est acté depuis début avril. J'ai le plaisir de rejoindre une belle équipe pour réaliser un beau projet tant d'un point de vue technique que d'un point de vue du sens et de son utilité. La seule ombre au tableau étant le contexte de pénurie de composants qui illustre bien la dimension "hard" d'un projet "hardware".

L'activité de CérénIT va donc ralentir puis se mettre en mode minimal ; le temps pour moi de finir quelques activités de support pour un client et ne conserver ensuite que l'infogérance de Compta-Online et un autre projet avec Fabrice Heuvrard à destination des experts comptables.

L'animation du meetup Time Series France sera moins régulière et surement de façon plus opportuniste que précédemment. Je continue à contribuer à BigData Hebdo même si mes contributions au podcast sont minimes depuis le début d'année.

Je remercie tous les clients et les personnes que j'ai pu accompagner pendant ces 6 ans ; j'ai appris énormément de choses grâce à eux et j'ai pu travailler sur des sujets et dans des entreprises sur/pour lesquel(le)s je n'aurais jamais pensé travailler. Je remercie plus particulièrement :

  • Frédéric Rocci, j'aurai du rejoindre Compta Online début 2017, cela ne s'est pas fait mais cela m'a permis de devenir indépendant
  • Vincent Heuschling : il était mon prestataire lorsque j'étais encore chez JCDecaux, il devient mon premier client en 2017 pour lancer les premières fondations de DataTask. On a remis ça en 2020/2021 mais les conditions sanitaires et économiques font que je ne peux pas rester sur le projet fin 2021. C'est grace à cette rencontre que je découvre le podcast BigData Hebdo puis rejoint l'équipe en 2019.
  • Thomas Bosviel, prestataire également chez JCDecaux en 2016/17 et qui me met en contact en 2019 avec Frédéric Mefiant de la SAFT et pouvoir ainsi commencer mon activité "Time Series".
  • Denis Rampnoux pour la mission chez LesFurets.com et Youen Chéné pour la mission chez Saagie.

Ces années ont été très riches et passionnantes mais j'aspirai à aller vers d'autres choses ; le projet et la rencontre avec Flovéa semblent être la réponse que j'attendais. Il est donc temps de tourner la page et de découvrir ces nouveaux horizons.

Web, Ops, IoT et Time Series - Février 2022

traefikiotedgehttpxsemiconducteurrgpdgoogle analyticsfontpodmannebulawireguardjlessjsonréseautsfeltimescale

Code & Langages

  • httpx : en gros, requests mais avec le support de l'asynchrone. L'API semble être la même. httpx peut aussi s'installer en tant que cli.
  • The Algorithms - Go : collection d'implémentation d'algorithme en Go à fin d'apprentissage

Fonts

  • Luciole : La police Luciole a été créée à destination des personnes malvoyantes et apporte un certain confort de lecture et une meilleure lisibilité.

Hardware

IoT

  • Anomaly Detection: Glimpse into the Future of IoT Data : intéressant le triplet Objet IoT, Edge / Data Routeur capable de réaliser des opérations et le noeud central. L'edge computing permet d'éviter de saturer le noeud central et de prendre des décisions au plus près de l'objet IoT.

Ops

Outils

RGPD & Vie Privée

Time Series

Bilan 2021 et perspectives 2022

bilanperspectivecérénittimeseriesbigdatahebdoinfluxaceiot

Routine habituelle de début d'année pour la clôture de ce 5ème exercice (déjà !).

Bilan 2021

Au global, une année mitigée qui se termine un peu sur le fil du rasoir au niveau comptable. Pour la partie positive, j'ai l'impression que cette année a été "l'année des possibles" où les efforts commencés les années précédentes commencent à payer. Des premiers projets en Go, des missions Time Series intéressantes et ambitieuses par certains aspects et un projet annexe en Python/Django sur la fin d'année qui consolide différents éléments permettant de gagner en confiance et de réduire un peu ce cher syndrome de l'imposteur avec lequel j'apprends à composer et à dépasser parfois.

CérénIT

D'un point de vue comptable, cela donne :

20212020201920182017Variation n/n-1
Chiffre d'affaires~130 K€~138 K€~150 K€~132 K€~100 K€-6%
Résultat après impôts~1K€~10 K€~13.5 K€~10 K€~20 K€-90% 😱
Jours facturés164175197178160-6%
TJM793€789€761€742€625€+0.5%

Les années précédentes, les missions annexes (missions courtes d'audit/expertise principalement) venaient apporter le bénéfice annuel et la mission principale couvrait les charges. Cette année, avec une réduction de mes missions principales en fin d'année, les missions annexes ont permis de couvrir les charges mais sans permettre de dégager de bénéfices. Un TJM plus élevé sur ces missions annexes permet d'amortir et de compenser la baisse d'activité.

202120202019Variation n/n-1
Chiffre d'affaires Total~130 K€~138 K€~150 K€-6%
Chiffre d'affaires Time Series~25K€~5 K€~2 K€+400% 💪

Une des grandes satisfactions de l'année est incontestablement l'essor de l'activité Time Series. Etre référencé comme InfluxAce m'a apporté une mission chez Axens sur les Shard Duration & Retention Policies et quelques contacts/prospects qui n'ont pas pu aboutir durant cette année. C'est aussi la poursuite de mon accompagnement des équipes de la SAFT pour la 3ème année consécutive et la mise à jour d'InfluxDB OSS v1.x vers v2.x avec un sujet de migration des alertes Kapacitor vers des Tasks en Flux.

Autres activités

Pour le reste, j'ai eu le plaisir de :

Pas de contribution de ma société à un quelconque projet comme en 2020 avec une contribution financière au projet Makair suite à la question du rôle social d'une entreprise dans notre société en temps de COVID. Petite déception à ce niveau-là.

Perspectives 2022

2021 s'est terminée avec une certaine forme de lassitude autour de l'activité de freelance (et de la solitude du freelance, même si j'ai toujours cherché à gommer cet aspect dans mes missions) et des activités autour de l'automatisation/industrialisation comme une fin en soit. J'éprouvais le besoin de créer un commun avec des personnes et je voulais accélérer la bascule vers l'IoT industriel mais ne sachant pas par quel bout attaquer le sujet. Une mission "DevOps AWS pour déployer un outil de CI/CD" sans autre finalité ne m'intéressait pas/plus (🤢).

Et là, de nulle part surgit une prise de contact sur LinkedIn par une recruteuse pour rejoindre une entreprise dans l'IoT industriel, avec une dimension environnementale qui cherchait un profil de responsable technique DevOps/IoT. Les astres ne pouvant pas être plus alignés, les cases du job idéal / de la mission idéale étant toutes cochées, je ne pouvais pas laisser passer cette opportunité. Depuis mi-janvier, j'ai donc commencé une mission pour cette entreprise pour qu'on fasse connaissance et pour avancer rapidement sur le sujet le temps que d'autres points soient traités de leur côté. De jolies choses semblent se dessiner et devraient aboutir prochainement... 🤩

Affaire à suivre comme on dit, 2022 semble pleine de promesses, de défis et de changements ! 😎

IoT - Qualité de l'air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery

iotmqttarduinowarp10thingspeakco2esp32discoverydashboard

Le projet Nous Aérons propose de réaliser ses propres détecteurs de CO2 avec un ESP32 avec un écran comme le Lilygo TTGo T-Display et un capteur Senseair S8-LP.

L'idée est donc de déployer plusieurs capteurs, faire remonter les valeurs via ThingSpeak et ensuite les ingérer puis analyser avec Warp 10 et faire un dashboard avec Discovery.

schema du projet

Montage

Pour le montage, je vous invite à consuler principalement :

ThingSpeak

L'exemple de code fourni utilise le service ThingSpeak pour la remontée des valeurs. Comme il s'agit de mon premier projet Arduino et que cela fonctionne, j'ai cherché à rester dans les clous du code proposé et tester par la même occasion ce service. J'aurais pu directement poster les valeurs sur mon instance Warp 10 mais c'est aussi l'occasion de tester la récupération d'informations via le client MQTT de Warp 10.

Il vous faut :

  • Créer un compte
  • Créer un channel avec le champ 1 qui accueillera vos données
  • Récupérer la clé d'API en écriture
  • Noter votre Channel ID

Code Arduino

Disclaimer : c'est mon premier projet Arduino.

En repartant du code fourni sur le site Capteur de CO2, j'ai fait quelques ajustements :

  • Remplacer l'appel HTTP par la librairie ThingSpeak
  • La librairie retourne des status code : 200 si c'est OK, 40x si incorrect et -XXX si erreur ; j'ai un amélioré le message de debug pour savoir si l'insertion était OK ou KO.
  • Ajustement de positionnement du "CO2" et du "ppm" sur l'écran pour une meilleure lisibilité.

Il vous faut modifier :

  • la configuration wifi : ssid1, password1 et éventuellement ssid2, password2
  • la configuration ThingSpeak : channelID, writeAPIKey

Compiler le tout et uploader le code sur votre ESP32.

/************************************************
 * 
 *   Capteur de CO2 par Grégoire Rinolfi
 *   https://co2.rinolfi.ch
 * 
 ***********************************************/
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFiMulti.h>
#include <ThingSpeak.h>

WiFiMulti wifiMulti;
TFT_eSPI tft = TFT_eSPI(135, 240);

/************************************************
 * 
 *   Paramètres utilisateur
 * 
 ***********************************************/
 
#define TXD2 21         // série capteur TX
#define RXD2 22         // série capteur RX
#define BOUTON_CAL 35
#define DEBOUNCE_TIME 1000

const char* ssid1     = "wifi1";
const char* password1 = "XXXXXXXXXXXXXXXX";
const char* ssid2     = "wifi2";
const char* password2 = "XXXXXXXXXXXXXXXX";

unsigned long channelID = XXXXXXXXXXXXXXXX;
char* readAPIKey = "XXXXXXXXXXXXXXXX";
char* writeAPIKey = "XXXXXXXXXXXXXXXX";
unsigned int dataFieldOne = 1;                       // Field to write temperature data
const unsigned long postingInterval = 12L * 1000L;   // 12s
unsigned long lastTime = 0;

// gestion de l'horloge pour la validation des certificats HTTPS
void setClock() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");

  Serial.print(F("Waiting for NTP time sync: "));
  time_t nowSecs = time(nullptr);
  while (nowSecs < 8 * 3600 * 2) {
    delay(500);
    Serial.print(F("."));
    yield();
    nowSecs = time(nullptr);
  }

  Serial.println();
  struct tm timeinfo;
  gmtime_r(&nowSecs, &timeinfo);
  Serial.print(F("Current time: "));
  Serial.print(asctime(&timeinfo));
}

/************************************************
 * 
 * Thinkgspeak functions
 * https://fr.mathworks.com/help/thingspeak/read-and-post-temperature-data.html
 * 
 ***********************************************/

float readTSData( long TSChannel,unsigned int TSField ){
    
  float data =  ThingSpeak.readFloatField( TSChannel, TSField, readAPIKey );
  Serial.println( " Data read from ThingSpeak: " + String( data, 9 ) );
  return data;

}

// Use this function if you want to write a single field.
int writeTSData( long TSChannel, unsigned int TSField, float data ){
  int  writeSuccess = ThingSpeak.writeField( TSChannel, TSField, data, writeAPIKey ); // Write the data to the channel
  if(writeSuccess == 200){
    Serial.println("Channel updated successfully!");
  }
  else{
    Serial.println("Problem updating channel. HTTP error code " + String(writeSuccess));
  }
  return writeSuccess;
}

// Use this function if you want to write multiple fields simultaneously.
int write2TSData( long TSChannel, unsigned int TSField1, long field1Data, unsigned int TSField2, long field2Data ){

  ThingSpeak.setField( TSField1, field1Data );
  ThingSpeak.setField( TSField2, field2Data );
    
  int writeSuccess = ThingSpeak.writeFields( TSChannel, writeAPIKey );
  if(writeSuccess == 200){
    Serial.println("Channel updated successfully!");
  }
  else{
    Serial.println("Problem updating channel. HTTP error code " + String(writeSuccess));
  }
  return writeSuccess;
}

/************************************************
 * 
 *   Code de gestion du capteur CO2 via ModBus
 *   inspiré de : https://github.com/SFeli/ESP32_S8
 * 
 ***********************************************/
volatile uint32_t DebounceTimer = 0;

byte CO2req[] = {0xFE, 0X04, 0X00, 0X03, 0X00, 0X01, 0XD5, 0XC5};
byte ABCreq[] = {0xFE, 0X03, 0X00, 0X1F, 0X00, 0X01, 0XA1, 0XC3}; 
byte disableABC[] = {0xFE, 0X06, 0X00, 0X1F, 0X00, 0X00, 0XAC, 0X03};  // écrit la période 0 dans le registre HR32 à adresse 0x001f
byte enableABC[] = {0xFE, 0X06, 0X00, 0X1F, 0X00, 0XB4, 0XAC, 0X74}; // écrit la période 180
byte clearHR1[] = {0xFE, 0X06, 0X00, 0X00, 0X00, 0X00, 0X9D, 0XC5}; // ecrit 0 dans le registe HR1 adresse 0x00
byte HR1req[] = {0xFE, 0X03, 0X00, 0X00, 0X00, 0X01, 0X90, 0X05}; // lit le registre HR1 (vérifier bit 5 = 1 )
byte calReq[] = {0xFE, 0X06, 0X00, 0X01, 0X7C, 0X06, 0X6C, 0XC7}; // commence la calibration background
byte Response[20];
uint16_t crc_02;
int ASCII_WERT;
int int01, int02, int03;
unsigned long ReadCRC;      // CRC Control Return Code 

void send_Request (byte * Request, int Re_len)
{
  while (!Serial1.available())
  {
    Serial1.write(Request, Re_len);   // Send request to S8-Sensor
    delay(50);
  }

  Serial.print("Requete : ");
  for (int02 = 0; int02 < Re_len; int02++)    // Empfangsbytes
  {
    Serial.print(Request[int02],HEX);
    Serial.print(" ");
  }
  Serial.println();
  
}

void read_Response (int RS_len)
{
  int01 = 0;
  while (Serial1.available() < 7 ) 
  {
    int01++;
    if (int01 > 10)
    {
      while (Serial1.available())
        Serial1.read();
      break;
    }
    delay(50);
  }

  Serial.print("Reponse : ");
  for (int02 = 0; int02 < RS_len; int02++)    // Empfangsbytes
  {
    Response[int02] = Serial1.read();
    
    Serial.print(Response[int02],HEX);
    Serial.print(" ");
  }
  Serial.println();
}

unsigned short int ModBus_CRC(unsigned char * buf, int len)
{
  unsigned short int crc = 0xFFFF;
  for (int pos = 0; pos < len; pos++) {
    crc ^= (unsigned short int)buf[pos];   // XOR byte into least sig. byte of crc
    for (int i = 8; i != 0; i--) {         // Loop over each bit
      if ((crc & 0x0001) != 0) {           // If the LSB is set
        crc >>= 1;                         // Shift right and XOR 0xA001
        crc ^= 0xA001;
      }
      else                            // else LSB is not set
        crc >>= 1;                    // Just shift right
    }
  }  // Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
  return crc;  
}

unsigned long get_Value(int RS_len)
{

// Check the CRC //
  ReadCRC = (uint16_t)Response[RS_len-1] * 256 + (uint16_t)Response[RS_len-2];
  if (ModBus_CRC(Response, RS_len-2) == ReadCRC) {
    // Read the Value //
    unsigned long val = (uint16_t)Response[3] * 256 + (uint16_t)Response[4];
    return val * 1;       // S8 = 1. K-30 3% = 3, K-33 ICB = 10
  }
  else {
    Serial.print("CRC Error");
    return 99;
  }
}

// interruption pour lire le bouton d'étalonnage
bool demandeEtalonnage = false;
void IRAM_ATTR etalonnage() {
  if ( millis() - DEBOUNCE_TIME  >= DebounceTimer ) {
    DebounceTimer = millis();
    
    Serial.println("Etalonnage manuel !!");

    tft.fillScreen(TFT_BLACK);
    tft.setTextSize(3);
    tft.setTextColor(TFT_WHITE);
    tft.drawString("Etalonnage", tft.width() / 2, tft.height()/2);

    demandeEtalonnage = true;
  } 
}

// nettoie l'écran et affiche les infos utiles
void prepareEcran() {
  tft.fillScreen(TFT_BLACK);
  // texte co2 à gauche
  tft.setTextSize(4);
  tft.setTextColor(TFT_WHITE);
  tft.drawString("CO",25, 120);
  tft.setTextSize(3);
  tft.drawString("2",60, 125);

  // texte PPM à droite ppm
  tft.drawString("ppm",215, 120);

  // écriture du chiffre
  tft.setTextColor(TFT_GREEN,TFT_BLACK);
  tft.setTextSize(8);
}

void setup() {
  // bouton de calibration
  pinMode(BOUTON_CAL, INPUT);

  // ports série de debug et de communication capteur
  Serial.begin(115200);
  Serial1.begin(9600, SERIAL_8N1, RXD2, TXD2);

  // initialise l'écran
  tft.init();
  delay(20);
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextDatum(MC_DATUM); // imprime la string middle centre
  
  // vérifie l'état de l'ABC
  send_Request(ABCreq, 8);
  read_Response(7);
  Serial.print("Période ABC : ");
  Serial.printf("%02ld", get_Value(7));
  Serial.println();
  int abc = get_Value(7);

  // active ou désactive l'ABC au démarrage
  if(digitalRead(BOUTON_CAL) == LOW){
    if(abc == 0){
      send_Request(enableABC, 8);
    }else{
      send_Request(disableABC, 8);
    }
    read_Response(7);
    get_Value(7);
  }
  
  tft.setTextSize(2);
  tft.setTextColor(TFT_BLUE,TFT_BLACK);
  tft.drawString("Autocalibration", tft.width() / 2, 10);
  if( abc != 0 ){
    tft.drawString(String(abc)+"h", tft.width() / 2, 40);
  }else{
    tft.drawString("OFF", tft.width() / 2, 40);
  }

  // gestion du wifi
  wifiMulti.addAP(ssid1, password1);
  wifiMulti.addAP(ssid2, password2);

  Serial.print("Connexion au wifi");
  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE,TFT_BLACK);
  tft.drawString("Recherche wifi", tft.width() / 2, tft.height() / 2);
  
  int i = 0;
  while(wifiMulti.run() != WL_CONNECTED && i < 3){
    Serial.print(".");
    delay(500);
    i++;
  }
  if(wifiMulti.run() == WL_CONNECTED){
    tft.setTextColor(TFT_GREEN,TFT_BLACK);
    Serial.println("Connecté au wifi");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    tft.drawString("Wifi OK", tft.width() / 2, 100);
    setClock();
  }else{
    tft.setTextColor(TFT_RED,TFT_BLACK);
    Serial.println("Echec de la connexion wifi");
    tft.drawString("Pas de wifi", tft.width() / 2, 100);
  }
  delay(3000); // laisse un temps pour lire les infos

  // préparation de l'écran
  prepareEcran();

  //interruption de lecture du bouton
  attachInterrupt(BOUTON_CAL, etalonnage, FALLING);
}  

unsigned long ancienCO2 = 0;
int seuil = 0;

void loop() {

  // effectue l'étalonnage si on a appuyé sur le bouton
  if( demandeEtalonnage ){
    demandeEtalonnage = false;
    // nettoye le registre de verification
    send_Request(clearHR1, 8); 
    read_Response(8); 
    delay(100);
    // demande la calibration
    send_Request(calReq, 8); 
    read_Response(8); 
    delay(4500); // attend selon le cycle de la lampe

    // lit le registre de verification
    send_Request(HR1req, 8); 
    read_Response(7); 
    int verif = get_Value(7);
    Serial.println("resultat calibration "+String(verif));
    if(verif == 32){
      tft.setTextColor(TFT_GREEN);
      tft.drawString("OK", tft.width() / 2, tft.height()/2+30);
    }else{
      tft.setTextColor(TFT_RED);
      tft.drawString("Erreur", tft.width() / 2, tft.height()/2+20);
    }
    delay(3000);
    prepareEcran();
    seuil = 0;
  }

  // lecture du capteur
  send_Request(CO2req, 8);
  read_Response(7);
  unsigned long CO2 = get_Value(7);
  
  String CO2s = "CO2: " + String(CO2);
  Serial.println(CO2s);

  // efface le chiffre du texte
  if(CO2 != ancienCO2){
    tft.fillRect(0,0, tft.width(), 60, TFT_BLACK);
  }

  if( CO2 < 800 ){
    tft.setTextColor(TFT_GREEN,TFT_BLACK);
    if( seuil != 1 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Excellent", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 1;
  }else if( CO2 >= 800 && CO2 < 1000){
    tft.setTextColor(TFT_ORANGE,TFT_BLACK);
    if( seuil != 2 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Moyen", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 2;
  }else if (CO2 >= 1000 && CO2 < 1500){
    tft.setTextColor(TFT_RED,TFT_BLACK);
    if( seuil != 3 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Mediocre", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 3;
  }else{
    tft.setTextColor(TFT_RED,TFT_BLACK);
    if( seuil != 4 ){
      tft.setTextSize(2);
      tft.fillRect(0,61, tft.width(), 25, TFT_BLACK);
      tft.drawString("Air Vicie", tft.width() / 2, tft.height() / 2 + 10);
    }
    seuil = 4;
  }

  tft.setTextSize(8);
  tft.drawString(String(CO2), tft.width() / 2, tft.height() / 2 - 30);


  // envoi de la valeur sur le cloud
  if((millis() - lastTime) >= postingInterval) {
    if((wifiMulti.run() == WL_CONNECTED)) {
      WiFiClient client;
      ThingSpeak.begin( client );
      
      writeTSData( channelID , dataFieldOne , CO2 );  
      lastTime = millis();
    }
  }

  ancienCO2 = CO2;
  delay(10000); // attend 10 secondes avant la prochaine mesure
}

MQTT

Sur ThingSpeak, aller dans Devices > MQTT et compléter si besoin avec la lecture de la documentation MQTT Basics:

  • Créer un device,
  • Ajouter la/les channel(s) voulu(s),
  • Limiter les droits à la partie "subscribe" ; notre client en effet n'est pas prévu pour publier des données sur ThingSpeak,
  • Conserver précautionneusement vos identifiants MQTT.

Sur l'instance Warp 10, déployer le plugin MQTT :

Avec le script /path/to/warp10/mqtt/test.mc2 :

// subscribe to the topics, attach a WarpScript™ macro callback to each message
// the macro reads ThingSpeak message to extract the first byte of payload,
// the server timestamp, the channel id and the value

'Loading MQTT ThingSpeak Air Quality Warpscript™' STDOUT
{
  'host' 'mqtt3.thingspeak.com'
  'port' 1883
  'user' 'XXXXXXXXXX'
  'password' 'XXXXXXXXXX'
  'clientid' 'XXXXXXXXXX'
  'topics' [
    'channels/channelID 1/subscribe'
    'channels/channelID 2/subscribe'
    'channels/channelID 3/subscribe'
  ]
  'timeout' 20000
  'parallelism' 1
  'autoack' true

  'macro'
  <%
    //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
    'message' STORE
    <% $message ISNULL ! %>
    <%
      // message structure :
      // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
      $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
      $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
      $TSmessage 'channel_id' GET 'channelId' STORE
      $TSmessage 'field1' GET 'sensorValue' STORE

      $message MQTTTOPIC ' ' +
      $ts ISO8601 + ' ' +
      $channelId TOSTRING + ' ' +
      $sensorValue +
      STDOUT // print to warp10.log
    %> IFT
  %>
}

Vous devriez avoir dans /path/to/warp10/log/warp10.log :

Loading MQTT ThingSpeak Air Quality Warpscript™
channels/<channelID 1>/subscribe 2022-01-11T10:30:51.000000Z <channelID 1> 820.00000
channels/<channelID 2>/subscribe 2022-01-11T10:30:53.000000Z <channelID 2> 715.00000
channels/<channelID 3>/subscribe 2022-01-11T10:30:54.000000Z <channelID 3> 410.00000

Maintenant que l'intégration MQTT est validée, supprimez ce fichier et passons à la gestion de la persistence des données dans Warp 10.

Avec le script suivant :

// subscribe to the topics, attach a WarpScript™ macro callback to each message
// the macro reads ThingSpeak message to extract the first byte of payload,
// the server timestamp, the channel id and the value.

{
  'host' 'mqtt3.thingspeak.com'
  'port' 1883
  'user' 'XXXXXXXXXX'
  'password' 'XXXXXXXXXX'
  'clientid' 'XXXXXXXXXX'
  'topics' [
    'channels/channelID 1/subscribe'
    'channels/channelID 2/subscribe'
    'channels/channelID 3/subscribe'
  ]
  'timeout' 20000
  'parallelism' 1
  'autoack' true

  'macro'
  <%
    //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
    'message' STORE
    <% $message ISNULL ! %>
    <%
      // message structure :
      // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
      $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
      $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
      $TSmessage 'channel_id' GET 'channelId' STORE
      $TSmessage 'field1' GET 'sensorValue' STORE

      // Tableau de correspondance entre mes channel IDs et mes devices en vue de définir des labels pour les GTS
      {
        <channelID 1> 'air1'
        <channelID 2> 'air2'
        <channelID 3> 'air3'
      } 'deviceMap' STORE
      // Récupération du nom du device dans la variable senssorId
      $deviceMap $channelId GET 'sensorId' STORE

      // Création d'une GTS air.quality.home
      // Le label "device" aura pour valeur le nom du device, via la variable sensorId
      // On crée une entrée qui correspond à la valeur que nous venons de récupérer
      // sensorValue est une string, il faut la repasser sur un format numérique
      // Une fois la GTS reconstituée avec son entrée, on la periste en base via UPDATE
      '<writeToken>' 'writeToken' STORE
      NEWGTS 'air.quality.home' RENAME
      { 'device' $sensorId } RELABEL
      $ts NaN NaN NaN $sensorValue TODOUBLE TOLONG ADDVALUE
      $writeToken UPDATE
    %> IFT
  %>
}

Depuis le WarpStudio, vérifiez la disposnibilité de vos données :

'<readToken>' 'readToken'  STORE
[ $readToken 'air.quality.home' {} NOW -1000 ] FETCH

warp10 - validation de l'ingestion des données IoT

Ensuite, il nous reste plus qu'à faire une petite macro et un dashboard pour présenter les données.

Pour la macro :

  • On lui passe un nom de device en paramètre qui servira à filtrer sur le label
  • Elle retourne une GTS avec l'ensemble des valeurs disponibles
<%
  {
    'name' 'cerenit/iot/co2'
    'desc' 'Provide CO2 levels per device'
    'sig' [ [ [ [  'device:STRING' ] ]  [ 'result:GTS' ] ] ]
    'params' {
        'device' 'String'
        'result' 'GTS'
    }
    'examples' [
    <'
air1 @cerenit/iot/co2
    '>
    ]
  } INFO

  // Actual code
  SAVE 'context' STORE

  'device' STORE // Save parameter as year

  '<readToken>' 'readToken' STORE
  [ $readToken 'air.quality.home' { 'device' $device } MAXLONG MINLONG ] FETCH
  0 GET

  $context RESTORE
%>
'macro' STORE
$macro

Et pour le dashboard Discovery :

  • Chaque tile utilise la macro que l'on vient de réaliser en lui passant le device en paramètre,
  • Chaque tile affiche un système de seuils avec des couleurs associées.
<%
{
    'title' 'Home CO2 Analysis'
    'description' 'esp32 + Senseair S8 sensors at home'
    'options' {
      'scheme' 'CHARTANA'
    }
    'tiles' [
        {
            'title' 'Informations'
            'type' 'display'
            'w' 6 'h' 1 'x' 0 'y' 0
            'data' {
                'data' 'D&eacute;tails et informations compl&eacute;mentaires :  <a href="https://www.cerenit.fr/blog/air-quality-iot-esp32-senseair-thingspeak-mqtt-warp10-discovery/">IoT - Qualit&eacute; de l air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery</a>'
            }
        }
        {
            'title' 'Device AIR1'
            'type' 'line'
            'w' 6 'h' 2 'x' 0 'y' 1
            'macro' <% 'air1' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
	        ]
	    }
        }
        {
            'title' 'Device AIR2'
            'type' 'line'
            'w' 6 'h' 2 'x' 6 'y' 1
            'macro' <% 'air2' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
	        ]
	    }
        }
        {
            'title' 'Device AIR3'
            'type' 'line'
            'w' 6 'h' 2 'x' 0 'y' 3
            'macro' <% 'air3' @cerenit/macros/co2 %>
            'options' {
                'thresholds' [
                    { 'value' 400 'color' '#008000' }
                    { 'value' 600 'color' '#329932' }
                    { 'value' 800 'color' '#66b266' }
                    { 'value' 960 'color' '#ffdb99' }
                    { 'value' 1210 'color' '#ffa500' }
                    { 'value' 1760 'color' '#ff0000' }
                ]
           }
        }
    ]
}
{ 'url' 'https://w.ts.cerenit.fr/api/v0/exec'  }
@senx/discovery2/render
%>

Le résultat est alors :

warp10 - dashboard IoT CO2

Bilan de ce que nous avons vu :

  • Comment monter son capteur de CO2 en profitant des ressources mises à disposition par le projet "Nous Aérons",
  • Comment envoyer les données du capteur vers le service ThingSpeak,
  • Comment récupérer les données du service ThingSpeak via le protoole MQTT et les stocker dans Warp 10,
  • Comment créer un dashboard Discovery avec une macro permettant de récupérer les données et mettre en place un système de seuils.

L'ensemble des fichiers peuvent être récupérés depuis cerenit/iot-air-quality.

Web, Ops, Data et Time Series - Mars 2021

gkegcpkubernetesnomadhashicorpconsulvaulttimescalewarp10iotptsmtsfrsparkdatabrickstempoindluxdata

La prochaine édition de Time Series France aura lieu le mardi 30 Mars à 18h avec la présentation de la base StuteoDB, basée sur Apache Cassandra. Par ici pour les détails et inscriptions.

Container et orchestration

Time Series

Web, Ops & Data - Janvier 2021

timeseriesprometheuspromqlovhcloudiotopenhabvectortimescaledbptsmanomalielabelmachine-learningiacansiblelibsshvectorlogwarp10influxdbopensshgpgpodmandocker-composesudo

Cloud

Code

  • GitLab release feature report : le code qui permet de générer le rapport ce qui a changé entre les versions de Gitlab.
  • SSH is the new GPG : les dernières versions d'OpenSSH permettent de signer un fichier. Une solution intermédiaire entre de la signature de fichiers à base de MD5 & co qui donnent des informations de conformité mais sans indiquer qui a signé le fichier et une solution GPG plus complexe à mettre en oeuvre ?

Container et orchestration

  • Using Podman and Docker Compose : podman, le "daemonless container engine" va permettre d'être utilisé avec docker-compose dans le cadre de la version 3.0. De quoi favoriser l'adoption de podman ?

Infra as code

  • New LibSSH Connection Plugin for Ansible Network Replaces Paramiko, Adds FIPS Mode Enablement : Ansible change de librairie pour les connexions ssh en remplaçant paramiko par libssh. Elle se veut plus performante et peut être requis dans un contexte demandant du FIPS. Pensez à installer le paquet libssh-dev(el) suivant votre distribution pour pouvoir installer ansible-pylibssh. Mes premiers essais ne notent pas une amélioration sensible des performances... à voir sur d'autres machines et dans la durée...

IoT

  • openHAB 3.0 Release et Release Notes : OpenHAB est une plateforme open source de gestion de périphétiques IoT et d'automatisation autour de ces périphériques. Elle est développée en Java, support 2000 "Things" (objets, équipements, protocoles). La version 3.0 apporte une refonte et l'unification de l'UI et des composants, le passage à Java 11 et plein d'autres choses. La migration depuis une version 2.x se fait assez simplement. Avec le nouveau moteur de règle, j'ai pu supprimer mon code spécifique. Reste encore la partie "Pages" à appréhender... J'avais préféré OpenHAB à Jeedom et Home Assistant
  • Meet Raspberry Silicon: Raspberry Pi Pico now on sale at $4 : la fondation Raspberry Pi se lance dans les micro-controlleurs avec le Pico au prix de 4$.
  • Raspberry Pi PICO la carte Microcontrôleur de la Fondation : un article très détaillé sur la prise en main du pico.

Observabilité

Système

Time Series

Bilan 2020 et perspectives 2021

bilanperspectivecérénittimeseriesbigdatahebdoinfluxaceiot

Routine habituelle de début d'année pour la clôture de ce 4ème exercice (déjà !).

Bilan 2020

Au global, une bonne année au regard des conditions - les objectifs sont remplis.

D'un point de vue comptable, cela donne :

2020201920182017Variation n/n-1
Chiffre d'affaires~138 K€~150 K€~132 K€~100 K€-8%
Résultat après impôts~8 K€~13.5 K€~10 K€~20 K€-41%
Jours facturés175197178160-11%
TJM789€761€742€625€+3.6%

Contrairement aux autres années, les jours facturés ne prennent plus en compte des prestatations forfaitaires (comme l'infogérance, etc) pour lesquelles je faisais un équivalent jour. J'ai ajusté les valeurs de ce tableau mais je n'ai pas mis à jour les synthèses 2019, 2018 et 2017. Cela a pour conséquence d'améliorer sensiblement le TJM.

L'épisode COVID n'a pas eu d'impact direct sur mon activité et je fais un chiffre d'affaire conforme à ce que j'avais prévu en début d'année. Clairement, je mesure ma chance d'avoir passé cette année sans encombres professionnels. J'avais dit que je passerai à 4/5 sur l'année. Dès lors je ne pouvais envisager de factuer plus de 80% des jours ouvrés et et je parviens à en factuer 77% (toujours hors prestatations forfaitaires). En faisait un TJM de 700€ et 80% des jours ouvrés, cela me donnait un chiffre d'affaires à atteindre de 128 K€. J'atteins à peu près cet objectif avec les jours facturés et je le dépasse grâce aux prestations forfaitaires. Ces prestations forfaitaires ayant sensiblement augmenté en 2020 (passage de ~10K€ à ~13K€) et même si l'une d'entre elles a généré un investissement matériel important et qui sera compensé sur les prochaines années. Cela explique principalement la chute du résultat (si on prend 2018 comme année de comparaison, pour une chiffre d'affaire et un volume de jours facturés similaire, le résultat est 20% inférieur).

Comme chaque année, j'en profite pour remercier Fabrice pour son accompagnement en tant qu'expert-comptable. Je le dis et le répête, mais avoir confiance dans son expert comptable et pouvoir compter sur lui pour apporter de bons conseils aux bons moments et être serein sur la gestion de l'entreprise, c'est indispensable - surtout en cette période. Même si je n'en ai pas bénéficié directement, les informations transmises pendant cette période sur les aides et autres mécanismes mis en place ont été très utiles.

D'un point de vue activité, c'est une bonne année en termes de contenus de missions :

Pour le reste, j'ai le plaisir de :

Petite déception toutefois sur la partie développement, où je n'ai pas pu me mettre sérieusement à Go ou Rust.

Enfin, je m'étais posé la question du rôle social d'une entreprise dans notre société en temps de COVID. Ma contribution a certes été modeste dans la limite de ce qui était autorisé par la loi d'une part et ne sachant pas trop comment se finirait l'année d'autre part. Je pense que je vais continuer dans cette voie et voir quel(s) projet(s) je pourrai soutenir en 2021. Content d'avoir contribué au projet Makair et de voir comment il évolue en tous cas.

Perspectives 2021

L'année commence bien avec la suite de la mission Warp 10/InfluxDB dans le monde nautique mentionnée précédemment. A celà s'ajoute une autre mission de conseil autour des usages de séries temporelles pour un autre acteur de l'énergie. J'ai du décliner un troisième appel d'offre sur un sujet similaire du fait de mes engagements actuels, mais j'espère qu'il y aura d'autres projets similaires.

Ayant aussi découvert le monde de l'impression 3D durant le premier confinement et plus récemment à jouer avec des cartes micro:bit (et peut être bientôt des ESP32), j'irais bien voir du coté de l'IoT et donner une dimension "plus industrielle" à mes usages de séries temporelles. Sortir des usages de monitoring serveur pour les séries temporelles et aller vers des usages plus industriels ou métiers est clairement intéressant. Osons le terme: direction l'industrie 4.0 !

Pour rebondir sur cette dimension usage, j'ambitionne pour le Paris Time Series Meetup d'avoir un focus usage plus important et avoir des retours d'expérience (et moins de présentation produit par des éditeurs).

Sur BigData Hebo, nous venons de lancer les brèves afin de mettre en avant les contributions des membres de la communauté. A suivre !

Pour le développement en Go et Rust, le premier devrait voir le jour dans l'année de façon assez certaine, c'est plus incertain pour le second.

Et enfin, pour le projet commencé en septembre et dont je ne peux pas encore parler, j'espère pouvoir lever le voile prochainement !

Si certains sujets vous interpellent ou si vous avez des contacts à me suggérer, n'hésitez pas à me contacter.

1 / 1