Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

  1. ioBroker Community Home
  2. Deutsch
  3. Tester
  4. Test Withings v0.0.x

NEWS

  • UPDATE 31.10.: Amazon Alexa - ioBroker Skill läuft aus ?
    apollon77A
    apollon77
    48
    3
    8.4k

  • Monatsrückblick – September 2025
    BluefoxB
    Bluefox
    13
    1
    2.0k

  • Neues Video "KI im Smart Home" - ioBroker plus n8n
    BluefoxB
    Bluefox
    15
    1
    2.5k

Test Withings v0.0.x

Geplant Angeheftet Gesperrt Verschoben Tester
550 Beiträge 51 Kommentatoren 127.8k Aufrufe 45 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • T Tino 0

    Hallo,
    ich habe mal chatGPT gefragt ob er nicht die letzten Messwerte in einem eigenen Datenpunkt speichern kann. Dabei ist das rausgekommen:

    Screenshot 2025-11-17 230600.png

    Es wurden 2 Blöcke hinzugefügt und eine Zeile geändert. Wer es Testen möchte hier ist die main.js

    "use strict";
    
    /*
    * Created with @iobroker/create-adapter v2.0.1
    */
    
    // The adapter-core module gives you access to the core ioBroker functions
    // you need to create an adapter
    const utils = require("@iobroker/adapter-core");
    const axios = require("axios");
    const qs = require("qs");
    const Json2iob = require("./lib/json2iob");
    const tough = require("tough-cookie");
    const { HttpsCookieAgent } = require("http-cookie-agent/http");
    
    class Withings extends utils.Adapter {
     /**
      * @param {Partial<utils.AdapterOptions>} [options={}]
      */
     constructor(options) {
       super({
         ...options,
         name: "withings",
       });
       this.on("ready", this.onReady.bind(this));
       this.on("stateChange", this.onStateChange.bind(this));
       this.on("unload", this.onUnload.bind(this));
       this.deviceArray = [];
       this.json2iob = new Json2iob(this);
     }
    
     /**
      * Is called when databases are connected and adapter received configuration.
      */
     async onReady() {
       // Reset the connection indicator during startup
       this.setState("info.connection", false, true);
       if (this.config.interval < 0.5) {
         this.log.info("Set interval to minimum 0.5");
         this.config.interval = 0.5;
       }
       if (!this.config.lastDays || this.config.lastDays < 1) {
         this.config.lastDays = 1;
       }
       if (!this.config.lastHours || this.config.lastHours < 1) {
         this.config.lastHours = 1;
       }
       if (!this.config.username || !this.config.password || !this.config.clientid || !this.config.clientsecret) {
         this.log.error("Please set username and password in the instance settings");
         return;
       }
       this.userAgent = "ioBroker v0.0.1";
       this.cookieJar = new tough.CookieJar();
       this.requestClient = axios.create({
         jar: this.cookieJar,
         withCredentials: true,
         httpsAgent: new HttpsCookieAgent({ cookies: { jar: this.cookieJar } }),
       });
    
       this.updateInterval = null;
       this.reLoginTimeout = null;
       this.refreshTokenTimeout = null;
       this.session = [];
       await this.cleanOldVersion();
       this.subscribeStates("*");
    
       await this.login();
    
       if (this.session.length > 0) {
         await this.getDeviceList();
         await this.updateDevices();
         this.updateInterval = setInterval(async () => {
           await this.updateDevices();
         }, this.config.interval * 60 * 1000);
         this.refreshTokenInterval = setInterval(() => {
           this.refreshToken();
         }, this.session[0].expires_in * 1000);
       }
     }
     async login() {
       let loginHtml = await this.requestClient({
         method: "get",
         url:
           "https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=" +
           this.config.clientid +
           "&state=h4fhjnc2daoc3m&scope=user.activity,user.metrics,user.info&redirect_uri=http://localhost",
         headers: {
           Accept: "*/*",
           "User-Agent": this.userAgent,
         },
         jar: this.cookieJar,
         withCredentials: true,
       })
         .then((res) => {
           this.log.debug(JSON.stringify(res.data));
           this.log.debug(res.request.path);
           return res.data;
         })
         .catch((error) => {
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
         });
       let form = this.extractHidden(loginHtml);
       form.email = this.config.username;
       loginHtml = await this.requestClient({
         method: "post",
         url:
           "https://account.withings.com/new_workflow/login?r=https://account.withings.com/oauth2_user/account_login?response_type=code&client_id=" +
           this.config.clientid +
           "&state=h4fhjnc2daoc3m&scope=user.activity%2Cuser.metrics%2Cuser.info&redirect_uri=http%3A%2F%2Flocalhost&b=authorize2",
         headers: {
           Accept:
             "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
           "Accept-Language": "de",
           "Content-Type": "application/x-www-form-urlencoded",
         },
         jar: this.cookieJar,
         withCredentials: true,
         data: qs.stringify(form),
       })
         .then(async (res) => {
           return res.data;
         })
         .catch((error) => {
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
           return;
         });
    
       form = this.extractHidden(loginHtml);
       form.password = this.config.password;
    
       const resultArray = await this.requestClient({
         method: "post",
         url:
           "https://account.withings.com/new_workflow/password_check?r=https%3A%2F%2Faccount.withings.com%2Foauth2_user%2Faccount_login%3Fresponse_type%3Dcode%26client_id%3D" +
           this.config.clientid +
           "%26state%3Dh4fhjnc2daoc3m%26scope%3Duser.activity%252Cuser.metrics%252Cuser.info%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%26b%3Dauthorize2",
         headers: {
           Accept:
             "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
           "Accept-Language": "de",
           "Content-Type": "application/x-www-form-urlencoded",
         },
         jar: this.cookieJar,
         withCredentials: true,
         data: qs.stringify(form),
       })
         .then(async (res) => {
           this.log.debug(JSON.stringify(res.data));
           if (res.data.errors) {
             this.log.error(JSON.stringify(res.data));
             return;
           }
           if (res.data.indexOf("user_selection") !== -1) {
             let urlArray = res.data.split("response_type=code");
             urlArray.shift();
             urlArray = urlArray.map((url) => {
               //eslint-disable-next-line no-useless-escape
               return url.split('"')[0].replace(/\&amp;/g, "&");
             });
             let nameArray = res.data.split("selecteduser=");
             nameArray.shift();
             nameArray = nameArray = nameArray.map((name) => {
               const key = name.split('"')[0];
               const value = name.split('group-item">')[1].split("<")[0];
               return { id: key, name: value };
             });
             for (const element of nameArray) {
               await this.setObjectNotExistsAsync(element.id, {
                 type: "device",
                 common: {
                   name: element.name,
                 },
                 native: {},
               });
             }
             const responseArray = [];
             for (const url of urlArray) {
               await this.requestClient({
                 method: "get",
                 url: "https://account.withings.com/oauth2_user/account_login?response_type=code" + url,
                 headers: {
                   Accept:
                     "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
                   "Accept-Language": "de",
                 },
                 jar: this.cookieJar,
                 withCredentials: true,
               })
                 .then((res) => {
                   this.log.debug(JSON.stringify(res.data));
                   this.log.debug(res.request.path);
                   responseArray.push(res);
                   return;
                 })
                 .catch((error) => {
                   this.log.error(error);
                   if (error.response) {
                     this.log.error(JSON.stringify(error.response.data));
                   }
                   return;
                 });
             }
             return responseArray;
           } else {
             return [res];
           }
         })
         .catch((error) => {
           if (error.response && error.response.status === 302) {
             return [];
           }
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
           return [];
         });
    
       for (const result of resultArray) {
         if (!result) {
           return;
         }
         form = this.extractHidden(result.data);
         form.authorized = "1";
         const code = await this.requestClient({
           method: "post",
           url: "https://account.withings.com" + result.request.path,
           headers: {
             Accept:
               "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
             "Accept-Language": "de",
             "Content-Type": "application/x-www-form-urlencoded",
           },
           jar: this.cookieJar,
           withCredentials: true,
           data: qs.stringify(form),
           maxRedirects: 0,
         })
           .then((res) => {
             this.log.debug(JSON.stringify(res.data));
             this.log.debug(res.request.path);
             this.log.debug(res.headers.location);
             if (res.headers.location) {
               return res.headers.location.split("code=")[1];
             }
             this.log.warn("Please check username and password");
             return;
           })
           .catch((error) => {
             if (error.response && error.response.status === 302) {
               this.log.debug(JSON.stringify(error.response.headers));
               if (error.response.headers.location.indexOf("code=") === -1) {
                 this.log.debug(JSON.stringify(error.response.headers));
                 this.log.error("No code found");
                 return null;
               }
               return error.response.headers.location.split("code=")[1].split("&")[0];
             }
    
             this.log.error(error);
             if (error.response) {
               this.log.error(JSON.stringify(error.response.data));
             }
           });
    
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/oauth2",
           headers: {
             "Content-Type": "application/x-www-form-urlencoded",
           },
           data: qs.stringify({
             action: "requesttoken",
             grant_type: "authorization_code",
             client_id: this.config.clientid,
             client_secret: this.config.clientsecret,
             code: code,
             redirect_uri: "http://localhost",
           }),
         })
           .then(async (res) => {
             this.log.debug(JSON.stringify(res.data));
             if (res.data.error) {
               this.log.error(JSON.stringify(res.data));
               return;
             }
             this.session.push(res.data.body);
             await this.setObjectNotExistsAsync(res.data.body.userid, {
               type: "device",
               common: {
                 name: "Hauptnutzer",
               },
               native: {},
             });
             this.setState("info.connection", true, true);
           })
           .catch((error) => {
             this.log.error(error);
             if (error.response) {
               this.log.error(JSON.stringify(error.response.data));
             }
           });
       }
     }
     async getDeviceList() {
       for (const session of this.session) {
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/user?action=getdevice",
           headers: {
             Authorization: "Bearer " + session.access_token,
             "Content-Type": "application/x-www-form-urlencoded",
           },
         })
           .then(async (res) => {
             this.log.debug(JSON.stringify(res.data));
             if (!res.data.body.devices) {
               return;
             }
             for (const device of res.data.body.devices) {
               const id = device.deviceid;
               if (this.deviceArray.indexOf(id) === -1) {
                 this.deviceArray.push(id);
               }
               const name = device.model;
    
               await this.setObjectNotExistsAsync(id, {
                 type: "device",
                 common: {
                   name: name,
                 },
                 native: {},
               });
               await this.setObjectNotExistsAsync(id + ".remote", {
                 type: "channel",
                 common: {
                   name: "Remote Controls",
                 },
                 native: {},
               });
    
               const remoteArray = [{ command: "Refresh", name: "True = Refresh" }];
               remoteArray.forEach((remote) => {
                 this.setObjectNotExists(id + ".remote." + remote.command, {
                   type: "state",
                   common: {
                     name: remote.name || "",
                     type: remote.type || "boolean",
                     role: remote.role || "boolean",
                     write: true,
                     read: true,
                   },
                   native: {},
                 });
               });
               this.json2iob.parse(id, device);
             }
           })
           .catch((error) => {
             this.log.error(error);
             error.response && this.log.error(JSON.stringify(error.response.data));
           });
       }
     }
    
     /**
      * Schreibe pro Measure-Type den letzten (neuesten) Messwert als State unter userid.lastMeasures.<type>
      * measuregrps wird idealerweise sortiert übergeben (neueste zuerst), aber wir schützen uns trotzdem.
      */
     async writeLastMeasures(userid, measuregrps, descriptions) {
       if (!Array.isArray(measuregrps)) return;
    
       // Channel anlegen
       await this.setObjectNotExistsAsync(userid + ".lastMeasures", {
         type: "channel",
         common: {
           name: "Letzte Messwerte",
         },
         native: {},
       });
    
       const seen = new Set();
    
       // Falls measuregrps nicht sortiert sind, ist das kein Problem – wir nehmen den ersten Vorkommnis je type,
       // weil im UpdateDevices die measuregrps bereits sortiert werden (neueste zuerst).
       for (const grp of measuregrps) {
         if (!grp || !Array.isArray(grp.measures)) continue;
         for (const m of grp.measures) {
           const t = m.type;
           if (seen.has(t)) continue; // schon den letzten Wert für diesen Typ geschrieben
           const val = m.value * Math.pow(10, m.unit);
           const stateId = `${userid}.lastMeasures.${t}`;
    
           await this.setObjectNotExistsAsync(stateId, {
             type: "state",
             common: {
               name: descriptions && descriptions[t] ? descriptions[t] : `Measure ${t}`,
               type: "number",
               role: "value",
               read: true,
               write: false,
             },
             native: {
               type: t,
               unit: m.unit,
             },
           });
    
           await this.setStateAsync(stateId, { val: val, ack: true });
           seen.add(t);
         }
       }
     }
    
     async updateDevices() {
       for (const session of this.session) {
         const userid = session.userid;
         const date = new Date().toISOString().split("T")[0];
         const startTimestampday = new Date().setDate(new Date().getDate() - this.config.lastDays);
         const startDateFormattedday = new Date(startTimestampday).toISOString().split("T")[0];
         const limitSeconds = this.config.lastDays * 24 * 60 * 60;
    
         const statusArray = [
           {
             path: "measures",
             url: "https://wbsapi.withings.net/measure",
             desc: "Measurements",
             data: {
               action: "getmeas",
               meastypes: "1,4,5,6,8,9,10,11,12,54,71,73,76,77,88,91,123,135,136,137,138,139,170",
    
               startdate: Math.round(Date.now() / 1000) - limitSeconds,
               enddate: Math.round(Date.now() / 1000),
             },
             forceIndex: false,
             preferedArrayName: "type",
           },
           {
             path: "activity",
             url: "https://wbsapi.withings.net/v2/measure",
             desc: "Activity",
             data: {
               action: "getactivity",
               data_fields:
                 "steps,distance,elevation,soft,moderate,intense,active,calories,totalcalories,hr_average,hr_min,hr_max,hr_zone_0,hr_zone_1,hr_zone_2,hr_zone_3",
               startdateymd: startDateFormattedday,
               enddateymd: date,
             },
             forceIndex: true,
           },
           {
             path: "heartList",
             url: "https://wbsapi.withings.net/v2/heart",
             desc: "List of ECG recordings",
             data: {
               action: "list",
               startdate: Math.round(Date.now() / 1000) - limitSeconds,
               enddate: Math.round(Date.now() / 1000),
             },
             forceIndex: true,
           },
           {
             path: "sleepSummary",
             url: "https://wbsapi.withings.net/v2/sleep",
             desc: "Basic information about a night",
             data: {
               action: "getsummary",
               startdateymd: startDateFormattedday,
               enddateymd: date,
               data_fields:
                 "breathing_disturbances_intensity,deepsleepduration,durationtosleep,durationtowakeup,hr_average,hr_max,hr_min,lightsleepduration,remsleepduration,rr_average,rr_max,rr_min,sleep_score,snoring,snoringepisodecount,wakeupcount,wakeupduration,nb_rem_episodes,sleep_efficiency,sleep_latency,total_sleep_time,total_timeinbed,wakeup_latency,waso,apnea_hypopnea_index,asleepduration,night_events,out_of_bed_count",
             },
             forceIndex: true,
           },
           {
             path: "sleep",
             url: "https://wbsapi.withings.net/v2/sleep",
             desc: "Sleep measures for the night ",
             data: {
               action: "get",
               startdate: Math.round(Date.now() / 1000) - this.config.lastHours * 60 * 60,
               enddate: Math.round(Date.now() / 1000),
               data_fields: "hr,rr,snoring",
             },
             forceIndex: true,
           },
         ];
         const headers = {
           authorization: "Bearer " + session.access_token,
           "user-agent": this.userAgent,
         };
         for (const element of statusArray) {
           await this.requestClient({
             method: "post",
             url: element.url,
             headers: headers,
             data: qs.stringify(element.data),
           })
             .then(async (res) => {
               this.log.debug(JSON.stringify(res.data));
               if (!res.data) {
                 return;
               }
               const data = res.data.body;
               if (Array.isArray(data?.measuregrps)) {
                 data.measuregrps.sort((a, b) => {
                   const numeric = (obj, key) => {
                     const value = obj && obj[key];
                     return typeof value === "number" ? value : Number(value) || 0;
                   };
                   const compareChain = ["date", "created", "modified", "grpid"];
                   for (const field of compareChain) {
                     const diff = numeric(b, field) - numeric(a, field);
                     if (diff !== 0) {
                       return diff;
                     }
                   }
                   return 0;
                 });
               }
               if (data.activities) {
                 data.activities.sort((a, b) => a.date.localeCompare(b.date));
               }
               if (element.path === "sleepSummary" || element.path === "sleep") {
                 if (data.series && data.series.sort) {
                   data.series.sort((a, b) => b.startdate - a.startdate);
                 }
               }
               if (element.path === "sleep" && data.series) {
                 data.series.map((element) => {
                   for (const key in element) {
                     if (typeof element[key] === "object") {
                       const newArray = [];
                       for (const timestamp in element[key]) {
                         newArray.push({ timestamp: timestamp, value: element[key][timestamp] });
                       }
                       newArray.sort((a, b) => b.timestamp - a.timestamp);
                       element[key] = newArray;
                     }
                   }
                 });
               }
               // if (data.measuregrps) {
               //     data.measuregrps.sort((a, b) => a.date - b.date);
               // }
               const descriptions = {
                 1: "Weight (kg)",
                 4: "Height (meter)",
                 5: "Fat Free Mass (kg)",
                 6: "Fat Ratio (%)",
                 8: "Fat Mass Weight (kg)",
                 9: "Diastolic Blood Pressure (mmHg)",
                 10: "Systolic Blood Pressure (mmHg)",
                 11: "Heart Pulse (bpm) - only for BPM and scale devices",
                 12: "Temperature (celsius)",
                 54: "SP02 (%)",
                 71: "Body Temperature (celsius)",
                 73: "Skin Temperature (celsius)",
                 76: "Muscle Mass (kg)",
                 77: "Hydration (kg)",
                 88: "Bone Mass (kg)",
                 91: "Pulse Wave Velocity (m/s)",
                 123: "VO2 max is a numerical measurement of your body’s ability to consume oxygen (ml/min/kg).",
                 135: "QRS interval duration based on ECG signal",
                 136: "PR interval duration based on ECG signal",
                 137: "QT interval duration based on ECG signal",
                 138: "Corrected QT interval duration based on ECG signal",
                 139: "Atrial fibrillation result from PPG",
                 170: "Visceral Fat (without unity)",
               };
    
               // === NEU: Schreibe die letzten Messwerte pro Type in userid.lastMeasures.<type>
               if (element.path === "measures" && Array.isArray(data?.measuregrps)) {
                 try {
                   await this.writeLastMeasures(userid, data.measuregrps, descriptions);
                 } catch (e) {
                   this.log.error("writeLastMeasures failed: " + e);
                 }
               }
               // === ENDE NEU
    
               this.json2iob.parse(userid + "." + element.path, data, {
                 forceIndex: element.forceIndex,
                 preferedArrayName: element.preferedArrayName,
                 channelName: element.desc,
                 descriptions: descriptions,
               });
             })
             .catch((error) => {
               if (error.response) {
                 if (error.response.status === 401) {
                   error.response && this.log.debug(JSON.stringify(error.response.data));
                   this.log.info(element.path + " receive 401 error. Refresh Token in 60 seconds");
                   this.refreshTokenTimeout && clearTimeout(this.refreshTokenTimeout);
                   this.refreshTokenTimeout = setTimeout(() => {
                     this.refreshToken();
                   }, 1000 * 60);
    
                   return;
                 }
               }
               this.log.error(element.url);
               this.log.error(error);
               error.response && this.log.error(JSON.stringify(error.response.data));
             });
         }
       }
     }
     async refreshToken() {
       if (this.session.length === 0) {
         this.log.error("No session found relogin");
         await this.login();
         return;
       }
    
       for (const session of this.session) {
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/oauth2",
           headers: {
             "Content-Type": "application/x-www-form-urlencoded",
           },
           data: qs.stringify({
             action: "requesttoken",
             grant_type: "refresh_token",
             client_id: this.config.clientid,
             client_secret: this.config.clientsecret,
             refresh_token: session.refresh_token,
           }),
         })
           .then((res) => {
             this.log.debug(JSON.stringify(res.data));
             if (res.data.body && res.data.body.access_token) {
               const index = this.session.indexOf(session);
               this.session[index] = res.data.body;
               this.setState("info.connection", true, true);
             }
           })
           .catch((error) => {
             this.log.error("refresh token failed");
             this.log.error(error);
             error.response && this.log.error(JSON.stringify(error.response.data));
             this.log.error("Start relogin in 1min");
             this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
             this.reLoginTimeout = setTimeout(() => {
               this.login();
             }, 1000 * 60 * 1);
           });
       }
     }
     async cleanOldVersion() {
       const cleanOldVersion = await this.getObjectAsync("oldVersionCleanedv2");
       if (!cleanOldVersion) {
         this.log.info("Please wait a few minutes.... clean old version");
         await this.delForeignObjectAsync(this.name + "." + this.instance, { recursive: true });
         await this.setObjectNotExistsAsync("oldVersionCleanedv2", {
           type: "state",
           common: {
             type: "boolean",
             role: "boolean",
             write: false,
             read: true,
           },
           native: {},
         });
    
         this.log.info("Done with cleaning, restart adapter");
         this.restart();
       }
     }
     extractHidden(body) {
       const returnObject = {};
       if (!body) {
         this.log.warn("No body found");
       }
       let matches;
       if (body.matchAll) {
         matches = body.matchAll(/<input (?=[^>]* name=["']([^'"]*)|)(?=[^>]* value=["']([^'"]*)|)/g);
       } else {
         this.log.warn(
           "The adapter needs in the future NodeJS v12. https://forum.iobroker.net/topic/22867/how-to-node-js-f%C3%BCr-iobroker-richtig-updaten",
         );
         matches = this.matchAll(/<input (?=[^>]* name=["']([^'"]*)|)(?=[^>]* value=["']([^'"]*)|)/g, body);
       }
       for (const match of matches) {
         returnObject[match[1]] = match[2];
       }
       return returnObject;
     }
     matchAll(re, str) {
       let match;
       const matches = [];
    
       while ((match = re.exec(str))) {
         // add all matched groups
         matches.push(match);
       }
    
       return matches;
     }
     /**
      * Is called when adapter shuts down - callback has to be called under any circumstances!
      * @param {() => void} callback
      */
     onUnload(callback) {
       try {
         this.setState("info.connection", false, true);
         this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
         this.refreshTokenTimeout && clearTimeout(this.refreshTokenTimeout);
         this.updateInterval && clearInterval(this.updateInterval);
         this.refreshTokenInterval && clearInterval(this.refreshTokenInterval);
         callback();
       } catch (e) {
         this.log.error(e);
         callback();
       }
     }
    
     /**
      * Is called if a subscribed state changes
      * @param {string} id
      * @param {ioBroker.State | null | undefined} state
      */
     async onStateChange(id, state) {
       if (state) {
         if (!state.ack) {
           // const deviceId = id.split(".")[2];
           const command = id.split(".")[4];
           if (id.split(".")[3] !== "remote") {
             return;
           }
    
           if (command === "Refresh") {
             this.updateDevices();
           }
         }
       }
     }
    }
    
    if (require.main !== module) {
     // Export the constructor in compact mode
     /**
      * @param {Partial<utils.AdapterOptions>} [options={}]
      */
     module.exports = (options) => new Withings(options);
    } else {
     // otherwise start the instance directly
     new Withings();
    }
    
    

    Gruß Tino

    T Online
    T Online
    Tino 0
    schrieb am zuletzt editiert von Tino 0
    #529

    hier ist noch eine Version mit lastActivity und lastSleep

    "use strict";
    
    /*
    * Created with @iobroker/create-adapter v2.0.1
    */
    
    // The adapter-core module gives you access to the core ioBroker functions
    // you need to create an adapter
    const utils = require("@iobroker/adapter-core");
    const axios = require("axios");
    const qs = require("qs");
    const Json2iob = require("./lib/json2iob");
    const tough = require("tough-cookie");
    const { HttpsCookieAgent } = require("http-cookie-agent/http");
    
    class Withings extends utils.Adapter {
     /**
      * @param {Partial<utils.AdapterOptions>} [options={}]
      */
     constructor(options) {
       super({
         ...options,
         name: "withings",
       });
       this.on("ready", this.onReady.bind(this));
       this.on("stateChange", this.onStateChange.bind(this));
       this.on("unload", this.onUnload.bind(this));
       this.deviceArray = [];
       this.json2iob = new Json2iob(this);
     }
    
     /**
      * Is called when databases are connected and adapter received configuration.
      */
     async onReady() {
       // Reset the connection indicator during startup
       this.setState("info.connection", false, true);
       if (this.config.interval < 0.5) {
         this.log.info("Set interval to minimum 0.5");
         this.config.interval = 0.5;
       }
       if (!this.config.lastDays || this.config.lastDays < 1) {
         this.config.lastDays = 1;
       }
       if (!this.config.lastHours || this.config.lastHours < 1) {
         this.config.lastHours = 1;
       }
       if (!this.config.username || !this.config.password || !this.config.clientid || !this.config.clientsecret) {
         this.log.error("Please set username and password in the instance settings");
         return;
       }
       this.userAgent = "ioBroker v0.0.1";
       this.cookieJar = new tough.CookieJar();
       this.requestClient = axios.create({
         jar: this.cookieJar,
         withCredentials: true,
         httpsAgent: new HttpsCookieAgent({ cookies: { jar: this.cookieJar } }),
       });
    
       this.updateInterval = null;
       this.reLoginTimeout = null;
       this.refreshTokenTimeout = null;
       this.session = [];
       await this.cleanOldVersion();
       this.subscribeStates("*");
    
       await this.login();
    
       if (this.session.length > 0) {
         await this.getDeviceList();
         await this.updateDevices();
         this.updateInterval = setInterval(async () => {
           await this.updateDevices();
         }, this.config.interval * 60 * 1000);
         this.refreshTokenInterval = setInterval(() => {
           this.refreshToken();
         }, this.session[0].expires_in * 1000);
       }
     }
     async login() {
       let loginHtml = await this.requestClient({
         method: "get",
         url:
           "https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=" +
           this.config.clientid +
           "&state=h4fhjnc2daoc3m&scope=user.activity,user.metrics,user.info&redirect_uri=http://localhost",
         headers: {
           Accept: "*/*",
           "User-Agent": this.userAgent,
         },
         jar: this.cookieJar,
         withCredentials: true,
       })
         .then((res) => {
           this.log.debug(JSON.stringify(res.data));
           this.log.debug(res.request.path);
           return res.data;
         })
         .catch((error) => {
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
         });
       let form = this.extractHidden(loginHtml);
       form.email = this.config.username;
       loginHtml = await this.requestClient({
         method: "post",
         url:
           "https://account.withings.com/new_workflow/login?r=https://account.withings.com/oauth2_user/account_login?response_type=code&client_id=" +
           this.config.clientid +
           "&state=h4fhjnc2daoc3m&scope=user.activity%2Cuser.metrics%2Cuser.info&redirect_uri=http%3A%2F%2Flocalhost&b=authorize2",
         headers: {
           Accept:
             "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
           "Accept-Language": "de",
           "Content-Type": "application/x-www-form-urlencoded",
         },
         jar: this.cookieJar,
         withCredentials: true,
         data: qs.stringify(form),
       })
         .then(async (res) => {
           return res.data;
         })
         .catch((error) => {
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
           return;
         });
    
       form = this.extractHidden(loginHtml);
       form.password = this.config.password;
    
       const resultArray = await this.requestClient({
         method: "post",
         url:
           "https://account.withings.com/new_workflow/password_check?r=https%3A%2F%2Faccount.withings.com%2Foauth2_user%2Faccount_login%3Fresponse_type%3Dcode%26client_id%3D" +
           this.config.clientid +
           "%26state%3Dh4fhjnc2daoc3m%26scope%3Duser.activity%252Cuser.metrics%252Cuser.info%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%26b%3Dauthorize2",
         headers: {
           Accept:
             "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
           "Accept-Language": "de",
           "Content-Type": "application/x-www-form-urlencoded",
         },
         jar: this.cookieJar,
         withCredentials: true,
         data: qs.stringify(form),
       })
         .then(async (res) => {
           this.log.debug(JSON.stringify(res.data));
           if (res.data.errors) {
             this.log.error(JSON.stringify(res.data));
             return;
           }
           if (res.data.indexOf("user_selection") !== -1) {
             let urlArray = res.data.split("response_type=code");
             urlArray.shift();
             urlArray = urlArray.map((url) => {
               //eslint-disable-next-line no-useless-escape
               return url.split('"')[0].replace(/\&amp;/g, "&");
             });
             let nameArray = res.data.split("selecteduser=");
             nameArray.shift();
             nameArray = nameArray = nameArray.map((name) => {
               const key = name.split('"')[0];
               const value = name.split('group-item">')[1].split("<")[0];
               return { id: key, name: value };
             });
             for (const element of nameArray) {
               await this.setObjectNotExistsAsync(element.id, {
                 type: "device",
                 common: {
                   name: element.name,
                 },
                 native: {},
               });
             }
             const responseArray = [];
             for (const url of urlArray) {
               await this.requestClient({
                 method: "get",
                 url: "https://account.withings.com/oauth2_user/account_login?response_type=code" + url,
                 headers: {
                   Accept:
                     "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
                   "Accept-Language": "de",
                 },
                 jar: this.cookieJar,
                 withCredentials: true,
               })
                 .then((res) => {
                   this.log.debug(JSON.stringify(res.data));
                   this.log.debug(res.request.path);
                   responseArray.push(res);
                   return;
                 })
                 .catch((error) => {
                   this.log.error(error);
                   if (error.response) {
                     this.log.error(JSON.stringify(error.response.data));
                   }
                   return;
                 });
             }
             return responseArray;
           } else {
             return [res];
           }
         })
         .catch((error) => {
           if (error.response && error.response.status === 302) {
             return [];
           }
           this.log.error(error);
           if (error.response) {
             this.log.error(JSON.stringify(error.response.data));
           }
           return [];
         });
    
       for (const result of resultArray) {
         if (!result) {
           return;
         }
         form = this.extractHidden(result.data);
         form.authorized = "1";
         const code = await this.requestClient({
           method: "post",
           url: "https://account.withings.com" + result.request.path,
           headers: {
             Accept:
               "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
             "Accept-Language": "de",
             "Content-Type": "application/x-www-form-urlencoded",
           },
           jar: this.cookieJar,
           withCredentials: true,
           data: qs.stringify(form),
           maxRedirects: 0,
         })
           .then((res) => {
             this.log.debug(JSON.stringify(res.data));
             this.log.debug(res.request.path);
             this.log.debug(res.headers.location);
             if (res.headers.location) {
               return res.headers.location.split("code=")[1];
             }
             this.log.warn("Please check username and password");
             return;
           })
           .catch((error) => {
             if (error.response && error.response.status === 302) {
               this.log.debug(JSON.stringify(error.response.headers));
               if (error.response.headers.location.indexOf("code=") === -1) {
                 this.log.debug(JSON.stringify(error.response.headers));
                 this.log.error("No code found");
                 return null;
               }
               return error.response.headers.location.split("code=")[1].split("&")[0];
             }
    
             this.log.error(error);
             if (error.response) {
               this.log.error(JSON.stringify(error.response.data));
             }
           });
    
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/oauth2",
           headers: {
             "Content-Type": "application/x-www-form-urlencoded",
           },
           data: qs.stringify({
             action: "requesttoken",
             grant_type: "authorization_code",
             client_id: this.config.clientid,
             client_secret: this.config.clientsecret,
             code: code,
             redirect_uri: "http://localhost",
           }),
         })
           .then(async (res) => {
             this.log.debug(JSON.stringify(res.data));
             if (res.data.error) {
               this.log.error(JSON.stringify(res.data));
               return;
             }
             this.session.push(res.data.body);
             await this.setObjectNotExistsAsync(res.data.body.userid, {
               type: "device",
               common: {
                 name: "Hauptnutzer",
               },
               native: {},
             });
             this.setState("info.connection", true, true);
           })
           .catch((error) => {
             this.log.error(error);
             if (error.response) {
               this.log.error(JSON.stringify(error.response.data));
             }
           });
       }
     }
     async getDeviceList() {
       for (const session of this.session) {
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/user?action=getdevice",
           headers: {
             Authorization: "Bearer " + session.access_token,
             "Content-Type": "application/x-www-form-urlencoded",
           },
         })
           .then(async (res) => {
             this.log.debug(JSON.stringify(res.data));
             if (!res.data.body.devices) {
               return;
             }
             for (const device of res.data.body.devices) {
               const id = device.deviceid;
               if (this.deviceArray.indexOf(id) === -1) {
                 this.deviceArray.push(id);
               }
               const name = device.model;
    
               await this.setObjectNotExistsAsync(id, {
                 type: "device",
                 common: {
                   name: name,
                 },
                 native: {},
               });
               await this.setObjectNotExistsAsync(id + ".remote", {
                 type: "channel",
                 common: {
                   name: "Remote Controls",
                 },
                 native: {},
               });
    
               const remoteArray = [{ command: "Refresh", name: "True = Refresh" }];
               remoteArray.forEach((remote) => {
                 this.setObjectNotExists(id + ".remote." + remote.command, {
                   type: "state",
                   common: {
                     name: remote.name || "",
                     type: remote.type || "boolean",
                     role: remote.role || "boolean",
                     write: true,
                     read: true,
                   },
                   native: {},
                 });
               });
               this.json2iob.parse(id, device);
             }
           })
           .catch((error) => {
             this.log.error(error);
             error.response && this.log.error(JSON.stringify(error.response.data));
           });
       }
     }
    
     // helper: unix seconds -> ISO string (UTC)
     toIso(val) {
       if (val === null || val === undefined) return null;
       const n = Number(val);
       if (Number.isNaN(n)) return null;
       return new Date(n * 1000).toLocaleString("de-DE").replace(",", "");
     }
    
     /**
      * Schreibe pro Measure-Type den letzten (neuesten) Messwert als State unter userid.lastMeasures.<type>
      * measuregrps wird idealerweise sortiert übergeben (neueste zuerst), aber wir schützen uns trotzdem.
      */
    async writeLastMeasures(userid, measuregrps, descriptions) {
       if (!Array.isArray(measuregrps) || measuregrps.length === 0) return;
    
       await this.setObjectNotExistsAsync(`${userid}.lastMeasures`, {
           type: "channel",
           common: { name: "Letzte Messwerte" },
           native: {},
       });
    
       const seen = new Set();
    
       for (const grp of measuregrps) {
           if (!grp?.measures) continue;
    
           //const tsRaw = Number(grp.date) || null; // Sekunden
       	const tsRaw = typeof grp.date === "number" ? grp.date * 1000 : null; // Millisekunden
    
           for (const m of grp.measures) {
               const t = m.type;
               if (seen.has(t)) continue;
    
               const val = m.value * Math.pow(10, m.unit);
               const base = `${userid}.lastMeasures.${t}`;
               const raw = `${userid}.lastMeasures.${t}_timestamp`;
    
               // --- Wert ---
               await this.setObjectNotExistsAsync(base, {
                   type: "state",
                   common: {
                       name: descriptions?.[t] || `Measure ${t}`,
                       type: "number",
                       role: "value",
                       read: true,
                       write: false,
                   },
                   native: { type: t, unit: m.unit },
               });
               await this.setStateAsync(base, { val: val, ack: true });
    
               // --- Raw Timestamp ---
               await this.setObjectNotExistsAsync(raw, {
                   type: "state",
                   common: {
                       name: `Timestamp Measure ${t}`,
                       type: "number",
                       role: "date", // <-- hier geändert
                       read: true,
                       write: false,
                   },
                   native: {},
               });
               await this.setStateAsync(raw, { val: tsRaw, ack: true });
    
               seen.add(t);
           }
       }
    }
    
    
     /**
      * Schreibe die letzte Aktivität als States unter userid.lastActivity.<key>
      * data ist die body-Antwort mit data.activities (Array)
      */
       async writeLastActivity(userid, data) {
       	try {
       		if (!data || !Array.isArray(data.activities) || data.activities.length === 0) return;
    
       		// Neueste Aktivität
       		const activity = data.activities[0];
       		if (!activity) return;
    
       		await this.setObjectNotExistsAsync(`${userid}.lastActivity`, {
       			type: "channel",
       			common: { name: "Letzte Aktivität" },
       			native: {},
       		});
    
       		// Felder, die eindeutig Zeitstempel darstellen
       		const isDateField = (key) =>
       			/(date|startdate|enddate|timestamp|modified|created)$/i.test(key);
    
       		for (const key of Object.keys(activity)) {
       			const rawValue = activity[key];
       			const stateId = `${userid}.lastActivity.${key}`;
    
       			const common = {
       				name: `Letzte Aktivität - ${key}`,
       				type: typeof rawValue === "number" ? "number" : "string",
       				role: isDateField(key) ? "date" : "value",
       				read: true,
       				write: false,
       			};
    
       			await this.setObjectNotExistsAsync(stateId, {
       				type: "state",
       				common,
       				native: {},
       			});
    
       			const outValue =
       				typeof rawValue === "object" ? JSON.stringify(rawValue) : rawValue;
    
       			await this.setStateAsync(stateId, { val: outValue, ack: true });
       		}
    
       	} catch (e) {
       		this.log.error("writeLastActivity failed: " + e);
       	}
       }
    
    
     /**
      * Schreibe die letzte Sleep Summary als States unter userid.lastSleep.<key>
      * data ist die body-Antwort, erwartet data.series (Array)
      * Konvertiert: created, startdate, enddate, modified -> ISO
      */
    async writeLastSleepSummary(userid, data) {
       try {
           if (!data || !Array.isArray(data.series) || data.series.length === 0) return;
    
           const series = data.series[0] || data.series[data.series.length - 1];
           if (!series) return;
    
           await this.setObjectNotExistsAsync(`${userid}.lastSleep`, {
               type: "channel",
               common: { name: "Letzte Sleep Summary" },
               native: {},
           });
    
           const isDateField = (key) =>
               /(date|startdate|enddate|modified|created)$/i.test(key);
    
           // --- Obere Ebene ---
           for (const key of Object.keys(series)) {
               if (key === "data") continue;
    
               const rawValue = series[key];
               const stateId = `${userid}.lastSleep.${key}`;
    
               const common = {
                   name: `Letzte Sleep - ${key}`,
                   type: typeof rawValue === "number" ? "number" : "string",
                   role: isDateField(key) ? "date" : "value",
                   read: true,
                   write: false,
               };
    
               await this.setObjectNotExistsAsync(stateId, {
                   type: "state",
                   common,
                   native: {},
               });
    
               const outValue =
                   typeof rawValue === "object" ? JSON.stringify(rawValue) : rawValue;
    
               await this.setStateAsync(stateId, { val: outValue, ack: true });
           }
    
           // --- data-Objekt ---
           if (series.data && typeof series.data === "object") {
               for (const key of Object.keys(series.data)) {
                   const rawValue = series.data[key];
                   const stateId = `${userid}.lastSleep.${key}`;
    
                   const common = {
                       name: `Letzte Sleep - ${key}`,
                       type: typeof rawValue === "number" ? "number" : "string",
                       role: isDateField(key) ? "date" : "value",
                       read: true,
                       write: false,
                   };
    
                   await this.setObjectNotExistsAsync(stateId, {
                       type: "state",
                       common,
                       native: {},
                   });
    
                   const outValue =
                       typeof rawValue === "object" ? JSON.stringify(rawValue) : rawValue;
    
                   await this.setStateAsync(stateId, { val: outValue, ack: true });
               }
           }
       } catch (e) {
           this.log.error("writeLastSleepSummary failed: " + e);
       }
    }
    
    
    
     async updateDevices() {
       for (const session of this.session) {
         const userid = session.userid;
         const date = new Date().toISOString().split("T")[0];
         const startTimestampday = new Date().setDate(new Date().getDate() - this.config.lastDays);
         const startDateFormattedday = new Date(startTimestampday).toISOString().split("T")[0];
         const limitSeconds = this.config.lastDays * 24 * 60 * 60;
    
         const statusArray = [
           {
             path: "measures",
             url: "https://wbsapi.withings.net/measure",
             desc: "Measurements",
             data: {
               action: "getmeas",
               meastypes: "1,4,5,6,8,9,10,11,12,54,71,73,76,77,88,91,123,135,136,137,138,139,170",
    
               startdate: Math.round(Date.now() / 1000) - limitSeconds,
               enddate: Math.round(Date.now() / 1000),
             },
             forceIndex: false,
             preferedArrayName: "type",
           },
           {
             path: "activity",
             url: "https://wbsapi.withings.net/v2/measure",
             desc: "Activity",
             data: {
               action: "getactivity",
               data_fields:
                 "steps,distance,elevation,soft,moderate,intense,active,calories,totalcalories,hr_average,hr_min,hr_max,hr_zone_0,hr_zone_1,hr_zone_2,hr_zone_3",
               startdateymd: startDateFormattedday,
               enddateymd: date,
             },
             forceIndex: true,
           },
           {
             path: "heartList",
             url: "https://wbsapi.withings.net/v2/heart",
             desc: "List of ECG recordings",
             data: {
               action: "list",
               startdate: Math.round(Date.now() / 1000) - limitSeconds,
               enddate: Math.round(Date.now() / 1000),
             },
             forceIndex: true,
           },
           {
             path: "sleepSummary",
             url: "https://wbsapi.withings.net/v2/sleep",
             desc: "Basic information about a night",
             data: {
               action: "getsummary",
               startdateymd: startDateFormattedday,
               enddateymd: date,
               data_fields:
                 "breathing_disturbances_intensity,deepsleepduration,durationtosleep,durationtowakeup,hr_average,hr_max,hr_min,lightsleepduration,remsleepduration,rr_average,rr_max,rr_min,sleep_score,snoring,snoringepisodecount,wakeupcount,wakeupduration,nb_rem_episodes,sleep_efficiency,sleep_latency,total_sleep_time,total_timeinbed,wakeup_latency,waso,apnea_hypopnea_index,asleepduration,night_events,out_of_bed_count",
             },
             forceIndex: true,
           },
           {
             path: "sleep",
             url: "https://wbsapi.withings.net/v2/sleep",
             desc: "Sleep measures for the night ",
             data: {
               action: "get",
               startdate: Math.round(Date.now() / 1000) - this.config.lastHours * 60 * 60,
               enddate: Math.round(Date.now() / 1000),
               data_fields: "hr,rr,snoring",
             },
             forceIndex: true,
           },
         ];
         const headers = {
           authorization: "Bearer " + session.access_token,
           "user-agent": this.userAgent,
         };
         for (const element of statusArray) {
           await this.requestClient({
             method: "post",
             url: element.url,
             headers: headers,
             data: qs.stringify(element.data),
           })
             .then(async (res) => {
               this.log.debug(JSON.stringify(res.data));
               if (!res.data) {
                 return;
               }
               const data = res.data.body;
               if (Array.isArray(data?.measuregrps)) {
                 data.measuregrps.sort((a, b) => {
                   const numeric = (obj, key) => {
                     const value = obj && obj[key];
                     return typeof value === "number" ? value : Number(value) || 0;
                   };
                   const compareChain = ["date", "created", "modified", "grpid"];
                   for (const field of compareChain) {
                     const diff = numeric(b, field) - numeric(a, field);
                     if (diff !== 0) {
                       return diff;
                     }
                   }
                   return 0;
                 });
               }
               if (data.activities) {
                 // sort activities by modified timestamp (newest first)
                 data.activities.sort((a, b) => (b.modified || 0) - (a.modified || 0));
               }
               if (element.path === "sleepSummary" || element.path === "sleep") {
                 if (data.series && data.series.sort) {
                   data.series.sort((a, b) => b.startdate - a.startdate);
                 }
               }
               if (element.path === "sleep" && data.series) {
                 data.series.map((element) => {
                   for (const key in element) {
                     if (typeof element[key] === "object") {
                       const newArray = [];
                       for (const timestamp in element[key]) {
                         newArray.push({ timestamp: timestamp, value: element[key][timestamp] });
                       }
                       newArray.sort((a, b) => b.timestamp - a.timestamp);
                       element[key] = newArray;
                     }
                   }
                 });
               }
               // if (data.measuregrps) {
               //     data.measuregrps.sort((a, b) => a.date - b.date);
               // }
               const descriptions = {
                 1: "Weight (kg)",
                 4: "Height (meter)",
                 5: "Fat Free Mass (kg)",
                 6: "Fat Ratio (%)",
                 8: "Fat Mass Weight (kg)",
                 9: "Diastolic Blood Pressure (mmHg)",
                 10: "Systolic Blood Pressure (mmHg)",
                 11: "Heart Pulse (bpm) - only for BPM and scale devices",
                 12: "Temperature (celsius)",
                 54: "SP02 (%)",
                 71: "Body Temperature (celsius)",
                 73: "Skin Temperature (celsius)",
                 76: "Muscle Mass (kg)",
                 77: "Hydration (kg)",
                 88: "Bone Mass (kg)",
                 91: "Pulse Wave Velocity (m/s)",
                 123: "VO2 max is a numerical measurement of your body’s ability to consume oxygen (ml/min/kg).",
                 135: "QRS interval duration based on ECG signal",
                 136: "PR interval duration based on ECG signal",
                 137: "QT interval duration based on ECG signal",
                 138: "Corrected QT interval duration based on ECG signal",
                 139: "Atrial fibrillation result from PPG",
                 170: "Visceral Fat (without unity)",
               };
    
               // === NEU: Schreibe die letzten Messwerte pro Type in userid.lastMeasures.<type>
               if (element.path === "measures" && Array.isArray(data?.measuregrps)) {
                 try {
                   await this.writeLastMeasures(userid, data.measuregrps, descriptions);
                 } catch (e) {
                   this.log.error("writeLastMeasures failed: " + e);
                 }
               }
               // === NEU: Schreibe die letzte Activity in userid.lastActivity.<key>
               if (element.path === "activity" && data) {
                 try {
                   await this.writeLastActivity(userid, data);
                 } catch (e) {
                   this.log.error("writeLastActivity failed: " + e);
                 }
               }
               // === NEU: Schreibe die letzte Sleep Summary in userid.lastSleep.<key>
               if (element.path === "sleepSummary" && data) {
                 try {
                   await this.writeLastSleepSummary(userid, data);
                 } catch (e) {
                   this.log.error("writeLastSleepSummary failed: " + e);
                 }
               }
               // === ENDE NEU
    
               this.json2iob.parse(userid + "." + element.path, data, {
                 forceIndex: element.forceIndex,
                 preferedArrayName: element.preferedArrayName,
                 channelName: element.desc,
                 descriptions: descriptions,
               });
             })
             .catch((error) => {
               if (error.response) {
                 if (error.response.status === 401) {
                   error.response && this.log.debug(JSON.stringify(error.response.data));
                   this.log.info(element.path + " receive 401 error. Refresh Token in 60 seconds");
                   this.refreshTokenTimeout && clearTimeout(this.refreshTokenTimeout);
                   this.refreshTokenTimeout = setTimeout(() => {
                     this.refreshToken();
                   }, 1000 * 60);
    
                   return;
                 }
               }
               this.log.error(element.url);
               this.log.error(error);
               error.response && this.log.error(JSON.stringify(error.response.data));
             });
         }
       }
     }
     async refreshToken() {
       if (this.session.length === 0) {
         this.log.error("No session found relogin");
         await this.login();
         return;
       }
    
       for (const session of this.session) {
         await this.requestClient({
           method: "post",
           url: "https://wbsapi.withings.net/v2/oauth2",
           headers: {
             "Content-Type": "application/x-www-form-urlencoded",
           },
           data: qs.stringify({
             action: "requesttoken",
             grant_type: "refresh_token",
             client_id: this.config.clientid,
             client_secret: this.config.clientsecret,
             refresh_token: session.refresh_token,
           }),
         })
           .then((res) => {
             this.log.debug(JSON.stringify(res.data));
             if (res.data.body && res.data.body.access_token) {
               const index = this.session.indexOf(session);
               this.session[index] = res.data.body;
               this.setState("info.connection", true, true);
             }
           })
           .catch((error) => {
             this.log.error("refresh token failed");
             this.log.error(error);
             error.response && this.log.error(JSON.stringify(error.response.data));
             this.log.error("Start relogin in 1min");
             this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
             this.reLoginTimeout = setTimeout(() => {
               this.login();
             }, 1000 * 60 * 1);
           });
       }
     }
     async cleanOldVersion() {
       const cleanOldVersion = await this.getObjectAsync("oldVersionCleanedv2");
       if (!cleanOldVersion) {
         this.log.info("Please wait a few minutes.... clean old version");
         await this.delForeignObjectAsync(this.name + "." + this.instance, { recursive: true });
         await this.setObjectNotExistsAsync("oldVersionCleanedv2", {
           type: "state",
           common: {
             type: "boolean",
             role: "boolean",
             write: false,
             read: true,
           },
           native: {},
         });
    
         this.log.info("Done with cleaning, restart adapter");
         this.restart();
       }
     }
     extractHidden(body) {
       const returnObject = {};
       if (!body) {
         this.log.warn("No body found");
       }
       let matches;
       if (body.matchAll) {
         matches = body.matchAll(/<input (?=[^>]* name=["']([^'"]*)|)(?=[^>]* value=["']([^'"]*)|)/g);
       } else {
         this.log.warn(
           "The adapter needs in the future NodeJS v12. https://forum.iobroker.net/topic/22867/how-to-node-js-f%C3%BCr-iobroker-richtig-updaten",
         );
         matches = this.matchAll(/<input (?=[^>]* name=["']([^'"]*)|)(?=[^>]* value=["']([^'"]*)|)/g, body);
       }
       for (const match of matches) {
         returnObject[match[1]] = match[2];
       }
       return returnObject;
     }
     matchAll(re, str) {
       let match;
       const matches = [];
    
       while ((match = re.exec(str))) {
         // add all matched groups
         matches.push(match);
       }
    
       return matches;
     }
     /**
      * Is called when adapter shuts down - callback has to be called under any circumstances!
      * @param {() => void} callback
      */
     onUnload(callback) {
       try {
         this.setState("info.connection", false, true);
         this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
         this.refreshTokenTimeout && clearTimeout(this.refreshTokenTimeout);
         this.updateInterval && clearInterval(this.updateInterval);
         this.refreshTokenInterval && clearInterval(this.refreshTokenInterval);
         callback();
       } catch (e) {
         this.log.error(e);
         callback();
       }
     }
    
     /**
      * Is called if a subscribed state changes
      * @param {string} id
      * @param {ioBroker.State | null | undefined} state
      */
     async onStateChange(id, state) {
       if (state) {
         if (!state.ack) {
           // const deviceId = id.split(".")[2];
           const command = id.split(".")[4];
           if (id.split(".")[3] !== "remote") {
             return;
           }
    
           if (command === "Refresh") {
             this.updateDevices();
           }
         }
       }
     }
    }
    
    if (require.main !== module) {
     // Export the constructor in compact mode
     /**
      * @param {Partial<utils.AdapterOptions>} [options={}]
      */
     module.exports = (options) => new Withings(options);
    } else {
     // otherwise start the instance directly
     new Withings();
    }
    
    
    
    

    PS: es war noch ein Fehler in der Reihenfolge bei lastActivity drin, ich habe die main.js aktualisiert.

    PS2: es ist jetzt bei lastMeasures zusätzlich das update Datum mit drin

    PS3: jetzt mit deutschem Datumsformat

    PS4: jetzt mit update + timestamp

    PS5: _update ist wieder raus

    PS6: Rolle date wird jetzt immer gesetzt bei (date|startdate|enddate|timestamp|modified|created)

    PS7: timestamp in Millisekunden geändert

    Gruß Tino

    1 Antwort Letzte Antwort
    0
    • A Abwesend
      A Abwesend
      ammawel
      schrieb am zuletzt editiert von
      #530

      @Tino-0
      Hallo,
      mindestens ebenso spannend wie das Ergebnis finde ich den Weg, eine Korrektur durch ChatGPT machen zu lassen.

      Bis vor ein paar Tagen dachte ich, dass nach der Anpassung durch Marc Berg die aktuellen Werte zuverlässig im Ordner measuregrps01 liegen, aber nach hinzufügen der Herzfrequenz zu den Messungen der Body-Smart-Waage fing das Chaos wieder an: aktuelle Herzfrequenz in 01 und aktuelles Gewicht in 02, dann mal beides in 01 u.s.w.. Das Entfernen der Herzfrequenzmessung brachte wieder Ordnung ins Chaos.

      Zwei Fragen zu Deiner Lösung:

      • Basiert Deine Modifikation auf der bereits geänderten main.js vom github-Verzeichnis des ursprünglichen Ersteller TA2K (https://github.com/TA2k/ioBroker.withings)? In dieser Version wurde die Sortierung von Marc Berg ja bereits eingepflegt.
      • Wirst Du auch ein pull-request erstellen, sodass Deine Änderungen eingepflegt werden können und hoffentlich eine stabil funktionierende Version des Adapters entsteht?

      Danke für Deine Mühen,
      vG, Achim

      1 Antwort Letzte Antwort
      0
      • A Abwesend
        A Abwesend
        ammawel
        schrieb am zuletzt editiert von
        #531

        @Tino-0
        Problem: Was nimmt man als Trigger? Eine Aktualisierung der Werte in lastMeasures tritt bei jedem refresh des eingestellten Updateintervalls auf, eine Änderung der Werte scheidet auch aus...
        Bisher habe ich auf eine Änderung von date in measuregrp01 getriggert - so 100%ig verlässlich ist das aber auch nicht mehr nach den Erfahrungen mit der Herzfrequenz (s.o.).

        T 1 Antwort Letzte Antwort
        0
        • A ammawel

          @Tino-0
          Problem: Was nimmt man als Trigger? Eine Aktualisierung der Werte in lastMeasures tritt bei jedem refresh des eingestellten Updateintervalls auf, eine Änderung der Werte scheidet auch aus...
          Bisher habe ich auf eine Änderung von date in measuregrp01 getriggert - so 100%ig verlässlich ist das aber auch nicht mehr nach den Erfahrungen mit der Herzfrequenz (s.o.).

          T Online
          T Online
          Tino 0
          schrieb am zuletzt editiert von
          #532

          @ammawel

          Hallo,

          ChatGPT hat es so implementiert, wobei bei lastActivity noch ein Fehler drin war und es ist oben eine neue Version drin.

          Zitat von ChatGPT
          Fazit: Woher kommt der „letzte Wert“? – Kurzfassung

          1. lastMeasures
            Die Withings-API liefert measuregrps ungeordnet.
            Dein Adapter sortiert sie zuerst nach Zeit absteigend (date → created → modified).
            Danach ist Index 0 immer die neueste Messung.
            writeLastMeasures() geht von oben nach unten durch und nimmt pro Mess-Typ nur den ersten Treffer.
            → Ergebnis: Der neueste Wert pro Typ.

          2. lastActivity
            Der Adapter sortiert activities nach modified absteigend.
            Danach ist activities[0] die neueste Aktivität.
            writeLastActivity() schreibt genau diesen Eintrag.
            → Ergebnis: Immer die aktuellste Aktivität.

          3. lastSleep / lastSleepSummary
            Sleep-Daten (series) werden nach startdate oder modified absteigend sortiert.
            Dadurch ist series[0] die neueste Nacht.
            writeLastSleepSummary() nimmt genau diesen Eintrag.
            → Ergebnis: Immer die aktuellste Sleep-Summary.

          Ultrakurz:
          Sortierung entscheidet, nicht Withings.
          Alles wird neueste zuerst sortiert.
          Der Adapter nimmt immer das erste Element [0] nach der Sortierung.
          Deshalb bekommst du überall den wirklich letzten Wert.

          Zitat Ende:

          Wenn morgen das Sleep durchläuft, werde morgen mal den pull-request erstellen.

          Gruß Tino

          1 Antwort Letzte Antwort
          0
          • A Abwesend
            A Abwesend
            ammawel
            schrieb am zuletzt editiert von
            #533

            @Tino-0
            Ja, der Algorithmus von ChatGPT ist schön und liefert die neuesten Werte. Genau das, was man braucht.
            Aber woran erkenne ich, dass der Wert erneuert wurde - das z.B. eine neue Wägung stattgefunden hat - und dass das neue Gewicht irgendwo gespeichert, verrechnet oder sonst was werden soll? Bei mir wird es z.B. mit anderen Parametern in eine csv-Datei geschrieben.
            Das Gewicht muss sich gegenüber der letzten Wägung nicht ändern und aktualisiert wird der Wert in lastMeasures immer, wenn das im Adapter eingestellte Updateintervall auslöst, bei mir alle 10 Minuten. Diese beiden Eigenschaften scheiden als Trigger also aus.

            T 1 Antwort Letzte Antwort
            0
            • A ammawel

              @Tino-0
              Ja, der Algorithmus von ChatGPT ist schön und liefert die neuesten Werte. Genau das, was man braucht.
              Aber woran erkenne ich, dass der Wert erneuert wurde - das z.B. eine neue Wägung stattgefunden hat - und dass das neue Gewicht irgendwo gespeichert, verrechnet oder sonst was werden soll? Bei mir wird es z.B. mit anderen Parametern in eine csv-Datei geschrieben.
              Das Gewicht muss sich gegenüber der letzten Wägung nicht ändern und aktualisiert wird der Wert in lastMeasures immer, wenn das im Adapter eingestellte Updateintervall auslöst, bei mir alle 10 Minuten. Diese beiden Eigenschaften scheiden als Trigger also aus.

              T Online
              T Online
              Tino 0
              schrieb am zuletzt editiert von
              #534

              @ammawel

              Es ist oben wieder eine neue Version jetzt mit Update Datum:

              ....lastMeasures.1
              ....lastMeasures.1_update
              ....lastMeasures.5
              ....lastMeasures.5_update

              Als Datum wurden "date" ausgewählt.

              Zitat CharGPT
              Welchen Zeitstempel willst du als „Update“-Zeit für lastMeasures verwenden?
              Die Withings-Measure-Group enthält drei mögliche Zeitfelder:

              ✔ date
              → Der eigentliche Messzeitpunkt (Unix)

              ✔ created
              → Wann die Messung auf dem Server erstellt wurde (Unix)

              ✔ modified
              → Wann die Messung zuletzt geändert wurde (Unix)

              Gruß Tino

              1 Antwort Letzte Antwort
              0
              • A Abwesend
                A Abwesend
                ammawel
                schrieb am zuletzt editiert von
                #535

                @Tino-0
                Super, fast perfekt. "Fast" weil die Zeit als String im ioBroker erscheint.
                Konkret:
                lastMeasures.1_update sagt 2025-11-18T06:06:05.000Z als String,
                die Messung war zur Zeit 1763445965000, entsprechend 18.11.2025 07:06:05
                Natürlich kann man das im Script umbasteln, aber vlt. kann ChatGPT das direkt in eine "normale" Zeitangabe ändern.

                Vielen Dnk für "eure" Mühen,
                vG Achim

                T 1 Antwort Letzte Antwort
                0
                • A ammawel

                  @Tino-0
                  Super, fast perfekt. "Fast" weil die Zeit als String im ioBroker erscheint.
                  Konkret:
                  lastMeasures.1_update sagt 2025-11-18T06:06:05.000Z als String,
                  die Messung war zur Zeit 1763445965000, entsprechend 18.11.2025 07:06:05
                  Natürlich kann man das im Script umbasteln, aber vlt. kann ChatGPT das direkt in eine "normale" Zeitangabe ändern.

                  Vielen Dnk für "eure" Mühen,
                  vG Achim

                  T Online
                  T Online
                  Tino 0
                  schrieb am zuletzt editiert von
                  #536

                  @ammawel

                  habe es geändert, oben ist eine neue Version drin.

                  Gruß Tino

                  1 Antwort Letzte Antwort
                  0
                  • A Abwesend
                    A Abwesend
                    ammawel
                    schrieb am zuletzt editiert von
                    #537

                    @Tino-0
                    die Zeitangabe ist immer noch vom Typ String, jetzt allerdings in einem ISO-Format.
                    Sollte das nicht wie für Datum-/Zeit-Werte in Javascript und ioBroker üblich eine Zahl sein, die die Millisekunden seit dem 1.1.1970 angibt (Unix-Angabe)?
                    Die date-Angaben der measuregrpsXX sind doch auch vom Typ Number und geben die Millisekunden an.

                    T 1 Antwort Letzte Antwort
                    0
                    • A ammawel

                      @Tino-0
                      die Zeitangabe ist immer noch vom Typ String, jetzt allerdings in einem ISO-Format.
                      Sollte das nicht wie für Datum-/Zeit-Werte in Javascript und ioBroker üblich eine Zahl sein, die die Millisekunden seit dem 1.1.1970 angibt (Unix-Angabe)?
                      Die date-Angaben der measuregrpsXX sind doch auch vom Typ Number und geben die Millisekunden an.

                      T Online
                      T Online
                      Tino 0
                      schrieb am zuletzt editiert von
                      #538

                      @ammawel

                      die war im ISO Format 2025-11-18T06:06:05.000Z jetzt ist sie in .toLocaleString("de-DE")

                      1. ISO-8601 (der Standard für alles, ideal fürs Logging & ioBroker)
                      new Date(ts * 1000).toISOString()
                      
                      Format:
                      2025-02-19T10:32:55.000Z
                      
                      
                      ✓ Maschinentauglich
                      ✓ Sortierbar
                      ✓ Universell
                      ✓ ioBroker liebt es
                      
                      1. Lokales Datum + Zeit (z.B. „2025-02-19 11:32:55“)
                      new Date(ts * 1000).toLocaleString("de-DE")
                      
                      Beispiel:
                      19.02.2025, 11:32:55
                      
                      1. Nur Datum
                      new Date(ts * 1000).toLocaleDateString("de-DE")
                      
                      Ergebnis:
                      19.02.2025
                      
                      1. Nur Uhrzeit
                      new Date(ts * 1000).toLocaleTimeString("de-DE")
                      
                      Ergebnis:
                      11:32:55
                      
                      1. UNIX Timestamp
                      Wenn du den Rohwert willst:
                      
                      ts    // Sekunden
                      Date.now() // Millisekunden
                      

                      Was willst du haben?

                      Gruß Tino

                      1 Antwort Letzte Antwort
                      0
                      • A Abwesend
                        A Abwesend
                        ammawel
                        schrieb am zuletzt editiert von
                        #539

                        "Was willst du haben?" :blush: ja ja, hab verstanden :blush:
                        ernsthaft: auf keinen Fall einen String, ganz normal die Millisekunden seit dem 1.1.1970 als Typ Number,
                        also z.B. 1763502691926 für jetzt, damit kann man problemlos jedes gewünschte Anwenderformat erzeugen

                        Gruß Achim

                        T 1 Antwort Letzte Antwort
                        0
                        • A ammawel

                          "Was willst du haben?" :blush: ja ja, hab verstanden :blush:
                          ernsthaft: auf keinen Fall einen String, ganz normal die Millisekunden seit dem 1.1.1970 als Typ Number,
                          also z.B. 1763502691926 für jetzt, damit kann man problemlos jedes gewünschte Anwenderformat erzeugen

                          Gruß Achim

                          T Online
                          T Online
                          Tino 0
                          schrieb am zuletzt editiert von
                          #540

                          @ammawel

                          so jetzt schaue mal es gibt jetzt zusätzlich noch den 1_timestamp.

                          1 Antwort Letzte Antwort
                          0
                          • A Abwesend
                            A Abwesend
                            ammawel
                            schrieb am zuletzt editiert von
                            #541

                            uff...

                            • XX_timestamp hat den richtigen Zahlenwert
                            • wenn die Eigenschaft "role" für XX_timestamp nicht "value" sondern "date" ist, erhält man im Objektbaum des ioBrokers auch eine Darstellung als Datum, obwohl es immer noch eine Zahl ist, nach der man sortieren kann, Differenzen bilden kann, in jedem beliebigen Datums- / Zeitformat anzeigen lassen kann etc.

                            Dann ist ein zusätzlicher Datenpunkt mit dem Datum als String nicht mehr erforderlich, eher verwirrend, und XX_update könnte entfallen.
                            Vorschlag: Entweder XX_timestamp (würde ich bevorzugen) oder XX_update

                            Ich habe das mal im Objektbaum für 1_timestamp geändert:

                            timestamp_1.png

                            timestamp_2.png

                            T 1 Antwort Letzte Antwort
                            0
                            • A ammawel

                              uff...

                              • XX_timestamp hat den richtigen Zahlenwert
                              • wenn die Eigenschaft "role" für XX_timestamp nicht "value" sondern "date" ist, erhält man im Objektbaum des ioBrokers auch eine Darstellung als Datum, obwohl es immer noch eine Zahl ist, nach der man sortieren kann, Differenzen bilden kann, in jedem beliebigen Datums- / Zeitformat anzeigen lassen kann etc.

                              Dann ist ein zusätzlicher Datenpunkt mit dem Datum als String nicht mehr erforderlich, eher verwirrend, und XX_update könnte entfallen.
                              Vorschlag: Entweder XX_timestamp (würde ich bevorzugen) oder XX_update

                              Ich habe das mal im Objektbaum für 1_timestamp geändert:

                              timestamp_1.png

                              timestamp_2.png

                              T Online
                              T Online
                              Tino 0
                              schrieb am zuletzt editiert von
                              #542

                              @ammawel

                              so aber jetzt!

                              Gruß Tino

                              1 Antwort Letzte Antwort
                              0
                              • A Abwesend
                                A Abwesend
                                ammawel
                                schrieb am zuletzt editiert von
                                #543

                                Perfekt!
                                Vielen Dank für Deine Arbeit, Mühen und die Geduld.
                                Mir hat der Abend viel Spaß bereitet, jetzt reicht es aber auch.
                                Viele Grüße und gute Nacht
                                Achim

                                T 1 Antwort Letzte Antwort
                                0
                                • A ammawel

                                  Perfekt!
                                  Vielen Dank für Deine Arbeit, Mühen und die Geduld.
                                  Mir hat der Abend viel Spaß bereitet, jetzt reicht es aber auch.
                                  Viele Grüße und gute Nacht
                                  Achim

                                  T Online
                                  T Online
                                  Tino 0
                                  schrieb am zuletzt editiert von
                                  #544

                                  @ammawel

                                  gleiche

                                  T 1 Antwort Letzte Antwort
                                  0
                                  • T Tino 0

                                    @ammawel

                                    gleiche

                                    T Online
                                    T Online
                                    Tino 0
                                    schrieb am zuletzt editiert von
                                    #545

                                    moin,

                                    ich habe noch einmal etwas geändert, die Rolle date wird immer gesetzt bei (date|startdate|enddate|timestamp|modified|created) in den Datenpunkten lastActivity und lastSleep. Neuste Version ist oben drin.

                                    Gruß Tino

                                    1 Antwort Letzte Antwort
                                    0
                                    • A Abwesend
                                      A Abwesend
                                      ammawel
                                      schrieb am zuletzt editiert von ammawel
                                      #546

                                      @Tino-0
                                      Moin,
                                      ich hab auch noch was:
                                      tsRaw ist um Faktor 1000 zu klein, zur Zeit sind das Sekunden, es müssen aber für ein richtiges Datum-Objekt Millisekunden sein.

                                      Zeile 398 müsste mMn dann so aussehen (gerade getestet):

                                      const tsRaw = Number(grp.date * 1000) || null; // Millisekunden
                                      

                                      Edit:
                                      Im Objektbaum wird das Datum mit dem zu kleinen Wert zwar richtig angezeigt (warum?), wenn man aber den Wert editiert, stimmt das Datum in den Datumsfeldern die dann angezeigt werden nicht mehr. Mit Faktor 1000 passt es.

                                      Edit:
                                      Die Withings-Api liefert wohl generell Sekunden statt Millisekunden...

                                      1 Antwort Letzte Antwort
                                      0
                                      • A Abwesend
                                        A Abwesend
                                        ammawel
                                        schrieb am zuletzt editiert von
                                        #547

                                        const tsRaw = typeof grp.date === "number" ? grp.date * 1000 : null;

                                        T 2 Antworten Letzte Antwort
                                        0
                                        • A ammawel

                                          const tsRaw = typeof grp.date === "number" ? grp.date * 1000 : null;

                                          T Online
                                          T Online
                                          Tino 0
                                          schrieb am zuletzt editiert von
                                          #548

                                          @ammawel

                                          ist geändert in

                                          const tsRaw = typeof grp.date === "number" ? grp.date * 1000 : null;
                                          
                                          
                                          1 Antwort Letzte Antwort
                                          0
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          859

                                          Online

                                          32.4k

                                          Benutzer

                                          81.4k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2025
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe