Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

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

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Tester
  4. ...nicht in offiziellem Repo
  5. Life360 NextGeneration

NEWS

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    6
    1
    218

  • Verwendung von KI bitte immer deutlich kennzeichnen
    HomoranH
    Homoran
    8
    1
    217

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    18
    1
    891

Life360 NextGeneration

Geplant Angeheftet Gesperrt Verschoben ...nicht in offiziellem Repo
copilotstandortgeo locationmap
130 Beiträge 14 Kommentatoren 1.1k Aufrufe 18 Beobachtet
  • Ä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.
  • Merlin123M Offline
    Merlin123M Offline
    Merlin123
    schrieb am zuletzt editiert von
    #107

    hab mal ChatGPT genutzt (ganz nach dem Vorbild eines hier auch aktiven Mitglieds 😀 )
    Herausgekommen ist ein Script, dass aus den Koordinaten zum einen ein Lookup nach der Adresse macht und schaut, ob es im näheren Umkreis einen benannten Ort gibt.
    Das ganze ist so gemacht, dass die Abfrageintervalle ausreichend groß sind und auch nur bei Positionsänderungen ein Lookup gemacht wird, damit die Dienste keinen Streß machen.
    Das ganze wird dann in Datenpunkten gespeichert und kann in der VIS genutzt werden.
    Ist jetzt die 2. Version, hat sicher noch Verbesserungspotential.
    Hab mal meine Laufrunde für nen Test benutzt:
    pos1.jpeg

    Der erste Standort ist der aus dem Adapter, die Adresse und der Name darunter aus dem Script. Wenn kein Ort gefunden wird ist das entsprechende Feld leer

    in den DPs sieht es so aus:

    pos2.png

    Und hier ist das Script.
    ACHTUNG: Bitte im USER_AGENT eine echte Kontaktmöglichkeit eintragen. Sonst kann es sein, dass die Dienste Stress machen....

    Gibt auch diverse Parameter, die man anpassen kann.
    Ihr müsst halt die Datenpunkte im Script entsprechend euren DPs anpassen...
    Ich werde auch mal schauen, wie gut man das Script auf mehrere Personen skalieren kann.

    /************************************************************
     * Oliver Standort aus Life360 -> menschenlesbarer Text
     * 
     * Ausgabe:
     *   0_userdata.0.VIS.Position.Oliver.Standort_Oliver
     *
     * Beispiel-Ergebnis:
     *   "REWE, Musterstraße 1, 12345 Musterstadt"
     * oder
     *   "Musterstraße 1, 12345 Musterstadt"
     *
     * Voraussetzungen:
     * - JavaScript-Adapter in ioBroker
     * - Internetzugriff
     *
     * WICHTIG:
     * Bitte im USER_AGENT eine echte Kontaktmöglichkeit eintragen.
     ************************************************************/
    
    const LAT_STATE = 'life360ng.0.people.bxxxxxxe.latitude';
    const LON_STATE = 'life360ng.0.people.bxxxxxa7e.longitude';
    
    const TARGET_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver';
    
    // Optional: zusätzliche Debug-/Hilfsstates
    const ADDRESS_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_Adresse';
    const POI_STATE     = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_POI';
    const LASTLAT_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLat';
    const LASTLON_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLon';
    const LASTTS_STATE  = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLookupTs';
    
    // Einstellungen
    const MIN_INTERVAL_MS   = 60 * 1000; // max. 1 Lookup pro Minute
    const MIN_DISTANCE_M    = 40;        // nur neu abfragen, wenn mind. 40 m Bewegung
    const DEBOUNCE_MS       = 5000;      // 5 Sekunden warten, falls lat/lon nacheinander aktualisiert werden
    const POI_RADIUS_M      = 15;        // POI-Suche im Umkreis von 40 m
    const SECOND_TRY_RADIUS = 25;       // optionaler zweiter Radius, falls bei 40 m nichts gefunden
    const GAP_BETWEEN_CALLS = 1500;      // 1,5 s Pause zwischen Nominatim und Overpass
    
    // WICHTIG: bitte anpassen
    const USER_AGENT = 'ioBroker-Standortscript/1.0 (Kontakt: deine@mailadresse.de)';
    
    let debounceTimer = null;
    let lookupRunning = false;
    
    /* ---------------------------------------------------------
     * Initiale States anlegen
     * --------------------------------------------------------- */
    createStateIfMissing(TARGET_STATE, '', 'string', 'Oliver Standort');
    createStateIfMissing(ADDRESS_STATE, '', 'string', 'Oliver Adresse');
    createStateIfMissing(POI_STATE, '', 'string', 'Oliver POI');
    createStateIfMissing(LASTLAT_STATE, null, 'number', 'Oliver letzte Lookup-Latitude');
    createStateIfMissing(LASTLON_STATE, null, 'number', 'Oliver letzte Lookup-Longitude');
    createStateIfMissing(LASTTS_STATE, 0, 'number', 'Oliver letzter Lookup-Zeitstempel');
    
    /* ---------------------------------------------------------
     * Trigger
     * --------------------------------------------------------- */
    on({id: LAT_STATE, change: 'ne'}, scheduleLookup);
    on({id: LON_STATE, change: 'ne'}, scheduleLookup);
    
    // optional einmal beim Scriptstart prüfen
    scheduleLookup();
    
    /* ---------------------------------------------------------
     * Hauptlogik
     * --------------------------------------------------------- */
    function scheduleLookup() {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
            runLookup().catch(err => log('Standortscript Fehler: ' + err, 'error'));
        }, DEBOUNCE_MS);
    }
    
    async function runLookup() {
        if (lookupRunning) {
            log('Lookup läuft bereits, überspringe parallelen Aufruf.', 'debug');
            return;
        }
    
        const lat = parseFloat(getState(LAT_STATE).val);
        const lon = parseFloat(getState(LON_STATE).val);
    
        if (!isFinite(lat) || !isFinite(lon)) {
            log('Ungültige Koordinaten, Lookup abgebrochen.', 'warn');
            return;
        }
    
        if (!shouldLookup(lat, lon)) {
            log('Lookup übersprungen (Cooldown oder Bewegung zu gering).', 'debug');
            return;
        }
    
        lookupRunning = true;
    
        try {
            // 1) Adresse holen
            const addressInfo = await getAddressFromNominatim(lat, lon);
            const addressText = buildAddressText(addressInfo);
    
            setState(TARGET_STATE, addressText || 'Unbekannter Standort', true);
            setState(ADDRESS_STATE, addressText || '', true);
            setState(POI_STATE, '', true);
            
            // Lookup-Metadaten sofort speichern, sobald die Adresse erfolgreich ermittelt wurde
            setState(LASTLAT_STATE, lat, true);
            setState(LASTLON_STATE, lon, true);
            setState(LASTTS_STATE, Date.now(), true);
    
            // 2) bewusste Pause zwischen den Requests
            await sleep(GAP_BETWEEN_CALLS);
    
            // 3) generischen POI im Umkreis suchen
            let poi = await getNearbyNamedPoi(lat, lon, POI_RADIUS_M);
    
            // optional zweiter Versuch mit größerem Radius
            if (!poi) {
                poi = await getNearbyNamedPoi(lat, lon, SECOND_TRY_RADIUS);
            }
    
            let finalText = addressText || 'Unbekannter Standort';
    
            if (poi && poi.name) {
                setState(POI_STATE, poi.name, true);
    
                // Doppelte Infos vermeiden
                if (addressText && !addressText.toLowerCase().includes((poi.name || '').toLowerCase())) {
                    finalText = poi.name + ', ' + addressText;
                } else {
                    finalText = poi.name;
                }
            }
    
            setState(TARGET_STATE, finalText, true);
    
            log('Standort aktualisiert: ' + finalText, 'info');
        } catch (err) {
            log('Fehler beim Standort-Lookup: ' + err, 'error');
        } finally {
            lookupRunning = false;
        }
    }
    
    /* ---------------------------------------------------------
     * Entscheiden, ob Lookup nötig ist
     * --------------------------------------------------------- */
    function shouldLookup(lat, lon) {
        const lastTs  = Number(getState(LASTTS_STATE).val || 0);
        const lastLat = parseFloat(getState(LASTLAT_STATE).val);
        const lastLon = parseFloat(getState(LASTLON_STATE).val);
    
        const now = Date.now();
    
        if (lastTs && (now - lastTs) < MIN_INTERVAL_MS) {
            return false;
        }
    
        if (isFinite(lastLat) && isFinite(lastLon)) {
            const dist = distanceMeters(lastLat, lastLon, lat, lon);
            if (dist < MIN_DISTANCE_M) {
                return false;
            }
        }
    
        return true;
    }
    
    /* ---------------------------------------------------------
     * Nominatim Reverse Geocoding
     * --------------------------------------------------------- */
    async function getAddressFromNominatim(lat, lon) {
        const url =
            'https://nominatim.openstreetmap.org/reverse' +
            '?format=jsonv2' +
            '&lat=' + encodeURIComponent(lat) +
            '&lon=' + encodeURIComponent(lon) +
            '&addressdetails=1' +
            '&zoom=18';
    
        const response = await httpGetAsync(url, {
            'User-Agent': USER_AGENT,
            'Accept': 'application/json'
        });
    
        if (response.statusCode < 200 || response.statusCode >= 300) {
            throw new Error('Nominatim HTTP ' + response.statusCode);
        }
    
        let data;
        try {
            data = JSON.parse(response.data);
        } catch (e) {
            throw new Error('Nominatim JSON konnte nicht geparst werden');
        }
    
        return data;
    }
    
    function buildAddressText(data) {
        if (!data) return '';
    
        const a = data.address || {};
    
        const road =
            a.road ||
            a.pedestrian ||
            a.footway ||
            a.path ||
            a.cycleway ||
            '';
    
        const houseNumber = a.house_number || '';
        const postcode = a.postcode || '';
        const city =
            a.city ||
            a.town ||
            a.village ||
            a.hamlet ||
            a.municipality ||
            '';
    
        let result = '';
    
        if (road) {
            result += road;
            if (houseNumber) result += ' ' + houseNumber;
        }
    
        if (postcode || city) {
            if (result) result += ', ';
            result += [postcode, city].filter(Boolean).join(' ');
        }
    
        // Fallback auf display_name, falls die Einzelteile fehlen
        if (!result && data.display_name) {
            result = data.display_name;
        }
    
        return cleanText(result);
    }
    
    /* ---------------------------------------------------------
     * Overpass: benannten Ort/POI in der Nähe finden
     * --------------------------------------------------------- */
    async function getNearbyNamedPoi(lat, lon, radius) {
        // Priorisiert benannte Objekte, die für Menschen sinnvoll sind
        const query = `
    [out:json][timeout:10];
    (
      nwr(around:${radius},${lat},${lon})["name"]["shop"];
      nwr(around:${radius},${lat},${lon})["name"]["amenity"];
      nwr(around:${radius},${lat},${lon})["name"]["tourism"];
      nwr(around:${radius},${lat},${lon})["name"]["leisure"];
      nwr(around:${radius},${lat},${lon})["name"]["office"];
      nwr(around:${radius},${lat},${lon})["name"]["building"];
    );
    out center tags;
    `;
    
        const response = await httpPostAsync(
            'https://overpass-api.de/api/interpreter',
            query,
            {
                'User-Agent': USER_AGENT,
                'Content-Type': 'text/plain; charset=utf-8',
                'Accept': 'application/json'
            }
        );
    
        if (response.statusCode < 200 || response.statusCode >= 300) {
            throw new Error('Overpass HTTP ' + response.statusCode);
        }
    
        let data;
        try {
            data = JSON.parse(response.data);
        } catch (e) {
            throw new Error('Overpass JSON konnte nicht geparst werden');
        }
    
        if (!data.elements || !data.elements.length) {
            return null;
        }
    
        // nächsten sinnvollen Treffer bestimmen
        const candidates = data.elements
            .map(el => {
                const cLat = el.lat || (el.center && el.center.lat);
                const cLon = el.lon || (el.center && el.center.lon);
                const name = el.tags && el.tags.name ? String(el.tags.name).trim() : '';
                const dist = (isFinite(cLat) && isFinite(cLon))
                    ? distanceMeters(lat, lon, cLat, cLon)
                    : 999999;
    
                return {
                    name: cleanText(name),
                    tags: el.tags || {},
                    lat: cLat,
                    lon: cLon,
                    dist: dist
                };
            })
            .filter(x => x.name);
    
        if (!candidates.length) return null;
    
        // etwas priorisieren: shop/amenity/tourism/leisure vor building
        candidates.sort((a, b) => {
            const pa = poiPriority(a.tags);
            const pb = poiPriority(b.tags);
            if (pa !== pb) return pa - pb;
            return a.dist - b.dist;
        });
    
        return candidates[0];
    }
    
    function poiPriority(tags) {
        if (tags.shop) return 1;
        if (tags.amenity) return 2;
        if (tags.tourism) return 3;
        if (tags.leisure) return 4;
        if (tags.office) return 5;
        if (tags.building) return 9;
        return 99;
    }
    
    /* ---------------------------------------------------------
     * HTTP-Helfer
     * --------------------------------------------------------- */
    function httpGetAsync(url, headers) {
        return new Promise((resolve, reject) => {
            httpGet(url, { headers: headers, timeout: 15000 }, (err, response) => {
                if (err) return reject(err);
                resolve(response);
            });
        });
    }
    
    function httpPostAsync(url, body, headers) {
        return new Promise((resolve, reject) => {
            httpPost(url, body, { headers: headers, timeout: 20000 }, (err, response) => {
                if (err) return reject(err);
                resolve(response);
            });
        });
    }
    
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    /* ---------------------------------------------------------
     * Mathe / Hilfsfunktionen
     * --------------------------------------------------------- */
    function distanceMeters(lat1, lon1, lat2, lon2) {
        const R = 6371000;
        const toRad = deg => deg * Math.PI / 180;
    
        const dLat = toRad(lat2 - lat1);
        const dLon = toRad(lon2 - lon1);
    
        const a =
            Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
            Math.sin(dLon / 2) * Math.sin(dLon / 2);
    
        return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    }
    
    function cleanText(str) {
        return String(str || '')
            .replace(/\s+/g, ' ')
            .replace(/\s+,/g, ',')
            .trim();
    }
    
    /* ---------------------------------------------------------
     * State-Helfer
     * --------------------------------------------------------- */
    function createStateIfMissing(id, def, type, name) {
        if (!existsState(id)) {
            createState(id, def, {
                name: name,
                type: type,
                role: 'text',
                read: true,
                write: true
            });
        }
    }
    

    Das ganz hat komplett ChatGPT gemacht... Bin durchaus sehr begeistert...... Meine JS Kenntnisse sind nur sehr rudimentär über "nicht vorhanden"....

    Beta-Tester

    skvarelS 1 Antwort Letzte Antwort
    1
    • Merlin123M Merlin123

      hab mal ChatGPT genutzt (ganz nach dem Vorbild eines hier auch aktiven Mitglieds 😀 )
      Herausgekommen ist ein Script, dass aus den Koordinaten zum einen ein Lookup nach der Adresse macht und schaut, ob es im näheren Umkreis einen benannten Ort gibt.
      Das ganze ist so gemacht, dass die Abfrageintervalle ausreichend groß sind und auch nur bei Positionsänderungen ein Lookup gemacht wird, damit die Dienste keinen Streß machen.
      Das ganze wird dann in Datenpunkten gespeichert und kann in der VIS genutzt werden.
      Ist jetzt die 2. Version, hat sicher noch Verbesserungspotential.
      Hab mal meine Laufrunde für nen Test benutzt:
      pos1.jpeg

      Der erste Standort ist der aus dem Adapter, die Adresse und der Name darunter aus dem Script. Wenn kein Ort gefunden wird ist das entsprechende Feld leer

      in den DPs sieht es so aus:

      pos2.png

      Und hier ist das Script.
      ACHTUNG: Bitte im USER_AGENT eine echte Kontaktmöglichkeit eintragen. Sonst kann es sein, dass die Dienste Stress machen....

      Gibt auch diverse Parameter, die man anpassen kann.
      Ihr müsst halt die Datenpunkte im Script entsprechend euren DPs anpassen...
      Ich werde auch mal schauen, wie gut man das Script auf mehrere Personen skalieren kann.

      /************************************************************
       * Oliver Standort aus Life360 -> menschenlesbarer Text
       * 
       * Ausgabe:
       *   0_userdata.0.VIS.Position.Oliver.Standort_Oliver
       *
       * Beispiel-Ergebnis:
       *   "REWE, Musterstraße 1, 12345 Musterstadt"
       * oder
       *   "Musterstraße 1, 12345 Musterstadt"
       *
       * Voraussetzungen:
       * - JavaScript-Adapter in ioBroker
       * - Internetzugriff
       *
       * WICHTIG:
       * Bitte im USER_AGENT eine echte Kontaktmöglichkeit eintragen.
       ************************************************************/
      
      const LAT_STATE = 'life360ng.0.people.bxxxxxxe.latitude';
      const LON_STATE = 'life360ng.0.people.bxxxxxa7e.longitude';
      
      const TARGET_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver';
      
      // Optional: zusätzliche Debug-/Hilfsstates
      const ADDRESS_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_Adresse';
      const POI_STATE     = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_POI';
      const LASTLAT_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLat';
      const LASTLON_STATE = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLon';
      const LASTTS_STATE  = '0_userdata.0.VIS.Position.Oliver.Standort_Oliver_LastLookupTs';
      
      // Einstellungen
      const MIN_INTERVAL_MS   = 60 * 1000; // max. 1 Lookup pro Minute
      const MIN_DISTANCE_M    = 40;        // nur neu abfragen, wenn mind. 40 m Bewegung
      const DEBOUNCE_MS       = 5000;      // 5 Sekunden warten, falls lat/lon nacheinander aktualisiert werden
      const POI_RADIUS_M      = 15;        // POI-Suche im Umkreis von 40 m
      const SECOND_TRY_RADIUS = 25;       // optionaler zweiter Radius, falls bei 40 m nichts gefunden
      const GAP_BETWEEN_CALLS = 1500;      // 1,5 s Pause zwischen Nominatim und Overpass
      
      // WICHTIG: bitte anpassen
      const USER_AGENT = 'ioBroker-Standortscript/1.0 (Kontakt: deine@mailadresse.de)';
      
      let debounceTimer = null;
      let lookupRunning = false;
      
      /* ---------------------------------------------------------
       * Initiale States anlegen
       * --------------------------------------------------------- */
      createStateIfMissing(TARGET_STATE, '', 'string', 'Oliver Standort');
      createStateIfMissing(ADDRESS_STATE, '', 'string', 'Oliver Adresse');
      createStateIfMissing(POI_STATE, '', 'string', 'Oliver POI');
      createStateIfMissing(LASTLAT_STATE, null, 'number', 'Oliver letzte Lookup-Latitude');
      createStateIfMissing(LASTLON_STATE, null, 'number', 'Oliver letzte Lookup-Longitude');
      createStateIfMissing(LASTTS_STATE, 0, 'number', 'Oliver letzter Lookup-Zeitstempel');
      
      /* ---------------------------------------------------------
       * Trigger
       * --------------------------------------------------------- */
      on({id: LAT_STATE, change: 'ne'}, scheduleLookup);
      on({id: LON_STATE, change: 'ne'}, scheduleLookup);
      
      // optional einmal beim Scriptstart prüfen
      scheduleLookup();
      
      /* ---------------------------------------------------------
       * Hauptlogik
       * --------------------------------------------------------- */
      function scheduleLookup() {
          if (debounceTimer) clearTimeout(debounceTimer);
          debounceTimer = setTimeout(() => {
              runLookup().catch(err => log('Standortscript Fehler: ' + err, 'error'));
          }, DEBOUNCE_MS);
      }
      
      async function runLookup() {
          if (lookupRunning) {
              log('Lookup läuft bereits, überspringe parallelen Aufruf.', 'debug');
              return;
          }
      
          const lat = parseFloat(getState(LAT_STATE).val);
          const lon = parseFloat(getState(LON_STATE).val);
      
          if (!isFinite(lat) || !isFinite(lon)) {
              log('Ungültige Koordinaten, Lookup abgebrochen.', 'warn');
              return;
          }
      
          if (!shouldLookup(lat, lon)) {
              log('Lookup übersprungen (Cooldown oder Bewegung zu gering).', 'debug');
              return;
          }
      
          lookupRunning = true;
      
          try {
              // 1) Adresse holen
              const addressInfo = await getAddressFromNominatim(lat, lon);
              const addressText = buildAddressText(addressInfo);
      
              setState(TARGET_STATE, addressText || 'Unbekannter Standort', true);
              setState(ADDRESS_STATE, addressText || '', true);
              setState(POI_STATE, '', true);
              
              // Lookup-Metadaten sofort speichern, sobald die Adresse erfolgreich ermittelt wurde
              setState(LASTLAT_STATE, lat, true);
              setState(LASTLON_STATE, lon, true);
              setState(LASTTS_STATE, Date.now(), true);
      
              // 2) bewusste Pause zwischen den Requests
              await sleep(GAP_BETWEEN_CALLS);
      
              // 3) generischen POI im Umkreis suchen
              let poi = await getNearbyNamedPoi(lat, lon, POI_RADIUS_M);
      
              // optional zweiter Versuch mit größerem Radius
              if (!poi) {
                  poi = await getNearbyNamedPoi(lat, lon, SECOND_TRY_RADIUS);
              }
      
              let finalText = addressText || 'Unbekannter Standort';
      
              if (poi && poi.name) {
                  setState(POI_STATE, poi.name, true);
      
                  // Doppelte Infos vermeiden
                  if (addressText && !addressText.toLowerCase().includes((poi.name || '').toLowerCase())) {
                      finalText = poi.name + ', ' + addressText;
                  } else {
                      finalText = poi.name;
                  }
              }
      
              setState(TARGET_STATE, finalText, true);
      
              log('Standort aktualisiert: ' + finalText, 'info');
          } catch (err) {
              log('Fehler beim Standort-Lookup: ' + err, 'error');
          } finally {
              lookupRunning = false;
          }
      }
      
      /* ---------------------------------------------------------
       * Entscheiden, ob Lookup nötig ist
       * --------------------------------------------------------- */
      function shouldLookup(lat, lon) {
          const lastTs  = Number(getState(LASTTS_STATE).val || 0);
          const lastLat = parseFloat(getState(LASTLAT_STATE).val);
          const lastLon = parseFloat(getState(LASTLON_STATE).val);
      
          const now = Date.now();
      
          if (lastTs && (now - lastTs) < MIN_INTERVAL_MS) {
              return false;
          }
      
          if (isFinite(lastLat) && isFinite(lastLon)) {
              const dist = distanceMeters(lastLat, lastLon, lat, lon);
              if (dist < MIN_DISTANCE_M) {
                  return false;
              }
          }
      
          return true;
      }
      
      /* ---------------------------------------------------------
       * Nominatim Reverse Geocoding
       * --------------------------------------------------------- */
      async function getAddressFromNominatim(lat, lon) {
          const url =
              'https://nominatim.openstreetmap.org/reverse' +
              '?format=jsonv2' +
              '&lat=' + encodeURIComponent(lat) +
              '&lon=' + encodeURIComponent(lon) +
              '&addressdetails=1' +
              '&zoom=18';
      
          const response = await httpGetAsync(url, {
              'User-Agent': USER_AGENT,
              'Accept': 'application/json'
          });
      
          if (response.statusCode < 200 || response.statusCode >= 300) {
              throw new Error('Nominatim HTTP ' + response.statusCode);
          }
      
          let data;
          try {
              data = JSON.parse(response.data);
          } catch (e) {
              throw new Error('Nominatim JSON konnte nicht geparst werden');
          }
      
          return data;
      }
      
      function buildAddressText(data) {
          if (!data) return '';
      
          const a = data.address || {};
      
          const road =
              a.road ||
              a.pedestrian ||
              a.footway ||
              a.path ||
              a.cycleway ||
              '';
      
          const houseNumber = a.house_number || '';
          const postcode = a.postcode || '';
          const city =
              a.city ||
              a.town ||
              a.village ||
              a.hamlet ||
              a.municipality ||
              '';
      
          let result = '';
      
          if (road) {
              result += road;
              if (houseNumber) result += ' ' + houseNumber;
          }
      
          if (postcode || city) {
              if (result) result += ', ';
              result += [postcode, city].filter(Boolean).join(' ');
          }
      
          // Fallback auf display_name, falls die Einzelteile fehlen
          if (!result && data.display_name) {
              result = data.display_name;
          }
      
          return cleanText(result);
      }
      
      /* ---------------------------------------------------------
       * Overpass: benannten Ort/POI in der Nähe finden
       * --------------------------------------------------------- */
      async function getNearbyNamedPoi(lat, lon, radius) {
          // Priorisiert benannte Objekte, die für Menschen sinnvoll sind
          const query = `
      [out:json][timeout:10];
      (
        nwr(around:${radius},${lat},${lon})["name"]["shop"];
        nwr(around:${radius},${lat},${lon})["name"]["amenity"];
        nwr(around:${radius},${lat},${lon})["name"]["tourism"];
        nwr(around:${radius},${lat},${lon})["name"]["leisure"];
        nwr(around:${radius},${lat},${lon})["name"]["office"];
        nwr(around:${radius},${lat},${lon})["name"]["building"];
      );
      out center tags;
      `;
      
          const response = await httpPostAsync(
              'https://overpass-api.de/api/interpreter',
              query,
              {
                  'User-Agent': USER_AGENT,
                  'Content-Type': 'text/plain; charset=utf-8',
                  'Accept': 'application/json'
              }
          );
      
          if (response.statusCode < 200 || response.statusCode >= 300) {
              throw new Error('Overpass HTTP ' + response.statusCode);
          }
      
          let data;
          try {
              data = JSON.parse(response.data);
          } catch (e) {
              throw new Error('Overpass JSON konnte nicht geparst werden');
          }
      
          if (!data.elements || !data.elements.length) {
              return null;
          }
      
          // nächsten sinnvollen Treffer bestimmen
          const candidates = data.elements
              .map(el => {
                  const cLat = el.lat || (el.center && el.center.lat);
                  const cLon = el.lon || (el.center && el.center.lon);
                  const name = el.tags && el.tags.name ? String(el.tags.name).trim() : '';
                  const dist = (isFinite(cLat) && isFinite(cLon))
                      ? distanceMeters(lat, lon, cLat, cLon)
                      : 999999;
      
                  return {
                      name: cleanText(name),
                      tags: el.tags || {},
                      lat: cLat,
                      lon: cLon,
                      dist: dist
                  };
              })
              .filter(x => x.name);
      
          if (!candidates.length) return null;
      
          // etwas priorisieren: shop/amenity/tourism/leisure vor building
          candidates.sort((a, b) => {
              const pa = poiPriority(a.tags);
              const pb = poiPriority(b.tags);
              if (pa !== pb) return pa - pb;
              return a.dist - b.dist;
          });
      
          return candidates[0];
      }
      
      function poiPriority(tags) {
          if (tags.shop) return 1;
          if (tags.amenity) return 2;
          if (tags.tourism) return 3;
          if (tags.leisure) return 4;
          if (tags.office) return 5;
          if (tags.building) return 9;
          return 99;
      }
      
      /* ---------------------------------------------------------
       * HTTP-Helfer
       * --------------------------------------------------------- */
      function httpGetAsync(url, headers) {
          return new Promise((resolve, reject) => {
              httpGet(url, { headers: headers, timeout: 15000 }, (err, response) => {
                  if (err) return reject(err);
                  resolve(response);
              });
          });
      }
      
      function httpPostAsync(url, body, headers) {
          return new Promise((resolve, reject) => {
              httpPost(url, body, { headers: headers, timeout: 20000 }, (err, response) => {
                  if (err) return reject(err);
                  resolve(response);
              });
          });
      }
      
      function sleep(ms) {
          return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      /* ---------------------------------------------------------
       * Mathe / Hilfsfunktionen
       * --------------------------------------------------------- */
      function distanceMeters(lat1, lon1, lat2, lon2) {
          const R = 6371000;
          const toRad = deg => deg * Math.PI / 180;
      
          const dLat = toRad(lat2 - lat1);
          const dLon = toRad(lon2 - lon1);
      
          const a =
              Math.sin(dLat / 2) * Math.sin(dLat / 2) +
              Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
              Math.sin(dLon / 2) * Math.sin(dLon / 2);
      
          return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
      }
      
      function cleanText(str) {
          return String(str || '')
              .replace(/\s+/g, ' ')
              .replace(/\s+,/g, ',')
              .trim();
      }
      
      /* ---------------------------------------------------------
       * State-Helfer
       * --------------------------------------------------------- */
      function createStateIfMissing(id, def, type, name) {
          if (!existsState(id)) {
              createState(id, def, {
                  name: name,
                  type: type,
                  role: 'text',
                  read: true,
                  write: true
              });
          }
      }
      

      Das ganz hat komplett ChatGPT gemacht... Bin durchaus sehr begeistert...... Meine JS Kenntnisse sind nur sehr rudimentär über "nicht vorhanden"....

      skvarelS Online
      skvarelS Online
      skvarel
      Developer
      schrieb am zuletzt editiert von
      #108

      @Merlin123 .. ich mag Chat GPT nicht so wirklich beim Scripten. Ich schaue mit das aber mal mit Copilot zusammen an ;)

      #TeamInventwo
      Unsere Adapter:
      Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

      Wer uns mit einem Kaffee unterstützen möchte: PayPal

      1 Antwort Letzte Antwort
      1
      • R RikDRS

        ich muss gestehen, ich verstehe die App noch nicht... :/
        Wann werden neue Standorte registriert als ein häufig besuchter Ort? Oder muss ich alle "relevanten" Standorte in der App oder im Adapter erfassen? Wie kommen "Location" in den Adapter von weiteren Anwendern (zB von meiner Frau, ohne das ich vor Ort gewesen oder sein muss
        Ich habe zB die Arbeitsstätte meiner Frau im Adapter angelegt (so wie meine auch), allerdings wird der Standort meiner Frau bzw dem Handy dort nicht bzw als "noch nicht registriert" erfasst

        ich habe nen Knoten im Kopf ;)

        skvarelS Online
        skvarelS Online
        skvarel
        Developer
        schrieb am zuletzt editiert von skvarel
        #109

        @RikDRS sagte:

        ich muss gestehen, ich verstehe die App noch nicht... :/
        Wann werden neue Standorte registriert als ein häufig besuchter Ort? Oder muss ich alle "relevanten" Standorte in der App oder im Adapter erfassen? Wie kommen "Location" in den Adapter von weiteren Anwendern (zB von meiner Frau, ohne das ich vor Ort gewesen oder sein muss
        Ich habe zB die Arbeitsstätte meiner Frau im Adapter angelegt (so wie meine auch), allerdings wird der Standort meiner Frau bzw dem Handy dort nicht bzw als "noch nicht registriert" erfasst

        ich habe nen Knoten im Kopf ;)

        Die App kann ich dir hier nicht erklären, das würde den Rahmen sprengen.

        Von allein wird da kein Standort angelegt, das muss man händisch machen. Entweder in der App, dann kommen die Orte auch in den ioBroker oder aber direkt im ioBroker, die gehen aber nicht in die App über! Der Adapter ist eine Einbahnstraße und ließt nur die App aus.

        #TeamInventwo
        Unsere Adapter:
        Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

        Wer uns mit einem Kaffee unterstützen möchte: PayPal

        1 Antwort Letzte Antwort
        1
        • Merlin123M Offline
          Merlin123M Offline
          Merlin123
          schrieb am zuletzt editiert von
          #110

          Hier ne version für mehrere Personen, aber noch nicht vollständig getestet...

          /************************************************************
           * Mehrpersonen-Standortscript für ioBroker + Life360
           *
           * Für jede Person:
           * - Reverse Geocoding per Nominatim
           * - optional POI-Suche per Overpass
           * - Ausgabe in eigenen States
           *
           * WICHTIG:
           * USER_AGENT bitte mit echter Kontaktmöglichkeit anpassen.
           ************************************************************/
          
          const PERSONS = [
              {
                  key: 'Oliver',
                  latState: 'life360ng.0.people.bxxxe.latitude',
                  lonState: 'life360ng.0.people.b9xxxxx7e.longitude',
                  targetState: '0_userdata.0.VIS.Position.Standort_Oliver',
                  baseState: '0_userdata.0.VIS.Position.Oliver'
              },
              {
                  key: 'Hannah',
                  latState: 'life360ng.0.people.f7xxx9d.latitude',
                  lonState: 'life360ng.0.people.f7fxxx9d.longitude',
                  targetState: '0_userdata.0.VIS.Position.Standort_Hannah',
                  baseState: '0_userdata.0.VIS.Position.Hannah'
              },
              {
                  key: 'Marleen',
                  latState: 'life360ng.0.people.88xxxea.latitude',
                  lonState: 'life360ng.0.people.88xxxaea.longitude',
                  targetState: '0_userdata.0.VIS.Position.Standort_Marleen',
                  baseState: '0_userdata.0.VIS.Position.Marleen'
              }
          ];
          
          // Einstellungen
          const MIN_INTERVAL_MS            = 60 * 1000; // max. 1 Lookup pro Person pro Minute
          const MIN_DISTANCE_M             = 40;        // nur neu abfragen, wenn mind. 40 m Bewegung
          const DEBOUNCE_MS                = 5000;      // 5 Sekunden warten, falls lat/lon nacheinander aktualisiert werden
          const POI_RADIUS_M               = 15;        // erste POI-Suche
          const SECOND_TRY_RADIUS          = 25;        // optionaler zweiter Versuch
          const MAX_ACCEPTED_POI_DISTANCE  = 20;        // nur wirklich nahe Treffer übernehmen
          const GAP_BETWEEN_CALLS          = 1500;      // Pause zwischen Nominatim und Overpass
          
          // WICHTIG: anpassen
          const USER_AGENT = 'ioBroker-Standortscript/1.0 (Kontakt: deine-mail@example.com)';
          
          // Laufzeitstatus pro Person
          const runtime = {};
          
          /* ---------------------------------------------------------
           * Initialisierung
           * --------------------------------------------------------- */
          for (const person of PERSONS) {
              person.addressState = `${person.baseState}.Standort_${person.key}_Adresse`;
              person.poiState     = `${person.baseState}.Standort_${person.key}_POI`;
              person.lastLatState = `${person.baseState}.Standort_${person.key}_LastLat`;
              person.lastLonState = `${person.baseState}.Standort_${person.key}_LastLon`;
              person.lastTsState  = `${person.baseState}.Standort_${person.key}_LastLookupTs`;
          
              runtime[person.key] = {
                  debounceTimer: null,
                  lookupRunning: false
              };
          
              createStateIfMissing(person.targetState,  '', 'string', `${person.key} Standort`);
              createStateIfMissing(person.addressState, '', 'string', `${person.key} Adresse`);
              createStateIfMissing(person.poiState,     '', 'string', `${person.key} POI`);
              createStateIfMissing(person.lastLatState, 0,  'number', `${person.key} letzte Lookup-Latitude`);
              createStateIfMissing(person.lastLonState, 0,  'number', `${person.key} letzte Lookup-Longitude`);
              createStateIfMissing(person.lastTsState,  0,  'number', `${person.key} letzter Lookup-Zeitstempel`);
          
              on({ id: person.latState, change: 'ne' }, () => scheduleLookup(person));
              on({ id: person.lonState, change: 'ne' }, () => scheduleLookup(person));
          
              scheduleLookup(person);
          }
          
          /* ---------------------------------------------------------
           * Hauptlogik
           * --------------------------------------------------------- */
          function scheduleLookup(person) {
              const rt = runtime[person.key];
          
              if (rt.debounceTimer) clearTimeout(rt.debounceTimer);
          
              rt.debounceTimer = setTimeout(() => {
                  runLookup(person).catch(err => log(`${person.key}: Standortscript Fehler: ${err}`, 'error'));
              }, DEBOUNCE_MS);
          }
          
          async function runLookup(person) {
              const rt = runtime[person.key];
          
              if (rt.lookupRunning) {
                  log(`${person.key}: Lookup läuft bereits, überspringe parallelen Aufruf.`, 'debug');
                  return;
              }
          
              const lat = parseFloat(getState(person.latState).val);
              const lon = parseFloat(getState(person.lonState).val);
          
              if (!isFinite(lat) || !isFinite(lon)) {
                  log(`${person.key}: Ungültige Koordinaten, Lookup abgebrochen.`, 'warn');
                  return;
              }
          
              if (!shouldLookup(person, lat, lon)) {
                  log(`${person.key}: Lookup übersprungen (Cooldown oder Bewegung zu gering).`, 'debug');
                  return;
              }
          
              rt.lookupRunning = true;
          
              try {
                  const addressInfo = await getAddressFromNominatim(lat, lon);
                  const addressText = buildAddressText(addressInfo);
          
                  setState(person.addressState, addressText || '', true);
                  setState(person.targetState, addressText || 'Unbekannter Standort', true);
                  setState(person.poiState, '', true);
          
                  setState(person.lastLatState, lat, true);
                  setState(person.lastLonState, lon, true);
                  setState(person.lastTsState, Date.now(), true);
          
                  await sleep(GAP_BETWEEN_CALLS);
          
                  let poi = await getNearbyNamedPoi(lat, lon, POI_RADIUS_M);
          
                  if (!poi) {
                      poi = await getNearbyNamedPoi(lat, lon, SECOND_TRY_RADIUS);
                  }
          
                  let finalText = addressText || 'Unbekannter Standort';
          
                  if (poi && poi.name) {
                      setState(person.poiState, poi.name, true);
          
                      if (addressText && !addressText.toLowerCase().includes(poi.name.toLowerCase())) {
                          finalText = poi.name + ', ' + addressText;
                      } else {
                          finalText = poi.name;
                      }
                  }
          
                  setState(person.targetState, finalText, true);
                  log(`${person.key}: Standort aktualisiert: ${finalText}`, 'info');
          
              } catch (err) {
                  log(`${person.key}: Fehler beim Standort-Lookup: ${err}`, 'error');
              } finally {
                  rt.lookupRunning = false;
              }
          }
          
          function shouldLookup(person, lat, lon) {
              const lastTsRaw  = getState(person.lastTsState).val;
              const lastLatRaw = getState(person.lastLatState).val;
              const lastLonRaw = getState(person.lastLonState).val;
          
              const lastTs  = Number(lastTsRaw || 0);
              const lastLat = parseFloat(lastLatRaw);
              const lastLon = parseFloat(lastLonRaw);
          
              const now = Date.now();
          
              if (lastTs > 0 && (now - lastTs) < MIN_INTERVAL_MS) {
                  return false;
              }
          
              if (isFinite(lastLat) && isFinite(lastLon) && !(lastLat === 0 && lastLon === 0)) {
                  const dist = distanceMeters(lastLat, lastLon, lat, lon);
                  if (dist < MIN_DISTANCE_M) {
                      return false;
                  }
              }
          
              return true;
          }
          
          /* ---------------------------------------------------------
           * Nominatim Reverse Geocoding
           * --------------------------------------------------------- */
          async function getAddressFromNominatim(lat, lon) {
              const url =
                  'https://nominatim.openstreetmap.org/reverse' +
                  '?format=jsonv2' +
                  '&lat=' + encodeURIComponent(lat) +
                  '&lon=' + encodeURIComponent(lon) +
                  '&addressdetails=1' +
                  '&zoom=18';
          
              const response = await httpGetAsync(url, {
                  'User-Agent': USER_AGENT,
                  'Accept': 'application/json'
              });
          
              if (response.statusCode < 200 || response.statusCode >= 300) {
                  throw new Error('Nominatim HTTP ' + response.statusCode);
              }
          
              let data;
              try {
                  data = JSON.parse(response.data);
              } catch (e) {
                  throw new Error('Nominatim JSON konnte nicht geparst werden');
              }
          
              return data;
          }
          
          function buildAddressText(data) {
              if (!data) return '';
          
              const a = data.address || {};
          
              const road =
                  a.road ||
                  a.pedestrian ||
                  a.footway ||
                  a.path ||
                  a.cycleway ||
                  '';
          
              const houseNumber = a.house_number || '';
              const postcode = a.postcode || '';
              const city =
                  a.city ||
                  a.town ||
                  a.village ||
                  a.hamlet ||
                  a.municipality ||
                  '';
          
              let result = '';
          
              if (road) {
                  result += road;
                  if (houseNumber) result += ' ' + houseNumber;
              }
          
              if (postcode || city) {
                  if (result) result += ', ';
                  result += [postcode, city].filter(Boolean).join(' ');
              }
          
              if (!result && data.display_name) {
                  result = data.display_name;
              }
          
              return cleanText(result);
          }
          
          /* ---------------------------------------------------------
           * Overpass: benannten Ort/POI in der Nähe finden
           * --------------------------------------------------------- */
          async function getNearbyNamedPoi(lat, lon, radius) {
              const query = `
          [out:json][timeout:10];
          (
            nwr(around:${radius},${lat},${lon})["name"]["shop"];
            nwr(around:${radius},${lat},${lon})["name"]["amenity"];
            nwr(around:${radius},${lat},${lon})["name"]["tourism"];
            nwr(around:${radius},${lat},${lon})["name"]["leisure"];
            nwr(around:${radius},${lat},${lon})["name"]["office"];
          );
          out center tags;
          `;
          
              const response = await httpPostAsync(
                  'https://overpass-api.de/api/interpreter',
                  query,
                  {
                      'User-Agent': USER_AGENT,
                      'Content-Type': 'text/plain; charset=utf-8',
                      'Accept': 'application/json'
                  }
              );
          
              if (response.statusCode < 200 || response.statusCode >= 300) {
                  throw new Error('Overpass HTTP ' + response.statusCode);
              }
          
              let data;
              try {
                  data = JSON.parse(response.data);
              } catch (e) {
                  throw new Error('Overpass JSON konnte nicht geparst werden');
              }
          
              if (!data.elements || !data.elements.length) {
                  return null;
              }
          
              const candidates = data.elements
                  .map(el => {
                      const cLat = el.lat || (el.center && el.center.lat);
                      const cLon = el.lon || (el.center && el.center.lon);
                      const name = el.tags && el.tags.name ? String(el.tags.name).trim() : '';
                      const dist = (isFinite(cLat) && isFinite(cLon))
                          ? distanceMeters(lat, lon, cLat, cLon)
                          : 999999;
          
                      return {
                          name: cleanText(name),
                          tags: el.tags || {},
                          dist: dist
                      };
                  })
                  .filter(x => x.name);
          
              if (!candidates.length) return null;
          
              candidates.sort((a, b) => {
                  const pa = poiPriority(a.tags);
                  const pb = poiPriority(b.tags);
                  if (pa !== pb) return pa - pb;
                  return a.dist - b.dist;
              });
          
              const best = candidates[0];
              if (!best || best.dist > MAX_ACCEPTED_POI_DISTANCE) {
                  return null;
              }
          
              return best;
          }
          
          function poiPriority(tags) {
              if (tags.shop) return 1;
              if (tags.amenity) return 2;
              if (tags.tourism) return 3;
              if (tags.leisure) return 4;
              if (tags.office) return 5;
              return 99;
          }
          
          /* ---------------------------------------------------------
           * HTTP-Helfer
           * --------------------------------------------------------- */
          function httpGetAsync(url, headers) {
              return new Promise((resolve, reject) => {
                  httpGet(url, { headers: headers, timeout: 15000 }, (err, response) => {
                      if (err) return reject(err);
                      resolve(response);
                  });
              });
          }
          
          function httpPostAsync(url, body, headers) {
              return new Promise((resolve, reject) => {
                  httpPost(url, body, { headers: headers, timeout: 20000 }, (err, response) => {
                      if (err) return reject(err);
                      resolve(response);
                  });
              });
          }
          
          function sleep(ms) {
              return new Promise(resolve => setTimeout(resolve, ms));
          }
          
          /* ---------------------------------------------------------
           * Hilfsfunktionen
           * --------------------------------------------------------- */
          function distanceMeters(lat1, lon1, lat2, lon2) {
              const R = 6371000;
              const toRad = deg => deg * Math.PI / 180;
          
              const dLat = toRad(lat2 - lat1);
              const dLon = toRad(lon2 - lon1);
          
              const a =
                  Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                  Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
                  Math.sin(dLon / 2) * Math.sin(dLon / 2);
          
              return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
          }
          
          function cleanText(str) {
              return String(str || '')
                  .replace(/\s+/g, ' ')
                  .replace(/\s+,/g, ',')
                  .trim();
          }
          
          function createStateIfMissing(id, def, type, name) {
              if (!existsState(id)) {
                  createState(id, def, {
                      name: name,
                      type: type,
                      role: type === 'number' ? 'value' : 'text',
                      read: true,
                      write: true
                  });
              }
          }
          

          Beta-Tester

          skvarelS 1 Antwort Letzte Antwort
          0
          • Merlin123M Merlin123

            Hier ne version für mehrere Personen, aber noch nicht vollständig getestet...

            /************************************************************
             * Mehrpersonen-Standortscript für ioBroker + Life360
             *
             * Für jede Person:
             * - Reverse Geocoding per Nominatim
             * - optional POI-Suche per Overpass
             * - Ausgabe in eigenen States
             *
             * WICHTIG:
             * USER_AGENT bitte mit echter Kontaktmöglichkeit anpassen.
             ************************************************************/
            
            const PERSONS = [
                {
                    key: 'Oliver',
                    latState: 'life360ng.0.people.bxxxe.latitude',
                    lonState: 'life360ng.0.people.b9xxxxx7e.longitude',
                    targetState: '0_userdata.0.VIS.Position.Standort_Oliver',
                    baseState: '0_userdata.0.VIS.Position.Oliver'
                },
                {
                    key: 'Hannah',
                    latState: 'life360ng.0.people.f7xxx9d.latitude',
                    lonState: 'life360ng.0.people.f7fxxx9d.longitude',
                    targetState: '0_userdata.0.VIS.Position.Standort_Hannah',
                    baseState: '0_userdata.0.VIS.Position.Hannah'
                },
                {
                    key: 'Marleen',
                    latState: 'life360ng.0.people.88xxxea.latitude',
                    lonState: 'life360ng.0.people.88xxxaea.longitude',
                    targetState: '0_userdata.0.VIS.Position.Standort_Marleen',
                    baseState: '0_userdata.0.VIS.Position.Marleen'
                }
            ];
            
            // Einstellungen
            const MIN_INTERVAL_MS            = 60 * 1000; // max. 1 Lookup pro Person pro Minute
            const MIN_DISTANCE_M             = 40;        // nur neu abfragen, wenn mind. 40 m Bewegung
            const DEBOUNCE_MS                = 5000;      // 5 Sekunden warten, falls lat/lon nacheinander aktualisiert werden
            const POI_RADIUS_M               = 15;        // erste POI-Suche
            const SECOND_TRY_RADIUS          = 25;        // optionaler zweiter Versuch
            const MAX_ACCEPTED_POI_DISTANCE  = 20;        // nur wirklich nahe Treffer übernehmen
            const GAP_BETWEEN_CALLS          = 1500;      // Pause zwischen Nominatim und Overpass
            
            // WICHTIG: anpassen
            const USER_AGENT = 'ioBroker-Standortscript/1.0 (Kontakt: deine-mail@example.com)';
            
            // Laufzeitstatus pro Person
            const runtime = {};
            
            /* ---------------------------------------------------------
             * Initialisierung
             * --------------------------------------------------------- */
            for (const person of PERSONS) {
                person.addressState = `${person.baseState}.Standort_${person.key}_Adresse`;
                person.poiState     = `${person.baseState}.Standort_${person.key}_POI`;
                person.lastLatState = `${person.baseState}.Standort_${person.key}_LastLat`;
                person.lastLonState = `${person.baseState}.Standort_${person.key}_LastLon`;
                person.lastTsState  = `${person.baseState}.Standort_${person.key}_LastLookupTs`;
            
                runtime[person.key] = {
                    debounceTimer: null,
                    lookupRunning: false
                };
            
                createStateIfMissing(person.targetState,  '', 'string', `${person.key} Standort`);
                createStateIfMissing(person.addressState, '', 'string', `${person.key} Adresse`);
                createStateIfMissing(person.poiState,     '', 'string', `${person.key} POI`);
                createStateIfMissing(person.lastLatState, 0,  'number', `${person.key} letzte Lookup-Latitude`);
                createStateIfMissing(person.lastLonState, 0,  'number', `${person.key} letzte Lookup-Longitude`);
                createStateIfMissing(person.lastTsState,  0,  'number', `${person.key} letzter Lookup-Zeitstempel`);
            
                on({ id: person.latState, change: 'ne' }, () => scheduleLookup(person));
                on({ id: person.lonState, change: 'ne' }, () => scheduleLookup(person));
            
                scheduleLookup(person);
            }
            
            /* ---------------------------------------------------------
             * Hauptlogik
             * --------------------------------------------------------- */
            function scheduleLookup(person) {
                const rt = runtime[person.key];
            
                if (rt.debounceTimer) clearTimeout(rt.debounceTimer);
            
                rt.debounceTimer = setTimeout(() => {
                    runLookup(person).catch(err => log(`${person.key}: Standortscript Fehler: ${err}`, 'error'));
                }, DEBOUNCE_MS);
            }
            
            async function runLookup(person) {
                const rt = runtime[person.key];
            
                if (rt.lookupRunning) {
                    log(`${person.key}: Lookup läuft bereits, überspringe parallelen Aufruf.`, 'debug');
                    return;
                }
            
                const lat = parseFloat(getState(person.latState).val);
                const lon = parseFloat(getState(person.lonState).val);
            
                if (!isFinite(lat) || !isFinite(lon)) {
                    log(`${person.key}: Ungültige Koordinaten, Lookup abgebrochen.`, 'warn');
                    return;
                }
            
                if (!shouldLookup(person, lat, lon)) {
                    log(`${person.key}: Lookup übersprungen (Cooldown oder Bewegung zu gering).`, 'debug');
                    return;
                }
            
                rt.lookupRunning = true;
            
                try {
                    const addressInfo = await getAddressFromNominatim(lat, lon);
                    const addressText = buildAddressText(addressInfo);
            
                    setState(person.addressState, addressText || '', true);
                    setState(person.targetState, addressText || 'Unbekannter Standort', true);
                    setState(person.poiState, '', true);
            
                    setState(person.lastLatState, lat, true);
                    setState(person.lastLonState, lon, true);
                    setState(person.lastTsState, Date.now(), true);
            
                    await sleep(GAP_BETWEEN_CALLS);
            
                    let poi = await getNearbyNamedPoi(lat, lon, POI_RADIUS_M);
            
                    if (!poi) {
                        poi = await getNearbyNamedPoi(lat, lon, SECOND_TRY_RADIUS);
                    }
            
                    let finalText = addressText || 'Unbekannter Standort';
            
                    if (poi && poi.name) {
                        setState(person.poiState, poi.name, true);
            
                        if (addressText && !addressText.toLowerCase().includes(poi.name.toLowerCase())) {
                            finalText = poi.name + ', ' + addressText;
                        } else {
                            finalText = poi.name;
                        }
                    }
            
                    setState(person.targetState, finalText, true);
                    log(`${person.key}: Standort aktualisiert: ${finalText}`, 'info');
            
                } catch (err) {
                    log(`${person.key}: Fehler beim Standort-Lookup: ${err}`, 'error');
                } finally {
                    rt.lookupRunning = false;
                }
            }
            
            function shouldLookup(person, lat, lon) {
                const lastTsRaw  = getState(person.lastTsState).val;
                const lastLatRaw = getState(person.lastLatState).val;
                const lastLonRaw = getState(person.lastLonState).val;
            
                const lastTs  = Number(lastTsRaw || 0);
                const lastLat = parseFloat(lastLatRaw);
                const lastLon = parseFloat(lastLonRaw);
            
                const now = Date.now();
            
                if (lastTs > 0 && (now - lastTs) < MIN_INTERVAL_MS) {
                    return false;
                }
            
                if (isFinite(lastLat) && isFinite(lastLon) && !(lastLat === 0 && lastLon === 0)) {
                    const dist = distanceMeters(lastLat, lastLon, lat, lon);
                    if (dist < MIN_DISTANCE_M) {
                        return false;
                    }
                }
            
                return true;
            }
            
            /* ---------------------------------------------------------
             * Nominatim Reverse Geocoding
             * --------------------------------------------------------- */
            async function getAddressFromNominatim(lat, lon) {
                const url =
                    'https://nominatim.openstreetmap.org/reverse' +
                    '?format=jsonv2' +
                    '&lat=' + encodeURIComponent(lat) +
                    '&lon=' + encodeURIComponent(lon) +
                    '&addressdetails=1' +
                    '&zoom=18';
            
                const response = await httpGetAsync(url, {
                    'User-Agent': USER_AGENT,
                    'Accept': 'application/json'
                });
            
                if (response.statusCode < 200 || response.statusCode >= 300) {
                    throw new Error('Nominatim HTTP ' + response.statusCode);
                }
            
                let data;
                try {
                    data = JSON.parse(response.data);
                } catch (e) {
                    throw new Error('Nominatim JSON konnte nicht geparst werden');
                }
            
                return data;
            }
            
            function buildAddressText(data) {
                if (!data) return '';
            
                const a = data.address || {};
            
                const road =
                    a.road ||
                    a.pedestrian ||
                    a.footway ||
                    a.path ||
                    a.cycleway ||
                    '';
            
                const houseNumber = a.house_number || '';
                const postcode = a.postcode || '';
                const city =
                    a.city ||
                    a.town ||
                    a.village ||
                    a.hamlet ||
                    a.municipality ||
                    '';
            
                let result = '';
            
                if (road) {
                    result += road;
                    if (houseNumber) result += ' ' + houseNumber;
                }
            
                if (postcode || city) {
                    if (result) result += ', ';
                    result += [postcode, city].filter(Boolean).join(' ');
                }
            
                if (!result && data.display_name) {
                    result = data.display_name;
                }
            
                return cleanText(result);
            }
            
            /* ---------------------------------------------------------
             * Overpass: benannten Ort/POI in der Nähe finden
             * --------------------------------------------------------- */
            async function getNearbyNamedPoi(lat, lon, radius) {
                const query = `
            [out:json][timeout:10];
            (
              nwr(around:${radius},${lat},${lon})["name"]["shop"];
              nwr(around:${radius},${lat},${lon})["name"]["amenity"];
              nwr(around:${radius},${lat},${lon})["name"]["tourism"];
              nwr(around:${radius},${lat},${lon})["name"]["leisure"];
              nwr(around:${radius},${lat},${lon})["name"]["office"];
            );
            out center tags;
            `;
            
                const response = await httpPostAsync(
                    'https://overpass-api.de/api/interpreter',
                    query,
                    {
                        'User-Agent': USER_AGENT,
                        'Content-Type': 'text/plain; charset=utf-8',
                        'Accept': 'application/json'
                    }
                );
            
                if (response.statusCode < 200 || response.statusCode >= 300) {
                    throw new Error('Overpass HTTP ' + response.statusCode);
                }
            
                let data;
                try {
                    data = JSON.parse(response.data);
                } catch (e) {
                    throw new Error('Overpass JSON konnte nicht geparst werden');
                }
            
                if (!data.elements || !data.elements.length) {
                    return null;
                }
            
                const candidates = data.elements
                    .map(el => {
                        const cLat = el.lat || (el.center && el.center.lat);
                        const cLon = el.lon || (el.center && el.center.lon);
                        const name = el.tags && el.tags.name ? String(el.tags.name).trim() : '';
                        const dist = (isFinite(cLat) && isFinite(cLon))
                            ? distanceMeters(lat, lon, cLat, cLon)
                            : 999999;
            
                        return {
                            name: cleanText(name),
                            tags: el.tags || {},
                            dist: dist
                        };
                    })
                    .filter(x => x.name);
            
                if (!candidates.length) return null;
            
                candidates.sort((a, b) => {
                    const pa = poiPriority(a.tags);
                    const pb = poiPriority(b.tags);
                    if (pa !== pb) return pa - pb;
                    return a.dist - b.dist;
                });
            
                const best = candidates[0];
                if (!best || best.dist > MAX_ACCEPTED_POI_DISTANCE) {
                    return null;
                }
            
                return best;
            }
            
            function poiPriority(tags) {
                if (tags.shop) return 1;
                if (tags.amenity) return 2;
                if (tags.tourism) return 3;
                if (tags.leisure) return 4;
                if (tags.office) return 5;
                return 99;
            }
            
            /* ---------------------------------------------------------
             * HTTP-Helfer
             * --------------------------------------------------------- */
            function httpGetAsync(url, headers) {
                return new Promise((resolve, reject) => {
                    httpGet(url, { headers: headers, timeout: 15000 }, (err, response) => {
                        if (err) return reject(err);
                        resolve(response);
                    });
                });
            }
            
            function httpPostAsync(url, body, headers) {
                return new Promise((resolve, reject) => {
                    httpPost(url, body, { headers: headers, timeout: 20000 }, (err, response) => {
                        if (err) return reject(err);
                        resolve(response);
                    });
                });
            }
            
            function sleep(ms) {
                return new Promise(resolve => setTimeout(resolve, ms));
            }
            
            /* ---------------------------------------------------------
             * Hilfsfunktionen
             * --------------------------------------------------------- */
            function distanceMeters(lat1, lon1, lat2, lon2) {
                const R = 6371000;
                const toRad = deg => deg * Math.PI / 180;
            
                const dLat = toRad(lat2 - lat1);
                const dLon = toRad(lon2 - lon1);
            
                const a =
                    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
                    Math.sin(dLon / 2) * Math.sin(dLon / 2);
            
                return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
            }
            
            function cleanText(str) {
                return String(str || '')
                    .replace(/\s+/g, ' ')
                    .replace(/\s+,/g, ',')
                    .trim();
            }
            
            function createStateIfMissing(id, def, type, name) {
                if (!existsState(id)) {
                    createState(id, def, {
                        name: name,
                        type: type,
                        role: type === 'number' ? 'value' : 'text',
                        read: true,
                        write: true
                    });
                }
            }
            
            skvarelS Online
            skvarelS Online
            skvarel
            Developer
            schrieb am zuletzt editiert von
            #111

            @Merlin123 .. ich habe da ein paar Fragen. Ich schreibe dich morgen dazu mal privat an.

            #TeamInventwo
            Unsere Adapter:
            Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

            Wer uns mit einem Kaffee unterstützen möchte: PayPal

            Merlin123M 1 Antwort Letzte Antwort
            0
            • skvarelS skvarel

              @Merlin123 .. ich habe da ein paar Fragen. Ich schreibe dich morgen dazu mal privat an.

              Merlin123M Offline
              Merlin123M Offline
              Merlin123
              schrieb am zuletzt editiert von
              #112

              @skvarel Mach das. Weiß aber nicht, ob ich die beantworten kann 🙄 Ist wie gesagt alles KI erzeugt....

              Beta-Tester

              1 Antwort Letzte Antwort
              1
              • D Offline
                D Offline
                Daniel81
                schrieb am zuletzt editiert von
                #113

                Super Arbeit, danke! Ich bin froh, dass der Adapter wieder funktioniert.

                Weiß jemand, ob sich der Token irgendwann ändert und ich dann wieder aktiv werden muss?
                Aber wie gesagt: Ich bin jetzt erstmal happy, dass ich wieder damit arbeiten kann und meine own Tracks vom Handy schmeißen kann.

                Vielen Dank und bitte dran bleiben – ich hoffe, der Adapter schafft es auch bald ins offizielle ioBroker Repository.

                Merlin123M skvarelS BananaJoeB 3 Antworten Letzte Antwort
                1
                • D Daniel81

                  Super Arbeit, danke! Ich bin froh, dass der Adapter wieder funktioniert.

                  Weiß jemand, ob sich der Token irgendwann ändert und ich dann wieder aktiv werden muss?
                  Aber wie gesagt: Ich bin jetzt erstmal happy, dass ich wieder damit arbeiten kann und meine own Tracks vom Handy schmeißen kann.

                  Vielen Dank und bitte dran bleiben – ich hoffe, der Adapter schafft es auch bald ins offizielle ioBroker Repository.

                  Merlin123M Offline
                  Merlin123M Offline
                  Merlin123
                  schrieb am zuletzt editiert von
                  #114

                  @Daniel81 Auf jeden Fall ist es sehr lange gültig. Wie lange ist meines Wissens nach nicht bekannt. Wird bei Home Assistant (?) schon länger so eingesetzt

                  Beta-Tester

                  1 Antwort Letzte Antwort
                  0
                  • D Daniel81

                    Super Arbeit, danke! Ich bin froh, dass der Adapter wieder funktioniert.

                    Weiß jemand, ob sich der Token irgendwann ändert und ich dann wieder aktiv werden muss?
                    Aber wie gesagt: Ich bin jetzt erstmal happy, dass ich wieder damit arbeiten kann und meine own Tracks vom Handy schmeißen kann.

                    Vielen Dank und bitte dran bleiben – ich hoffe, der Adapter schafft es auch bald ins offizielle ioBroker Repository.

                    skvarelS Online
                    skvarelS Online
                    skvarel
                    Developer
                    schrieb am zuletzt editiert von skvarel
                    #115

                    @Daniel81 sagte:

                    Vielen Dank und bitte dran bleiben – ich hoffe, der Adapter schafft es auch bald ins offizielle ioBroker Repository.

                    Danke. Ja, ich werde dranbleiben. Dieser Adapter ist eine Herzensangelegenheit ;)

                    Eine Aufnahme ins ioBroker Repository ist bereits beantragt. @mcm1957 hat bereits einen ersten Blick drüber geworfen .... an dieser Stelle, vielen Dank an mcm für seinen Support!

                    #TeamInventwo
                    Unsere Adapter:
                    Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                    Wer uns mit einem Kaffee unterstützen möchte: PayPal

                    1 Antwort Letzte Antwort
                    1
                    • D Daniel81

                      Super Arbeit, danke! Ich bin froh, dass der Adapter wieder funktioniert.

                      Weiß jemand, ob sich der Token irgendwann ändert und ich dann wieder aktiv werden muss?
                      Aber wie gesagt: Ich bin jetzt erstmal happy, dass ich wieder damit arbeiten kann und meine own Tracks vom Handy schmeißen kann.

                      Vielen Dank und bitte dran bleiben – ich hoffe, der Adapter schafft es auch bald ins offizielle ioBroker Repository.

                      BananaJoeB Offline
                      BananaJoeB Offline
                      BananaJoe
                      Most Active
                      schrieb am zuletzt editiert von
                      #116

                      @Daniel81 sagte:

                      Weiß jemand, ob sich der Token irgendwann ändert und ich dann wieder aktiv werden muss?

                      Ich setze das unter Home Assistant schon seit März 2025 ein und musste etwa vor 3 Wochen zum ersten mal das Token erneuern, hat also ein Jahr gehalten

                      ioBroker@Ubuntu 24.04 LTS (VMware) für: >260 Geräte, 5 Switche, 7 AP, 10 IP-Cam, 1 NAS 42TB, 1 ESXi 15TB, 4 Proxmox 1TB, 1 Hyper-V 48TB, 14 x Echo, 5x FireTV, 5 x Tablett/Handy VIS || >=160 Tasmota/Shelly || >=95 ZigBee || PV 8.1kW / Akku 14kWh || 2x USV APC 750W kaskadiert || Creality CR-10 SE 3D-Drucker

                      1 Antwort Letzte Antwort
                      1
                      • skvarelS Online
                        skvarelS Online
                        skvarel
                        Developer
                        schrieb am zuletzt editiert von
                        #117

                        Ich werde noch eine bessere Meldung über den Log ausgeben lassen, wenn der Token abgelaufen ist. So muss man nicht unnötig nach Fehlern suchen, wenn die Instanz rot sein sollte.

                        #TeamInventwo
                        Unsere Adapter:
                        Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                        Wer uns mit einem Kaffee unterstützen möchte: PayPal

                        1 Antwort Letzte Antwort
                        0
                        • stefu87_CHS Online
                          stefu87_CHS Online
                          stefu87_CH
                          schrieb am zuletzt editiert von
                          #118

                          Kannst du dir das erklären @skvarel das nur bei der Tochter der Batteriewert auf -1 geht? Wenn ich aber den Adapter neu starte, habe ich ca 2 min den aktuellen Wert und dann wieder -1.

                          339b127f-b565-4d7c-a8f0-ac2cc143a23f-image.jpeg

                          Model : AMD Ryzen 7 8745HS w/ Radeon 780M Graphics
                          Cores: 1 × 8 = 8 cores
                          Threads: 16
                          mit Proxmox
                          LXC Container mit Iobroker

                          skvarelS 1 Antwort Letzte Antwort
                          0
                          • stefu87_CHS stefu87_CH

                            Kannst du dir das erklären @skvarel das nur bei der Tochter der Batteriewert auf -1 geht? Wenn ich aber den Adapter neu starte, habe ich ca 2 min den aktuellen Wert und dann wieder -1.

                            339b127f-b565-4d7c-a8f0-ac2cc143a23f-image.jpeg

                            skvarelS Online
                            skvarelS Online
                            skvarel
                            Developer
                            schrieb am zuletzt editiert von
                            #119

                            @stefu87_CH .. Ich tippe auf Einstellungen in der App oder generell im Handy. Irgendein Sleep-Mode ?!

                            Ich habe die Möglichkeiten alle vom Ursprungs-Adapter übernommen und nur den Login geändert und die Karte für den iFrame hinzugefügt.

                            Vielen Dank an @migoller an dieser Stelle!

                            #TeamInventwo
                            Unsere Adapter:
                            Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                            Wer uns mit einem Kaffee unterstützen möchte: PayPal

                            stefu87_CHS 1 Antwort Letzte Antwort
                            0
                            • skvarelS skvarel

                              @stefu87_CH .. Ich tippe auf Einstellungen in der App oder generell im Handy. Irgendein Sleep-Mode ?!

                              Ich habe die Möglichkeiten alle vom Ursprungs-Adapter übernommen und nur den Login geändert und die Karte für den iFrame hinzugefügt.

                              Vielen Dank an @migoller an dieser Stelle!

                              stefu87_CHS Online
                              stefu87_CHS Online
                              stefu87_CH
                              schrieb am zuletzt editiert von
                              #120

                              @skvarel sagte:

                              Ich tippe auf Einstellungen in der App oder generell im Handy. Irgendein Sleep-Mode ?!

                              Aber was mir dann spanisch vorkommt, das in der APP auf dem Handy immer der aktuelle Wert angezeigt wird. Werde das aber mal beobachten ob das einfach gerade Zufall ist, oder ob es sporadisch vorkommt.

                              Model : AMD Ryzen 7 8745HS w/ Radeon 780M Graphics
                              Cores: 1 × 8 = 8 cores
                              Threads: 16
                              mit Proxmox
                              LXC Container mit Iobroker

                              skvarelS 1 Antwort Letzte Antwort
                              1
                              • stefu87_CHS stefu87_CH

                                @skvarel sagte:

                                Ich tippe auf Einstellungen in der App oder generell im Handy. Irgendein Sleep-Mode ?!

                                Aber was mir dann spanisch vorkommt, das in der APP auf dem Handy immer der aktuelle Wert angezeigt wird. Werde das aber mal beobachten ob das einfach gerade Zufall ist, oder ob es sporadisch vorkommt.

                                skvarelS Online
                                skvarelS Online
                                skvarel
                                Developer
                                schrieb am zuletzt editiert von skvarel
                                #121

                                @stefu87_CH .. ich werde das hier auch mal beobachten. Ich habe nie auf die Werte geachtet, weil mir der Akku anderer Familienmitglieder nicht so wichtig ist.

                                #TeamInventwo
                                Unsere Adapter:
                                Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                                Wer uns mit einem Kaffee unterstützen möchte: PayPal

                                V 1 Antwort Letzte Antwort
                                0
                                • skvarelS skvarel

                                  @stefu87_CH .. ich werde das hier auch mal beobachten. Ich habe nie auf die Werte geachtet, weil mir der Akku anderer Familienmitglieder nicht so wichtig ist.

                                  V Online
                                  V Online
                                  Verblizz
                                  schrieb zuletzt editiert von
                                  #122

                                  @skvarel , @stefu87_ch :
                                  Ich kann dazu berichten, dass meine Tochter grundsätzlich den Akkusparmodus eingeschaltet hat, aber der Battariestand wird bei mir trotzdem in den neuen Adapter übertragen... In der App auf dem Handy ist der natürlich auch sichtbar...

                                  1 Antwort Letzte Antwort
                                  1
                                  • skvarelS Online
                                    skvarelS Online
                                    skvarel
                                    Developer
                                    schrieb zuletzt editiert von
                                    #123

                                    Mal eine Spielerei mit dem vis-maps Widget

                                    Beide Marker in einer Karte, Die Bilder der Personen kommen aus der App und liegen als URL in den Adapterobjekten.

                                    fa5d2ad8-46ee-430a-9e8e-299dfaa68916-image.jpeg

                                    #TeamInventwo
                                    Unsere Adapter:
                                    Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                                    Wer uns mit einem Kaffee unterstützen möchte: PayPal

                                    Merlin123M 1 Antwort Letzte Antwort
                                    0
                                    • skvarelS skvarel

                                      Mal eine Spielerei mit dem vis-maps Widget

                                      Beide Marker in einer Karte, Die Bilder der Personen kommen aus der App und liegen als URL in den Adapterobjekten.

                                      fa5d2ad8-46ee-430a-9e8e-299dfaa68916-image.jpeg

                                      Merlin123M Offline
                                      Merlin123M Offline
                                      Merlin123
                                      schrieb zuletzt editiert von
                                      #124

                                      @skvarel Ist ja nett.... Skaliert die Map dann automatisch, je nach Abstand?

                                      Beta-Tester

                                      skvarelS 1 Antwort Letzte Antwort
                                      0
                                      • Merlin123M Merlin123

                                        @skvarel Ist ja nett.... Skaliert die Map dann automatisch, je nach Abstand?

                                        skvarelS Online
                                        skvarelS Online
                                        skvarel
                                        Developer
                                        schrieb zuletzt editiert von
                                        #125

                                        @Merlin123 .. jepp

                                        #TeamInventwo
                                        Unsere Adapter:
                                        Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                                        Wer uns mit einem Kaffee unterstützen möchte: PayPal

                                        1 Antwort Letzte Antwort
                                        0
                                        • skvarelS Online
                                          skvarelS Online
                                          skvarel
                                          Developer
                                          schrieb zuletzt editiert von
                                          #126

                                          So gefällt es mir noch besser.

                                          Für einen schnellen Überblick:

                                          5db62443-f21a-433f-97b8-ec10c709ee95-image.jpeg

                                          Wenn ich dann oben rechts auf das Icon klicke, gibt es mehr Details:

                                          4448a8f4-9fff-4216-86ca-c467d704f85e-image.jpeg

                                          #TeamInventwo
                                          Unsere Adapter:
                                          Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

                                          Wer uns mit einem Kaffee unterstützen möchte: PayPal

                                          1 Antwort Letzte Antwort
                                          0

                                          Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                                          Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                                          Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                                          Registrieren Anmelden
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          510

                                          Online

                                          32.8k

                                          Benutzer

                                          82.8k

                                          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