Utiliser son Roomba i7 sans internet avec openHAB

La société iRobot qui commercialise le Roomba a déclaré être en faillite le 14 décembre 2025, après une vente ratée à Amazon en 2022. Mon robot a plus de 6 ans et il fonctionne encore très bien. Même s’il est dit que les robots continueront de fonctionner, c’est une bonne opportunité de tenter une séparation de sa dépendance au cloud.

Je ne suis pas du tout connaisseur du domaine de la domotique, mais il semble que deux alternatives ont émergé: openHAB et Home Assistant. Comme il fallait faire un choix, je suis parti sur openHAB qui m’a paru être l’option la plus sérieuse mais ce n’est qu’une impression générale.

La solution openHAB est composée du serveur openHAB qu’il faut héberger dans son réseau local, d’un site web hébergé par le serveur, et d’une application mobile (sur Android, iOS, Apple Watch, Garmin). Il est préférable d’héberger le serveur sur le même subnet que les objets à contrôler mais dans mon cas ça sera sur un subnet différent car je le déploie sur mon cluster Kubernetes local qui a son propre subnet. Cela limitera certaines fonctionnalités, principalement l’auto discovery des objets.

Il faudra aussi couper l’accès Internet de son robot pour: éviter qu’une connexion soit établie depuis l’app mobile iRobot (en effet une seule connexion à la fois est supportée), éviter les mises à jour de firmware qui pourraient casser l’intégration avec openHAB. Dans mon cas, je le fais très simplement depuis mon pare-feu (qui est pfSense), le robot ayant sa propre adresse IP statique assignée par celui-ci.

La fonction « cartographie » ne sera plus supportée mais je ne l’utilisais quasiment pas.

Déployer le serveur openHAB

La documentation fournie par openHAB est assez bien détaillée. Je me suis appuyé sur leurs exemples docker-compose pour obtenir les manifestes que j’utilise pour Kubernetes.
A noter que j’utilise ArgoCD et Traefik, comme mentionné dans des articles précédents, je ne couvre donc aucun détail. L’image docker choisie est openhab/openhab:5.1.0.RC2-alpine (la version 5.1.0 stable devrait sortir prochainement).


apiVersion: v1
kind: Namespace
metadata:
  name: openhab
  labels:
    name: openhab
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: openhab-userdata-pvc
  namespace: openhab
  annotations:
    argocd.argoproj.io/sync-options: Delete=false
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 5Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: openhab-config-pvc
  namespace: openhab
  annotations:
    argocd.argoproj.io/sync-options: Delete=false
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 5Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: openhab-addons-pvc
  namespace: openhab
  annotations:
    argocd.argoproj.io/sync-options: Delete=false
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
---
# https://hub.docker.com/r/openhab/openhab/
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openhab-deployment
  namespace: openhab
  labels:
    app: openhab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openhab
  template:
    metadata:
      labels:
        app: openhab
    spec:
      volumes:
      - name: config-volume
        persistentVolumeClaim:
          claimName: openhab-config-pvc
      - name: addons-volume
        persistentVolumeClaim:
          claimName: openhab-addons-pvc
      - name: userdata-volume
        persistentVolumeClaim:
          claimName: openhab-userdata-pvc
      containers:
      - name: openhab
        image: openhab/openhab:5.1.0.RC2-alpine
        ports:
        - containerPort: 8080
        - containerPort: 8443
        - containerPort: 5007
        - containerPort: 8101
        volumeMounts:
        - name: config-volume
          mountPath: /openhab/conf
        - name: userdata-volume
          mountPath: /openhab/userdata
        - name: addons-volume
          mountPath: /openhab/addons
        env:
        - name: CRYPTO_POLICY
          value: "unlimited"
        - name: EXTRA_JAVA_OPTS
          value: "-Duser.timezone=Europe/Paris"
---
apiVersion: v1
kind: Service
metadata:
  name: openhab-service
  namespace: openhab
  labels:
    app: openhab
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: openhab
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: openhab-ingress-https
  namespace: openhab
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`home.my-domain.com`)
    kind: Rule
    services:
    - name: openhab-service
      namespace: openhab
      port: 8080
      scheme: http
  tls:
    certResolver: acme

Le premier déploiement s’est fait sans accroc, la dernière ligne des logs indique:


Launching the openHAB runtime...

Pour la suite, j’ai suivi le tutoriel pour les débutants, qui opte pour la configuration par l’UI et non par fichiers. On commence par un rapide wizard de configuration quand on se connecte à l’URL du site (https://home.my-domain.com dans mon exemple): création de l’utilisateur administrateur et les premiers paramétrages.

Une fois le wizard terminé, on arrive sur l’interface principale d’openHAB, vide puisque rien n’a été configuré.

Interface web d'openHAB juste après l'installation

On peut aussi vérifier que l’app mobile fonctionne. Pour iOS, après l’installation il faut aller dans ses paramètres (dans l’app), désactiver le mode demo, et renseigner l’URL et le compte d’accès à son instance locale, et éventuellement désactiver openHAB Cloud Service si on ne souhaite pas l’utiliser (c’est pour recevoir des notifications sur iOS).

Le modèle conceptuel d’openHAB est assez riche et peu intuitif au premier abord. Il y a une page de leur documentation assez indigeste sur ce qu’ils appellent leur modèle sémantique. Je suppose que ce modèle est ce qui rend leur solution flexible.

En simplifiant, il nous faut:

  • Installer l’add-on iRobot Binding via l’Add-on Store
  • Ajouter un Objet (Thing), qui est notre robot
  • Créer un item de type Equipment, qui est le lien entre l’interface utilisateur et notre objet. Cet item est un groupe d’autres items, typiquement des Point items que sont les fonctionnalités exposées par l’équipement, mais tout cela est géré automatiquement par le binding.
  • Afficher l’item (correspondant à notre équipment) dans l’UI

Pour appréhender tout ça, j’ai commencé par ajouter mes interrupteurs connectés (pour contrôler des lampes). Je vous épargne cette partie, mais c’est un bon point de départ si vous en avez.

Modèle racine

Comme nous n’avons encore rien créé dans openHAB, il faut commencer par modéliser sa maison. Le Model dans openHAB est simplement l’arborescence des localisations qui vont accueillir les objets connectés. Par exemple, dans un appartement, le noeud racine est sa localisation géographique (un nom évocateur sur un noeud de type sémantique Location), puis un sous-noeud pour l’appartement (de type Group et sémantique Apartment), puis un noeud par pièce qui le compose (toujours un Group du type de pièce, par exemple Bedroom, Living Room, etc.).

Une fois ce modèle créé, on peut facilement créer un Equipment (qui est un type d’item) depuis le noeud le plus approprié de son modèle. Par exemple, pour un interrupteur de lampe qui se trouve dans une chambre, on se placerait sur le noeud de la chambre, pour cliquer sur Create Equipment from Thing. Pour le Roomba, c’est plus discutable: dans mon cas je l’ai créé sur le noeud de mon appartement entier, plutôt que le noeud du salon où sa base est installée.

Ce qu’il faut avoir en tête en faisant cette modélisation, c’est quel rendu vous souhaitez dans l’UI, notamment dans la section Location. L’UI est découpée en quatre sections:

  • Overview: section principale, vide par défaut, à configurer manuellement dans la suite de cet article.
  • Location: liste les équipements par localisation, en fonction du modèle (grille plate de tuiles, chaque tuile est une localisation pouvant regrouper des équipements).
  • Equipment: liste simple des équipments (grille plate de tuiles, chaque tuile est un équipement).
  • Properties: permet de voir l’historique de certaines propriétés des équipements, par type de propriété, et de les afficher sur un graphique.

Ajouter son Roomba à openHAB

On peut commencer par installer l’add-on iRobot Binding via l’Add-on Store. Une fois ceci fait, on doit donc ajouter un Equipment (un type d’item) à notre modèle. Mais avant cela, il faut obtenir le compte d’accès local au robot, on aura besoin de ces informations pour configurer le binding.

Obtenir le compte local du robot

En suivant ces instructions: https://github.com/koalazak/dorita980

Le principe est d’utiliser son compte iRobot (email et mot de passe personnel) pour lancer un script qui va obtenir l’ID et mot de passe de son robot.

Dans mon cas, avec Docker Desktop, sur le même subnet que mon robot (pour que l’Auto Discovery fonctionne), j’ai lancé la commande:


docker run -it node sh -c "npm install -g dorita980 && get-roomba-password-cloud ""username"" ""password"""

Le résultat est le suivant:


Found 1 robot(s)!
Robot "i7" (sku: i755840 SoftwareVer: lewis+22.52.10+2023-10-03-e032ab4903c+Firmware-Build+4820):
BLID=> ***
Password=> *** <= Yes, all this string.

Use this credentials in dorita980 lib :)

Créer l'objet Roomba

Avant de continuer, je vous conseille de couper l'accès Internet du Roomba sur votre pare-feu, de le redémarrer (pour le mien c'est un appui de 20 secondes sur son bouton) et de vérifier ensuite que le robot est bien coupé d'internet via son application. En effet, une seule connexion au robot est possible, et cela évitera aussi les mises à jour de firmware qui pourraient casser le binding.

Dans les Things, on peut ajouter notre robot grâce au binding iRobot. Le binding propose un scan automatique que vous pouvez peut-être utiliser. Dans mon cas ce n'est pas possible car openHAB ne se trouve pas sur le même subnet que mon robot, je choisis donc la méthode manuelle avec son adresse IP que je connais. Le Robot ID correspond au champ BLID fourni par le script dorita980, et le mot de passe au champ Password.

Interface web d'openHAB pour ajouter le Roomba manuellement

Si tout se passe bien, le statut affiché passera immédiatement à ONLINE.

Ajouter le Roomba au modèle

Dans Model, se placer sur le noeud dans lequel on souhaite ajouter le Roomba. Je l'ai ajouté à mon appartement puisque je considère qu'il peut aller dans différentes pièces. Cliquer sur Create Equipment from Thing.

Assurez-vous de bien nommer l'équipement "Roomba_i7" (dans le champ Name qui ne pourra pas être modifié par la suite), sinon il vous faudra adapter les exemples donnés dans la suite de l'article.

Dans la partie Channel, j'ai a peu près tout coché sauf les planifications que je n'utilise pas. Penser à cocher Show advanced pour voir l'élément Last Command:

  • Command
  • Last Command
  • Mission
  • State
  • Battery
  • Bin
  • Error

Parmi ces channels, seul Command est une entrée, les autres sont des sorties (lecture seule). Tout est expliqué sur la documentation du binding iRobot.

Afficher le Roomba dans l'UI

Le robot peut déjà être contrôlé en allant dans la partie Equipment ou dans Locations, mais il faut encore un peu de travail pour l'afficher de façon plus jolie dans la section Overview.

Le rendu le plus joli que j'ai trouvé est via la création d'un widget trouvé ici: https://github.com/yuguar/roomba_OH3_UI_widget

Il faut commencer par télécharger l'image roomba.png et la copier au bon endroit dans le volume contenant la configuration d'openHAB. Selon l'exemple de manifeste donné plus haut, cela donne une commande telle que:


kubectl cp .\roomba.png openhab/openhab-deployment-5b49c68858-4x98t:/openhab/conf/html/roomba.png

Cela suppose de lancer la commande depuis le répertoire qui contient le fichier à copier dans l'unique conteneur du pod openhab-deployment-5b49c68858-4x98t (ce nom de pod change évidemment à chaque déploiement) dans le namespace openhab.

Puis on crée le widget en copiant le script YAML fourni dans le dépôt GitHub mentionné plus haut (le script n'est pas reproduit ici). Pour créer le widget, c'est dans la section Developer Tools, Widgets. Si cela fonctionne bien, il y a même une prévisualisation avec l'image copiée précédemment.

Une fois le widget enregistré, on peut l'utiliser dans la section Settings, Pages, Overview. Il suffit de créer un bloc, une ligne, une colonne, une cellule, et enfin d'ajouter le widget du Roomba dans la cellule.
Alternativement, on peut aussi le faire via l'onget code avec ce type de code:


config:
  label: Overview
blocks:
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: widget:Roomba_widget
                      config:
                        battery: Roomba_i7_Battery
                        state: Roomba_i7_State
                        command: Roomba_i7_Command
                        mission: Roomba_i7_Mission
                        error: Roomba_i7_Error
                        bin: Roomba_i7_Bin
masonry: []
grid: []
canvas: []

Une fois sauvegardé, si on retourne sur la page d'accueil d'openHAB, on devrait voir apparaitre le robot sur une interface minimaliste que je trouve très réussie. Avec mon premier test sur mes interrupteurs de lampe, cela donne:

Overview d'openHAB avec le Roomba

Et sur iPhone:

openHAB sur iphone

Pour envoyer une commande au robot, il faut savoir qu'il faut cliquer sur le bouton du robot (à peu près au centre de l'image). Au premier abord, ce n'est pas si évident à comprendre.

Afficher le Roomba sur Apple Watch

Il existe même une app openHAB sur Apple Watch. Cependant, lors de mes tests, ça semble encore un peu immature. Il est documenté qu'il faut créer un sitemap nommé "watch.sitemap" mais cela n'a pas suffit. J'ai dû aller dans les paramètres de l'application sur iOS, et spécifiquement sélectionner le sitemap dans la sélection proposée. J'ai trouvé cet indice sur cette page. Ensuite j'ai dû kill l'application sur la montre pour la forcer à redémarrer pour afficher le sitemap.

OpenHAB sur Apple Watch

Pour créer le sitemap, aller dans Pages et ajouter un sitemap, et y coller un code tel que:


sitemap sitemap_watch_6316a9a2c9 label="Watch" {
    Frame label="Roomba" {
        Switch item=Roomba_i7_Command label="Commande" mappings=["clean"="Clean", pause="Pause", stop="Stop"]
        Text item=Roomba_i7_Mission label="Cycle"
        Text item=Roomba_i7_State label="Phase"
        Text item=Roomba_i7_Bin label="Bin"
        Text item=Roomba_i7_Error label="Error"
        Text item=Roomba_i7_Battery label="Battery [%d %%]"
    }
}

J'ai volontairement réduit le nombre de commandes sur la Watch. Une fois enregistré, aller dans les paramètres de l'app iOS et sélectionner ce sitemap pour la Watch. Ensuite, ouvrir l'app sur la montre, et si ça ne fonctionne pas du premier coup, essayer de kill l'app pour la relancer.

Personaliser les commandes pour nettoyer des pièces précises

Sans surprise, le binding iRobot dans openHAB ne supporte pas vraiment la fonction cartographie, en revanche, tant que les zones configurées dans le robot sont encore fonctionnelles, il est possible de préciser la zone à aspirer dans la commande.

Il faut trouver un map id et un region id, et c'est peu pratique. Deux options semble possibles:

  • Désactiver l'objet du robot dans openHAB, réactiver l'accès Internet au robot, utiliser l'application propriétaire iRobot pour lancer l'aspiration dans la pièce souhaitée, puis réactiver l'objet dans openHAB (après avoir coupé la connexion du robot à Internet) pour trouver bon paramètres dans la propriété Last_Command.
  • Utiliser le script dorita980 mentionné plus haut pour récupérer l'objet State du robot, et en déduire les bons paramètres (il faut aussi désactiver l'objet du robot dans openHAB pour que le script dorita980 puisse se connecter).

Personellement, j'ai pris l'option 2 avec le script dorita980. Voici le script utilisé:


var dorita980 = require('dorita980');

var myRobotViaLocal = new dorita980.Local(
    'blid',
    'password',
    'ip'); // robot IP address

myRobotViaLocal.on('connect', init);

function init() {
    myRobotViaLocal
        .getRobotState(['batPct', 'bbchg3'])
        .then((actualState) => {
            console.log(actualState);
            myRobotViaLocal.end();
        })
        .catch(console.log);
}

La commande pour exécuter le script enregistré dans un fichier "test.js" avec Node.js:


node test.js

La sortie est très verbeuse, aussi je ne reproduis que les sections utiles:


{
  pmaps: [
    { <base64 string>: '<kind of timestamp>' },
    { <base64 string>: '<kind of timestamp>' },
    { '<base64 string>': '<kind of timestamp>' },
    { '<base64 string>': '<kind of timestamp>' }
  ]
}

Les valeurs intéressantes sont dans pmaps. Le tableau contient des couples clé/valeur. La clé semble être le map id. Le region id semble être un entier qui commence à 1. Dans mon cas, j'ai déduit que mon salon était le premier élément de ce tableau pour le map id, puis j'ai testé la valeur 1 comme region id et bingo! J'admets que la méthode est un peu aléatoire, mais cela me suffit. De toutes façons, ce zonage risque de ne plus être supporté à terme (en particulier si je déménage).

Donc une fois ces valeurs connues, on peut modifier la commande dans le widget qu'on a créé avant (reproduit seulement partiellement ici):


- component: f7-block
  config:
    style:
      left: 46px
      top: 46px
      z-index: 10
  slots:
    default:
      - component: oh-link
        config:
          action: options
          actionItem: =[props.command]
          actionOptions: clean=Clean,"cleanRegions:<base64 string>;1"="Salon",spot=Spot,pause=Pause,stop=Stop,dock=Dock,reset=Reset
          color: green
          f7: circle_fill
          size: 35
          style:
            background: '=(items[props.state].state == "charge") ? "" :
              (items[props.state].state ==
              "hmUsrDock") ? "blue" :
              ((items[props.state].state == "run" ?
              "green" : "red"))'
            border-radius: 50%
            height: 22px
            width: 22px

La seule ligne modifiée est actionOptions dont la valeur originale était actionOptions: clean=Clean,spot=Spot,pause=Pause,stop=Stop,dock=Dock,reset=Reset. Evidemment il faut remplacer <base64 string> par la valeur que vous avez trouvé avec le script.

Commande pour vider le bac

L'application propriétaire iRobot permet de vider le bac manuellement via un bouton quand le robot est sur sa base. Le binding iRobot ne propose pas cette commande. En cherchant un peu, je suis tombé sur cette page qui indique qu'en appuyant sur le bouton Home du robot physique alors qu'il est sur sa base, cela déclenche le vidage du bac. C'est bon à savoir.

Le binding propose une commande Dock pour renvoyer le robot à sa base. J'ai donc testé cette commande quand il est déjà sur sa base. Encore bingo, le vidage s'est bien déclenché.

Pour améliorer un peu les choses, j'ai encore modifié le widget créé un peu plus tôt pour que lorsque le robot est en charge (donc sur sa base), des commandes plus pertinentes soient affichées (lancer une mission d'aspiration ou vider le bac). J'en ai aussi profité pour retirer la commande Reset qui n'est pas documentée par le binding iRobot, je préfère ne pas la proposer (cette commande peut probablement être lancé physiquement sur le robot par un appui long sur un bouton).

Voici ce que ca donne (YAML toujours partiel):


- component: f7-block
  config:
    style:
      left: 46px
      top: 46px
      z-index: 10
  slots:
    default:
      - component: oh-link
        config:
          action: options
          actionItem: =[props.command]
          actionOptions: '=(items[props.state].state == "charge") ?
                                        "clean=Clean,dock=Empty bin" :
                                        "clean=Clean,spot=Spot,pause=Pause,stop=Stop,dock=Dock"'
          color: green
          f7: circle_fill
          size: 35
          style:
            background: '=(items[props.state].state == "charge") ? "" :
              (items[props.state].state ==
              "hmUsrDock") ? "blue" :
              ((items[props.state].state == "run" ?
              "green" : "red"))'
            border-radius: 50%
            height: 22px
            width: 22px

Je suis tombé par hasard sur une autre commande evac (qui apparait dans le statut quand on vide le bac). J'ai testé de remplacer dock par evac, et ca donne le même résultat: le bac se vide quand le robot est sur sa base.

A savoir en cas de remplacement de la batterie

J'en ai aussi profité pour remplacer ma batterie d'origine de 1800mAh par une nouvelle de 6800mAh (trouvée sur le site iFixit). Il semble y avoir des instructions particulières à respecter pour éviter une "Erreur 23" et qui nécessite de restaurer la connexion du robot au cloud (donc après avoir désactivé le binding iRobot dans openHAB) pour que l'app propriétaire établisse la connexion avant de remplacer la batterie. Une fois que la nouvelle batterie est en place, et le robot replacé en charge, attendre son redémarrage complet avant de rouvrir l'app propriétaire d'iRobot (j'ai dû réouvrir l'app une deuxième fois). La connexion s'est bien établie sans message d'erreur.

J'ai pu de nouveau couper l'accès internet. J'ai laissé le robot se charger entièrement (2h sans vérifier son état), je l'ai redémarré, puis j'ai rétabli le binding iRobot dans openHAB.

Pouvoir remplacer la batterie de nouveau dans quelques années sera donc assez incertain (risque qu'un firmware incompatible avec le binding iRobot d'openHAB soit téléchargé). S'il dure encore 5-6 ans de plus, il aura eu une durée de vie assez honorable.

Impression générale à l'usage

La gestion de mes quelques objets connectés est déjà bien plus efficace que dans chaque application propriétaire (iRobot pour le Roomba et Kasa pour les interrupteurs de lampes).

Les contrôles via l'Apple Watch sont un réel bonus, en échange de la cartographie que l'on perd.

References