Segway Navimow - SDK

Bonjour,

Segway a créé une intégration officielle Home Assistant pour ses tondeuses Navimow.

Mais ils ont également créé un repository avec un SDK.
GitHub - segwaynavimow/navimow-sdk · GitHub

Ne serait-il pas possible à partir de ce SDK de créer un plugin Jeedom ?

1 « J'aime »

hier soir, je me suis amusé avec Claude pour essayer de l’intégrer dans Jeedom. Je ne sais absolument pas faire un plugin mais je l’ai orienté vers docker. Peut-être que cela pour servir à un développeur de plugin.

C’est donc fonctionnel avec mon navimow H1500. Via MQTT discovery, je récupère les informations de la batterie, l’état de tondeuse, le le temps de tonte (calculé par le docker). Il y a également 3 commandes actions : démarrer , pause, et retour à la base.

## Étape 1 — Installer Docker (si pas déjà fait)

bash

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --version

## Étape 2 — Récupérer votre token Navimow

2.1 — Installer les dépendances Python sur votre PC Windows

pip install requests flask

2.2 — Créer le script get_navimow_token.py

Ouvrez le Bloc-notes, collez le code ci-dessous, et enregistrez sous get_navimow_token.py sur votre Bureau (choisissez « Tous les fichiers » dans le type).

python

import webbrowser
import threading
import json
from flask import Flask, request
import requests

app = Flask(__name__)

AUTH_URL      = "https://navimow-h5-fra.willand.com/smartHome/login"
TOKEN_URL     = "https://navimow-fra.ninebot.com/openapi/oauth/getAccessToken"
CLIENT_ID     = "homeassistant"
CLIENT_SECRET = "57056e15-722e-42be-bbaa-b0cbfb208a52"
REDIRECT      = "http://localhost:8765/callback"

@app.route("/callback")
def callback():
    code = request.args.get("code")
    if not code:
        return f"Pas de code reçu : {dict(request.args)}"
    r = requests.post(TOKEN_URL, data={
        "grant_type":    "authorization_code",
        "code":          code,
        "redirect_uri":  REDIRECT,
        "client_id":     CLIENT_ID,
        "client_secret": CLIENT_SECRET
    })
    data = r.json()
    token   = data.get("access_token")
    refresh = data.get("refresh_token")
    expires = data.get("expires_in")
    with open("navimow_token_full.json", "w") as f:
        json.dump(data, f, indent=2)
    if token:
        print(f"\n✅ ACCESS TOKEN :\n{token}")
        print(f"\n🔄 REFRESH TOKEN :\n{refresh}")
        print(f"\n⏱️  Expire dans : {expires}s")
    return f"""
    <h2>✅ Token récupéré !</h2>
    <h3>Réponse complète :</h3><pre>{json.dumps(data, indent=2)}</pre>
    <h3>Access Token :</h3><code style="word-break:break-all">{token}</code>
    <h3>Refresh Token :</h3><code style="word-break:break-all">{refresh}</code>
    """

if __name__ == "__main__":
    url = f"{AUTH_URL}?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT}&channel=homeassistant"
    threading.Timer(1, lambda: webbrowser.open(url)).start()
    print("Ouverture du navigateur Navimow...")
    app.run(port=8765)

2.3 — Lancer le script

cd Desktop
python get_navimow_token.py

Une page de login Navimow s’ouvre. Connectez-vous, puis notez l’access_token et le refresh_token affichés.

:information_source: Ce script n’est nécessaire qu’une seule fois. Le bridge renouvelle ensuite les tokens automatiquement.


Étape 3 — Créer les fichiers du bridge

bash


mkdir -p ~/navimow-jeedom
cd ~/navimow-jeedom

3.1 — Dockerfile

bash

cat > ~/navimow-jeedom/Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY navimow_bridge.py .
RUN mkdir -p /data && chmod 777 /data
RUN useradd -m appuser && chown -R appuser /app
USER appuser
CMD ["python", "-u", "navimow_bridge.py"]
EOF

3.2 — requirements.txt

bash

cat > ~/navimow-jeedom/requirements.txt << 'EOF'
navimow-sdk
aiohttp>=3.9.0
paho-mqtt>=1.6.1
EOF

3.3 — navimow_bridge.py

bash

cat > ~/navimow-jeedom/navimow_bridge.py << 'EOF'
#!/usr/bin/env python3
import asyncio, json, logging, os, signal, sys, time
import aiohttp
import paho.mqtt.client as mqtt
from mower_sdk.api import MowerAPI
from mower_sdk.models import MowerCommand

NAVIMOW_TOKEN         = os.environ["NAVIMOW_TOKEN"]
NAVIMOW_REFRESH_TOKEN = os.environ.get("NAVIMOW_REFRESH_TOKEN", "")
NAVIMOW_API_URL       = os.environ.get("NAVIMOW_API_URL", "https://navimow-fra.ninebot.com")
NAVIMOW_DEVICE_ID     = os.environ.get("NAVIMOW_DEVICE_ID", "")
MQTT_HOST             = os.environ["MQTT_HOST"]
MQTT_PORT             = int(os.environ.get("MQTT_PORT", "1883"))
MQTT_USER             = os.environ.get("MQTT_USER", "")
MQTT_PASS             = os.environ.get("MQTT_PASS", "")
MQTT_BASE_TOPIC       = os.environ.get("MQTT_BASE_TOPIC", "navimow")
POLL_INTERVAL         = int(os.environ.get("POLL_INTERVAL", "30"))
POLL_INTERVAL_MOWING  = int(os.environ.get("POLL_INTERVAL_MOWING", "15"))
DISCOVERY_PREFIX      = os.environ.get("DISCOVERY_PREFIX", "homeassistant")
TOKEN_FILE            = os.environ.get("TOKEN_FILE", "/data/tokens.json")
CLIENT_ID     = "homeassistant"
CLIENT_SECRET = "57056e15-722e-42be-bbaa-b0cbfb208a52"
TOKEN_URL     = "https://navimow-fra.ninebot.com/openapi/oauth/getAccessToken"
TOKEN_REFRESH_MARGIN = 300
MOWING_STATES = {"mowing", "isrunning", "running", "returning"}

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
log = logging.getLogger("navimow-bridge")
def topic(suffix): return f"{MQTT_BASE_TOPIC}/{suffix}"
def safe_value(val): return val.value if hasattr(val, 'value') else val

class TokenManager:
    def __init__(self):
        self.session = None
        saved = self._load_from_file()
        if saved:
            self.access_token = saved["access_token"]; self.refresh_token = saved["refresh_token"]; self.expires_at = saved["expires_at"]
            log.info(f"💾 Tokens chargés depuis {TOKEN_FILE} (expire dans {int(self.expires_at - time.time())}s)")
        else:
            self.access_token = NAVIMOW_TOKEN; self.refresh_token = NAVIMOW_REFRESH_TOKEN; self.expires_at = time.time() + 3600
            log.info("🔑 Tokens chargés depuis les variables d'environnement"); self._save_to_file()
    def set_session(self, s): self.session = s
    def is_expired(self): return time.time() >= (self.expires_at - TOKEN_REFRESH_MARGIN)
    def _load_from_file(self):
        try:
            if os.path.exists(TOKEN_FILE):
                with open(TOKEN_FILE) as f: data = json.load(f)
                if all(k in data for k in ("access_token","refresh_token","expires_at")): return data
        except Exception as e: log.warning(f"⚠️  Impossible de lire {TOKEN_FILE} : {e}")
        return None
    def _save_to_file(self):
        try:
            os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True)
            with open(TOKEN_FILE,"w") as f: json.dump({"access_token":self.access_token,"refresh_token":self.refresh_token,"expires_at":self.expires_at},f,indent=2)
            log.info(f"💾 Tokens sauvegardés dans {TOKEN_FILE}")
        except Exception as e: log.warning(f"⚠️  Impossible de sauvegarder {TOKEN_FILE} : {e}")
    async def refresh(self):
        if not self.refresh_token: log.error("❌ Pas de refresh_token"); return False
        try:
            log.info("🔄 Renouvellement du token...")
            async with self.session.post(TOKEN_URL, data={"grant_type":"refresh_token","refresh_token":self.refresh_token,"client_id":CLIENT_ID,"client_secret":CLIENT_SECRET}) as resp:
                data = await resp.json()
                if data.get("access_token"):
                    self.access_token = data["access_token"]; self.refresh_token = data.get("refresh_token", self.refresh_token)
                    self.expires_at = time.time() + int(data.get("expires_in",3600)); self._save_to_file()
                    log.info(f"✅ Token renouvelé, expire dans {int(data.get('expires_in',3600))}s"); return True
                log.error(f"❌ Pas de access_token : {data}")
        except Exception as e: log.error(f"❌ Exception refresh : {e}", exc_info=True)
        return False

class JeedomMQTT:
    def __init__(self, loop):
        self.loop = loop
        self.client = mqtt.Client(client_id="navimow-bridge", clean_session=True)
        if MQTT_USER: self.client.username_pw_set(MQTT_USER, MQTT_PASS)
        self.client.on_connect = self._on_connect; self.client.on_message = self._on_message; self.client.on_disconnect = self._on_disconnect
        self.command_callback = None
    def connect(self):
        log.info(f"Connexion MQTT → {MQTT_HOST}:{MQTT_PORT}"); self.client.connect(MQTT_HOST, MQTT_PORT, keepalive=60); self.client.loop_start()
    def _on_connect(self, client, userdata, flags, rc):
        if rc==0: log.info("✅ MQTT connecté"); client.subscribe(topic("cmnd/#")); self.publish("bridge/status","online")
        else: log.error(f"❌ MQTT erreur : code {rc}")
    def _on_disconnect(self, client, userdata, rc): log.warning(f"⚠️  MQTT déconnecté ({rc})")
    def _on_message(self, client, userdata, msg):
        payload = msg.payload.decode("utf-8",errors="ignore").strip()
        log.info(f"📨 MQTT reçu : {msg.topic} → {payload}")
        if self.command_callback:
            parts = msg.topic.split("/")
            if len(parts)>=3: asyncio.run_coroutine_threadsafe(self.command_callback(parts[-1].lower(),payload),self.loop)
    def publish(self, suffix, value, retain=False): self.client.publish(topic(suffix), str(value), retain=retain)
    def publish_discovery(self, component, object_id, config):
        t = f"{DISCOVERY_PREFIX}/{component}/{object_id}/config"; self.client.publish(t, json.dumps(config), retain=True); log.info(f"📣 Discovery : {t}")

class NavimowBridge:
    def __init__(self, loop):
        self.loop=loop; self.token_manager=TokenManager(); self.jeedom=JeedomMQTT(loop)
        self.jeedom.command_callback=self.handle_command; self.api=None
        self.device_id=NAVIMOW_DEVICE_ID or None; self.device_name="Navimow H1500"
        self._running=True; self._is_mowing=False; self._mowing_start=None
    def _current_poll_interval(self): return POLL_INTERVAL_MOWING if self._is_mowing else POLL_INTERVAL
    async def start(self):
        self.jeedom.connect(); await asyncio.sleep(2)
        session = aiohttp.ClientSession(); self.token_manager.set_session(session)
        self.api = MowerAPI(session=session, token=self.token_manager.access_token, base_url=NAVIMOW_API_URL)
        if not self.device_id: await self.discover_device()
        if not self.device_id: log.error("❌ Aucun appareil trouvé."); return
        self.publish_discovery_messages(); await asyncio.sleep(2)
        log.info(f"🤖 Robot : {self.device_id}")
        while self._running:
            await self._refresh_token_if_needed(); await self.refresh_status(); await asyncio.sleep(self._current_poll_interval())
    async def _refresh_token_if_needed(self):
        if self.token_manager.is_expired():
            if await self.token_manager.refresh(): self.api.set_token(self.token_manager.access_token)
    async def discover_device(self):
        try:
            devices = await self.api.async_get_devices()
            if devices:
                d=devices[0]; self.device_id=getattr(d,'id',None) or str(d); self.device_name=getattr(d,'name','Navimow')
                log.info(f"🔍 Appareil : {self.device_id} ({self.device_name})")
        except Exception as e: log.error(f"Erreur découverte : {e}", exc_info=True)
    def publish_discovery_messages(self):
        di={"identifiers":[self.device_id],"name":self.device_name,"model":"H1500","manufacturer":"Segway Navimow"}
        self.jeedom.publish_discovery("sensor","navimow_state",         {"name":"Etat tondeuse",      "unique_id":f"{self.device_id}_state",         "state_topic":topic("stat/state"),        "icon":"mdi:robot-mower","device":di})
        self.jeedom.publish_discovery("sensor","navimow_vehicle_state", {"name":"Etat détaillé",      "unique_id":f"{self.device_id}_vehicle_state", "state_topic":topic("stat/vehicleState"), "icon":"mdi:robot-mower","device":di})
        self.jeedom.publish_discovery("sensor","navimow_battery",       {"name":"Batterie",           "unique_id":f"{self.device_id}_battery",       "state_topic":topic("stat/battery"),      "unit_of_measurement":"%","device_class":"battery","icon":"mdi:battery","device":di})
        self.jeedom.publish_discovery("sensor","navimow_battery_level", {"name":"Niveau batterie",    "unique_id":f"{self.device_id}_battery_level", "state_topic":topic("stat/batteryLevel"), "icon":"mdi:battery-heart","device":di})
        self.jeedom.publish_discovery("sensor","navimow_mowing_time_min",{"name":"Duree tonte (min)", "unique_id":f"{self.device_id}_mowing_time_min","state_topic":topic("stat/mowing_time_min"),"unit_of_measurement":"min","icon":"mdi:timer","device":di})
        self.jeedom.publish_discovery("button","navimow_start",         {"name":"Démarrer tonte",     "unique_id":f"{self.device_id}_start",         "command_topic":topic("cmnd/start"),      "payload_press":"go","icon":"mdi:play","device":di})
        self.jeedom.publish_discovery("button","navimow_pause",         {"name":"Pause",              "unique_id":f"{self.device_id}_pause",         "command_topic":topic("cmnd/pause"),      "payload_press":"pause","icon":"mdi:pause","device":di})
        self.jeedom.publish_discovery("button","navimow_dock",          {"name":"Retour au dock",     "unique_id":f"{self.device_id}_dock",          "command_topic":topic("cmnd/dock"),       "payload_press":"dock","icon":"mdi:home","device":di})
        log.info("📣 Messages MQTT Discovery publiés")
    async def refresh_status(self):
        try:
            status = await self.api.async_get_device_status(self.device_id)
            if status: self._publish_status(status)
        except Exception as e: log.error(f"Erreur status : {e}", exc_info=True)
    def _publish_status(self, status):
        state=safe_value(getattr(status,'status','unknown')); battery=getattr(status,'battery',0)
        error=safe_value(getattr(status,'error_code','none')); extra=getattr(status,'extra',{}) or {}
        was_mowing=self._is_mowing; self._is_mowing=str(state).lower() in MOWING_STATES
        if self._is_mowing and not was_mowing: self._mowing_start=time.time(); log.info("⏱️  Début de tonte enregistré")
        elif not self._is_mowing and was_mowing: self._mowing_start=None; log.info("⏱️  Fin de tonte")
        if self._is_mowing and self._mowing_start:
            elapsed=int(time.time()-self._mowing_start)
            self.jeedom.publish("stat/mowing_time_min", int(elapsed/60), retain=True)
        self.jeedom.publish("stat/state",   state,   retain=True)
        self.jeedom.publish("stat/battery", battery, retain=True)
        self.jeedom.publish("stat/error",   error,   retain=True)
        if 'vehicleState' in extra: self.jeedom.publish("stat/vehicleState", extra['vehicleState'], retain=True)
        if 'descriptiveCapacityRemaining' in extra: self.jeedom.publish("stat/batteryLevel", extra['descriptiveCapacityRemaining'], retain=True)
        log.info(f"📊 état={state}  batterie={battery}%  erreur={error}  (poll={self._current_poll_interval()}s)")
    async def handle_command(self, command, payload):
        await self._refresh_token_if_needed()
        try:
            cmd_map={"start":MowerCommand.START,"pause":MowerCommand.PAUSE,"resume":MowerCommand.RESUME,"dock":MowerCommand.DOCK}
            if command=="status": await self.refresh_status(); return
            if command not in cmd_map: log.warning(f"Commande inconnue : {command}"); return
            await self.api.async_send_command(self.device_id, cmd_map[command])
            log.info(f"✅ Commande '{command}' envoyée"); self.jeedom.publish("stat/last_command", command)
            await asyncio.sleep(3); await self.refresh_status()
        except Exception as e: log.error(f"Erreur commande '{command}' : {e}", exc_info=True)
    async def stop(self):
        self._running=False; self.jeedom.publish("bridge/status","offline")
        self.jeedom.client.loop_stop(); self.jeedom.client.disconnect()

async def main():
    loop=asyncio.get_event_loop(); bridge=NavimowBridge(loop)
    for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, lambda: asyncio.create_task(bridge.stop()))
    try: await bridge.start()
    except Exception as e: log.error(f"Erreur fatale : {e}", exc_info=True); sys.exit(1)

if __name__ == "__main__": asyncio.run(main())
EOF

3.4 — docker-compose.yml

Remplacez VOTRE_ACCESS_TOKEN et VOTRE_REFRESH_TOKEN par les valeurs obtenues à l’étape 2 :

bash

cat > ~/navimow-jeedom/docker-compose.yml << 'EOF'
services:
  navimow-bridge:
    build: .
    container_name: navimow-bridge
    restart: unless-stopped
    environment:
      NAVIMOW_TOKEN: "VOTRE_ACCESS_TOKEN"
      NAVIMOW_REFRESH_TOKEN: "VOTRE_REFRESH_TOKEN"
      MQTT_HOST: "127.0.0.1"
      MQTT_PORT: "1883"
      MQTT_USER: ""
      MQTT_PASS: ""
      MQTT_BASE_TOPIC: "navimow"
      NAVIMOW_DEVICE_ID: ""
      POLL_INTERVAL: "30"
      POLL_INTERVAL_MOWING: "15"
      NAVIMOW_API_URL: "https://navimow-fra.ninebot.com"
      DISCOVERY_PREFIX: "homeassistant"
      TOKEN_FILE: "/data/tokens.json"
    volumes:
      - navimow_data:/data
    network_mode: host
    logging:
      driver: "json-file"
      options:
        max-size: "5m"
        max-file: "3"

volumes:
  navimow_data:
EOF

Étape 4 — Lancer le bridge

bash

cd ~/navimow-jeedom
docker compose build
docker compose up -d
docker compose logs -f

Les logs doivent afficher :

💾 Tokens chargés depuis les variables d'environnement
💾 Tokens sauvegardés dans /data/tokens.json
✅ MQTT connecté
🔍 Appareil : XXXXXXXX (Navimow H1500)
📣 Messages MQTT Discovery publiés
🤖 Robot : XXXXXXXX
📊 état=docked  batterie=100%  erreur=none  (poll=30s)

Lors des redémarrages suivants :

💾 Tokens chargés depuis /data/tokens.json (expire dans XXXXs)

Étape 5 — Jeedom : vérifier les équipements

Dans Jeedom → Plugins → Protocole domotique → MQTT Manager. Les équipements suivants sont créés automatiquement via MQTT Discovery :

Nom Type Topic
Etat tondeuse Info chaîne navimow/stat/state
Etat détaillé Info chaîne navimow/stat/vehicleState
Batterie Info numérique (%) navimow/stat/battery
Niveau batterie Info chaîne navimow/stat/batteryLevel
Durée tonte (min) Info numérique (min) navimow/stat/mowing_time_min
Démarrer tonte Action bouton navimow/cmnd/start
Pause Action bouton navimow/cmnd/pause
Retour au dock Action bouton navimow/cmnd/dock

États possibles du robot

Valeur stat/state Valeur stat/vehicleState Signification
docked isDocked Dans le dock
charging — En charge
mowing isRunning En tonte
paused — En pause
returning — Retour au dock
error — Erreur

Niveau batterie (stat/batteryLevel)

Valeur Signification
FULL 100%
HIGH > 50%
MEDIUM 25-50%
LOW < 25%
EMPTY Vide

Gestion des tokens

Le bridge renouvelle automatiquement le token OAuth2 toutes les ~55 minutes. Les tokens sont persistés dans un volume Docker (/data/tokens.json) et survivent aux redémarrages.

En cas d’expiration du refresh_token (après plusieurs semaines d’inactivité), les logs afficheront CODE_OAUTH_INFO_ILLEGAL. Il faudra alors relancer get_navimow_token.py sur le PC Windows :

bash

nano ~/navimow-jeedom/docker-compose.yml
# Modifier NAVIMOW_TOKEN et NAVIMOW_REFRESH_TOKEN
docker compose restart

Commandes utiles

bash

# Logs en direct
docker compose -f ~/navimow-jeedom/docker-compose.yml logs -f

# Redémarrer
docker compose -f ~/navimow-jeedom/docker-compose.yml restart

# Arrêter
docker compose -f ~/navimow-jeedom/docker-compose.yml down

# Statut
docker compose -f ~/navimow-jeedom/docker-compose.yml ps

Crédits

Frédéric

Petit bonus. Un petit widget à utiliser avec le plugin html display. A ajuster en fonction de vos besoin. Il est fait sur le base du thème de mon design.
image

Il vous faudra juste adapter les ID de vos commandes :
state: 25614,
battery: 25616,
duration: 25639
start: 25617,
pause: 25618,
dock: 25619

<style>
@keyframes breathe-nm{0%,100%{width:30%;opacity:0.5}50%{width:80%;opacity:1}}
.nm3{background:#1d1d1d;border:1px solid #2a2a2a;border-radius:3px;overflow:hidden;width:380px;height:113px;font-family:Montserrat,Arial,sans-serif;font-variant:small-caps;font-weight:normal;font-size:11px;box-sizing:border-box;display:flex;flex-direction:column;}
.nm3-header{background:#222;padding:3px 10px 0;flex-shrink:0;}
.nm3-title{font-size:11px;font-weight:600;color:#ccc;letter-spacing:1.5px;text-transform:uppercase;text-align:center;line-height:16px;}
.nm3-line-wrap{padding-bottom:2px;margin-top:2px;display:flex;justify-content:center;}
.nm3-line{height:1px;background:linear-gradient(90deg,transparent,#98c379,transparent);animation:breathe-nm 2.5s ease-in-out infinite;}
.nm3-body{background:#191919;padding:4px 8px 4px;flex:1;display:flex;align-items:center;gap:8px;overflow:hidden;}
.nm3-middle{display:flex;flex-direction:column;gap:3px;width:100px;flex-shrink:0;}
.nm3-state-badge{display:flex;align-items:center;gap:4px;padding:3px 6px;border-radius:3px;font-size:10px;letter-spacing:0.8px;border:1px solid;white-space:nowrap;}
.nm3-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
.nm3-batt-row{display:flex;align-items:center;gap:4px;}
.nm3-batt-wrap{flex:1;height:5px;background:#111;border-radius:2px;border:1px solid #252830;overflow:hidden;}
.nm3-batt-bar{height:100%;border-radius:2px;transition:width 0.6s ease;}
.nm3-batt-val{font-size:10px;color:#555;min-width:26px;text-align:right;flex-shrink:0;}
.nm3-duration{display:flex;align-items:center;gap:4px;height:14px;}
.nm3-duration-val{font-size:11px;font-weight:500;}
.nm3-duration-lbl{font-size:9px;color:#444;letter-spacing:0.5px;}
.nm3-right{display:flex;flex-direction:column;gap:4px;flex:1;}
.nm3-btn{width:100%;padding:4px 6px;border-radius:3px;border:1px solid #2e2e2e;background:#1a1a1a;color:#888;font-family:Montserrat,Arial,sans-serif;font-variant:small-caps;font-size:10px;letter-spacing:0.8px;cursor:pointer;transition:border-color 0.2s,color 0.2s,background 0.2s;text-align:center;box-sizing:border-box;display:block;line-height:14px;}
.nm3-btn:hover{background:#222;color:#bbb;border-color:#444;}
.nm3-btn.start:hover{border-color:#98c37960;color:#98c379;}
.nm3-btn.pause:hover{border-color:#e5c07b60;color:#e5c07b;}
.nm3-btn.dock:hover{border-color:#61afef60;color:#61afef;}
.nm3-btn.hidden{visibility:hidden;}
.nm3-footer{background:#1d1d1d;border-top:1px solid #252525;padding:2px 10px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
.nm3-foot-lbl{font-family:monospace;font-size:8px;color:#313131;letter-spacing:0.5px;}
.nm3-led{width:6px;height:6px;border-radius:50%;border:1px solid #3a5a3a;}
</style>

<div class="nm3">
  <div class="nm3-header">
    <div class="nm3-title">Tondeuse Navimow</div>
    <div class="nm3-line-wrap"><div class="nm3-line" style="width:30%"></div></div>
  </div>
  <div class="nm3-body">

    <div style="flex-shrink:0">
      <svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <clipPath id="deck-clip">
            <circle cx="32" cy="30" r="12"/>
          </clipPath>
        </defs>
        <ellipse cx="32" cy="60" rx="18" ry="3" fill="#000" opacity="0.3"/>
        <circle cx="32" cy="30" r="24" fill="#1e1e1e" stroke="#2e2e2e" stroke-width="1.5"/>
        <circle cx="32" cy="30" r="20" fill="none" stroke="#252525" stroke-width="1"/>
        <rect x="6" y="22" width="5" height="15" rx="2.5" fill="#252525" stroke="#333" stroke-width="1"/>
        <rect x="53" y="22" width="5" height="15" rx="2.5" fill="#252525" stroke="#333" stroke-width="1"/>
        <circle id="nm3-led-l" cx="23" cy="13" r="2" fill="#98c379" opacity="0.9"/>
        <circle id="nm3-led-r" cx="41" cy="13" r="2" fill="#98c379" opacity="0.9"/>
        <rect x="26" y="34" width="12" height="7" rx="1" fill="#1a241a" stroke="#243024" stroke-width="0.5"/>
        <line x1="30" y1="34" x2="30" y2="41" stroke="#243024" stroke-width="0.5"/>
        <line x1="32" y1="34" x2="32" y2="41" stroke="#243024" stroke-width="0.5"/>
        <line x1="34" y1="34" x2="34" y2="41" stroke="#243024" stroke-width="0.5"/>
        <line x1="26" y1="37.5" x2="38" y2="37.5" stroke="#243024" stroke-width="0.5"/>
        <circle cx="32" cy="30" r="12" fill="#1c1c1c" stroke="#2a2a2a" stroke-width="1"/>
        <g id="nm3-deck" clip-path="url(#deck-clip)">
          <rect x="30.5" y="18" width="3" height="12" rx="1.5" fill="#404040" stroke="#505050" stroke-width="0.5"/>
          <rect x="30.5" y="18" width="3" height="12" rx="1.5" fill="#404040" stroke="#505050" stroke-width="0.5" transform="rotate(120 32 30)"/>
          <rect x="30.5" y="18" width="3" height="12" rx="1.5" fill="#404040" stroke="#505050" stroke-width="0.5" transform="rotate(240 32 30)"/>
        </g>
        <circle cx="32" cy="30" r="3.5" fill="#1a1a1a" stroke="#333" stroke-width="1"/>
        <circle cx="32" cy="30" r="1.5" fill="#2a2a2a"/>
      </svg>
    </div>

    <div class="nm3-middle">
      <div class="nm3-state-badge" id="nm3-badge" style="background:#55555510;border-color:#55555540;color:#555">
        <div class="nm3-dot" id="nm3-dot" style="background:#555"></div>
        <span id="nm3-lbl">&#8212;</span>
      </div>
      <div class="nm3-batt-row">
        <svg width="11" height="11" viewBox="0 0 12 12" fill="none" style="flex-shrink:0">
          <rect x="0.5" y="2.5" width="10" height="7" rx="1.5" stroke="#444" stroke-width="1" fill="none"/>
          <rect x="10.5" y="4.5" width="1.5" height="3" rx="0.5" fill="#444"/>
          <rect id="nm3-batt-inner" x="1.5" y="3.5" width="7" height="5" rx="1" fill="#98c379"/>
        </svg>
        <div class="nm3-batt-wrap"><div class="nm3-batt-bar" id="nm3-batt-bar" style="width:0%;background:#98c379"></div></div>
        <div class="nm3-batt-val" id="nm3-batt-val">&#8212;</div>
      </div>
      <div class="nm3-duration" id="nm3-duration" style="visibility:hidden">
        <svg width="10" height="10" viewBox="0 0 12 12" fill="none" style="flex-shrink:0">
          <circle cx="6" cy="6" r="5" stroke="#98c37980" stroke-width="1" fill="none"/>
          <line x1="6" y1="3" x2="6" y2="6" stroke="#98c379" stroke-width="1.2" stroke-linecap="round"/>
          <line x1="6" y1="6" x2="8.5" y2="7.5" stroke="#98c379" stroke-width="1.2" stroke-linecap="round"/>
        </svg>
        <span class="nm3-duration-val" id="nm3-duration-val" style="color:#98c379">0</span>
        <span class="nm3-duration-lbl">min</span>
      </div>
    </div>

    <div class="nm3-right">
      <button class="nm3-btn start"        id="nm3-btn-start"  onclick="nm3Exec('start')">&#9654; démarrer</button>
      <button class="nm3-btn pause hidden"  id="nm3-btn-pause"  onclick="nm3Exec('pause')">&#9646;&#9646; pause</button>
      <button class="nm3-btn start hidden"  id="nm3-btn-resume" onclick="nm3Exec('start')">&#9654; démarrer</button>
      <button class="nm3-btn dock"          onclick="nm3Exec('dock')">&#8962; retour dock</button>
    </div>
  </div>
  <div class="nm3-footer">
    <span class="nm3-foot-lbl" id="nm3-foot">navimow &#8212; chargement...</span>
    <div class="nm3-led" id="nm3-led"></div>
  </div>
</div>

<script>
(function(){
  var CMD = {
    state:    25614,
    battery:  25616,
    duration: 25639
  };
  var ACTIONS = {
    start: 25617,
    pause: 25618,
    dock:  25619
  };

  var SM = {
    'docked':    {lbl:'au garage',   col:'#555',    bg:'#55555510', border:'#55555540', blade:false},
    'charging':  {lbl:'en charge',   col:'#e5c07b', bg:'#e5c07b10', border:'#e5c07b40', blade:false},
    'mowing':    {lbl:'en tonte',    col:'#98c379', bg:'#98c37910', border:'#98c37940', blade:true},
    'paused':    {lbl:'en pause',    col:'#e5c07b', bg:'#e5c07b10', border:'#e5c07b40', blade:false},
    'returning': {lbl:'retour dock', col:'#61afef', bg:'#61afef10', border:'#61afef40', blade:false},
    'error':     {lbl:'erreur !',    col:'#e06060', bg:'#e0606010', border:'#e0606040', blade:false},
    'idle':      {lbl:'en attente',  col:'#555',    bg:'#55555510', border:'#55555540', blade:false}
  };

  var angle=0, anim=null;

  function spin(){
    angle=(angle+3)%360;
    var d=document.getElementById('nm3-deck');
    if(d) d.setAttribute('transform','rotate('+angle+',32,30)');
    anim=requestAnimationFrame(spin);
  }
  function stopSpin(){ if(anim){cancelAnimationFrame(anim);anim=null;} }
  function battCol(p){ return p>50?'#98c379':p>20?'#e5c07b':'#e06060'; }

  function updateState(raw){
    var s=(raw||'').toLowerCase();
    var m=SM[s]||{lbl:raw||'?',col:'#555',bg:'#55555510',border:'#55555540',blade:false};
    var badge=document.getElementById('nm3-badge');
    badge.style.background=m.bg; badge.style.borderColor=m.border; badge.style.color=m.col;
    document.getElementById('nm3-dot').style.background=m.col;
    document.getElementById('nm3-lbl').textContent=m.lbl;
    document.getElementById('nm3-led').style.borderColor=m.col;
    document.getElementById('nm3-foot').textContent='navimow \u2014 '+m.lbl;
    var ll=document.getElementById('nm3-led-l'),lr=document.getElementById('nm3-led-r');
    if(ll){ll.setAttribute('fill',m.col);lr.setAttribute('fill',m.col);}
    if(m.blade) spin(); else stopSpin();
    var active=['mowing','returning'].indexOf(s)>=0, paused=s==='paused', mowing=s==='mowing';
    document.getElementById('nm3-btn-start').className  ='nm3-btn start' +(active||paused?' hidden':'');
    document.getElementById('nm3-btn-pause').className  ='nm3-btn pause' +(active?'':' hidden');
    document.getElementById('nm3-btn-resume').className ='nm3-btn start' +(paused?'':' hidden');
    document.getElementById('nm3-duration').style.visibility = mowing ? 'visible' : 'hidden';
    document.getElementById('nm3-duration-val').style.color  = m.col;
  }

  function updateBattery(raw){
    var p=parseFloat(raw)||0, c=battCol(p);
    document.getElementById('nm3-batt-bar').style.width=p+'%';
    document.getElementById('nm3-batt-bar').style.background=c;
    document.getElementById('nm3-batt-inner').setAttribute('width',Math.round(p/100*7));
    document.getElementById('nm3-batt-inner').setAttribute('fill',c);
    document.getElementById('nm3-batt-val').textContent=p+'%';
  }

  function updateDuration(raw){
    document.getElementById('nm3-duration-val').textContent=parseInt(raw)||0;
  }

  window.nm3Exec=function(type){
    var id=ACTIONS[type];
    if(!id) return;
    jeedom.cmd.execute({id:id});
  };

  jeedom.cmd.addUpdateFunction(CMD.state,    function(o){ updateState(o.display_value);    });
  jeedom.cmd.addUpdateFunction(CMD.battery,  function(o){ updateBattery(o.display_value);  });
  jeedom.cmd.addUpdateFunction(CMD.duration, function(o){ updateDuration(o.display_value); });

  function refresh(){
    jeedom.cmd.execute({id:CMD.state,    success:function(v){ updateState(v);    }});
    jeedom.cmd.execute({id:CMD.battery,  success:function(v){ updateBattery(v);  }});
    jeedom.cmd.execute({id:CMD.duration, success:function(v){ updateDuration(v); }});
  }
  refresh();
  setInterval(refresh, 15000);

})();
</script>
1 « J'aime »

Bonjour,

J’ai aussi fait un plugin jeenavimow.

Je vais publeir un message sur le forum

1 « J'aime »

tu as demandé à l’IA de faire un plugin.

Oui

Attention, l’ia est très forte pour faire des plugins, mais il lui faut un guide.

oui, un bon guide, parce que l’IA invente beaucoup de choses qu’il ne faut pas faire…

1 « J'aime »

Bonjour,

je suis très intéressée par ce plugin mais je n’arrive pas a lancer les dépendances

merci d’avance de votre aide

0000|[2026-06-22 16:50:04] [jeenavimow] Début installation dépendances
0001|[2026-06-22 16:50:04] [jeenavimow] Répertoire resources : /var/www/html/plugins/jeenavimow/resources
0002|[2026-06-22 16:50:04] [jeenavimow] Répertoire venv      : /var/www/html/plugins/jeenavimow/resources/venv
0003|[2026-06-22 16:50:04] [jeenavimow] Vérification de python3...
0004|[2026-06-22 16:50:04] Python 3.9.2
0005|[2026-06-22 16:50:04] [jeenavimow] Python détecté : 3.9
0006|[2026-06-22 16:50:04] [jeenavimow] ERREUR: navimow-sdk requiert Python 3.11+ (détecté 3.9).
0007|[2026-06-22 16:50:04] [jeenavimow]   → Installez Python 3.11 ou supérieur, puis relancez l'installation.

bonjour

version de python non supporté vous etes en 3.9 le plugin requier 3.11

1 « J'aime »

Malheureusement le log est assez clair.
Toutefois, si tu as un python d’installer sur ton OS à part, j’ai mis à jour le script install_dep.sh pour qu’il cherche d’autre interpréteur.

J’utilise le SDK de Navimow qui nécessite python 3.11.

Bonjour,

La parfaite occasion pour passer à Debian 12 (le support de Debian 11 s’arrête en août). :wink::blush: