Neato-Botvac-Saugroboter lokal aus FHEM steuern – ohne Cloud, über die serielle Konsole, die in jedem Botvac steckt.
The Neato cloud was switched off in the fourth quarter of 2025. That took the
app and every cloud-based integration with it, including the previous FHEM
module 74_BOTVAC.pm. The robot itself is fine: navigation, SLAM, cleaning
and docking all run in its own firmware. The only thing missing is the
trigger that used to come from the cloud.
74_NeatoLocal.pm replaces that trigger. It talks to the serial console
built into every Botvac and turns it into an FHEM device with set, get
and readings – over USB, or through a small Wi-Fi bridge inside the robot.
-
a Botvac with a serial console:
Model Status Botvac Connected, D3, D4, D5, D6, D7 supported, internal debug port or USB Botvac 65/70e/75/80/85, D75/D80/D85, XV supported, board-edge connector P7/P25 or USB Botvac D8, D9, D10 not supported: different board, the serial port is password-protected Developed and tested on a BotVac D6 Connected with software 4.5.3.189. Other models are expected to work but have not been tested yet. Its full console transcript is in
docs/reference-dump-botvac-d6.txtand is what the tests run against. -
a way to reach the console – three transports, all handled by the same module:
Transport In defineUse serial /dev/ttyACM0@115200the robot's USB port, good for exploring TCP 192.168.1.42:23Wi-Fi bridge inside the robot HTTP http://neato.localOpenNeato on an ESP32-C3 (untested) For TCP,
firmware/contains a matching bridge: a sketch for an ESP32-C3 that sits inside the robot and puts its console on the network. The module can flash it itself (see Setting up the bridge from FHEM). Wiring and power are described in docs/hardware.md (German).About USB: some firmware refuses to clean while a USB host is plugged in (error 220). For everyday use the internal debug port is the intended connection; USB is for exploring and diagnosis.
-
FHEM, running on the machine the robot or the bridge is reachable from.
update add https://raw.githubusercontent.com/chrisse1/neato-FHEM/main/controls_neatolocal.txt
update
shutdown restart
After that, a plain update keeps the module current; update check shows
beforehand what would change, and update delete <url> removes the source
again. FHEM writes straight into /opt/fhem/FHEM/, so the user FHEM runs as
needs write access there.
From a checkout of this repository:
sudo cp FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/
sudo mkdir -p /opt/fhem/FHEM/lib
sudo cp FHEM/lib/NeatoLocalPlan.pm /opt/fhem/FHEM/lib/
sudo chown fhem:dialout /opt/fhem/FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/lib/NeatoLocalPlan.pm
sudo chmod 644 /opt/fhem/FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/lib/NeatoLocalPlan.pmThen, on the FHEM command line:
reload 74_NeatoLocal.pm
define Staubsauger NeatoLocal 192.168.1.42:23
attr Staubsauger interval 60
save
The address may be left out: define Staubsauger NeatoLocal creates the
device without connecting, for when the bridge still has to be flashed.
reload is not optional: FHEM reads its FHEM/ directory at startup and
does not know about a file added later – define would fail with Cannot
load module NeatoLocal. Restarting FHEM does the same. Without save, the
definition is gone after the next restart.
Only needed for the serial transport, not for the Wi-Fi bridge. The file
permissions of the module have nothing to do with access to /dev/ttyACM0:
the port usually belongs to root:dialout, so the user FHEM runs as has to
be in that group.
id fhem # is dialout listed?
sudo usermod -aG dialout fhem # if not
sudo systemctl restart fhem # group changes only apply after a restartOnly one process can have the port open at a time – a running
dump_robot.py blocks FHEM and vice versa.
| Command | Effect |
|---|---|
startCleaning [house|spot|explore|persistent] |
house cleaning (default), spot cleaning; explore and persistent see below |
stop |
end cleaning |
pause / resume |
pause and resume cleaning |
sendToBase |
return to the base |
findMe |
play a sound to find the robot |
clearError |
acknowledge the reported error or alert |
navigationMode Normal|Gentle|Deep|Quick |
cleaning mode |
ecoMode on|off |
quieter, less suction |
intenseClean on|off |
intense cleaning |
binFullDetect on|off |
full dust bin detection |
syncTime |
set the robot's clock from FHEM |
button <name> |
simulate any button press |
flashESP [<image>] [<ssid> <password>] |
write the bridge firmware to a board on a USB port |
wifiESP <ssid> <password> |
give a flashed board new Wi-Fi credentials over USB |
otaESP [<ip>] [<image>] [force] |
update the installed bridge over Wi-Fi |
buildPlan |
compute a shared floor plan from the latest recordings |
statusRequest |
poll the robot now |
reconnect |
rebuild the connection |
testMode on|off |
the console's diagnostic mode, see below |
raw <command> |
send any console command |
testMode puts the robot into diagnostic mode, where it neither reacts
to its buttons nor cleans. The module never enables it on its own and always
sends TestMode Off on shutdown, delete and disable.
| Query | Content |
|---|---|
help [command] |
the robot's command list, or help for one command |
version |
model, serial number, firmware |
state |
state as reported by the robot |
charger |
battery and charging values |
battery |
readings from the smart battery |
warranty |
lifetime counters |
settings |
user settings |
accel |
the robot's tilt: readings pitch, roll, accelSum |
motors, sensors, usage, wifiStatus |
raw data |
serialPorts |
the host's serial ports with their by-id names |
raw <command> |
any console command |
state is one of cleaning, paused, suspended, docking, charging,
docked, idle, error, robotSilent, unreachable and disconnected.
suspended– the robot interrupted the cleaning itself, almost always because of a low battery, and wants to continue after charging. IfisDockedis0at the same time, it did not make it back to the base.robotSilent– the connection to the bridge is up, but the robot does not answer: it is asleep, or the bridge is not wired to it yet. Right after flashing this is the normal state and says nothing bad about the bridge.unreachable– the connection itself is gone: the bridge is not on the network or has no power.
In both cases polling backs off step by step up to 16 times the interval, at most one hour, instead of filling the log with timeouts. The first answer resets everything.
uiState and robotState pass on the state exactly as the robot reports it.
| Reading | Meaning |
|---|---|
batteryPercent |
charge level in percent |
batteryHealth |
remaining capacity as a percentage of the design capacity |
batteryCapacityFull, batteryCapacityDesign |
current and original capacity in mAh |
batteryCycles, cleaningHours |
lifetime counters |
batteryVoltage, batteryTemperature, batteryState |
voltage, temperature, ok/low |
isCharging, isDocked |
charging and docking state |
batteryHealth comes from the measuring electronics inside the battery and
reliably predicts when a robot will get stranded. Below about 50 % it
increasingly fails to make it back to the base, even though the charge
display looks fine until shortly before:
define di_battery DOIF ([Staubsauger:batteryHealth] < 50) (set Message battery weak)
error/errorCode and alert/alertCode are kept apart: a full dust bin
(alert 248) is not an error and does not put the device into the error
state, a missing bin (error 249) does. Code 200 (UI_ALERT_INVALID) means
"nothing to report".
ecoMode, intenseClean, binFullDetect, wallFollower, clickSounds,
melodySounds, warningSounds, led, wifiEnabled, language,
filterChangeTime, brushChangeTime, dirtBinInterval, scheduleEnabled,
scheduledCleanings – read on connect and after every change.
model, serialNumber, firmware, ldsSoftware, hardware, commandApi.
navigationMode is the exception: the console has no command to read it.
The reading therefore holds the value last set. Because the robot does not
keep the mode across runs, the module sends it again before every house
cleaning.
The reading names deliberately follow those of 74_BOTVAC.pm, so existing
notify and DOIF definitions keep working with small adjustments.
| Attribute | Default | Meaning |
|---|---|---|
interval |
60 | polling interval in seconds |
timeout |
10 | how long to wait for an answer |
connectTimeout |
2 | upper limit for one connection attempt |
espPort |
/dev/ttyACM0 |
USB port of the bridge board when flashing |
espImage |
– | image flashESP writes when none is given |
espAppImage |
– | application otaESP sends when none is given |
pollState |
1 | also poll GetState |
pollErrors |
1 | also poll GetErr |
pollMotors |
0 | also poll GetMotors |
pollSettings |
0 | read the user settings on every poll |
useSetEvent |
1 | use the event interface when available |
cmdCleanHouse, cmdCleanSpot, cmdCleanExplore, cmdCleanPersistent, cmdCleanStop, cmdCleanPause, cmdCleanResume, cmdSendToBase, cmdFindMe |
– | override the console command behind a set command |
httpPath, httpMethod |
/api/serial, POST |
HTTP transport only |
disable |
0 | close the connection and stop polling |
disabledForIntervals |
– | time windows in which the device rests (FHEM standard attribute) |
The attributes for recording and the map are described with their sections:
trackRuns, trackDir, trackInterval, trackPose, trackKeepDays,
mapInterval and mapMaxRange under
Recording runs, planAuto, planSources and planCell
under A floor plan from several runs.
A brand-new ESP32-C3 can be put into service from the FHEM host, without an
Arduino installation and without the credentials ending up in the firmware.
It needs esptool (pip3 install esptool or your distribution's package)
and the board on a USB port.
Finding the right port. An ESP32-C3 and the robot both show up as
/dev/ttyACM*, so the number alone says nothing about what is behind it.
The module answers that itself, even without a configured bridge:
get Staubsauger serialPorts
/dev/ttyACM0 ESP32 (native USB)
/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_9C-if00
/dev/ttyACM1 Neato robot
/dev/serial/by-id/usb-Neato_Robotics_Botvac_D6-if00
The by-id name is the better choice for espPort or a define: it stays
the same across restarts and does not depend on which USB socket the device
is in. If no port shows up at all, it is usually a charge-only cable without
data lines.
From a bare board to a running device:
define Staubsauger NeatoLocal
attr Staubsauger espPort /dev/ttyACM0
set Staubsauger flashESP "My WLAN" "long passphrase"
save
The define without an address is the key: flashESP needs a device,
but the bridge has no address yet at that point. The device sits in state
unconfigured, does not connect and does not poll. flashESP fetches the
image this project's CI builds, writes it, stores the credentials on the
board, and finally asks the board over the same USB port which address it
got. That address goes into the reading bridgeAddress, and a device
defined without an address points itself at it and connects. save keeps
it. A device that already has an address keeps it; the log only notes where
the new bridge can be reached.
Quote SSID and password if they contain spaces. A semicolon has to be
written as ;;, because FHEM splits commands on it. Flashing runs in its
own process, so FHEM stays responsive; the result is in the reading
lastFlash.
The ready-made image is in firmware/prebuilt/, built
by CI from source; the text file next to it names version, commit and
SHA-256. FHEM update does not fetch the image – flashESP downloads it
when needed. An image of your own takes precedence:
set Staubsauger flashESP /path/to/image.bin, or permanently through the
attribute espImage.
wifiESP is for when the Wi-Fi changes later: it hands the credentials to
the firmware's configuration console over USB and reports the new address in
bridgeAddress. If the board ends up in its access point instead, lastFlash
says why (password rejected, network not found, turned away by the router).
Updating the installed bridge. Once it is built in, the bridge is powered by the robot's 3.3 V rail and no longer hangs on the server's USB port. From then on updates go over Wi-Fi:
set Staubsauger otaESP
otaESP fetches the published application and pushes it to the bridge with
the ArduinoOTA protocol. The address comes from the device; a different one
can be given: set Staubsauger otaESP 192.168.1.150.
One exception, and it matters: firmware before 0.4.0 had SSID and
password compiled into the image. An update replaces that image – and with
it the only copy of the credentials. The bridge then comes up without a
network and opens the access point neato-setup, where the credentials can
be entered at http://192.168.4.1/ from a phone. No cable needed, but
someone has to be next to the robot. otaESP reads the running version and
refuses such an update until you say set <dev> otaESP force. From
0.4.0 on, the credentials live in flash and survive every update.
Not flashing from the FHEM host? A board without stored credentials opens
the access point neato-setup with an input page, and the firmware takes
the same commands over a terminal on the USB port (help lists them).
Flashing by hand with the Arduino IDE is described in
docs/flashing-esp32c3.md (German). Why the
bridge's Wi-Fi behaves the way it does is in
docs/hintergrund.md (German).
The robot keeps no log of its own and does not hand out its map over the
console. What can be reconstructed is the path it drove: during a run
the module polls GetRobotPos every trackInterval seconds and writes each
position as one JSON line. OpenNeato's "Cleaning History" is built the same
way, by recording rather than fetching.
Recording starts and ends with the run, even if it was started on the robot
itself or by its schedule. A session is stored as
<trackDir>/<device>-<timestamp>.jsonl:
{"device":"Staubsauger","started":"2026-09-19_15-04-05","module":"0.16.0","unit":"m"}
{"t":1281.84,"x":0.000,"y":0.000,"th":0.0}
{"t":1284.91,"x":0.412,"y":0.003,"th":1.2}
{"summary":{"points":812,"distance":41.2,"rotation":3600,"seconds":2431}}Coordinates in metres, th in degrees. The summary also goes into the
readings trackPoints, trackDistance and trackDuration, the file name
into trackFile.
It is off by default, because it creates one file per run, which is of no use to anyone who does not display it:
attr Staubsauger trackRuns 1
The default directory is ./www/neato, i.e. /opt/fhem/www/neato. Finished
sessions are removed after trackKeepDays days, 14 by default; 0 keeps
everything. Deleting is tightly limited: only inside trackDir, only names
matching <device>-<timestamp>.jsonl, only older than the limit, and never
the session being written. Your own files and other devices' sessions stay
untouched.
trackPose picks the position the path is built from: Smooth (default,
the position corrected by the robot) or Raw, the wheel encoders alone,
which drift over a run. Measured, that is the difference between 0.156 and
0.268 occupied cells per point; Raw is only there for comparison.
Lidar scans. The lidar spins on its own during a run – measured on a
D6 at about 5 revolutions per second with real distances. When idle it
stands still, and it could only be started with a TestMode command, in which
the robot does not clean. mapInterval (default 0 = off) fetches a
GetLDSScan every n seconds and stores it next to the path:
{"scan":{"x":1.204,"y":0.418,"th":92.0,"speed":5.02,"pts":[[0,1284],[1,1266]]}}Angle in degrees, distance in millimetres, both relative to the pose on the
same line. Readings with an error code are dropped, and so is anything
beyond mapMaxRange (default 6000 mm): a real scan returned rows without
an error code at about 16.8 m where nothing reflected, while a Botvac's lidar
reaches about five metres.
How the points are placed in the room (the formula is measured, not assumed)
and how a floor plan is made from them is described in
docs/ftui3-map.md (German). FHEM itself draws nothing;
the map component for FTUI3 is
fhem-ftui-components-neatomaps.
For a quick look without FHEM, tools/render_track.py draws a session as
SVG:
python3 tools/render_track.py /opt/fhem/www/neato/Staubsauger-2026-09-20_11-59-11.jsonlA single run is the map of that run, not of the flat: its own origin, its own north, and only the rooms the robot got into that day. Laid on top of each other, several runs can do two things none can alone – fill in missing walls and let every cell be voted on. What three out of four runs call a wall is a wall; what one calls a wall while the others looked at the same spot and found floor was the drying rack.
attr Staubsauger planAuto 1
set Staubsauger buildPlan
The result is a JSON file next to the recordings, plan-<device>-<epoch>.json,
named in the reading planFile. Every plan gets a new name, so a page that is
already open picks it up; the ones it replaces are removed, and a copy under
the fixed name plan-<device>.json is kept for markup that names the file
instead of binding planFile. The FTUI component <ftui-neato-map view="plan">
loads and draws it; file format and procedure are in docs/plan-format.md
of the
component repository.
| Attribute | Default | Meaning |
|---|---|---|
planAuto |
0 | recompute after every cleaning |
planSources |
8 | how many recordings are used, newest first |
planMinScans |
50 | smaller recordings (a run stopped early) are passed over for the next older one |
planCell |
0.10 | cell size in metres; stored in the file, the display follows it |
It takes a while. Every run is rotated and shifted against the frame
until it fits, across all rotations – measured at roughly one minute per
recording on an ordinary PC, longer on a small board. It runs in a separate,
lower-priority process; FHEM itself does not stall. For a shorter run, use
fewer planSources.
Runs that do not fit are rejected rather than forced in – pressing a different floor into the same frame draws walls straight through rooms.
planState ok
ok, 1 did not fit (0.33)
ok, 2 did not fit (0.41, 0.33)
failed: <reason>
The number in brackets is the score reached. This reading is worth
logging: the threshold below which a run is rejected (0.45) is an
assumption, not a measurement, and the evidence is still contradictory –
three real runs of the same flat scored 0.64 to 1.0, but a partial run
against a full one scored 0.33. Over a few weeks, planState is the series
that settles it. The same numbers are in the field scores of the plan file.
The same can be done by hand, without FHEM:
perl tools/neato_plan.pl /opt/fhem/www/neato StaubsaugerStray points. Now and then a point lies behind a wall. The obvious
suspect is the robot tilting at tight spots, so since 0.22.0 every scan also
records the tilt ("tilt":[pitch, roll, |a|], and get <dev> accel by
hand). Measured over a full run, the tilt does not explain the stray
points (correlation +0.05); they are single weak returns at long range, and
the occupancy grid already filters them out. There is therefore deliberately
no tilt filter. tools/stray_points.py repeats the analysis on any session;
the full measurement is in docs/ftui3-map.md (German).
The robot's own help describes startCleaning explore and
startCleaning persistent as the equivalent of starting from the Smart App –
unlike house and spot. Observed on a D6 without cloud: the command is
accepted, the robot raises alert 236 UI_ALERT_ACQUIRING_PERSISTENT_MAP_IDS
and does not move.
Worse: afterwards it accepts further cleaning commands without doing
anything. Even startCleaning house goes nowhere until the robot is
switched off and on again. An explore attempt costs not only itself but
the next run as well.
Both modes are therefore refused unless force is appended:
set Staubsauger startCleaning explore force
The console has no command to create, assign or query persistent map IDs,
so it is likely those IDs came from the cloud – that is an inference from
the observed behaviour, not proven. house and spot work normally.
The robot's Help output is incomplete. Pause, resume and return to base go
through SetEvent, the authenticated event interface the cloud used to
control the robot, which appears in no command list. Its key is computed
from the MAC address that GetVersion reports.
| set command | Route |
|---|---|
startCleaning, stop |
SetEvent, otherwise Clean House / Clean Stop |
pause / resume |
SetEvent, without a key SetButton start as a toggle |
sendToBase |
only SetEvent – the documented commands offer nothing for it |
findMe |
PlaySound SoundID 20 |
The reading commandApi shows whether the interface is unlocked (setEvent
or legacy). attr <dev> useSetEvent 0 forces the documented commands.
The event interface, GetState and the other undocumented commands were
found and decoded by the OpenNeato
project (MIT, © 2026 Soner Köksal). This module contains an independent Perl
implementation, checked against their C++ original on known values. All the
details are in docs/serial-commands.md (German).
- No-go lines and zone cleaning. The D6 had them in the app, but they
came through its cloud –
GetVersionnames it in plain text,nucleo.neatocloud.com– and that is switched off. None of the 44 commands in the console's help has anything to do with maps. What is known, what is unproven and which routes remain is in docs/serial-commands.md (German). The one solution that works today is Neato's magnetic boundary strip. - Robot firmware updates. They only ever came through the cloud.
- Reading the robot's own map. The console has no command for it.
tools/dump_robot.py asks the robot for its command list, fetches the help
text for every command it names and adds the output of the harmless Get*
commands. The result documents what that particular firmware understands.
python3 tools/dump_robot.py --device /dev/ttyACM0
python3 tools/dump_robot.py --tcp 192.168.1.42:23
python3 tools/dump_robot.py --device /dev/ttyACM0 --diagnose
The script only reads: it sends nothing but Help and Get*. No
TestMode, no motor commands, no settings changes. Serial numbers are
masked so a dump can be shared safely (--no-redact turns that off). It
needs only a plain Python 3; pyserial is not required.
If you own a model other than the D6, a dump from it is the most useful thing you can contribute. Everything the module knows about the console comes from one robot.
--diagnose checks the device, permissions and processes holding it,
reports the USB ID, listens passively for five seconds and tries all three
line endings, each with a hex dump. The most common causes of a silent
console, in this order: a sleeping robot, the wrong port, a port in use,
and a charge-only cable.
tools/neato_sim.py emulates a Botvac console over TCP, with command echo,
the Ctrl-Z terminator, the CSV output of the real device and plausible
behaviour: the battery drains while cleaning and charges on the base.
python3 tools/neato_sim.py
define Staubsauger NeatoLocal 127.0.0.1:8888
With --usb the simulator refuses to clean with error 220, like a robot
with a USB host plugged in.
perl tools/check_module.pl # module: loading, transports, parsers, state logic
perl tools/check_ota.pl # update over Wi-Fi, against a stand-in for the bridge
python3 tools/check_sim.py # simulator: protocol and state transitions
python3 tools/check_dump.py # dump tool, serial over a PTY and over TCP
python3 tools/check_track.py # session format and coordinate convention
python3 tools/check_partition.py # partition table of the image
perl tools/check_plan.pl # floor plan against the reference case (takes a minute)
perl tools/check_plan.pl --quick # only the fast checks of the above
check_plan.pl rebuilds the reference case from docs/reference-plan/ and
compares the result with what the JavaScript reference makes of the same
three recordings. It does not demand cell-for-cell equality; the bounds are
in docs/plan-format.md of the component repository.
All of them run without an FHEM installation and without a robot. The test data are verbatim console outputs of a BotVac D6. CI runs them on every push and also builds the bridge firmware for the ESP32-C3.
FHEM freezes briefly. FHEM opens TCP connections synchronously; while an
attempt is running, the whole process waits. The bridge is powered by the
robot and disappears when the robot is off. The module limits an attempt to
2 seconds, connectTimeout lowers that further. apptime on the FHEM
command line shows it: NeatoLocal_Ready or DevIo_OpenDev with a long
runtime means it is the connection setup.
The robot does not answer. state is robotSilent: wake the robot,
check the bridge's wiring (http://neato.local/ shows the byte counters in
both directions), or run --diagnose on the USB port.
The bridge is gone. state is unreachable: the connection does not
come up. Check power and Wi-Fi, not the wiring to the robot.
A command has no effect. get <dev> help Clean shows what the firmware
understands; the cmd* attributes let you adjust any command.
Why some things are built the way they are – the bridge's Wi-Fi, flashing, updates – is written down separately in docs/hintergrund.md (German). The console commands are in docs/serial-commands.md, the map in docs/ftui3-map.md, and the hardware in docs/hardware.md.
GPLv2, like FHEM itself – the full text is in LICENSE.
The SKey computation for the SetEvent commands is an independent
reimplementation of what OpenNeato
(MIT, © 2026 Soner Köksal) reverse-engineered, checked against its C++
original on known values.
Die Neato-Cloud wurde im 4. Quartal 2025 abgeschaltet. Damit sind die App und
alle cloudbasierten Anbindungen tot, darunter das bisherige FHEM-Modul
74_BOTVAC.pm. Der Roboter selbst ist es nicht: Navigation, SLAM, Reinigung
und Andocken laufen vollständig in seiner Firmware. Es fehlt nur der Auslöser,
der bisher aus der Cloud kam.
74_NeatoLocal.pm ersetzt diesen Auslöser. Es spricht die serielle Konsole an,
die in jedem Botvac steckt, und macht daraus ein FHEM-Gerät mit set, get
und Readings – über USB oder über eine kleine WLAN-Brücke im Roboter.
-
einen Botvac mit serieller Konsole:
Modell Status Botvac Connected, D3, D4, D5, D6, D7 unterstützt, interner Debug-Port oder USB Botvac 65/70e/75/80/85, D75/D80/D85, XV unterstützt, Kartenrand-Stecker P7/P25 oder USB Botvac D8, D9, D10 nicht unterstützt: anderes Board, serieller Port ist passwortgeschützt Entwickelt und geprüft an einem BotVac D6 Connected mit Software 4.5.3.189. Die anderen Modelle sollten funktionieren, sind aber noch nicht geprüft. Der vollständige Mitschnitt seiner Konsole liegt in
docs/reference-dump-botvac-d6.txtund dient den Tests als Grundlage. -
einen Weg zur Konsole – drei Transportwege, alle vom selben Modul bedient:
Transport Angabe im defineEinsatz seriell /dev/ttyACM0@115200USB-Port des Roboters, gut zum Erkunden TCP 192.168.1.42:23WLAN-Brücke im Roboter HTTP http://neato.localOpenNeato auf einem ESP32-C3 (ungetestet) Für TCP liegt in
firmware/eine passende Brücke: ein Sketch für einen ESP32-C3, der im Roboter sitzt und dessen Konsole ins Netz bringt. Das Modul kann sie selbst flashen (siehe Brücke aus FHEM heraus einrichten). Verdrahtung und Stromversorgung stehen in docs/hardware.md.Hinweis zu USB: Manche Firmware verweigert die Reinigung, solange ein USB-Host angesteckt ist (Fehler 220). Für den Dauerbetrieb ist der interne Debug-Port vorgesehen; USB eignet sich zum Erkunden und für die Diagnose.
-
FHEM, auf dem Rechner, von dem aus Roboter oder Brücke erreichbar sind.
update add https://raw.githubusercontent.com/chrisse1/neato-FHEM/main/controls_neatolocal.txt
update
shutdown restart
Danach genügt ein update, um auf den neuesten Stand zu kommen; update check
zeigt vorher, was sich ändern würde, update delete <url> entfernt die Quelle
wieder. FHEM schreibt dabei direkt nach /opt/fhem/FHEM/, der Benutzer, unter
dem FHEM läuft, braucht dort Schreibrecht.
Aus einem Klon dieses Repos:
sudo cp FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/
sudo mkdir -p /opt/fhem/FHEM/lib
sudo cp FHEM/lib/NeatoLocalPlan.pm /opt/fhem/FHEM/lib/
sudo chown fhem:dialout /opt/fhem/FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/lib/NeatoLocalPlan.pm
sudo chmod 644 /opt/fhem/FHEM/74_NeatoLocal.pm /opt/fhem/FHEM/lib/NeatoLocalPlan.pmDann in der FHEM-Kommandozeile:
reload 74_NeatoLocal.pm
define Staubsauger NeatoLocal 192.168.1.42:23
attr Staubsauger interval 60
save
Die Adresse darf auch fehlen: define Staubsauger NeatoLocal legt das Gerät an,
ohne sich zu verbinden – für den Fall, dass die Brücke erst noch geflasht werden
muss.
reload ist nicht optional: FHEM liest das Verzeichnis FHEM/ beim Start ein
und kennt eine danach hinzugekommene Datei nicht – define scheitert sonst mit
Cannot load module NeatoLocal. Ein FHEM-Neustart tut es genauso. Ohne save
ist die Definition nach dem nächsten Neustart wieder weg.
Nur für den seriellen Weg nötig, bei der WLAN-Brücke entfällt er. Die Rechte an
der Moduldatei haben nichts mit dem Zugriff auf /dev/ttyACM0 zu tun: der Port
gehört üblicherweise root:dialout, also muss der Benutzer, unter dem FHEM
läuft, in dieser Gruppe sein.
id fhem # steht dialout dabei?
sudo usermod -aG dialout fhem # falls nicht
sudo systemctl restart fhem # Gruppenwechsel wirkt erst nach NeustartEs kann immer nur ein Prozess den Port offen haben – ein laufendes
dump_robot.py blockiert FHEM und umgekehrt.
| Kommando | Wirkung |
|---|---|
startCleaning [house|spot|explore|persistent] |
Hausreinigung (Vorgabe), Spot-Reinigung; explore und persistent siehe unten |
stop |
Reinigung beenden |
pause / resume |
Reinigung unterbrechen und fortsetzen |
sendToBase |
zurück zur Basis |
findMe |
Tonsignal zum Auffinden |
clearError |
gemeldeten Fehler oder Hinweis quittieren |
navigationMode Normal|Gentle|Deep|Quick |
Reinigungsmodus |
ecoMode on|off |
leiser, geringere Saugleistung |
intenseClean on|off |
Intensivreinigung |
binFullDetect on|off |
Erkennung des vollen Staubbehälters |
syncTime |
Uhr des Roboters aus FHEM stellen |
button <name> |
beliebigen Tastendruck simulieren |
flashESP [<image>] [<ssid> <passwort>] |
Brücken-Firmware auf ein Board am USB-Port schreiben |
wifiESP <ssid> <passwort> |
einem geflashten Board neue WLAN-Zugangsdaten über USB geben |
otaESP [<ip>] [<image>] [force] |
die eingebaute Brücke über Funk aktualisieren |
buildPlan |
aus den letzten Aufzeichnungen einen gemeinsamen Grundriss rechnen |
statusRequest |
Zustand sofort abfragen |
reconnect |
Verbindung neu aufbauen |
testMode on|off |
Diagnosemodus der Konsole, siehe unten |
raw <Kommando> |
beliebiges Konsolenkommando senden |
testMode schaltet den Roboter in den Diagnosemodus. Dort reagiert er
weder auf seine Tasten noch reinigt er. Das Modul aktiviert ihn nie von selbst
und sendet bei Shutdown, Löschen und disable immer TestMode Off.
| Abfrage | Inhalt |
|---|---|
help [Kommando] |
Kommandoliste des Roboters bzw. Hilfe zu einem Kommando |
version |
Modell, Seriennummer, Firmware |
state |
Zustand laut Roboter |
charger |
Akku- und Ladewerte |
battery |
Messwerte der Smart Battery |
warranty |
Lebensdauerzähler |
settings |
Benutzereinstellungen |
accel |
Neigung des Roboters: Readings pitch, roll, accelSum |
motors, sensors, usage, wifiStatus |
Rohdaten |
serialPorts |
serielle Schnittstellen des Rechners mit ihren by-id-Namen |
raw <Kommando> |
beliebiges Konsolenkommando |
state kennt cleaning, paused, suspended, docking, charging,
docked, idle, error, robotSilent, unreachable und disconnected.
suspended– der Roboter hat die Reinigung selbst unterbrochen, in aller Regel wegen leerem Akku, und will sie nach dem Laden fortsetzen. Steht dabeiisDocked 0, hat er die Basis nicht mehr erreicht.robotSilent– die Verbindung zur Brücke steht, aber der Roboter antwortet nicht: er schläft, oder die Brücke ist noch nicht mit ihm verdrahtet. Nach dem Flashen ist das der normale Zustand und kein Hinweis auf ein Problem mit der Brücke.unreachable– die Verbindung selbst ist weg: die Brücke ist nicht im Netz oder ohne Strom.
In beiden Fällen geht die Abfrage schrittweise bis auf das 16-fache Intervall zurück, höchstens eine Stunde, statt Zeitüberschreitungen ins Log zu schreiben. Die erste Antwort setzt alles zurück.
uiState und robotState geben den Zustand unverändert so wieder, wie der
Roboter ihn meldet.
| Reading | Bedeutung |
|---|---|
batteryPercent |
Ladestand in Prozent |
batteryHealth |
Restkapazität in Prozent der Nennkapazität |
batteryCapacityFull, batteryCapacityDesign |
aktuelle und ursprüngliche Kapazität in mAh |
batteryCycles, cleaningHours |
Lebensdauerzähler |
batteryVoltage, batteryTemperature, batteryState |
Spannung, Temperatur, ok/low |
isCharging, isDocked |
Lade- und Dockzustand |
batteryHealth kommt aus der Messelektronik im Akku selbst und sagt zuverlässig
voraus, wann ein Roboter unterwegs liegenbleibt. Unterhalb von etwa 50 % schafft
er es zunehmend nicht mehr zurück zur Basis, obwohl die Ladeanzeige bis kurz
davor brauchbar aussieht:
define di_akku DOIF ([Staubsauger:batteryHealth] < 50) (set Nachricht Akku schwach)
error/errorCode und alert/alertCode sind getrennt: ein voller
Staubbehälter (Alert 248) ist kein Fehler und setzt das Gerät nicht in den
Fehlerzustand, ein fehlender Behälter (Error 249) schon. Code 200
(UI_ALERT_INVALID) bedeutet „nichts zu melden“.
ecoMode, intenseClean, binFullDetect, wallFollower, clickSounds,
melodySounds, warningSounds, led, wifiEnabled, language,
filterChangeTime, brushChangeTime, dirtBinInterval, scheduleEnabled,
scheduledCleanings – beim Verbinden und nach jeder Änderung gelesen.
model, serialNumber, firmware, ldsSoftware, hardware, commandApi.
navigationMode ist eine Ausnahme: die Konsole kennt kein Kommando, ihn
auszulesen. Das Reading hält deshalb den zuletzt gesetzten Wert. Weil der
Roboter den Modus nicht über Läufe hinweg behält, sendet das Modul ihn vor
jeder Hausreinigung erneut.
Die Reading-Namen folgen bewusst denen von 74_BOTVAC.pm, damit bestehende
notify- und DOIF-Definitionen mit geringen Anpassungen weiterlaufen.
| Attribut | Vorgabe | Bedeutung |
|---|---|---|
interval |
60 | Abfrageintervall in Sekunden |
timeout |
10 | wie lange auf eine Antwort gewartet wird |
connectTimeout |
2 | Obergrenze für einen Verbindungsversuch |
espPort |
/dev/ttyACM0 |
USB-Port des Brücken-Boards beim Flashen |
espImage |
– | Image, das flashESP ohne Angabe schreibt |
espAppImage |
– | Anwendung, die otaESP ohne Angabe sendet |
pollState |
1 | GetState mitabfragen |
pollErrors |
1 | GetErr mitabfragen |
pollMotors |
0 | GetMotors mitabfragen |
pollSettings |
0 | Benutzereinstellungen bei jedem Durchlauf mitlesen |
useSetEvent |
1 | die Event-Schnittstelle nutzen, wenn verfügbar |
cmdCleanHouse, cmdCleanSpot, cmdCleanExplore, cmdCleanPersistent, cmdCleanStop, cmdCleanPause, cmdCleanResume, cmdSendToBase, cmdFindMe |
– | Konsolenkommando je set-Kommando überschreiben |
httpPath, httpMethod |
/api/serial, POST |
nur für den HTTP-Transport |
disable |
0 | Verbindung schließen und Abfrage anhalten |
disabledForIntervals |
– | Zeitfenster, in denen das Gerät ruht (FHEM-Standardattribut) |
Die Attribute rund um Aufzeichnung und Karte stehen bei den jeweiligen
Abschnitten: trackRuns, trackDir, trackInterval, trackPose,
trackKeepDays, mapInterval und mapMaxRange unter
Gefahrene Spur aufzeichnen, planAuto,
planSources und planCell unter
Aus mehreren Läufen ein Grundriss.
Ein fabrikneuer ESP32-C3 lässt sich vom FHEM-Rechner aus in Betrieb nehmen,
ohne Arduino-Installation und ohne dass die Zugangsdaten in der Firmware
stehen. Voraussetzung ist esptool (pip3 install esptool oder das
gleichnamige Paket der Distribution) und ein Board am USB-Port.
Den richtigen Port finden. Ein ESP32-C3 und der Roboter melden sich
beide als /dev/ttyACM* – die Nummer allein sagt also nichts darüber aus, was
dahintersteckt. Das Modul beantwortet die Frage selbst, auch ohne
konfigurierte Brücke:
get Staubsauger serialPorts
/dev/ttyACM0 ESP32 (native USB)
/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_9C-if00
/dev/ttyACM1 Neato robot
/dev/serial/by-id/usb-Neato_Robotics_Botvac_D6-if00
Der by-id-Name ist die bessere Angabe für espPort oder ein define: er
bleibt über Neustarts gleich und hängt nicht daran, in welcher USB-Buchse das
Gerät steckt. Erscheint gar kein Port, ist es meist ein reines Ladekabel ohne
Datenleitungen.
Von einem nackten Board zum laufenden Gerät:
define Staubsauger NeatoLocal
attr Staubsauger espPort /dev/ttyACM0
set Staubsauger flashESP "Mein WLAN" "lange Passphrase"
save
Das define ohne Adresse ist der Schlüssel: flashESP braucht ein Gerät,
aber die Adresse der Brücke gibt es zu diesem Zeitpunkt noch nicht. Das Gerät
steht dann im Zustand unconfigured, verbindet sich nicht und fragt nichts
ab. flashESP holt das Image, das die CI dieses Projekts baut, schreibt es,
legt die Zugangsdaten auf dem Board ab und fragt es am Ende über denselben
USB-Port, welche Adresse es im Netz bekommen hat. Die landet im Reading
bridgeAddress, und ein ohne Adresse definiertes Gerät trägt sie sich selbst
ein und verbindet sich. save hält das fest. Ein Gerät, das bereits eine
Adresse hat, behält sie – es wird nur im Log vermerkt, unter welcher Adresse
die neue Brücke erreichbar ist.
Enthalten Name oder Passwort Leerzeichen, gehören sie in Anführungszeichen.
Ein Semikolon muss als ;; geschrieben werden, weil FHEM daran Befehle
trennt. Das Flashen läuft in einem eigenen Prozess, FHEM bleibt also bedienbar;
das Ergebnis steht im Reading lastFlash.
Das fertige Image liegt in firmware/prebuilt/ und wird
von der CI aus dem Quelltext gebaut; die Textdatei daneben nennt Version,
Commit und SHA-256. Der FHEM-Updatemechanismus holt das Image nicht –
flashESP lädt es bei Bedarf selbst. Ein eigenes Image geht vor:
set Staubsauger flashESP /pfad/zum.bin, oder dauerhaft über das Attribut
espImage.
wifiESP ist für den Fall, dass sich das WLAN später ändert: es übergibt die
Zugangsdaten über den USB-Port an die Konfigurationskonsole der Firmware und
meldet die neue Adresse in bridgeAddress. Landet das Board stattdessen in
seinem Access Point, steht in lastFlash, warum (Passwort abgelehnt, Netz
nicht gefunden, vom Router abgewiesen).
Die eingebaute Brücke aktualisieren. Im Roboter wird die Brücke von dessen 3,3-V-Schiene versorgt und hängt nicht mehr am USB-Port des Servers. Ab dann führt der Weg über Funk:
set Staubsauger otaESP
otaESP holt die veröffentlichte Anwendung und schiebt sie über das
ArduinoOTA-Protokoll auf die Brücke. Die Adresse nimmt der Befehl vom Gerät;
eine abweichende lässt sich angeben: set Staubsauger otaESP 192.168.1.150.
Eine Ausnahme, und sie ist wichtig: Firmware vor 0.4.0 hielt SSID und
Passwort als einkompilierte Konstanten im Image. Ein Update ersetzt dieses
Image – und damit die einzige Kopie der Zugangsdaten. Die Brücke kommt dann
ohne Netz hoch und öffnet den Access Point neato-setup; eingetragen werden
sie unter http://192.168.4.1/ vom Handy aus. Kein Kabel nötig, aber jemand
muss neben dem Roboter stehen. otaESP liest die laufende Version und
verweigert ein solches Update, bis man set <dev> otaESP force sagt. Ab
0.4.0 liegen die Zugangsdaten im Flash und überleben jedes Update.
Wer nicht vom FHEM-Rechner aus flasht: ein Board ohne gespeicherte
Zugangsdaten öffnet den Access Point neato-setup mit einer Eingabeseite, und
dieselben Kommandos nimmt die Firmware auch über ein Terminal am USB-Port
entgegen (help listet sie). Flashen von Hand mit der Arduino-IDE beschreibt
docs/flashing-esp32c3.md. Warum sich das WLAN der
Brücke so verhält, wie es sich verhält, steht in
docs/hintergrund.md.
Der Roboter führt kein eigenes Protokoll und gibt seine Wohnungskarte über die
Konsole nicht heraus. Was sich rekonstruieren lässt, ist die gefahrene
Spur: während eines Laufs fragt das Modul alle trackInterval Sekunden
GetRobotPos ab und schreibt jede Position als eine JSON-Zeile weg.
OpenNeato löst es genauso – dort entsteht die „Cleaning History" ebenfalls
durch Mitschreiben, nicht durch Abholen.
Die Aufzeichnung startet und endet mit dem Lauf, auch wenn er am Roboter selbst
oder per Zeitplan begonnen wurde. Eine Sitzung liegt als
<trackDir>/<Gerät>-<Zeitstempel>.jsonl:
{"device":"Staubsauger","started":"2026-09-19_15-04-05","module":"0.16.0","unit":"m"}
{"t":1281.84,"x":0.000,"y":0.000,"th":0.0}
{"t":1284.91,"x":0.412,"y":0.003,"th":1.2}
{"summary":{"points":812,"distance":41.2,"rotation":3600,"seconds":2431}}Koordinaten in Metern, th in Grad. Die Zusammenfassung landet zusätzlich in
den Readings trackPoints, trackDistance und trackDuration, der Dateiname
in trackFile.
Standardmäßig ist das aus. Es entsteht eine Datei je Lauf, und die nützt niemandem, der sie nirgends darstellt:
attr Staubsauger trackRuns 1
Standardverzeichnis ist ./www/neato, also /opt/fhem/www/neato. Fertige
Sitzungen werden nach trackKeepDays Tagen entfernt, Standard 14; 0 behält
alles. Gelöscht wird dabei eng umgrenzt: nur innerhalb von trackDir, nur
Namen im Muster <Gerät>-<Zeitstempel>.jsonl, nur älter als die Grenze – und
nie die Sitzung, die gerade geschrieben wird. Eigene Dateien und Sitzungen
eines anderen Geräts bleiben unangetastet.
trackPose bestimmt, aus welcher Position die Spur entsteht: Smooth
(Standard, die vom Roboter korrigierte Position) oder Raw – die Radencoder
allein, die über einen Lauf driften. Gemessen macht das den Unterschied
zwischen 0,156 und 0,268 belegten Zellen je Punkt; Raw ist nur zum
Vergleichen da.
Lidar-Scans. Der Lidar dreht während eines Laufs von allein – gemessen
an einem D6 mit rund fünf Umdrehungen je Sekunde und echten Distanzen. Im
Leerlauf steht er, und einschalten ließe er sich nur mit einem
TestMode-Kommando, in dem der Roboter nicht reinigt. mapInterval (Standard
0 = aus) holt alle n Sekunden einen GetLDSScan und legt ihn neben die
Spur:
{"scan":{"x":1.204,"y":0.418,"th":92.0,"speed":5.02,"pts":[[0,1284],[1,1266]]}}Winkel in Grad, Distanz in Millimetern, jeweils relativ zur Pose derselben
Zeile. Zeilen mit Fehlercode fallen weg, und ebenso alles jenseits von
mapMaxRange (Standard 6000 mm): ein echter Scan lieferte Zeilen ohne
Fehlercode mit rund 16,8 m, dort wo nichts zurückgestrahlt hat, während der
Lidar eines Botvac etwa fünf Meter reicht.
Wie die Punkte an ihren Platz im Raum gerechnet werden (die Formel ist
gemessen, nicht angenommen) und wie daraus ein Grundriss wird, steht in
docs/ftui3-map.md. FHEM selbst zeichnet nichts; die
Kartenkomponente für FTUI3 ist
fhem-ftui-components-neatomaps.
Für einen schnellen Blick ohne FHEM zeichnet tools/render_track.py eine
Sitzung als SVG:
python3 tools/render_track.py /opt/fhem/www/neato/Staubsauger-2026-09-20_11-59-11.jsonlEin einzelner Lauf ist die Karte dieses Laufs, nicht der Wohnung: eigener Nullpunkt, eigene Nordrichtung, und nur die Räume, in die der Roboter an dem Tag kam. Mehrere übereinandergelegt können zweierlei, was keiner allein kann – fehlende Wände ergänzen und über jede Zelle abstimmen lassen. Was drei von vier Läufen Wand nennen, ist eine Wand; was einer Wand nennt, während die anderen an dieselbe Stelle sahen und Boden fanden, war der Wäscheständer.
attr Staubsauger planAuto 1
set Staubsauger buildPlan
Das Ergebnis ist eine JSON-Datei neben den Aufzeichnungen,
plan-<Gerät>-<Epoche>.json, und das Reading planFile nennt sie. Jeder
Grundriss bekommt einen neuen Namen, damit eine schon offene Seite ihn
bemerkt; abgelöste werden entfernt, und eine Kopie unter dem festen Namen
plan-<Gerät>.json bleibt für Markup, das die Datei direkt nennt, statt
planFile zu binden. Die FTUI-Komponente
<ftui-neato-map view="plan"> lädt sie und zeichnet sie; Dateiformat und
Verfahren stehen in docs/plan-format.md des
Komponenten-Repos.
| Attribut | Vorgabe | Bedeutung |
|---|---|---|
planAuto |
0 | nach jeder Reinigung neu rechnen |
planSources |
8 | wie viele Aufzeichnungen eingehen, die neuesten zuerst |
planMinScans |
50 | kleinere Aufzeichnungen (abgebrochene Läufe) werden übergangen, die nächstältere rückt nach |
planCell |
0.10 | Zellgröße in Metern; steht in der Datei, die Anzeige übernimmt sie |
Es dauert. Jeder Lauf wird gegen den Rahmen gedreht und geschoben, bis er
passt, und zwar über alle Drehungen – gemessen rund eine Minute je
Aufzeichnung auf einem gewöhnlichen Rechner, auf einem kleinen Board
entsprechend länger. Das läuft in einem eigenen, heruntergestuften Prozess;
FHEM selbst hält nichts an. Wer es kürzer braucht, nimmt weniger
planSources.
Läufe, die nicht passen, werden abgelehnt statt hineingezwungen – eine andere Etage in denselben Rahmen zu pressen zieht Wände quer durch Räume.
planState ok
ok, 1 did not fit (0.33)
ok, 2 did not fit (0.41, 0.33)
failed: <Grund>
Die Zahl in Klammern ist die erreichte Güte. Dieses Reading lohnt sich
mitzuloggen: die Schwelle, unter der ein Lauf verworfen wird (0,45), ist eine
Annahme und keine Messung, und die Belege widersprechen sich noch – drei echte
Läufe derselben Wohnung kamen auf 0,64 bis 1,0, ein Teillauf gegen einen vollen
aber auf 0,33. Über ein paar Wochen ist planState die Reihe, die das
entscheidet. Dieselben Zahlen stehen im Feld scores der Plandatei.
Von Hand, ohne FHEM, geht dasselbe mit
perl tools/neato_plan.pl /opt/fhem/www/neato StaubsaugerIrrläufer. Vereinzelt liegt ein Punkt hinter einer Wand. Naheliegend ist
der Verdacht, dass der Roboter an Engstellen kippt, deshalb trägt seit 0.22.0
jeder Scan auch die Neigung ("tilt":[Pitch, Roll, |a|], von Hand mit
get <dev> accel). Über einen vollen Lauf gemessen erklärt die Neigung die
Irrläufer nicht (Korrelation +0,05); es sind einzelne schwache
Rückläufer auf große Entfernung, und das Belegungsgitter fängt sie schon ab.
Einen Neigungsfilter gibt es deshalb bewusst nicht. tools/stray_points.py
rechnet das auf jeder Sitzung nach, die ganze Messung steht in
docs/ftui3-map.md.
startCleaning explore und startCleaning persistent beschreibt die Hilfe des
Roboters selbst als Entsprechung zum Start aus der Smart App – bei house und
spot tut sie das nicht. An einem D6 ohne Cloud beobachtet: das Kommando wird
angenommen, der Roboter setzt Alarm 236 UI_ALERT_ACQUIRING_PERSISTENT_MAP_IDS
und fährt nicht los.
Schlimmer noch: Danach nimmt der Roboter weitere Reinigungsbefehle an, ohne
etwas zu tun. Auch startCleaning house läuft dann ins Leere, bis der Roboter
aus- und wieder eingeschaltet wird. Ein Explore-Versuch kostet also nicht
nur sich selbst, sondern den nächsten Lauf.
Beide Modi werden deshalb abgelehnt, solange nicht force angehängt wird:
set Staubsauger startCleaning explore force
Die Konsole kennt keinen Befehl, um persistente Karten-IDs anzulegen,
zuzuweisen oder abzufragen. Die Vermutung liegt daher nahe, dass diese IDs von
der Cloud kamen – belegt ist das nicht, nur das beobachtete Verhalten. house
und spot laufen normal.
Die Help-Ausgabe des Roboters ist nicht vollständig. Pause, Fortsetzen und
Rückkehr zur Basis laufen über SetEvent, die authentifizierte
Event-Schnittstelle, über die früher die Cloud den Roboter gesteuert hat und
die in keiner Kommandoliste auftaucht. Ihr Schlüssel wird aus der MAC-Adresse
berechnet, die GetVersion mitliefert.
| set-Kommando | Weg |
|---|---|
startCleaning, stop |
SetEvent, sonst Clean House / Clean Stop |
pause / resume |
SetEvent, ohne Schlüssel SetButton start als Umschalter |
sendToBase |
nur SetEvent – die dokumentierten Kommandos bieten dafür nichts |
findMe |
PlaySound SoundID 20 |
Das Reading commandApi zeigt, ob die Schnittstelle freigeschaltet ist
(setEvent oder legacy). attr <dev> useSetEvent 0 erzwingt die
dokumentierten Kommandos.
Gefunden und entschlüsselt hat die Event-Schnittstelle, GetState und die
übrigen undokumentierten Kommandos das Projekt
OpenNeato (MIT, © 2026 Soner Köksal).
Dieses Modul enthält eine eigenständige Perl-Umsetzung, die gegen deren
C++-Original auf bekannten Werten geprüft ist. Alle Einzelheiten stehen in
docs/serial-commands.md.
- No-Go-Linien und Zonenreinigung. Der D6 konnte sie in der App, aber sie
kamen über seine Cloud –
GetVersionnennt sie im Klartext,nucleo.neatocloud.com–, und die ist abgeschaltet. Keines der 44 Kommandos in der Hilfe der Konsole hat mit Karten zu tun. Was bekannt ist, was nur unbelegt ist und welche Wege offen bleiben, steht in docs/serial-commands.md. Die einzige Lösung, die heute funktioniert, ist Neatos Magnetband. - Firmware-Updates des Roboters. Gab es nur über die Cloud.
- Die Karte des Roboters auslesen. Die Konsole bietet dafür kein Kommando.
tools/dump_robot.py fragt den Roboter nach seiner Kommandoliste, holt zu jedem
genannten Kommando den Hilfetext und dazu die Ausgaben der harmlosen Get*-
Kommandos. Das Ergebnis dokumentiert, was die jeweilige Firmware versteht.
python3 tools/dump_robot.py --device /dev/ttyACM0
python3 tools/dump_robot.py --tcp 192.168.1.42:23
python3 tools/dump_robot.py --device /dev/ttyACM0 --diagnose
Das Skript liest nur: es sendet ausschließlich Help und Get*. Kein
TestMode, keine Motorkommandos, keine Einstellungsänderungen. Seriennummern
werden maskiert, damit sich ein Dump gefahrlos weitergeben lässt
(--no-redact schaltet das ab). Es braucht nur ein normales Python 3;
pyserial ist nicht nötig.
Wer ein anderes Modell als den D6 hat: ein Dump davon ist der wertvollste Beitrag. Alles, was das Modul über die Konsole weiß, stammt von einem einzigen Roboter.
--diagnose prüft Gerät, Rechte und belegende Prozesse, meldet die USB-Kennung,
hört fünf Sekunden passiv mit und probiert alle drei Zeilenenden durch, jeweils
mit Hexdump. Die häufigsten Ursachen für eine stumme Konsole sind, in dieser
Reihenfolge: ein schlafender Roboter, der falsche Port, ein belegter Port und
ein Ladekabel ohne Datenleitungen.
tools/neato_sim.py emuliert die Konsole eines Botvac über TCP, mit
Kommando-Echo, Ctrl-Z-Terminator, den CSV-Ausgaben des echten Geräts und
plausiblem Verhalten: der Akku entlädt sich beim Saugen und lädt in der Basis.
python3 tools/neato_sim.py
define Staubsauger NeatoLocal 127.0.0.1:8888
Mit --usb verweigert der Simulator die Reinigung mit Fehler 220 wie ein
Roboter mit angestecktem USB-Host.
perl tools/check_module.pl # Modul: Laden, Transporte, Parser, Zustandslogik
perl tools/check_ota.pl # Update über Funk, gegen einen Stellvertreter der Brücke
python3 tools/check_sim.py # Simulator: Protokoll und Zustandsübergänge
python3 tools/check_dump.py # Dump-Werkzeug, seriell über ein PTY und über TCP
python3 tools/check_track.py # Sitzungsformat und Koordinatenkonvention
python3 tools/check_partition.py # Partitionstabelle des Images
perl tools/check_plan.pl # Grundriss gegen den Referenzfall (dauert eine Minute)
perl tools/check_plan.pl --quick # davon nur die schnellen Prüfungen
check_plan.pl baut den Referenzfall aus docs/reference-plan/ neu und hält
das Ergebnis gegen das, was die JavaScript-Referenz aus denselben drei
Aufzeichnungen macht. Verlangt wird keine Gleichheit bis auf die Zelle – die
Schranken stehen in docs/plan-format.md des Komponenten-Repos.
Alle laufen ohne FHEM-Installation und ohne Roboter. Die Testdaten sind wörtliche Konsolenausgaben eines BotVac D6. Die CI führt sie bei jedem Push aus und übersetzt zusätzlich die Brücken-Firmware für den ESP32-C3.
FHEM bleibt kurz stehen. FHEM baut TCP-Verbindungen synchron auf; solange
ein Verbindungsversuch läuft, steht der ganze Prozess. Die Brücke wird vom
Roboter versorgt und ist weg, sobald er aus ist. Das Modul begrenzt den Versuch
auf 2 Sekunden, connectTimeout senkt das weiter. apptime in der
FHEM-Kommandozeile weist es nach: erscheint dort NeatoLocal_Ready oder
DevIo_OpenDev mit langer Laufzeit, ist es der Verbindungsaufbau.
Der Roboter antwortet nicht. state steht auf robotSilent: Roboter
wecken, Verdrahtung der Brücke prüfen (http://neato.local/ zeigt die
Byte-Zähler in beide Richtungen), oder --diagnose am USB-Port laufen lassen.
Die Brücke ist weg. state steht auf unreachable: die Verbindung kommt
nicht zustande. Stromversorgung und WLAN prüfen, nicht die Verdrahtung zum
Roboter.
Ein Kommando bleibt wirkungslos. get <dev> help Clean zeigt, was die
jeweilige Firmware versteht; über die cmd*-Attribute lässt sich jedes
Kommando anpassen.
Warum einiges so gebaut ist, wie es gebaut ist – WLAN der Brücke, Flashen, Updates –, steht getrennt in docs/hintergrund.md. Die Konsolenkommandos stehen in docs/serial-commands.md, die Karte in docs/ftui3-map.md und die Hardware in docs/hardware.md.
GPLv2, wie FHEM selbst – der vollständige Text liegt in LICENSE.
Die SKey-Berechnung für die SetEvent-Kommandos ist eine eigenständige
Neuimplementierung dessen, was OpenNeato
(MIT, © 2026 Soner Köksal) reverse engineered hat, gegen dessen C++-Original an
bekannten Werten geprüft.