iRobot Roomba i7+ Configuration using Rest980

I believe there are issues with the J7+ and dorita980 (which this uses). Try setting this ENV Variable and see if that works?

What errors are you seeing? If you navigate to http://<ip or fqdn of docker host>:<port>/api/local/info/state what do you get?

Hi Tigon, welcome to the community! can you check in devtools for the sensor.rest980 sensor - do you see the softwareVer section?

Have you configured all the maintenance checks? this uses a filter to find all the sensors based on the same, make sure you dont have any missing/error ones which would cause this calculation to fail

This FAQ Item provides an explanation of how the map creation works. it looks like the coordinates is not updating from your roomba - is sensor.vacuum location attribute populating?

Hello Mirek,

I have similar, maybe the same problem. I have no position in vacuum.log, sensor.vacuumlocation shows n-a and sensor.vacuum did not show me location. And this cause, that I do not see location in map.
It started maybe week ago.

Navigating to http://(ip or fqdn of docker host):(port)/api/local/info/state times out. Please correct me if I’m wrong but (ip or fqdn of docker host) should be the roomba’s IP Address and the (port) is from rest980 Docker Image?

I’ve also tried following the addresses for $robot_log and $ha_rest980 from the image.php. It either times out or shows 401:Unauthorized.

The php-nginx Docker Image log shows the following warning:

[WARN tini (7)] Tini is not running as PID 1 and isn’t registered as a child subreaper.
Zombie processes will not be re-parented to Tini, so zombie reaping won’t work.
To fix the problem, use the -s option or set the environment variable TINI_SUBREAPER to register Tini as a child subreaper, or run Tini as PID 1.
writing nginx config

And the rest980 Docker Image log only shows:

[email protected] start /usr/src/app
node ./bin/www

Hello, can somebody tell me how to setup that card. I have irobot integration and I install that lovelace card. I add that code to card:

type: custom:roomba-vacuum-card
entity: vacuum.jimmy_joe
sensor: sensor.jimmy_joe_battery_level
binary_sensor: binary_sensor.jimmy_joe_bin_full
image: /local/community/lovelace-roomba-vacuum-card/vacuum.png
name: Jimmy Joe
clean_base: false
buttons:
  blank: true
  stop: false
labels:
  status: Status
  battery: Bateria
  mode: vacuum
  area_cleaned: Pomieszczenie
  job_time: Czas pracy
  total_jobs: Wszystkie prace
  evac_events: Envases

My entities of my Roomba 980 are:

  1. entity: vacuum.jimmy_joe
  2. sensor.jimmy_joe_battery_level
  3. binary_sensor.jimmy_joe_bin_full
    I’m not programmer and I don’t know how to make code that will work like in Your pictures.
    Every state on my roomba card I have unavailable.
    Regards.

Ok, it looks like there might be something different in rest980/dorita980/robot firmware so location data isnt passed anymore. Refer this issue I will keep an eye on it and comment when there is an update.

I am currently moving house, so my roomba is out of action for a while

It needs to be the IP Address/Port of the rest980 container, by default this should be your HA IP:3000 or different depending on how your running it. the rest980 container talks to your roomba using MQTT.

I havent seen this → can you please raise an issue on GitHub ?

this is all its expected to show, until you access the API as mentioned above.

the lovelace card isnt supported using the native home assistant roomba vacuum component.

Thank you for explaining the IP Address/Port question. I have updated the image.php and I am now able to get some data when navigating to http://(HA-IP Address):(Rest980 Port)/api/local/info/state. I’m only including part of it since it’s a lot of text but let me know if I need to add all of it.

{“batPct”:96,“batteryType”:“F12432832R”,“batInfo”:{“mDate”:“2020-4-20”,“mName”:“PanasonicEnergy”,“mDaySerial”:35347,“mData”:“303030333034303200000000000000000000000000”,“mLife”:“0C4B0B5910820A744EB0000C04D2F4C600CDFE7F2B11FFFF066D0C8E00000000”,“cCount”:35,“afCount”:0}

Now checking the logs for rest980 Docker Image I get the following:

[email protected] start /usr/src/app
node ./bin/www
GET /api/local/info/image.php 404 33.456 ms - 136
Error: Endpoint not found.
at /usr/src/app/app.js:63:13
at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/usr/src/app/node_modules/express/lib/router/index.js:317:13)
at /usr/src/app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/usr/src/app/node_modules/express/lib/router/index.js:335:12)
at next (/usr/src/app/node_modules/express/lib/router/index.js:275:10)
at /usr/src/app/node_modules/express/lib/router/index.js:635:15
at next (/usr/src/app/node_modules/express/lib/router/index.js:260:14)
at Function.handle (/usr/src/app/node_modules/express/lib/router/index.js:174:3)
at router (/usr/src/app/node_modules/express/lib/router/index.js:47:12)
at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/usr/src/app/node_modules/express/lib/router/index.js:317:13)
at /usr/src/app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/usr/src/app/node_modules/express/lib/router/index.js:335:12)
at next (/usr/src/app/node_modules/express/lib/router/index.js:275:10)
at /usr/src/app/node_modules/express/lib/router/index.js:635:15
at next (/usr/src/app/node_modules/express/lib/router/index.js:260:14)
at jsonParser (/usr/src/app/node_modules/body-parser/lib/types/json.js:110:7)
at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/usr/src/app/node_modules/express/lib/router/index.js:317:13)
at /usr/src/app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/usr/src/app/node_modules/express/lib/router/index.js:335:12)

This error occurs 2 times and is then followed by:

GET /api/local/info/state 200 114.190 ms - 4498
GET /favicon.ico 200 13.630 ms - 1150
GET /api/local/info/state 200 103.679 ms - 4499
GET /favicon.ico 304 5.057 ms - -

But I am still unable to see sensor.rest980.

The php-nginx Docker Image log is now showing more information but the warning is still there. I can raise an issue on GitHub for the php-nginx Docker Image warning that I get. Would you like for me to include the full content of the log that’s available?

SOLVED

Hi Syrius,

I have implemented second mop. I changed my lovelace.yaml from vaccum.

      - entities:
          - input_boolean.mop_clean_stairs
          - input_boolean.mop_clean_hall
          - input_boolean.mop_clean_living_room
          - input_boolean.mop_clean_bathroom
          - input_boolean.mop_clean_kitchen
          - input_boolean.mop_clean_dining_room
          - input_boolean.mop_clean_wc
          - entity: automation.mop_clean_rooms
            lock:
              enabled: |
                [[[
                  if ((states['group.mop_rooms'].state == "on") && (states['sensor.mop'].state == "Ready"))
                    return false;
                  return true;
                ]]]
              exemptions: []
            name: Start cleaning
            styles:
              card:
                - height: 50px
            tap_action:
              action: call-service
              service: automation.trigger
              service_data:
                entity_id: automation.mop_clean_rooms
            type: custom:button-card
        head:
          label: Room selection
          type: section
        type: custom:fold-entity-row

and when I try to start cleaning selective room, the mop says: Help you will find in app.
I do not what is wrong. Will you hel me?

Solution: I do not use multifloor so I forgot change automation
in mop.yaml.

1 Like

ok so in your configuration, when referencing the image.php file - you need to use the port of php-nginx (by default is 3001), not rest980’s port (default of 3000)

so you dont have a sensor.rest980 or sensor.vacuum in HA?

just the brief info will be fine - i believe there is a new version of the container out - so this will probably fix it.

Hi. Probably stupid question but I am new to HA. Can I run this on HA installed directly on a RPi4? I don’t use docker. If I try adding as a custom repository i HACS, it says that Repository structure for 0.11 is not compliant
image

Hi. I tried installing the Roombapw docker image on my RPi4. I have Roomba i7. But the process failed. I pushed the HOME button until it beeped and the circle around CLEAN button started flashing blue. Then I started the addon and checked the log with this info.

> [email protected] getpassword /usr/src/app
> node ./bin/getpassword.js
Make sure your robot is on the Home Base and powered on (green lights on). Then press and hold the HOME button on your robot until it plays a series of tones (about 2 seconds). Release the button and your robot will flash WIFI light.
This step will continue in 10 seconds...
events.js:160
      throw er; // Unhandled 'error' event
      ^
Error: connect ECONNREFUSED 192.168.89.159:8883
    at Object.exports._errnoException (util.js:1020:11)
    at exports._exceptionWithHostPort (util.js:1043:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1099:14)
npm ERR! Linux 5.10.17-v8
npm ERR! argv "/usr/local/bin/node" "/usr/local/bin/npm" "run" "getpassword"
npm ERR! node v6.17.1
npm ERR! npm  v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! [email protected] getpassword: `node ./bin/getpassword.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the [email protected] getpassword script 'node ./bin/getpassword.js'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the dorita980 package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     node ./bin/getpassword.js
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs dorita980
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls dorita980
npm ERR! There is likely additional logging output above.
npm ERR! Please include the following file with any support request:
npm ERR!     /usr/src/app/npm-debug.log

Any advice, please?

Hi again. Sorry for spamming this thread, but I apparently don’t understand how is this supposed to work. I managed to get the password and configured the rest980 Docker Image. But since then I’ve lost connection via the built-in Roomba integration. So now I don’t see my Roomba at all in HA because the original integration is disconnected and this new integration does not display Roomba at all nowhere.

I copied the provided lovelace.yaml file and got this

I assume I should see the sensor.vacuum in entities or devices, or not? Because I don’t see it there.

Installation worked like a charm. Since a few days the map seems not to work anymore and I get the following errors on the web server. Any immediate thoughts what might have changed?

Notice : Undefined offset: -1 in /config/vacuum/image.php on line 57

Warning : Cannot modify header information - headers already sent by (output started at /config/vacuum/image.php:57) in /config/vacuum/image.php on line 241
�PNG IHDRxL#��8 pHYs���+e IDATx���{��u}���9� ��p��\j@�"�/�B)�b� ���v�:��A0":ժ�u��V�aa%"��\�r�d���qɞ���’y�f�${��<�7’���{~�;�n��-F��j5z�v�wa�g/@(� ��J�%��xB ��^�P/@(� ��J��?{���N�7wVUUu:�Z�rm���e��o�V�X��v����8���9�����ii}򫿪���͜V���oUU}��{j�]��}g^�����t�^���#v����ǝቮ�jU����u�w��&����_UW,XR��ɽUU5����’P��u����mw��7��z�a~���’��s���.YY~~�ޟ����eN>��y�N�n]���nI]�?wT�����}D��s�_�w�yj��Wo��e��Ǝ?�"e����O�#ٹڭ�Ew-�K�sG��������󏩋��˺~�q���7M^K7���[�?o��u�;����0>�^�_���e��-B ��^�P/@(� ��J�%��xB��z⤉�:����g�T;L�X�v�>|����X��3N=�V�^W����{��h��\&Nhץ�SUU�N�V�^W7��������G��9��2�>t�����֝�{�I�/0>�x_}¾u�����O���~�X���Pw�vE��kr���k��ӟ��Њ���]���_zޢᰃv�˯Y����A����V��7�_�v���8�V����?���s�.uł%���ΰc��[�q�A�~鬽���[��y�����Z���_�O�׻������+�����S'O��~����]]�^[UUϛ9�N<���������v�~�z�)aցӧֺu���}������ t�_wB�.8�Ⱥ�Uu����VTU�Ӷ���^QU���Z���}dV���?�c��[s����G�_��n�{�F�WU͛;k���:o��о)�WU�0k�z��{�.;N�v�U.��􁟎�}�<=����y����U>��:�Н��т%5{���т%������9�k�����nU���_�����_��^��+�s�����ߘS_2���n��߮�w^x}���:x�퇽��jU�������͜V���\߿�w#:��S�Vxxm���k��E֫O�w��Ϟ3�^���|��^�w�x�S�o��N�O��Ko��@�z^����G�_?~cu������c�ڭ>�E���n�ܩ�'���۳���M����PUU�Z��(e�b��:h��:d������E_�eMyʄa�ytm��:oA����۞YUU�������v��K�e��UUU7ܺ�;h����’�����]��ʭu�VU��kGtm�y=�����u��Մ�v����ؤs��{JUU-������q}�;��w��m���}�]o8���v�e?q�{� u������V=���7~�c\u����n�[’�Gt�����Sj�m��E�xf�u୪Z��[�_����u��ϯ�l0Jv֡�uC����_��{��������Ϝ����Ջ������ej�ݟRo�}P���.YU�}��u�Kf�S'n�|M������wif�gcxw�e�z���U?[����s��5���j�i�ֶ���߫��{V�@�[/>z���]�q�A�ˎ�jB{��_�f�V�YWGu�W8�W�{�Λ;���:�n�X��~~˲�·ne��^��f�9��}�̪���������Њ������N;q�z�q{�w���.�fq��ëݮ�t������}�z͉��)/�^ߛO��W���?u���_��^���kڎ�j�@��fy]2�ܪ��C�:�����oo�YG�s-�y�F�eK{�QUUu�a���=��ׯy�S_zK����%��Y�]}o}�w�e����2�y�Ny��mR/|����G�^���M�e5����m���M����?��F�?&[4��}��k_�_����ߪC�߾^�}j��K� ���h �}��m'�������� ��ek��?�����{� �w+������-jz�l�J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� T��’�w�#����j�o�7=Ƹ’�@�:��Ǵm�6����lz� ��ה�G�͞3��6�?�tF�#���)�n��Ǵm�p ���:����*r�/8�ڭV�cЀN�[���+�ƅ���n���x���-B ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB�7=��7wV�#l���%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB�7=��=g~�#l�ysg5=��X�J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ������=g~�#����^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@���a/�͝���/:�����7=Ƹf/@(� ��J�%��xB ��^�P/@(� T��H�ۭ�7wV�c��/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J����l���[�VC���I�� ����AB/� ��134�>�j�Z����#�0&�F��>��w�>�az#�0f�n��Y�x������s@�^Fm�j�N�S500������WUU�v{�1�^�Ć������;wa�h�v��.�������_7=[��v>���=�J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@���ax"�V:����ꪦG�D���ozh̼����fa�����u�˚� v�M��������?���G]}��T�MVӣ�[V��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� �����ز��?���հ� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�����5oG�/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@���a�r�o�eo]��O�w��������Q�� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��xB ��^�P/@(� ��J�%��;w��d�a�&)� �����1dD��1X4�F� ����� B����u7y��D �Q/@��%�D �Q/@��%�D �Q/@��u6{s���m�������� �׸��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� �$q/ /IDATJ��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x��f8dY����f���<��4{�QxC��mߍ{��3~i�ߏݗ��gp��,�zs;{�I;����j;^��8{aw.�̞���z3{�A���+�����x�y>{�I;���>����̞@�w�����q��������%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D �Q/@��%�D���g������x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^�(� J��x�^���b��CKL�IEND�B`�
Notice: Undefined variable: roomba_stuck in /config/vacuum/image.php on line 249

Warning: imagedestroy() expects parameter 1 to be resource, null given in /config/vacuum/image.php on line 249

Warning: imagedestroy(): supplied resource is not a valid Image resource in /config/vacuum/image.php on line 250

I’ve made a little progress. I copied files to vacuum folder, edited as few as possible to avoid any issues but getting this error when trying to restart HA.

Logger: homeassistant.components.websocket_api.http.connection
Source: components/hassio/__init__.py:598
Integration: Home Assistant WebSocket API (documentation, issues)
First occurred: 14:58:48 (2 occurrences)
Last logged: 14:58:55

[547922853744] The system cannot restart because the configuration is not valid: expected a dictionary for dictionary value @ data['packages']['type']
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 185, in handle_call_service
    await hass.services.async_call(
  File "/usr/src/homeassistant/homeassistant/core.py", line 1495, in async_call
    task.result()
  File "/usr/src/homeassistant/homeassistant/core.py", line 1530, in _execute_service
    await handler.job.target(service_call)
  File "/usr/src/homeassistant/homeassistant/components/hassio/__init__.py", line 598, in async_handle_core_service
    raise HomeAssistantError(
homeassistant.exceptions.HomeAssistantError: The system cannot restart because the configuration is not valid: expected a dictionary for dictionary value @ data['packages']['type']

Files in the vacuum folder
image

vacuum.yaml (just removed the rooms that I don’t have)

###################################
# iRobot Vacuum Package
###################################

###################################
# Sensor
###################################

sensor:
  # Roomba via Rest980 Docker Image
  - platform: rest
    name: rest980
    json_attributes:
      - batPct
      - bin
      - cleanMissionStatus
      - dock
      - pose
      - signal
      - bbmssn
      - bbrun
      - name
      - pmaps
      - vacHigh
      - openOnly
      - twoPass
      - noAutoPasses
      - softwareVer
    resource: !secret vacuum_state
    value_template: 'OK'
    scan_interval: 10
  - platform: template
    sensors:
      vacuum:
        friendly_name_template: >-
          {{ state_attr('sensor.rest980', 'name') }}
        value_template: >-
          {% if state_attr('sensor.rest980', 'cleanMissionStatus')['cycle'] == 'none' and state_attr('sensor.rest980', 'cleanMissionStatus')['notReady'] == 39 %}
            Pending
          {% elif state_attr('sensor.rest980', 'cleanMissionStatus')['notReady'] > 0 %}
            Not Ready
          {% else %}
          {% set mapper =  {
            'clean' : 'Clean',
            'quick' : 'Clean',
            'spot' : 'Spot',
            'evac' : 'Empty',
            'dock' : 'Dock',
            'train' : 'Train',
            'none' : 'Ready' } %}
          {% set state =  state_attr('sensor.rest980', 'cleanMissionStatus')['cycle'] %}
          {{ mapper[state] if state in mapper else state }}
          {% endif %}
        icon_template: mdi:robot-vacuum
        attribute_templates:
          notready_msg: >-
            {% set mapper =  {
              0 : 'n-a',
              2 : 'Uneven Ground',
              15 : 'Low Battery',
              39 : 'Pending',
              48 : 'Path Blocked' } %}
            {% set state =  state_attr('sensor.rest980', 'cleanMissionStatus')['notReady'] %}
            {{ mapper[state] if state in mapper else state }}
          error_msg: >-
            {% set mapper =  {
              0 : 'n-a',
              15 : 'Reboot Required',
              18 : 'Docking Issue'} %}
            {% set state =  state_attr('sensor.rest980', 'cleanMissionStatus')['error'] %}
            {{ mapper[state] if state in mapper else state }}
          battery: >-
            {{ state_attr('sensor.rest980', 'batPct') }} %
          software_ver: >-
            {% if state_attr('sensor.rest980', 'softwareVer') is defined %}
            {% set version = state_attr('sensor.rest980', 'softwareVer') %}
              {{ version.split('+')[1] }} 
            {% else %}
              n-a
            {% endif %}
          phase: >-
            {% if state_attr('sensor.rest980', 'cleanMissionStatus')['phase'] == 'charge' and state_attr('sensor.rest980', 'batPct') == 100 %}
              Idle
            {% elif state_attr('sensor.rest980', 'cleanMissionStatus')['cycle'] == 'none' and state_attr('sensor.rest980', 'cleanMissionStatus')['phase'] == 'stop' %}
              Stopped
            {% else %}
            {% set mapper =  {
              'charge' : 'Charge',
              'run' : 'Run',
              'evac' : 'Empty',
              'stop' : 'Paused',
              'stuck' : 'Stuck',
              'hmUsrDock' : 'Sent Home',
              'hmMidMsn' : 'Mid Dock',
              'hmPostMsn' : 'Final Dock' } %}
            {% set state =  state_attr('sensor.rest980', 'cleanMissionStatus')['phase'] %}
            {{ mapper[state] if state in mapper else state }}
            {% endif %}
          bin: >-
            {% set mapper =  {
              true : 'Full',
              false : 'Not Full' } %}
            {% set state =  state_attr('sensor.rest980', 'bin')['full'] %}
            {{ mapper[state] if state in mapper else state }}
          bin_present: >-
            {% set mapper =  {
              true : 'Yes',
              false : 'No' } %}
            {% set state =  state_attr('sensor.rest980', 'bin')['present'] %}
            {{ mapper[state] if state in mapper else state }}
          clean_base: >-
            {% if state_attr('sensor.rest980', 'dock')['state'] is defined %}
              {% set mapper =  {
                300 : 'Ready',
                301 : 'Ready',
                302 : 'Empty',
                303 : 'Empty',
                350 : 'Bag Missing',
                351 : 'Clogged',
                352 : 'Sealing Problem',
                353 : 'Bag Full',
                360 : 'Comms Problem' } %}
              {% set state =  state_attr('sensor.rest980', 'dock')['state'] %}
              {{ mapper[state] if state in mapper else state }}
            {% else %}
              n-a
            {% endif %}
          location: >-
            {% if state_attr('sensor.rest980', 'pose')['theta'] is defined %}
              ({{ state_attr('sensor.rest980', 'pose')['point']['x'] }}, {{ state_attr('sensor.rest980', 'pose')['point']['y'] }}, {{ state_attr('sensor.rest980', 'pose')['theta'] }})
            {% else %}
              n-a
            {% endif %}
          rssi: >-
            {% if state_attr('sensor.rest980', 'signal')['rssi'] is defined %}
              {{ state_attr('sensor.rest980', 'signal')['rssi'] }}
            {% else %}
              n-a
            {% endif %}
          total_area: >-
            {% if state_attr('sensor.rest980', 'bbrun')['sqft'] is defined %}
              {{ (state_attr('sensor.rest980', 'bbrun')['sqft'] / 10.764 * 100)| round() }}m²
            {% else %}
              n-a
            {% endif %}
          #   {{ (state_attr('sensor.rest980', 'bbrun')['sqft'] }}ft²
          total_time: >-
            {% if state_attr('sensor.rest980', 'bbrun')['hr'] is defined %}
              {{ state_attr('sensor.rest980', 'bbrun')['hr'] }}h {{ state_attr('sensor.rest980', 'bbrun')['min'] }}m
            {% else %}
              n-a
            {% endif %}
          total_jobs: >-
            {% if state_attr('sensor.rest980', 'bbmssn')['nMssn'] is defined %}
              {{ state_attr('sensor.rest980', 'bbmssn')['nMssn'] }}
            {% else %}
              n-a
            {% endif %}
          dirt_events: >-
            {% if state_attr('sensor.rest980', 'bbrun')['nScrubs'] is defined %}
              {{ state_attr('sensor.rest980', 'bbrun')['nScrubs'] }}
            {% else %}
              n-a
            {% endif %}
          # evac_events I7+/S9+ Models (Clean Base)
          evac_events: >-
            {% if state_attr('sensor.rest980', 'bbrun')['nEvacs'] is defined %}
              {{ state_attr('sensor.rest980', 'bbrun')['nEvacs'] }}  
            {% else %}
              n-a
            {% endif %}
          job_initiator: >-
            {% set mapper =  {
              'schedule' : 'Scheduler',
              'rmtApp' : 'App',
              'manual' : 'Robot',
              'localApp' : 'HA' } %}
            {% set state =  state_attr('sensor.rest980', 'cleanMissionStatus')['initiator'] %}
            {{ mapper[state] if state in mapper else state }}
          job_time: >-
            {% if state_attr('sensor.rest980', 'cleanMissionStatus')['mssnStrtTm'] is defined %}
              {% if state_attr('sensor.rest980', 'cleanMissionStatus')['mssnStrtTm'] != 0 %}
                {% set time = state_attr('sensor.rest980', 'cleanMissionStatus')['mssnStrtTm'] | timestamp_local %}
                {% set elapsed = ((as_timestamp(now()) - as_timestamp(time)) / 60) | round(0) %}
                {% if elapsed > 60 %}
                  {{ elapsed // 60 }}h {{ '{:0>2d}'.format(elapsed%60) }}m
                {% else %}
                  {{elapsed}}m
                {% endif %}
              {% else %}
                n-a
              {% endif %}
            {% else %}
              n-a
            {% endif %}
          job_recharge: >-
            {% if state_attr('sensor.rest980', 'cleanMissionStatus')['rechrgTm'] is defined %}
              {% if state_attr('sensor.rest980', 'cleanMissionStatus')['rechrgTm'] != 0 %}
                {% set time = state_attr('sensor.rest980', 'cleanMissionStatus')['rechrgTm'] | timestamp_local %}
                {% set resume = ((as_timestamp(time) - as_timestamp(now())) / 60) | round(0) %}
                {% if resume > 60 %}
                  {{ resume // 60 }}h {{ '{:0>2d}'.format(resume%60) }}m
                {% else %}
                  {{resume}}m
                {% endif %}
              {% else %}
                n-a
              {% endif %}
            {% else %}
              n-a
            {% endif %}
          job_expires: >-
            {% if state_attr('sensor.rest980', 'cleanMissionStatus')['expireTm'] is defined %}
              {% if state_attr('sensor.rest980', 'cleanMissionStatus')['expireTm'] != 0 %}
                {% set time = state_attr('sensor.rest980', 'cleanMissionStatus')['expireTm'] | timestamp_local %}
                {% set resume = ((as_timestamp(time) - as_timestamp(now())) / 60) | round(0) %}
                {% if resume > 60 %}
                  {{ resume // 60 }}h {{ '{:0>2d}'.format(resume%60) }}m
                {% else %}
                  {{resume}}m
                {% endif %}
              {% else %}
                n-a
              {% endif %}
            {% else %}
              n-a
            {% endif %}
          clean_mode: >-
            {% if state_attr('sensor.rest980', 'noAutoPasses') is defined and state_attr('sensor.rest980', 'twoPass') is defined %}
              {% if state_attr('sensor.rest980', 'noAutoPasses') == true and state_attr('sensor.rest980', 'twoPass') == false %}
                One
              {% elif state_attr('sensor.rest980', 'noAutoPasses') == true and state_attr('sensor.rest980', 'twoPass') == true %}
                Two
              {% else %}
                Auto
              {% endif %} 
            {% else %}
              n-a
            {% endif %}
          carpet_boost: >-
            {% if state_attr('sensor.rest980', 'vacHigh') is defined %}
              {% if state_attr('sensor.rest980', 'vacHigh') == false and state_attr('sensor.rest980', 'carpetBoost') == false %}
                Eco
              {% elif state_attr('sensor.rest980', 'vacHigh') == true and state_attr('sensor.rest980', 'carpetBoost') == false %}
                Performance
              {% else %}
                Auto
              {% endif %}
            {% else %}
              n-a
            {% endif %}
          clean_edges: >-
            {% if state_attr('sensor.rest980', 'openOnly') is defined %}
              {% if state_attr('sensor.rest980', 'openOnly') == true %}
                False
              {% else %}
                True
              {% endif %}
            {% else %}
              n-a
            {% endif %}
          maint_due: >-
            {% if is_state('input_boolean.vacuum_maint_due', 'on') %}
              True
            {% else %}
              False
            {% endif %}
          # pmap0_id I7/S9 Models
          pmap0_id: >-
            {% if state_attr('sensor.rest980', 'pmaps')[0] is defined %}
              {{ state_attr('sensor.rest980', 'pmaps')[0] | regex_findall_index("{'([\w\-]+)': '\w+'}") }}
            {% else %}
              n-a
            {% endif %}
      vacuum_location:
        friendly_name_template: >-
          {{ state_attr('sensor.rest980', 'name') }} Location
        value_template: >-
          {{ state_attr('sensor.vacuum', 'location') }}
        icon_template: mdi:home-map-marker

###################################
# Rest Command
###################################

rest_command:
  vacuum_action:
    url: >-
      {{ states('input_text.vacuum_action') }}{{ command }}
    verify_ssl: !secret vacuum_verify_ssl
    method: 'get'
    timeout: 20
  vacuum_clean:
    url: >-
      {{ states('input_text.vacuum_action') }}cleanRoom
    verify_ssl: !secret vacuum_verify_ssl
    method: POST
    content_type: 'application/json'
    payload: '{{ payload }}'

###################################
# Input Boolean
###################################

input_boolean:
  vacuum_clean_kitchen:
    name: Kitchen
    icon: mdi:silverware-fork-knife
  vacuum_clean_entry:
    name: Entry
    icon: mdi:coat-rack
  vacuum_clean_hall:
    name: Hall
    icon: mdi:ceiling-light
  vacuum_clean_living_room:
    name: Living Room
    icon: mdi:sofa
  vacuum_clean_bathroom:
    name: Bathroom
    icon: mdi:shower
  vacuum_clean_bedroom:
    name: Bedroom
    icon: mdi:bed-empty
  vacuum_clean_master_bedroom:
    name: Master Bedroom
    icon: mdi:bed-empty
  vacuum_schedule_1:
    name: Vacuum Schedule 1
    icon: mdi:timetable
  vacuum_schedule_2:
    name: Vacuum Schedule 2
    icon: mdi:timetable

###################################
# Input Text
###################################

input_text:
  vacuum_action:
    name: Vacuum Action URL
    initial: !secret vacuum_action
  vacuum_map:
    name: Vacuum Map URL
    initial: !secret vacuum_map
  vacuum_log:
    name: Vacuum Log Path
    initial: !secret vacuum_log
  vacuum_dir:
    name: Vacuum Dir Path
    initial: !secret vacuum_dir
  vacuum_rooms:
    name: Vacuum Rooms
    max: 255

  vacuum_clean_kitchen:
    name: Kitchen
    initial: !secret vacuum_kitchen
  vacuum_clean_entry:
    name: Entry
    initial: !secret vacuum_entry
  vacuum_clean_hall:
    name: Hall
    initial: !secret vacuum_hall
  vacuum_clean_living_room:
    name: Living Room
    initial: !secret vacuum_living_room
  vacuum_clean_bathroom:
    name: Bathroom
    initial: !secret vacuum_bathroom
  vacuum_clean_bedroom:
    name: Bedroom
    initial: !secret vacuum_bedroom
  vacuum_clean_master_bedroom:
    name: Master Bedroom
    initial: !secret vacuum_master_bedroom

###################################
# Group
###################################

group:
  vacuum_rooms:
    entities:
      - input_boolean.vacuum_clean_kitchen
      - input_boolean.vacuum_clean_entry
      - input_boolean.vacuum_clean_hall
      - input_boolean.vacuum_clean_living_room
      - input_boolean.vacuum_clean_bathroom
      - input_boolean.vacuum_clean_bedroom
      - input_boolean.vacuum_clean_master_bedroom

###################################
# Automation
###################################

automation:
  # Initiate Selective Room Clean
  - alias: Vacuum Clean Rooms
    trigger:
    - platform: event
      event_type: initiate_vacuum_clean
    condition:
      condition: not
      conditions:
        - condition: state
          entity_id: input_text.vacuum_rooms
          state: ''
    action:
      - service: rest_command.vacuum_clean
        data_template:
          payload: >
            {% set rooms = states('input_text.vacuum_rooms') %}
            {% if rooms[-1:] == ',' %}
            {% set rooms = rooms[:-1] %}
            {% endif %}
            {% set rooms = rooms.split(",") %}
            {
              "ordered": 1,
              "pmap_id": "{{ state_attr('sensor.vacuum', 'pmap0_id') | string }}",
              "regions": [{% for id in rooms %}
                {% set room = 'input_text.vacuum_clean_' + id %} {{ states(room) | string }} {%- if not loop.last %},{%- endif %}
                {%- endfor %}
              ]
            }
      - service: input_text.set_value
        data:
          entity_id: input_text.vacuum_rooms
          value: ''
      - service: input_boolean.turn_off
        data:
          entity_id: group.vacuum_rooms

  # Update Vacuum REST Sensor for Location Details
  - alias: Vacuum Update Location
    initial_state: true
    trigger:
      - platform: time_pattern
        seconds: /2
      - platform: event
        event_type: call_service
        event_data:
          domain: rest_command
          service: vacuum_clean
    condition: 
      condition: or
      conditions:
        - condition: template
          value_template: "{{ is_state_attr('sensor.vacuum', 'phase', 'Run') }}"
        - condition: template
          value_template: "{{ is_state_attr('sensor.vacuum', 'phase', 'Sent Home') }}"
        - condition: template
          value_template: "{{ is_state_attr('sensor.vacuum', 'phase', 'Mid Dock') }}"
        - condition: template
          value_template: "{{ is_state_attr('sensor.vacuum', 'phase', 'Final Dock') }}"
    action:
      - service: homeassistant.update_entity
        entity_id: sensor.rest980
  
  # Log Vacuum Location to File
  - alias: Vacuum Log Position
    initial_state: true
    trigger:
        platform: state
        entity_id: sensor.vacuum_location
    condition: 
      condition: or
      conditions:
        - condition: state
          entity_id: sensor.vacuum
          state: 'Clean'
        - condition: state
          entity_id: sensor.vacuum
          state: 'Train'
    action:
      - service: notify.vacuumfile
        data_template: 
          message: "{{ states('sensor.vacuum_location') }}"
               
  # Initialize Blank Vacuum Log File
  - alias: Vacuum Clean Log
    initial_state: true
    trigger:
      - platform: state
        entity_id: sensor.vacuum
        from: 'Ready'
        to: 'Clean'
      - platform: state
        entity_id: sensor.vacuum
        from: 'Ready'
        to: 'Train'
    action:
      - service: shell_command.vacuum_clear_log
      - service: shell_command.vacuum_clear_image
          
  # Update Vacuum Log File with Finished Status
  - alias: Vacuum Notify on Finished Cleaning
    initial_state: true
    trigger:
      - platform: state
        entity_id: sensor.vacuum
        from: 'Clean'
        to: 'Ready'
      - platform: state
        entity_id: sensor.vacuum
        from: 'Train'
        to: 'Ready'
      - platform: state
        entity_id: sensor.vacuum
        from: 'Pending'
        to: 'Ready'
    action:
      - service: notify.vacuumfile
        data_template: 
          message: "Finished"

  # Update Vacuum Log File with Stuck Status
  - alias: Vacuum Notify on Stuck Status
    initial_state: true
    trigger:
      platform: template
      value_template: "{{ is_state_attr('sensor.vacuum', 'phase', 'Stuck') }}"
    action:
      - service: notify.vacuumfile
        data_template: 
          message: "Stuck"
      # DELETE BELOW SECTION IF YOU DONT WANT NOTIFICATIONS
      - delay: 5
      - service: !secret vacuum_notify
        data_template:
          title: "{{ state_attr('sensor.rest980', 'name') }} requires your attention"
          message: "{{ state_attr('sensor.rest980', 'name') }} is stuck."
          # =============
          # NOTICE THIS SECTION IS VALID FOR IOS USERS ONLY!
          data:
            attachment:
              content-type: jpeg
            push:
              category: camera
            entity_id: camera.roomba
          # =============

  # Generate Complete Vacuum Map
  - alias: Vacuum Generate Image after Cleaning
    initial_state: true
    trigger:
      - platform: state
        entity_id: sensor.vacuum
        from: 'Clean'
        to: 'Ready'
        for:
          seconds: 10
      - platform: state
        entity_id: sensor.vacuum
        from: 'Train'
        to: 'Ready'
        for:
          seconds: 10
      - platform: state
        entity_id: sensor.vacuum
        from: 'Pending'
        to: 'Ready'
        for:
          seconds: 10
    action:
      - service: shell_command.vacuum_generate_image
      # DELETE BELOW SECTION IF YOU DONT WANT NOTIFICATIONS
      - delay: 5
      - service: !secret vacuum_notify
        data_template:
          title: "{{ state_attr('sensor.rest980', 'name') }}"
          message: "{{ state_attr('sensor.rest980', 'name') }} successfully completed a job!"
          # =============
          # NOTICE THIS SECTION IS VALID FOR IOS USERS ONLY!
          data:
            attachment:
              content-type: jpeg
            push:
              category: camera
            entity_id: camera.roomba
          # =============

  # Maintenance Check
  - alias: Vacuum Maintenance Check
    initial_state: true
    trigger:
      - platform: time_pattern
        minutes: /15
      - platform: state
        entity_id: [
          'sensor.vacuum_maint_clean_brushes',
          'sensor.vacuum_maint_clean_contacts',
          'sensor.vacuum_maint_clean_filter',
          'sensor.vacuum_maint_clean_wheel',
          'sensor.vacuum_maint_clean_bin',
          'sensor.vacuum_maint_replace_brushes',
          'sensor.vacuum_maint_replace_filter',
          'sensor.vacuum_maint_replace_wheel'
        ]
    action:
      - service: >-
          {% set ns = namespace(count = 0) %}
          {% for item in states.sensor if 'sensor.vacuum_maint' in item.entity_id and (state_attr(item.entity_id, 'timeout_timestamp') < as_timestamp(now())) %}
            {% set ns.count = loop.index %}
          {% endfor %}
          {% if ns.count > 0 %}
            input_boolean.turn_on
          {% else %}
            input_boolean.turn_off
          {% endif %}
        entity_id: input_boolean.vacuum_maint_due

  # Add Rooms for Ordered Cleaning
  - alias: Vacuum Add Rooms for Cleaning
    initial_state: true
    trigger:
      platform: state
      entity_id: [
        'input_boolean.vacuum_clean_kitchen',
        'input_boolean.vacuum_clean_entry',
        'input_boolean.vacuum_clean_hall',
        'input_boolean.vacuum_clean_living_room',
        'input_boolean.vacuum_clean_bathroom',
        'input_boolean.vacuum_clean_bedroom',
        'input_boolean.vacuum_clean_master_bedroom'
      ]
      to: 'on'
    action:
      service: input_text.set_value
      data_template:
        entity_id: input_text.vacuum_rooms
        value: |
          {% set room = trigger.entity_id %}
          {% set room = room.replace("input_boolean.vacuum_clean_","") %}
          {% if ((states('input_text.vacuum_rooms') == "unknown") or (states('input_text.vacuum_rooms') == "")) %}
            {{ room }},
          {% else %}
            {{ states('input_text.vacuum_rooms') }}{{ room }},
          {% endif %}

  # Remove Rooms for Ordered Cleaning
  - alias: Vacuum Remove Rooms for Cleaning
    initial_state: true
    trigger:
      platform: state
      entity_id: [
        'input_boolean.vacuum_clean_kitchen',
        'input_boolean.vacuum_clean_entry',
        'input_boolean.vacuum_clean_hall',
        'input_boolean.vacuum_clean_living_room',
        'input_boolean.vacuum_clean_bathroom',
        'input_boolean.vacuum_clean_bedroom',
        'input_boolean.vacuum_clean_master_bedroom'
      ]
      to: 'off'
    action:
      service: input_text.set_value
      data_template:
        entity_id: input_text.vacuum_rooms
        value: |
          {% set room = trigger.entity_id %}
          {% set room = room.replace("input_boolean.vacuum_clean_","") %}
          {% set result = states('input_text.vacuum_rooms') %}
          {% set result = result.replace(room + ",","") %}
          {{ result }}

  # Vacuum Cleaning Schedule 1
  - alias: Vacuum Cleaning Schedule 1
    initial_state: true
    trigger:
      platform: time_pattern
      hours: "6"
    condition: 
      condition: and
      conditions:
        - condition: state
          entity_id: input_boolean.vacuum_schedule_1
          state: 'on'
        - condition: state
          entity_id: input_boolean.vacation
          state: 'off'
    action:
      - service: input_text.set_value
        entity_id: input_text.vacuum_rooms
        data_template: 
          entity_id: input_text.vacuum_rooms
          value: "kitchen"
      - service: automation.trigger
        entity_id: automation.vacuum_clean_rooms

  # Vacuum Cleaning Schedule 2
  - alias: Vacuum Cleaning Schedule 2
    initial_state: true
    trigger:
      platform: time_pattern
      hours: "10"
    condition: 
      condition: and
      conditions:
        - condition: state
          entity_id: input_boolean.vacuum_schedule_2
          state: 'on'
        - condition: state
          entity_id: input_boolean.vacation
          state: 'off'
        - condition: time
          weekday:
            - mon
            - wed
            - fri
    action:
      - service: input_text.set_value
        entity_id: input_text.vacuum_rooms
        data_template: 
          entity_id: input_text.vacuum_rooms
          value: "kitchen,entry,hall,living_room,bathroom,bedroom,master_bedroom"
      - service: automation.trigger
        entity_id: automation.vacuum_clean_rooms

  # Delete Old Vacuum Image Files
  - alias: Vacuum Delete Images
    initial_state: true
    trigger:
      platform: time_pattern
      hours: "00"
    action:
      - service: shell_command.vacuum_delete_images

###################################
# Notify
###################################

notify:
    - name: VacuumFile
      platform: file
      filename: !secret vacuum_log

###################################
# Camera
###################################

camera:
  - platform: generic
    still_image_url: >-
      {{ states('input_text.vacuum_map') }}
    content_type: image/png
    name: Roomba

###################################
# Shell Command
###################################

shell_command:
  vacuum_clear_log: cp /dev/null {{ states('input_text.vacuum_log') }}
  vacuum_clear_image: curl -X GET -s -O /dev/null '{{ states("input_text.vacuum_map") }}?clear=true'
  vacuum_generate_image: curl -X GET -s -O /dev/null '{{ states("input_text.vacuum_map") }}?last=true'
  vacuum_delete_images: find {{ states('input_text.vacuum_dir') }} -regex ".*[0-9]\.png" -type f -mtime +20 -exec rm -f {} \;

image.php

<?php
//error_reporting(E_ALL);
//ini_set('display_errors', 1);

// ADJUST THESE PARAMETERS
$robot_log = 'http://192.168.89.241:3001/vacuum.log'; # Could also be HTTPS, or mop.log
$file_append = ''; # Allows differentiation of files for different floors or robots
$robot_type = 'roomba'; # Select between roomba and braava for different icons
$set_first_coordinate = 3; # Ability to skip initial coordinate(s) if incorrect data logged
$overlay_image = 'floor.png'; # Background Layer
$overlay_walls = false; # Allows overlaying of walls, used in fill mode to cover 'spray'
$walls_image = 'walls.png'; # Walls Image must contain transparent floor
$show_stuck_positions = true; 
$line_thickness = 2; # Default 2, Set to ~60 for Fill Mode
$map_width = 1050; # Ensure overlay and wall images match this size
$map_height = 900; # Ensure overlay and wall images match this size
$x_offset = 220;
$y_offset = 220;
$flip_vertical = false;
$flip_horizontal = false;
$render_status_text = true;
$rotate_angle = 0; # Allows rotating of the robot lines
$x_scale=1.00; # Allows scaling of roomba x lines
$y_scale=1.00; # Allows scaling of roomba y lines
$ha_rest980 = 'https://my.publicdomainname.cz:443/api/states/sensor.rest980'; # sensor.rest980_2, if configured for Mop
$ha_token = 'myTokenIsCopiedHere';
$ha_timezone = 'Europe/Prague'; # Supported Timezones https://www.php.net/manual/en/timezones.php
$ha_text_delimiter = " \n"; # How text is displayed on the map top " \n" --> New Line ## " |" --> Show on one line
//
// Line Color - RGB
// -1 represents gradual increase from 0 to 255 based on number of logged locations
//
$color_red = -1;
$color_green = 255;
$color_blue = -1;
//
// Examples
// red = -1 , green = 255 , blue = -1  ---> Green to White Fade
// red = 0 , green = -1 , blue = 255   ---> Blue to Aqua Fade
// red = 0 , green = 0 , blue = 255    ---> Solid Blue
//
$path_opacity = 0.5; # Opacity of Roomba path --> 0.0 = completely transparent, 1.0 = completely opaque
//
///////////////////////////////////////////////////////////////////

if(isset($_GET['clear'])) {
  @unlink("latest".$file_append.".png");
  die();  
}
if(is_file("latest".$file_append.".png")&&!isset($_GET['last'])) {
  header("Content-Type: image/png");
  echo file_get_contents("latest".$file_append.".png");
  die();
}

$coords = file_get_contents($robot_log."?v=".time());
$coords = str_replace("(", "", $coords);
$coords = str_replace(")", "", $coords);
$coords = explode("\n", $coords);

if (count($coords) < 2) {
  echo "No Coordinates found in file, is it reachable and populated? Log file - $robot_log?";
  die();
}

$date = strtotime(substr($coords[0], 42));

$lastline = $coords[sizeof($coords)-2];
$end = ["Stuck", "Finished"]; // PAUSE also available

array_shift($coords);
array_shift($coords);
array_pop($coords);

function imagelinethick($image, $x1, $y1, $x2, $y2, $color, $thick = 1)
{
    if ($thick == 1) {
        return imageline($image, $x1, $y1, $x2, $y2, $color);
    }
    $t = $thick / 2 - 0.5;
    if ($x1 == $x2 || $y1 == $y2) {
        return imagefilledrectangle($image, round(min($x1, $x2) - $t), round(min($y1, $y2) - $t), round(max($x1, $x2) + $t), round(max($y1, $y2) + $t), $color);
    }
    $k = ($y2 - $y1) / ($x2 - $x1);
    $a = $t / sqrt(1 + pow($k, 2));
    $points = array(
        round($x1 - (1+$k)*$a), round($y1 + (1-$k)*$a),
        round($x1 - (1-$k)*$a), round($y1 - (1+$k)*$a),
        round($x2 + (1+$k)*$a), round($y2 - (1-$k)*$a),
        round($x2 + (1-$k)*$a), round($y2 + (1+$k)*$a),
    );
    imagefilledpolygon($image, $points, 4, $color);
    return imagepolygon($image, $points, 4, $color);
}

$image = imagecreatetruecolor($map_width,$map_height);
imagesavealpha($image, true);
$black = imagecolorallocatealpha($image,0,0,0, 127);
imagefill($image,0,0,$black);

$robot = imagecreatefrompng($robot_type.'.png');
imagealphablending($robot, false);
imagesavealpha($robot, true);

foreach($coords as $i => $coord) {
  # Skip initial coordinates if needed
  if ($i < $set_first_coordinate) {
    continue;
  }
  $split = explode(",", $coord);
  if(sizeof($split)<2) {
    if(($coord == "Stuck") & ($show_stuck_positions)) {
      $robot_stuck = imagecreatefrompng($robot_type.'_stuck.png');
      imagealphablending($robot_stuck, false);
      imagesavealpha($robot_stuck, true);
      $robot_stuck = imagerotate($robot_stuck, $oldtheta*-1, imageColorAllocateAlpha($robot_stuck, 0, 0, 0, 127));
      imagealphablending($robot_stuck, false);
      imagesavealpha($robot_stuck, true);
      imagecopy($image, $robot_stuck, $oldx-10, $oldy-5, 0, 0, imagesx($robot_stuck), imagesy($robot_stuck));
      imagedestroy($robot_stuck);
    }
    continue;
  }
  
  $part= hexdec("ff");
  $part = round($part * $i/sizeof($coords));

  // Calculate Line Color
  $red = ($color_red === -1 ? $part : $color_red);
  $green = ($color_green === -1 ? $part : $color_green);
  $blue = ($color_blue === -1 ? $part : $color_blue);
  
  $alpha = (1.0 - $path_opacity) * 127;
  $color = imagecolorallocatealpha($image, $red, $green, $blue, $alpha);
  $tmpx = $split[1]+$x_offset;
  $tmpy = $split[0]+$y_offset;
  $theta = $split[2];
  
  // Rotate Calculations
  $x=($tmpx*cos(deg2rad($rotate_angle))+$tmpy*sin(deg2rad($rotate_angle)))*$x_scale;
  $y=(-1*$tmpx*sin(deg2rad($rotate_angle))+$tmpy*cos(deg2rad($rotate_angle)))*$y_scale;
  
  $boxsize=4;
  $shift_y = 2;
  $shift_x = -2;
  
  imagerectangle($image, $x+$shift_x, $y+$shift_y, $x+$boxsize+$shift_x, $y+$boxsize+$shift_y, $color);
  if(isset($oldx) && isset($oldy)) {
    imagelinethick($image, $oldx+($boxsize/2)+$shift_x, $oldy+($boxsize/2)+$shift_y, $x+($boxsize/2)+$shift_x, $y+($boxsize/2)+$shift_y, $color, $line_thickness);
  }
  
  if($i+1==sizeof($coords)) {
    if (sizeof($split)>2) {
      $robot = imagerotate($robot, $theta*-1, imageColorAllocateAlpha($robot, 0, 0, 0, 127));
      imagealphablending($robot, false);
      imagesavealpha($robot, true);
      imagecopy($image, $robot, $x-10, $y-5, 0, 0, imagesx($robot), imagesy($robot));
    }
  }
  
  $oldx = $x;
  $oldy = $y;
  $oldtheta = $theta;
}

if(in_array($lastline, $end)) {
  imagedestroy($robot);
  
  if($lastline == "Stuck") {
    $overlayImage = imagecreatefrompng($robot_type.'_stuck.png');
    imagealphablending($overlayImage, false);
    imagesavealpha($overlayImage, true);
    $color = imagecolorallocate($image, 0, 149, 223);
    $finishedRoomba = imagerotate($overlayImage, $theta*-1, imageColorAllocateAlpha($overlayImage, 0, 0, 0, 127));
    imagelinethick($image, $oldx+($boxsize/2), $oldy+($boxsize/2), $x+($boxsize/2)+3, $y+($boxsize/2)+10, $color, 2);
    imagecopy($image, $finishedRoomba, $oldx-10, $oldy-5, 0, 0, imagesx($finishedRoomba), imagesy($finishedRoomba));
  }
  else if($lastline == "Finished") {
    $overlayImage = imagecreatefrompng($robot_type.'_charging.png');
    imagealphablending($overlayImage, false);
    imagesavealpha($overlayImage, true);
    $color = imagecolorallocate($image, 0, 149, 223);
    $finishedRoomba = imagerotate($overlayImage, $theta*-1, imageColorAllocateAlpha($overlayImage, 0, 0, 0, 127));
    imagelinethick($image, $oldx+($boxsize/2), $oldy+($boxsize/2), $x+($boxsize/2)+3, $y+($boxsize/2)+10, $color, 2);
    imagecopy($image, $finishedRoomba, $oldx-10, $oldy-5, 0, 0, imagesx($finishedRoomba), imagesy($finishedRoomba));
  }
  
}
if($flip_vertical) {
  imageflip( $image, IMG_FLIP_VERTICAL );
}

if($flip_horizontal) {
  imageflip( $image, IMG_FLIP_HORIZONTAL );
}

// Create Final Image
$dest = imagecreatetruecolor($map_width,$map_height);
imagesavealpha($dest, true);
// Create Background Image
$overlayImage = imagecreatefrompng($overlay_image);
// Merge Background Image
imagecopy($dest, $overlayImage, 0, 0, 0, 0, imagesx($overlayImage), imagesy($overlayImage));
imagedestroy($overlayImage);
// Merge Roomba Lines
imagecopy($dest, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));

if ($overlay_walls) {
  // Create Walls Image
  $overlayWalls = imagecreatefrompng($walls_image);
  // Merge Walls Image
  imagecopy($dest, $overlayWalls, 0, 0, 0, 0, imagesx($overlayWalls), imagesy($overlayWalls));
  imagedestroy($overlayWalls);
}

$string = "";

if($lastline == "Finished") {
  $finished=true;
  $status="Finished";
  
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $ha_rest980);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  $headers = [
      'Authorization: Bearer '.$ha_token,
      'Content-Type: application/json'
  ];
  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  $server_output = curl_exec ($ch);
  curl_close ($ch);
  $data = json_decode($server_output);
  $battery_level = $data->attributes->batPct;
  $string.=$ha_text_delimiter." Battery: ".$battery_level."%";
  
}
else if($lastline == "Stuck"){
  $finished=false;
  $status="Stuck";
}
else {
  $finished=false;
  $status="Running";
}

if ($render_status_text) {
  date_default_timezone_set($ha_timezone);
  $dt = date('H:i:s Y-m-d', $date);
  $txt = " Started: ".$dt.$ha_text_delimiter." Status: ".$status.$string;
  $white = imagecolorallocate($dest, 255, 255, 255);
  $font = "./monaco.ttf";
  imagettftext($dest, 10, 0, 5, 15, $white, $font, $txt);
} 

header("Content-Type: image/png");
imagepng($dest);
if(isset($_GET['last'])) {
  imagepng($dest, "latest".$file_append.".png");
  imagepng($dest, $date.$file_append.".png");
}
imagedestroy($dest);
imagedestroy($robot);
imagedestroy($robot_stuck);
imagedestroy($overlayImage);
exit;

secrets.yaml

###################################
# Secrets
###################################

# Vacuum
vacuum_state: http://192.168.89.241:3000/api/local/info/state
vacuum_action: http://192.168.89.241:3000/api/local/action/
vacuum_verify_ssl: false
vacuum_notify: notify.mobile_app_appIdIsHere # You can also use a notify group here
vacuum_map: http://192.168.89.241:3001/image.php
vacuum_log: /config/vacuum/vacuum.log
vacuum_dir: /config/vacuum
vacuum_kitchen: '{"region_id":"10","type":"rid"}'
vacuum_entry: '{"region_id":"9","type":"rid"}'
vacuum_hall: '{"region_id":"4","type":"rid"}'
vacuum_living_room: '{"region_id":"6","type":"rid"}'
vacuum_bedroom: '{"region_id":"12","type":"rid"}'
vacuum_bathroom: '{"region_id":"8","type":"rid"}'
vacuum_master_bedroom: '{"region_id":"11","type":"rid"}'

And I added this to the main configuration.yaml file

homeassistant:
  packages: !include_dir_merge_named vacuum/

i finally had time to add the 2nd robot, when i did the first one i had allready added the 2nd docker image to the pi’s ha install but just disabled it,

i gave my 2nd bot a static ip and adjusted the config for the 2nd docker image, no problem there

but the 2nd bot does not seem to get an entity like the first one does, first one shows up as sensor.vacuum…

how should the 2nd one show up? do i miss a special config/setup instruction?

edit: looked in github and used the search function here and cant find what extra i need to do to go from a single robot setup to 2 robot setup… ie what to copy from where to where and what to change

EDIT 2: i was comparing vacum.yaml and mop.yaml, i see they are mostly the same so was wondering if i should start by creating a new file based on vacum.yaml and replace rest980 with rest980_2 ?

EDIT 3: adding _2 to sensor.rest980 in vacuum.yaml was easy enough, but what about sensor.vacuum? and input_text.vacuum_action?

of course this a lux problem, having 2 robots are rare. in my case they will be on each floor and i will give them each their floor plan just to make it simple

so far, my secrets.yaml: #################################### Secrets################################ - Pastebin.com
and an attempt at a 2nd vacuum.yaml called vacuum2.yaml: #################################### iRobot Vacuum Package################## - Pastebin.com

another attempt at a seperate vacuum2.yaml: #################################### iRobot Vacuum2 Package################# - Pastebin.com

EDIT4: http://192.168.0.9:3002/api/local/info/state and http://192.168.0.9:3000/api/local/info/state do report different bot names so i assume most is correct, its just the yaml and php files

i’m lost on what to do here, sum up of what i have:

php-nginx Docker Image set to /config/vacuum2 and port 3001
php-nginx_2 Docker Image set to /config/vacuum and port 3003
rest980 Docker Image set to port 3000
rest980_2 Docker Image set to port 3002

the _2 are the ones i have my first bot on and works. as said above i can get both names of the bots so i expect the docker images to be ok, BUT for completeness sake how do i check ?

in \packages i have vacuum.yaml and vacuum2.yaml the contents of vacuum.yaml: #################################### iRobot Vacuum Package################## - Pastebin.com and the content of vacuum2.yaml: #################################### iRobot Vacuum2 Package################# - Pastebin.com

i copied the content of \vacuum to \vacuum2 and the only difference here are in image.php where i have changed $file_append and $ha_rest980 =

/vacuum: ---->>>>>> this is my first bot

$file_append = ‘Solveig’; # Allows differentiation of files for different floors or robots
$ha_rest980 = ‘http://192.168.0.9:8123/api/states/sensor.rest980’;

/vacuum2:

$file_append = ‘Robert’; # Allows differentiation of files for different floors or robots
$ha_rest980 = ‘http://192.168.0.9:8123/api/states/sensor.rest980_2’;

my secrets.yaml is still the same: #################################### Secrets################################ - Pastebin.com

but now my skills to figure out what should go where are up, i did even look at the mop example but got no wiser

hass-addons is a repo, but ha-rest980-roomba you need to manaully install using the steps.

i see you have this sorted.

my implementation of the rest980 sensors is not compatible with the native roomba integration. you need to run one or the other.

you need to follow all the steps outlined in the repo linked in the first post.

there is an issue with the upstream dorita980/rest980 image which is not providing the location data used to generate the map.

the error your seeing

Notice : Undefined offset: -1 in /config/vacuum/image.php on line 57

specifically is calling out that there is no position points in the log file

instead create a folder called packages in your config folder, move the vacuum.yaml file into this packages folder and replace with this

homeassistant:
  packages: !include_dir_named packages/ 

if you are getting the right info from the info/state api url - then your rest980 images should be good

im my exmaples, i didnt create a second folder, just use different image.php file names for the different robots/floors

do you have sensor.vacuum and sensor.vacuum2 in dev tools ?

1 Like

:rotating_light: :rotating_light: :rotating_light:

Please note - location data is currently broken in iRobot FW 3.20.x

This is being tracking in the dorita980 repo.

nope…

will try and see if fix’s things

what about /packages with vacuum.yaml ? do you just create a copy of the file like i attempted?

and just so i get it right, the 2nd image.php file which i just named image2.php shall i just change settings of these lines ?

$vacuum_log
$file_append
$ha_rest980

last one is a copy of the first one and i just added _2 to it so its now:

$ha_rest980 = ‘http://192.168.0.9:8123/api/states/sensor.rest980_2’;

the other 2 are:

$vacuum_log = ‘http://192.168.0.9:3001/vacuum.log’; # Could also be HTTPS
$file_append = ‘Robert’; # Allows differentiation of files for different floors or robots

but i guess the log file name needs to change?