Hi @ia74 . I have tried following the steps and have been able to partly add the integration. What I mean is that, the integration is added but with a Failed setup where is says “Error Communicating with API”. I have got the same result for both Roomba S9+ and Roomba Combo 10 Max (I have 2 Roombas and 1 Braava M6). Would appreciate any guidance on what I can do from here as I am stuck. Thanks
Roomba i7+ (Rest980) with Selective Room Cleaning - Integrate your iRobot Roomba with Home Assistant
Hello everyone, I integrated my Roomba i7+ using rest980 and I can see all the entities. I had two entities called “unnamed room” that I disabled because they didn’t belong to any of the rooms I have.
I tried selecting One Pass on a couple of rooms and starting the vacuum, but after exactly 6 minutes—when in the app it shows “discovering”—the robot returns to the base and I get a localization error. Am I doing something wrong?
UPDATE:
Maybe I found the problem, but I don’t know how to solve it:
I have two maps because I have two houses.
In the attributes of vacuum.robotname, pmap0_id is equal to the map ID of the other house, and I have no way to force it to use the correct one.
Any ideas?
Hey there! Sorry, this is unfortunately a known issue. I’m working on a fix that won’t have too much collateral damage, as it has to do with unique ID.
Hey! You know, this isn’t a scenario I had in mind, but this is definitely an easier fix from my end [than the multiple roomba bug]. Sorryabout that! It happens because starting a job defaults on Roomba’s default pmap0 ID for every room.
Hi, I’m happy that it can be easily resolved.
Looking forward to the update ![]()
Hey all, sorry for the wait!
v1.18.0 is out now and fixes the issue with multiple maps selecting the wrong room for the wrong map ID. It’s a simple fix; for now, you can only execute cleaning rooms in one map per run (as of right now). I’ll look into multiple selections across maps, but I’m not sure how that’ll look.
As for the multiple Roomba situation, the reason why that’s taking so long has to do with the way roomba_rest980 handles unique IDs. The way it works now is: HA selects a unique ID, which should be per Roomba but is instead per server, by MD5 hashing your local server URL. This will be fixed with a config flow version upgrade (automatic migration, this time it will be based on BLID.)
Hello.
Sorry for dumb questions, but I just want to make sure I got it right.
You mentioned you’re updating dorita/rest980.
Is it this one
GitHub - ia74/roomba_rest980: Integrate Home Assistant with your iRobot Roomba using rest980. ?
I am using the old one, so I need to double check.
The Docker image is koalazak/rest980:latest, right?
Would all of this work with cloud features disabled?
I’m asking because in GitHub - ia74/roomba_rest980: Integrate Home Assistant with your iRobot Roomba using rest980. you say
“You must check “Enable cloud features?” for the cloud API to be used.”
And after irobot filing for bancrupcy and being overtaken by some Chinese company, I don’t feel ok with using cloud anymore.
Would I miss selective room clearing and other features? I do know room ids and I’m fine using those.
Thank you very much for all the work.
No dumb questions here!
- Yes, correct, the docker image is by koalazak.
- It does work without cloud features; they’re all supplemental. You wouldn’t miss selective room cleaning, as you can re-implement it with the start API/event (there will be a guide to do so in the future, but you can also read the start implementation).
- Without Cloud, pressing Start on HA’s native entity just starts a clean everywhere/default routine.
Thanks for giving my integration a try!
Thank you.
How substantial was the recent change to rest980 image? I noticed it grew by some 100-200 MB.
How can I read start implementation? Sorry, I’m rather new to this.
Would something like this work?
curl -X POST http://192.168.60.139:3011/api/local
-H “Content-Type: application/json”
-d ‘{“command”:“start”}’
I tried to follow this:
But it failed utterly. I suppose because he used cloud and he also implemented 2pass vacuuming on his own before you did.
Yes, except you should POST this to /api/local/action/cleanRoom
{
"ordered": 1,
"pmap_id": "ID",
"regions": [
{
"region_id": 0,
"type": "rid",
"params": {"noAutoPasses": False, "twoPass": False}
},
]
}
Where for zones it should be “zid” and i think “zone_id” if i haven’t forgot
Hey all!
HA is adding room-based vacuum support natively in 2026.3.3!
Naturally, my integration will be updated as soon as possible (once HA updates too & docs are provided) to support this feature!
Alongside this, I’ll look into adding support for MatterHub too!
And maybe add some more local and easier to read guides.
However! I am currently busier than usual as of right now, so these features will take a little longer, as I aim for quality.
Thank you all for the support! Feel free to open issues, they’re helpful as I only own an i7.
Do you plan to provide some examples of use?
I use HA only for a while and this is for me quite a challenge to put together.
With the help of AI I hallucinated this roomba.yaml and lovelace card.
# Roomba custom cleaning package
# /config/automations/roomba.yaml
sensor:
- platform: rest
name: roomba_state_raw
resource: "http://192.168.60.139:3011/api/local/info/state"
method: GET
scan_interval: 60
json_attributes:
- pmaps
template:
- sensor:
- name: roomba_user_pmapv_id
state: >
{% set p = state_attr('sensor.roomba_state_raw','pmaps') %}
{% if p %}
{{ p[0]['VabtFPvZSveRlmdU2K1n4Q'] }}
{% else %}
unavailable
{% endif %}
rest_command:
roomba_clean_custom:
url: "http://192.168.60.139:3011/api/local/action/cleanRoom"
method: POST
content_type: "application/json"
payload: "{{ payload }}"
input_select:
vacuum_room_kitchen:
name: Kitchen
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:chef-hat
vacuum_room_diningroom:
name: Dining Room
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:table-chair
vacuum_room_livingroom:
name: Living Room
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:sofa-outline
vacuum_room_hallway:
name: Hallway
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:shoe-print
vacuum_room_bedroom:
name: Bedroom
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:bed-outline
vacuum_room_childsroom:
name: Child Room
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:teddy-bear
vacuum_room_bathroom:
name: Bathroom
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:shower-head
vacuum_room_eating_area:
name: Eating Area
options: ["off", "1x", "2x"]
initial: "off"
icon: mdi:cake-variant-outline
script:
roomba_cycle_room:
mode: parallel
fields:
entity:
description: "input_select room entityi"
sequence:
- variables:
current: "{{ states(entity) }}"
- choose:
- conditions: "{{ current == 'off' }}"
sequence:
- service: input_select.select_option
target: { entity_id: "{{ entity }}" }
data: { option: "1x" }
- conditions: "{{ current == '1x' }}"
sequence:
- service: input_select.select_option
target: { entity_id: "{{ entity }}" }
data: { option: "2x" }
- conditions: "{{ current == '2x' }}"
sequence:
- service: input_select.select_option
target: { entity_id: "{{ entity }}" }
data: { option: "off" }
roomba_start_selected_rooms:
mode: single
sequence:
- variables:
room_map:
vacuum_room_kitchen: { id: 7, type: "rid" }
vacuum_room_diningroom: { id: 1, type: "rid" }
vacuum_room_livingroom: { id: 10, type: "rid" }
vacuum_room_hallway: { id: 8, type: "rid" }
vacuum_room_bedroom: { id: 4, type: "rid" }
vacuum_room_childsroom: { id: 3, type: "rid" }
vacuum_room_bathroom: { id: 9, type: "rid" }
vacuum_room_eating_area: { id: 0, type: "zid" }
regions_list: >
{% set ns = namespace(regions=[]) %}
{% for entity, data in room_map.items() %}
{% set value = states('input_select.' ~ entity) %}
{% if value in ['1x', '2x'] %}
{% set two_pass = (value == '2x') %}
{% if data.type == 'zid' %}
{% set region = {
'zone_id': data.id | int,
'type': data.type,
'params': {'noAutoPasses': true, 'twoPass': two_pass}
} %}
{% else %}
{% set region = {
'region_id': data.id | int,
'type': data.type,
'params': {'noAutoPasses': true, 'twoPass': two_pass}
} %}
{% endif %}
{% set ns.regions = ns.regions + [region] %}
{% endif %}
{% endfor %}
{{ ns.regions }}
ordered_flag: >
{{ 1 if regions_list | length > 1 else 0 }}
clean_payload: >
{{ {
'ordered': ordered_flag,
'pmap_id': 'VabtFPvZSveRlmdU2K1n4Q',
'user_pmapv_id': states('sensor.roomba_user_pmapv_id'),
'regions': regions_list
} | to_json }}
- condition: template
value_template: "{{ regions_list | length > 0 }}"
# --- DEBUG ---
- service: persistent_notification.create
data:
title: "Roomba debug payload"
message: "{{ clean_payload }}"
# ------------------------------------------------------------
- service: rest_command.roomba_clean_custom
data:
payload: "{{ clean_payload }}"
- service: input_select.select_option
target:
entity_id: >
{{ room_map.keys() | map('regex_replace', '^', 'input_select.') | list }}
data:
option: "off"
Lovelace card:
type: vertical-stack
cards:
- type: custom:roomba-vacuum-card
entity: vacuum.robik
battery_entity: sensor.robik_battery
clean_base: false
- type: grid
columns: 2
square: false
cards:
- type: tile
entity: input_select.vacuum_room_kitchen
name: Kitchen
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_kitchen
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_kitchen', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_kitchen', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_diningroom
name: Dining Room
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_diningroom
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_diningroom', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_diningroom', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_eating_area
name: Eating Area
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_eating_area
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_eating_area', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_eating_area', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_livingroom
name: Livingroom
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_livingroom
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_livingroom', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_livingroom', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_hallway
name: Hallway
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_hallway
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_hallway', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_hallway', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_bedroom
name: Bedroom
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_bedroom
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_bedroom', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_bedroom', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_childsroom
name: Child Room
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_childsroom
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_childsroom', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_childsroom', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: tile
entity: input_select.vacuum_room_bathroom
name: Bathroom
tap_action:
action: call-service
service: script.roomba_cycle_room
data:
entity: input_select.vacuum_room_bathroom
card_mod:
style: |
:host {
{% if is_state('input_select.vacuum_room_bathroom', '1x') %}
--tile-color: var(--warning-color);
{% elif is_state('input_select.vacuum_room_bathroom', '2x') %}
--tile-color: var(--error-color);
{% else %}
--tile-color: var(--state-inactive-color);
{% endif %}
}
- type: button
name: Start Vacuuming
icon: mdi:play-circle
tap_action:
action: call-service
service: script.roomba_start_selected_rooms
visibility:
- condition: or
conditions:
- condition: state
entity: input_select.vacuum_room_kitchen
state_not: "off"
- condition: state
entity: input_select.vacuum_room_diningroom
state_not: "off"
- condition: state
entity: input_select.vacuum_room_eating_area
state_not: "off"
- condition: state
entity: input_select.vacuum_room_livingroom
state_not: "off"
- condition: state
entity: input_select.vacuum_room_hallway
state_not: "off"
- condition: state
entity: input_select.vacuum_room_bedroom
state_not: "off"
- condition: state
entity: input_select.vacuum_room_childsroom
state_not: "off"
- condition: state
entity: input_select.vacuum_room_bathroom
state_not: "off"
It looks nice, but doesn’t work at all and I fail to recognize valid code from halucination.
I just want to be able to select rooms as I like and let roomba vacuum. That’s it.
Hi @ia74
Thanks for the awesome work on this, it’s been working great. I’ve migrated from a Hubitat setup whereby I already had 2 instances of the Rest980 running on a remote RPi. So when I setup my Roombas originally I just pointed your integration to those instances and it worked straight away. I’m now at the point where I want to move the Rest980 integration from the RPi to my local HA instance. And for the life of me I cannot find where to change the base URL address in my existing configs. I’d like to keep all my configurations/dashboards, etc intact and just point my existing Roombas to the new base URL. I’m probably missing something stupid but where/how can I do that?
Thanks!
Due to the current way the integration works, you do unfortunately have to re-setup the whole thing. The integration takes the unique ID as the md5 hash of the URL (was meant to be for development), so as far as editing the URL for now, thats how it has to be.
Ok, thanks, at least I know I’m not overlooking something simple.
Hi @ia74, I’ve been reading that iRobot was rolling out firmware that disables all smart features on older gen Roombas and turns them into a dumb robot (push button to clean only). Now whether that report is true is unknown, but just in case I was thinking about disabling internet access for the Roomba so that it can’t receive an OTA update from iRobot. Will that affect any of the features in your integration? When I was using Hubitat I had to create special calls to the Roomba including the map and room ids and it was all local. So, I’m thinking your integration, which works great BTW, won’t be affected but I wanted to get your opinion.
Thanks again for the great work!
Mark
If we can get confirmation of this, that is definitely bad news, and I’ll probably end up quickly releasing a modified version of the jailbreak1 I found to block future firmware updates.
But, for now, as a preventative measure we can block https://content-prod.iot.irobotapi.com/v2/firmware on the router/dns filter/etc, as this is where the queries for updates are from.
If you can’t block the specific path, the domain content-prod.iot.irobotapi.com should be okay, but a little bit of the iRobot Home’s app content is from that domain.
Oh, and disabling all outgoing internet access for the Roomba will make all cloud related stuff fail. If you’re looking to use purely local, as long as it stays on LAN you can set the HA integration config option to be only local and it should work fine.
I believe a few people have tested going from cloud to local. Since it doesn’t totally affect operation, the room-based cleaning still is available since the room IDs shouldn’t change. Let me know how it goes!
1: For a quick update on that, I’m still looking at capabilities of the jailbreak, like talking to the on-robot navigation motherboard with serial. Still trying to figure a way to get back the real-time pose. Fun fact, the internal MQTT server exposes the Timeline Report that my older robot “doesn’t support”!
Thanks for the reply, I only use the Roombas locally through the HA Dashboard or automation so I don’t use the cloud integration. I’ll block outgoing internet and see what happens.
Side question, if/when the jailbreak is released, would it then be possible to downgrade to the last version of firmware the exposed the x,y,theta?
You know, good point. It very well may be easy, since the update process only replaces some part of an overlay-fs.
The Roomba has a clever segmentation of its filesystem to make sure persistent things like maps and bootloader stuff don’t get completely broken after bad FW updates (if they happened). Most of the system is actually unwritable because of this; theoretically my idea is we place our own copied ROM/FW image in its spot, and it replaces most if not all of the bits we need.
Partition map (parted -l):
Mounts (cat /proc/mounts, somehow this has parted but not lsblk?!):
There is a built in OTA manager with a lot of nice strings left in that should more or less help with trying to get it to work in our favor. This should manage OTA updates or atleast download them
More versioning stuff, however all the stuff here is actually given to us in FW update packages (go figure! but hey, it’s less harder than it has to be)
I think I’ve fudged my robot a little, as this actually shouldn’t say
Build Type: development because it’s a production build. (oops?)
In my search for how the update system works, I found the update script. There is no downgrade protection, and you can straight up force an update from other robot types onto yours (provided you have the FW package)!
The hardest part is getting it into the script, which is simply adding the ota file (downloaded firmware package) to the end of the script.
The fuse stuff is only for checking dev/prod robots, except for some reason you just do -f or -F to force it anyway… Unfortunately I don’t have a dev build to test, but we can get retail ones just fine from the servers. I think with a modification to pass all files we can even do unsigned firmware.
We do need to modify the OTA script to make the jailbreak retain it’s persistence, but that’s just adding like 1 or 2 lines to keep SYSTEM_ACCESS=unlocked (no I’m not kidding).
This is actually really much simpler than I thought to be honest.
Side fun fact of the post: The Roomba is part ROS2, part Qualcomm, part Android, part Ubuntu, and part BusyBox. Literally Frankenstein. They do not advertise it’s ROS2 compatibility, infact go as far as to deny it but it’s used internally! It’s all over the strings of cleantrack.
At most costs, don’t update your robot! Not sure if they’re gonna make an update patching at least some of the stuff I’ve shown but you never know.
Thanks for the info, that was an interesting read.






