Sommaire
En ce mois de juillet 2026, dans le sud, il fait chaud, nous avons soif et la pelouse aussi. Certains d'entre vous se demandent s'ils ne doivent pas investir dans un arrosage automatique afin de préserver le gazon bien vert, mais comment faire sans retourner tout le jardin pour y enterrer les tuyaux ?
Aiper a décidé de répondre à cette question et a sorti, il y a un an l'Irrisense, qui a été remplacé par l'Irrisense 2 en janvier 2026. Un arroseur robotisé de qualité entièrement paramétrable, qui s'adapte à presque tous les jardins, qui peut arroser jusqu'à une distance de 12 m.
Cela fait donc quasiment un an que je teste la bête et elle a désormais son intégration non officielle Home Assistant, ce qui la rend encore plus intéressante. Voici donc mon retour d'expérience.
Contexte du test : jardin de 150 m², en forme de W, avec deux arbres, des parterres et des carrés potagers. J'ai aussi un puits commandé via Home Assistant.

core-2026.8.1 de Home Assistant, et la version 0.4.1 de l'intégration Aiper Irrisense 2.
Installation
L'appareil s'installe en 5 min. Le plus long est de trouver le meilleur emplacement pour optimiser la surface, il faut aussi optimiser la pression. Ma pompe de puit a une pression de 3 bars, mais j'ai dû mettre un tuyau de petit diamètre pour augmenter la distance. En le plaçant tel qu'indiqué sur le plan, je couvre 98 % de la surface (la zone entre l'angle du cabanon et du mur du voisin n'est que rarement atteignable).

Une fois l'appareil alimenté en eau et en électricité, il faut créer son compte, le connecter à l'application et créer les différentes zones si besoin. Vous pouvez commencer à arroser la quantité que vous souhaitez ou programmer des arrosages à heures fixes.
Un plot à enfoncer dans le sol est fourni afin d'enlever l'appareil en hiver et de le repositionner sans souci au bon endroit le moment venu.
Utilisation
Vous avez la possibilité de faire une cartographie de zone, de ligne, ou de point.
- La première permet de couvrir l'ensemble de la zone avec une dose uniforme d'eau.
- La deuxième permet d'arroser une ou plusieurs lignes, comme une haie.
- La troisième permet d'arroser un ou plusieurs points pour des massifs par exemple.




Aperçu de différents écrans de l'application
Mais à l'usage, il s'avère que tout cela, est très théorique, j'y reviendrai, mais d'abord quelques détails d'utilisation :
L'arrosage en zone procède selon un algorithme qui commence par le pourtour de la zone, y compris les côtés. À 50 %, il commence à réduire le jet afin de bien arroser la partie la plus proche de l'appareil.
L'arrosage en ligne permet un balayage rapide de la zone, sans avoir l'algorithme de l'arrosage en zone.
L'arrosage en point est pratique : il permet d'arroser un point, pendant une durée prédéfinie en minute (1 min minimum) pour arroser vos parterres ou vos arbres en votre absence.
Les performances
Comme je disais plus haut, l'Irrisense 2 asperge jusqu'à 12 mètres, une grande quantité d'eau, mais dans les zones les plus proches de l'appareil, une brume est générée, ce qui arrose également cette partie. Cela aura donc un impact sur le choix de la cartographie.

En mode zone, le parcours du jet aura tendance à inonder la partie la plus proche de l'appareil. Mais aussi, dans le cas d'un semi, à déplacer les graines situées en bordure de la zone, en raison du va-et-vient incessant du jet de l'intérieur vers l'extérieur. J'ai contacté le service client qui m'a confirmé que cet appareil n'est pas adapté aux semis.
Pour palier à ce souci, j'ai donc opté pour une cartographie en ligne, ainsi, je choisis le parcours exact que fait le jet, et ma zone est arrosée de façon homogène grâce à la brumisation devant l'appareil. Mais cette méthode n'est efficace que si l'arrosage est à son maximum, ce qui pose des problèmes pour les petites surfaces.En effet, plus le jet se raccourci, moins le jet produit de brume et plus le jet s'épaissi au point d'impact, ce qui entraine un sur-arrosage de la zone concernée (x2), mais aussi un déplacement des graines dans le cas d'un semis.


cartographie en ligne à gauche, déplacement de terreau à cause du jet court à droite
Mais son plus gros défaut, c'est que les quantités qui s'affichent à l'écran ne correspondent pas du tout à la réalité et elles sont impossibles à augmenter au-delà de ce qui est indiqué (23 mm), ce qui peut devenir très frustrant, quand comme moi, on a un semis gourmand en eau les premières semaines.
En effet, en étalonnant l'appareil à l'aide d'un pluviomètre, posé à 10 m environ, j'ai remarqué que 3 mm dans l'application n'arrosaient que 1 mm, 10 mm n'arrosaient que 5 mm, et enfin 23 mm n'arrosaient que 10 mm. Or mon semis a besoin de 15 mm par arrosage pour s'enraciner profondément.
Home Assistant à la rescousse
Agacé par cette problématique, j'ai cherché une intégration Home Assistant et c'est sur Reddit que j'ai trouvé :
https://www.reddit.com/r/homeassistant/s/QnpUwsMxWH
L'auteur ne répond plus, mais le repository est accessible et l'intégration fonctionne parfaitement sauf pour la connexion, et elle est mise à jour par son auteur.
En effet, Aiper utilise AWS IoT qui n'autorise qu'une seule connexion à la fois. Du coup, si vous utilisez l'application, ça déconnectera l'intégration HA et vice versa. Et pour le moment, pour se reconnecter sur l'intégration, il faut un redémarrage de Home Assistant. Rien de bloquant, mais il faut l'avoir en tête.
Voici mon dashboard une fois tout installé et personnalisé, l'auteur met à disposition des cartes plus complètes si besoin :

Une fois cela fait, il ne me restait plus qu'à régler le problème de quantité d'eau aspergée. C'est dans le fichier const.py de l'intégration que j'ai pu modifier les valeurs pour coller à la réalité (à 10 m).
"""Constants for the Aiper Irrisense 2 integration."""
from __future__ import annotations
import re
from typing import Final
DOMAIN: Final = "aiper_irrisense"
# Mobile-app headers observed from Charles capture (iOS v3.3.0)
APP_VERSION: Final = "3.3.0"
APP_OS: Final = "ios"
# Static app key observed in `requestidkey` header across all requests.
REQUEST_ID_KEY: Final = "K6!R]y_]Q!gA,5vy"
# REST base URLs per region. The actual `domain` returned by /login is authoritative
# — these are only used to issue the login call.
API_ENDPOINTS: Final = {
"us": "https://apiamerica.aiper.com",
"eu": "https://apieurope.aiper.com",
"asia": "https://apiasia.aiper.com",
}
# Irrisense serial prefixes. The Aiper app's own device-list classifies
# IrriSense models by the first **two** letters of the serial:
# * `WR` — IrriSense 2 (original / Irrigo WR / IrriSense WR)
# * `WG` — IrriSense N2 (big-box retail; Home Depot / Lowe's / Best Buy)
# * `WC` — IrriSense 2 Chromatic / IrriSense II (further retail variant)
# * `WL` — IrriSense 2 SE
# The third letter is a production-batch indicator (WRX, WRZ, WGX, WCX, ...);
# all share the same wire protocol. Matching on the 2-letter family keeps us
# in sync with the app's behaviour and avoids a new release every time a new
# batch letter ships.
IRRISENSE_SERIAL_PREFIXES: Final = ("WR", "WG", "WC", "WL")
# Model string as written into the S3 zone-map path.
IRRISENSE_MODEL: Final = "IrriSense_2"
# Config / Options keys
CONF_REGION: Final = "region"
CONF_ENABLE_MQTT: Final = "enable_mqtt"
CONF_MQTT_DEBUG: Final = "mqtt_debug"
CONF_POLL_INTERVAL: Final = "poll_interval"
CONF_MAP_REFRESH_HOURS: Final = "map_refresh_hours"
CONF_HISTORY_REFRESH_HOURS: Final = "history_refresh_hours"
CONF_REMINDER_REFRESH_HOURS: Final = "reminder_refresh_hours"
CONF_WEATHER_REFRESH_HOURS: Final = "weather_refresh_hours"
CONF_ENABLE_WEATHER: Final = "enable_weather"
CONF_ENABLE_EXPERIMENTAL_SENSORS: Final = "enable_experimental_sensors"
DEFAULT_SCAN_INTERVAL: Final = 120 # seconds
DEFAULT_FAST_SCAN_INTERVAL: Final = 5 # seconds during an active watering event
DEFAULT_FAST_WINDOW_SECONDS: Final = 60
DEFAULT_MAP_REFRESH_HOURS: Final = 6
DEFAULT_HISTORY_REFRESH_HOURS: Final = 6
DEFAULT_REMINDER_REFRESH_HOURS: Final = 24
DEFAULT_WEATHER_REFRESH_HOURS: Final = 1
# Weather is opt-in: enabling it sends HA's home latitude/longitude to Aiper's
# cloud on every refresh, so it defaults OFF.
DEFAULT_ENABLE_WEATHER: Final = False
# ---- MQTT topic patterns --------------------------------------------------
# Same AWS IoT infrastructure as the pool cleaner integration — the Irrisense
# takes the non-X9 "downChan"/"upChan" path confirmed from the decompiled APK.
TOPIC_READ: Final = "aiper/things/{sn}/upChan" # device → cloud / app
TOPIC_WRITE: Final = "aiper/things/{sn}/downChan" # app → device
TOPIC_CLOUD_REPORT: Final = "aiper/things/{sn}/WR/cloud/report" # plain-JSON heartbeats
# Shadow (NetStat, OpInfo, AlarmReport only — no watering state)
TOPIC_SHADOW_GET: Final = "$aws/things/{sn}/shadow/get/accepted"
TOPIC_SHADOW_GET_REQUEST: Final = "$aws/things/{sn}/shadow/get"
TOPIC_SHADOW_UPDATE: Final = "$aws/things/{sn}/shadow/update"
TOPIC_SHADOW_UPDATE_ACCEPTED: Final = "$aws/things/{sn}/shadow/update/accepted"
TOPIC_SHADOW_UPDATE_DELTA: Final = "$aws/things/{sn}/shadow/update/delta"
TOPIC_SHADOW_UPDATE_DOCUMENTS: Final = "$aws/things/{sn}/shadow/update/documents"
# ---- MQTT downChan command types ------------------------------------------
# Reverse-engineered from decompiled `com.aiper.device.i` (Irrisense) Android
# module and cross-confirmed via frida capture of the live Android app
# (AWSIotMqttManager.publishString + MqttManager.publishToTopic).
# Wire envelope: {"<cmd>": {...data}}, plain JSON, QoS 1, isEncrypt=false.
# The command name is the top-level key; there is no "type"/"data" wrapper.
CMD_SET_WORK_MODE: Final = "setWorkMode" # zone start/stop
CMD_WR_CONTROL: Final = "WrControl" # {cmd:0}=reset, {cmd:1}=start (manual valve)
CMD_WORK_INFO: Final = "workInfo" # query current work snapshot
CMD_REAL_TIME_PROGRESS: Final = "realTimeProgress"
CMD_REAL_TIME_STATUS: Final = "realtimeStatus"
CMD_GET_WATER_YIELD: Final = "getWaterYield"
CMD_SET_WATER_YIELD: Final = "setWaterYield"
CMD_WR_MAP_BUILD_EXIT: Final = "WrMapBuildExit"
# ---- Region / Zone types (from S3 zone-map JSON) --------------------------
REGION_TYPE_AREA: Final = 0 # polygon → waterYield (mm)
REGION_TYPE_LINE: Final = 1 # path → waterYield (mm)
REGION_TYPE_POINT: Final = 2 # point → point_time (MINUTES)
# waterYield presets — wire floats mapped from the app's three-step selector.
# The app displays these as "3 mm / 6 mm / 13 mm" but the on-wire floats are
# 0.1 / 0.25 / 0.5 (confirmed in WrPanelWorkInfoViewModel.startWork$start,
# APK line 1846). Device firmware appears to silently drop off-preset values.
WATER_YIELD_MINI: Final = 0.1 # UI: "1 mm"
WATER_YIELD_VERY_LOW: Final = 0.5 # UI: "2 mm"
WATER_YIELD_LOW: Final = 1.25 # UI: "5 mm"
WATER_YIELD_MEDIUM: Final = 2.4 # UI: "10 mm"
WATER_YIELD_HIGH: Final = 3.1 # UI: "15 mm"
WATER_YIELD_VERY_HIGH: Final = 4.5 # UI: "20 mm"
WATER_YIELD_PRESETS: Final = (WATER_YIELD_MINI, WATER_YIELD_VERY_LOW, WATER_YIELD_LOW, WATER_YIELD_MEDIUM, WATER_YIELD_HIGH, WATER_YIELD_VERY_HIGH)
# point_time presets — in MINUTES (confirmed APK line 1844; UI labels
# "1 min / 5 min / 10 min").
POINT_TIME_LOW: Final = 1
POINT_TIME_MEDIUM: Final = 5
POINT_TIME_HIGH: Final = 10
POINT_TIME_PRESETS: Final = (POINT_TIME_LOW, POINT_TIME_MEDIUM, POINT_TIME_HIGH)
# Free dose bounds — the device accepts the full slider range, not just the
# three presets (2026-07). Depth: 0.1..0.9 in on the wire, shown mm x25.4
# (3..23 mm; 1.0 in / 25 mm is NOT selectable). Point time: 1..150 minutes.
WATER_YIELD_MIN: Final = 0.1
WATER_YIELD_MAX: Final = 4.5
DEPTH_MM_MIN: Final = 3
DEPTH_MM_MAX: Final = 23
POINT_TIME_MIN: Final = 1
POINT_TIME_MAX: Final = 150
# ---- Dose / Duration labels (shown in the HA Select) ----------------------
# The Aiper app displays the three waterYield presets as "3 mm / 6 mm / 13 mm"
# and the three point_time presets as "1 min / 5 min / 10 min". We keep the
# exact same labels so HA users see identical vocabulary and so automation
# YAML reads naturally ("dose: 6 mm").
#
# These dicts are authoritative for two directions:
# * label → wire value (Select → _publish_cmd)
# * wire value → label (incoming MQTT snapshot → status banner)
# Neither direction tolerates floats beyond the three presets — the firmware
# silently drops off-preset values (see coordinator._snap_to_preset).
WATER_YIELD_LABELS: Final[dict[float, str]] = {
WATER_YIELD_MINI: "1 mm",
WATER_YIELD_VERY_LOW: "2 mm",
WATER_YIELD_LOW: "5 mm",
WATER_YIELD_MEDIUM: "10 mm",
WATER_YIELD_HIGH: "15 mm",
WATER_YIELD_VERY_HIGH: "20 mm",
}
POINT_TIME_LABELS: Final[dict[int, str]] = {
POINT_TIME_LOW: "1 min",
POINT_TIME_MEDIUM: "5 min",
POINT_TIME_HIGH: "10 min",
}
# Default selection when the user picks a zone without having touched dose yet.
# Matches the Aiper app default (the lowest preset is pre-selected).
DEFAULT_WATER_YIELD_LABEL: Final = WATER_YIELD_LABELS[WATER_YIELD_LOW] # "3 mm"
DEFAULT_POINT_TIME_LABEL: Final = POINT_TIME_LABELS[POINT_TIME_LOW] # "1 min"
def dose_options_for_region_type(region_type: int | None) -> list[str]:
"""Return the list of human labels the Dose select should show.
Point zones get the minutes presets ("1 min" / "5 min" / "10 min").
Area / Line (and the unknown-type fallback) get the mm presets.
"""
if region_type == REGION_TYPE_POINT:
return list(POINT_TIME_LABELS.values())
return list(WATER_YIELD_LABELS.values())
def default_dose_label_for_region_type(region_type: int | None) -> str:
"""Return the label to pre-select when the user picks a zone of this type."""
if region_type == REGION_TYPE_POINT:
return DEFAULT_POINT_TIME_LABEL
return DEFAULT_WATER_YIELD_LABEL
def parse_dose_label(label: str) -> tuple[str, float | int] | None:
"""Reverse-map a Select option back to a wire value.
Returns a tuple ``(kind, value)`` where ``kind`` is ``"waterYield"`` for
Area/Line presets and ``"point_time"`` for Point presets. Returns None
on unknown labels so callers can fall back to the zone-map default.
Preset labels win; free "N mm" / "N min" values are parsed too.
"""
for wy, lbl in WATER_YIELD_LABELS.items():
if lbl == label:
return ("waterYield", wy)
for pt, lbl in POINT_TIME_LABELS.items():
if lbl == label:
return ("point_time", pt)
# Free (non-preset) values from the Number entities: "18 mm" / "120 min".
# Depth is shown in mm but sent as the inch float (1 in = 25.4 mm); the
# device accepts the full 0.1..0.9 in / 1..150 min range, not just the
# three presets above.
m = re.fullmatch(r"(\d+(?:\.\d+)?) mm", label)
if m:
return ("waterYield", float(m.group(1)) / 25.4)
m = re.fullmatch(r"(\d+) min", label)
if m:
return ("point_time", int(m.group(1)))
return None
def label_for_water_yield(value: float | None) -> str | None:
"""Nearest-preset label for a waterYield float, or None on garbage input."""
if value is None:
return None
try:
v = float(value)
except (TypeError, ValueError):
return None
# Snap to the closest preset so off-preset values (which shouldn't occur
# but might on very old firmware echoes) still render sensibly.
closest = min(WATER_YIELD_LABELS, key=lambda p: abs(p - v))
return WATER_YIELD_LABELS[closest]
def label_for_point_time(value: int | float | None) -> str | None:
"""Nearest-preset label for a point_time (minutes), or None on garbage input."""
if value is None:
return None
try:
v = int(round(float(value)))
except (TypeError, ValueError):
return None
closest = min(POINT_TIME_LABELS, key=lambda p: abs(p - v))
return POINT_TIME_LABELS[closest]
# ---- Status / Mode maps ---------------------------------------------------
# setWorkMode.status: 1=start, 0=stop
STATUS_RUNNING: Final = 1
STATUS_STOPPED: Final = 0
# setWorkMode.mode: 0=regular watering, 1=pesticide application.
# Source: WrPanelWorkInfoViewModel.startWork (APK :1837) —
# mode = ((consumeSn is blank) ? 1 : 0) ^ 1
# i.e. no pesticide SN → mode 0; pesticide SN present → mode 1.
# The stop envelope also uses mode=0.
MODE_WATERING: Final = 0
MODE_PESTICIDE: Final = 1
# ---- Nozzle types ---------------------------------------------------------
# Per the iOS app UI, only two nozzle types are exposed:
# * Standard Nozzle — factory default, general water-source conditions
# * Jet Nozzle — for low water pressure / low flow (ships in the box)
# Device-side / UI encoding (what we use internally and send to the user):
# 0 = Standard, 1 = Jet
# Server REST encoding (returned by /wr/getNozzleTypeSetting and accepted by
# /wr/updateNozzleTypeSetting) is 1-indexed and ambiguous on the Standard
# side: a value of 0 _or_ 1 means "Standard", and 2 means "Jet". See the
# decompiled NozzleViewModel (`queryNozzleSettingsByServer` builds an
# IN-array of {0,1} and XORs with 1 → result 0=Standard, 1=Jet; and
# `updateNozzleSettingByServer` sends `value == 1 ? 2 : 1`).
NOZZLE_TYPE_STANDARD: Final = 0
NOZZLE_TYPE_JET: Final = 1
NOZZLE_TYPE_LABELS: Final[dict[int, str]] = {
NOZZLE_TYPE_STANDARD: "Standard",
NOZZLE_TYPE_JET: "Jet",
}
# Server GET → device/UI index helper. Used by the select entity to render
# `current_option`.
NOZZLE_SERVER_TO_DEVICE: Final[dict[int, int]] = {
0: NOZZLE_TYPE_STANDARD,
1: NOZZLE_TYPE_STANDARD,
2: NOZZLE_TYPE_JET,
}

Depuis, je peux enfin arroser ma pelouse sans me questionner sur la véritable quantité déversée et mes programmations sont centralisée dans Home Assistant.
Seule la création des zones reste dans l'application d'origine, mais cela n'est pas censé changer tous les quatre matins.
Points forts
✅ Arrosage d'une grande surface sans travaux
✅ Moins cher qu'un arrosage intégré
✅ Compatible Home Assistant
✅Longueur du jet (jusqu'à 12m)
✅Base de repère à enfoncer dans le sol
✅Connectique fournie
Points faibles
❌Intégration Home Assistant non officielle
❌Très mauvaise homogénéité d'arrosage entre les parties éloignées et les parties proches
❌Algorithme du mode zone qui questionne
❌Service client inutile (aucune mise à jour corrective malgré toutes mes remontées)
❌ Dépendant du cloud
Ce test n’est pas sponsorisé. J'ai acheté le produit et l'ai testé en conditions réelles, et ce compte rendu reflète uniquement mon expérience.