Le programme COP...CLAUDE
Le programme COP...CLAUDE est disponible ci-dessous.
D'origine il est de plus de 300 lignes au format .txt
on espère que la copie est resté fidèle pour l'installer en suivant les consignes.
/************************************************************
* RELAIS-SENIORS — Programme de surveillance automatique
*
* Ce programme interroge les prises Konyks (via le service
* Tuya) deux fois par jour et colorie le tableau :
* - vers 9 h : case VERTE si la prise répond (allumée)
* - vers 21 h : case GRISE si la prise est éteinte / hors ligne
* - case restée BLANCHE = pas de signal attendu
* => e-mail d'alerte automatique au Veilleur.
*
* Le tableau du mois se crée tout seul dans la feuille
* Google Sheets à laquelle ce programme est attaché.
*
* À PERSONNALISER : uniquement la partie CONFIG ci-dessous —
* remplacez chaque « A_REMPLIR » (rien d'autre à toucher,
* aucun numéro de ligne à connaître).
* Puis exécuter une fois la fonction installer (menu en haut).
*
* Programme « COP-CLAUDE » créé par l'association RELAIS-SENIORS
* (relais-seniors.fr) avec l'aide de Claude (Anthropic).
* Diffusé librement : prenez-le, adaptez-le, veillez-vous.
************************************************************/
var CONFIG = {
// --- Nom de votre association (affiché sur le tableau et les alertes) ---
NOM_ASSOCIATION: "A_REMPLIR", // exemple : "RELAIS-SENIORS"
// --- Clés du compte développeur Tuya (voir guide, étape 2) ---
TUYA_ACCESS_ID: "A_REMPLIR", // "Access ID/Client ID"
TUYA_ACCESS_SECRET: "A_REMPLIR", // "Access Secret/Client Secret"
// Serveur Tuya pour l'Europe (ne pas changer si le projet
// Tuya a été créé sur "Central Europe Data Center")
TUYA_HOST: "https://openapi.tuyaeu.com",
// --- Qui prévenir en cas d'alerte (une ou plusieurs adresses) ---
EMAILS_ALERTE: ["A_REMPLIR"], // adresse e-mail du Veilleur
// --- Les personnes suivies (5 maximum dans le tableau) ---
// deviceId = "ID de l'appareil" visible dans le projet Tuya
// Laisser deviceId: "" pour une ligne non utilisée.
PERSONNES: [
{ numero: 1, nom: "A_REMPLIR", adresse: "A_REMPLIR", deviceId: "A_REMPLIR" },
{ numero: 2, nom: "", adresse: "", deviceId: "" },
{ numero: 3, nom: "", adresse: "", deviceId: "" },
{ numero: 4, nom: "", adresse: "", deviceId: "" },
{ numero: 5, nom: "", adresse: "", deviceId: "" }
]
};
// Couleurs des cases
var VERT = "#00b050"; // prise allumée le matin
var GRIS = "#999999"; // prise éteinte le soir
var BLANC = "#ffffff"; // pas de signal => alerte
var JOUR_INEXISTANT = "#404040"; // jours 29-31 absents du mois
/************************************************************
* FONCTIONS À EXÉCUTER À LA MAIN (menu déroulant en haut)
************************************************************/
/** À exécuter UNE FOIS après avoir rempli CONFIG :
* crée le tableau du mois et programme les vérifications
* quotidiennes de 9 h et de 21 h. */
function installer() {
// Supprime d'éventuels anciens déclencheurs
ScriptApp.getProjectTriggers().forEach(function (t) {
ScriptApp.deleteTrigger(t);
});
// Vérification du matin : entre 9 h et 10 h chaque jour
ScriptApp.newTrigger("verificationMatin")
.timeBased().everyDays(1).atHour(9).nearMinute(15).create();
// Vérification du soir : entre 21 h et 22 h chaque jour
ScriptApp.newTrigger("verificationSoir")
.timeBased().everyDays(1).atHour(21).nearMinute(15).create();
feuilleDuMois(new Date()); // crée le tableau du mois en cours
try {
SpreadsheetApp.getUi().alert(
"Installation terminée.\n\n" +
"Le tableau sera vérifié chaque jour entre 9h et 10h, " +
"puis entre 21h et 22h.\n" +
"Alerte envoyée à : " + CONFIG.EMAILS_ALERTE.join(", "));
} catch (e) { /* le tableau créé dans la feuille confirme l'installation */ }
}
/** Pour tester tout de suite la liaison avec les prises :
* affiche l'état de chaque prise sans rien colorier. */
function testerMaintenant() {
var lignes = [];
CONFIG.PERSONNES.forEach(function (p) {
if (!p.deviceId || p.deviceId === "A_REMPLIR" || p.deviceId === "") return;
try {
var etat = etatPrise(p.deviceId);
lignes.push("N°" + p.numero + " " + p.nom + " : " +
(etat.enLigne ? (etat.allumee ? "ALLUMÉE (vert)" : "en ligne mais éteinte")
: "HORS LIGNE (gris)"));
} catch (e) {
lignes.push("N°" + p.numero + " " + p.nom + " : ERREUR — " + e.message);
}
});
if (lignes.length === 0) lignes.push("Aucune prise configurée dans CONFIG.");
ecrireDansTest("État actuel des prises", lignes);
}
/************************************************************
* VÉRIFICATIONS AUTOMATIQUES (lancées par les déclencheurs)
************************************************************/
function verificationMatin() { verification(true); }
function verificationSoir() { verification(false); }
function verification(matin) {
var maintenant = new Date();
var feuille = feuilleDuMois(maintenant);
var jour = maintenant.getDate(); // 1 à 31
var colonne = 3 + jour; // jour 1 = colonne D (4)
var alertes = [];
CONFIG.PERSONNES.forEach(function (p, i) {
if (!p.deviceId || p.deviceId === "A_REMPLIR" || p.deviceId === "") return;
var ligne = 5 + i * 2 + (matin ? 0 : 1); // ligne 9h ou ligne 21h
var cellule = feuille.getRange(ligne, colonne);
var etat;
try {
etat = etatPrise(p.deviceId);
} catch (e) {
// Impossible d'interroger Tuya : case blanche + alerte technique
alertes.push("N°" + p.numero + " " + p.nom + " (" + p.adresse +
") : ERREUR TECHNIQUE — " + e.message);
cellule.setBackground(BLANC);
return;
}
if (matin) {
if (etat.enLigne && etat.allumee) {
cellule.setBackground(VERT); // signal du matin reçu
} else {
cellule.setBackground(BLANC); // pas de signal => alerte
alertes.push("N°" + p.numero + " " + p.nom + " (" + p.adresse +
") : PAS DE SIGNAL ce matin — la prise n'a pas été allumée avant 9h.");
}
} else {
if (!etat.enLigne || !etat.allumee) {
cellule.setBackground(GRIS); // extinction constatée
} else {
cellule.setBackground(BLANC); // toujours allumée => alerte
alertes.push("N°" + p.numero + " " + p.nom + " (" + p.adresse +
") : la prise est TOUJOURS ALLUMÉE à 21h — extinction non faite.");
}
}
});
if (alertes.length > 0) {
MailApp.sendEmail(
CONFIG.EMAILS_ALERTE.join(","),
"⚠ ALERTE " + CONFIG.NOM_ASSOCIATION + " — vérification de " + (matin ? "9 h" : "21 h"),
"Vérification du " + Utilities.formatDate(maintenant, Session.getScriptTimeZone(), "dd/MM/yyyy à HH:mm") + "\n\n" +
alertes.join("\n\n") +
"\n\nMerci de prendre contact ou de déclencher la procédure d'aide.\n" +
"— Programme automatique " + CONFIG.NOM_ASSOCIATION + " (COP-CLAUDE)");
}
}
/************************************************************
* INTERROGATION DES PRISES via le service Tuya
************************************************************/
/** Renvoie { enLigne: vrai/faux, allumee: vrai/faux } pour une prise.
* Interroge la porte moderne v2.0 de Tuya (la seule ouverte
* aux comptes récents — vérifié à l'API Explorer le 27/08/2026). */
function etatPrise(deviceId) {
var reponse = requeteTuya("GET", "/v2.0/cloud/thing/" + deviceId + "/shadow/properties");
var allumee = false;
((reponse && reponse.properties) || []).forEach(function (p) {
if (p.code === "switch_1" || p.code === "switch") allumee = (p.value === true);
});
// La réponse a été obtenue : la fiche de la prise est joignable.
return { enLigne: true, allumee: allumee };
}
/** Appel signé à l'API Tuya (signature HMAC-SHA256 officielle). */
function requeteTuya(methode, chemin) {
var token = jetonTuya();
var t = String(new Date().getTime());
var corpsHache = sha256Hex(""); // corps vide pour un GET
var aSigner = methode + "\n" + corpsHache + "\n" + "\n" + chemin;
var signature = hmacHex(CONFIG.TUYA_ACCESS_ID + token + t + aSigner,
CONFIG.TUYA_ACCESS_SECRET);
var reponse = UrlFetchApp.fetch(CONFIG.TUYA_HOST + chemin, {
method: methode.toLowerCase(),
headers: {
"client_id": CONFIG.TUYA_ACCESS_ID,
"access_token": token,
"t": t,
"sign": signature,
"sign_method": "HMAC-SHA256"
},
muteHttpExceptions: true
});
var json = JSON.parse(reponse.getContentText());
if (!json.success) throw new Error("Tuya : " + (json.msg || "réponse invalide") + " (code " + json.code + ")");
return json.result;
}
/** Obtient (et garde en mémoire ~1h30) le jeton d'accès Tuya. */
function jetonTuya() {
// Ticket neuf à chaque passage (pas de mise en mémoire :
// supprime définitivement les pannes "token invalid").
var chemin = "/v1.0/token?grant_type=1";
var t = String(new Date().getTime());
var aSigner = "GET" + "\n" + sha256Hex("") + "\n" + "\n" + chemin;
var signature = hmacHex(CONFIG.TUYA_ACCESS_ID + t + aSigner,
CONFIG.TUYA_ACCESS_SECRET);
var reponse = UrlFetchApp.fetch(CONFIG.TUYA_HOST + chemin, {
method: "get",
headers: {
"client_id": CONFIG.TUYA_ACCESS_ID,
"t": t,
"sign": signature,
"sign_method": "HMAC-SHA256"
},
muteHttpExceptions: true
});
var json = JSON.parse(reponse.getContentText());
if (!json.success) throw new Error("Connexion Tuya refusée : " + (json.msg || "?") +
" — vérifier TUYA_ACCESS_ID et TUYA_ACCESS_SECRET dans CONFIG.");
return json.result.access_token;
}
// --- petits outils de calcul de signature ---
function sha256Hex(texte) {
return octetsVersHex(Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256, texte, Utilities.Charset.UTF_8));
}
function hmacHex(texte, cle) {
return octetsVersHex(Utilities.computeHmacSha256Signature(
texte, cle, Utilities.Charset.UTF_8)).toUpperCase();
}
function octetsVersHex(octets) {
return octets.map(function (o) {
var v = (o < 0 ? o + 256 : o).toString(16);
return v.length === 1 ? "0" + v : v;
}).join("");
}
/************************************************************
* CRÉATION AUTOMATIQUE DU TABLEAU DU MOIS
************************************************************/
var MOIS_FR = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet",
"Août", "Septembre", "Octobre", "Novembre", "Décembre"];
function feuilleDuMois(date) {
var nom = MOIS_FR[date.getMonth()] + " " + date.getFullYear();
var classeur = SpreadsheetApp.getActive();
var feuille = classeur.getSheetByName(nom);
if (feuille) return feuille;
feuille = classeur.insertSheet(nom, 0);
var DERNIERE_COL = 34; // colonne AH = jour 31
// Titre
feuille.getRange(1, 1, 1, DERNIERE_COL).merge()
.setValue(CONFIG.NOM_ASSOCIATION).setFontSize(20).setFontWeight("bold")
.setHorizontalAlignment("center");
feuille.getRange(2, 1, 1, DERNIERE_COL).merge()
.setValue(nom).setFontStyle("italic").setFontSize(12)
.setHorizontalAlignment("center");
// En-têtes
feuille.getRange(4, 1).setValue("N°");
feuille.getRange(4, 2).setValue("Nom et adresse");
feuille.getRange(4, 3).setValue("Heure");
for (var j = 1; j <= 31; j++) feuille.getRange(4, 3 + j).setValue(j);
feuille.getRange(4, 1, 1, DERNIERE_COL)
.setFontWeight("bold").setHorizontalAlignment("center")
.setBackground("#d9e2f3");
// 5 personnes x 2 lignes (9h / 21h)
CONFIG.PERSONNES.forEach(function (p, i) {
var l = 5 + i * 2;
feuille.getRange(l, 1, 2, 1).merge().setValue(p.numero)
.setFontWeight("bold").setFontSize(14)
.setHorizontalAlignment("center").setVerticalAlignment("middle");
feuille.getRange(l, 2, 2, 1).merge()
.setValue(p.nom ? p.nom + "\n" + p.adresse : "")
.setWrap(true).setVerticalAlignment("middle");
feuille.getRange(l, 3).setValue("9 h");
feuille.getRange(l + 1, 3).setValue("21 h");
feuille.getRange(l, 3, 2, 1).setFontWeight("bold")
.setHorizontalAlignment("center").setBackground("#f2f2f2");
});
// Bordures : fines partout, épaisses entre les personnes
var bloc = feuille.getRange(4, 1, 11, DERNIERE_COL);
bloc.setBorder(true, true, true, true, true, true);
for (var i2 = 0; i2 <= 5; i2++) {
feuille.getRange(4 + i2 * 2, 1, 1, DERNIERE_COL)
.setBorder(null, null, true, null, null, null,
"black", SpreadsheetApp.BorderStyle.SOLID_THICK);
}
// Jours inexistants du mois (ex. 31 avril) : colonnes noircies
var joursDansLeMois = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
if (joursDansLeMois < 31) {
feuille.getRange(5, 3 + joursDansLeMois + 1, 10, 31 - joursDansLeMois)
.setBackground(JOUR_INEXISTANT);
}
// Largeurs de colonnes et lignes
feuille.setColumnWidth(1, 35);
feuille.setColumnWidth(2, 210);
feuille.setColumnWidth(3, 45);
for (var c = 4; c <= DERNIERE_COL; c++) feuille.setColumnWidth(c, 34);
for (var r = 5; r <= 14; r++) feuille.setRowHeight(r, 28);
// Légende
feuille.getRange(16, 1, 1, DERNIERE_COL).merge().setValue(
"VERT = signal du matin reçu (prise allumée avant 9 h) • " +
"GRIS = extinction du soir constatée (21 h) • " +
"BLANC = pas de signal : ALERTE — appeler / déclencher l'aide.")
.setFontStyle("italic").setFontSize(10).setWrap(true);
// Fige les colonnes de gauche et l'en-tête
feuille.setFrozenRows(4);
// feuille.setFrozenColumns(3); (retiré : incompatible avec le titre fusionné)
return feuille;
}
/************************************************************
* OUTILS
************************************************************/
/** Écrit des lignes de résultat dans l'onglet "Test" de la feuille. */
function ecrireDansTest(titre, lignes) {
var classeur = SpreadsheetApp.getActive();
var f = classeur.getSheetByName("Test") || classeur.insertSheet("Test");
f.clear();
f.getRange(1, 1).setValue(titre + " — " +
Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "dd/MM/yyyy HH:mm"))
.setFontWeight("bold");
lignes.forEach(function (l, i) { f.getRange(2 + i, 1).setValue(l); });
f.setColumnWidth(1, 600);
}
/** Déposition brute de la prise n°1, écrite dans l'onglet "Test". */
function etatBrut() {
var lignes = [];
var id = CONFIG.PERSONNES[0].deviceId;
try {
var r = requeteTuya("GET", "/v2.0/cloud/thing/" + id + "/shadow/properties");
lignes.push(JSON.stringify(r));
} catch (e) { lignes.push("ERREUR : " + e.message); }
ecrireDansTest("État brut de la prise", lignes);
}
// ============================================================
// === FIN DU PROGRAMME — si cette ligne est visible tout ===
// === en bas dans Apps Script, le collage est COMPLET. ===
// ============================================================