Neato BotVac Vacuum Cleaner Widget for HABPanel

Hi,

Over the last days i played a bit with the Matrix Theme for HABPanel…

This is the widget i made for Neato BotVac Vacuum Cleaner Connected Series… I may share the code later, need a bit of clean up.. it looks pretty neat and i’m very happy with the result… even the way was very long and included some pip python installs…

  • grab the Last Cleaning Map from Neato via pybotvac
  • take that image, crop/resize and change the background to grafana “fill” color
  • “Update Map” runs script via Exec binding
  • since the “DOCK” funktion only works if the basestation has been seen from the Robot, this button is only visible and clickable if DockHasBeenSeen item is ON

Thanks to @ysc @pmpkk for this very cool UI!

Edit 28/03/2019:

Here is the code…

HABPanel widget code:

<div class="section">
	<div class="sectionIconContainer"><div class="sectionIcon"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#vacuum-cleaner"></use></svg></div></div>
	<div class="bigDash">
		<div class="description">HoLLe's Alfred - Aktuell</div>
		<div class="top">
			<div class="icon on"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#event-list"></use></svg></div>
			<div class="value">
				<div class="main">{{itemValue('AlfredState')}}</div>
			</div>
		</div>
		<div class="bottom">
			<div class="sceneGroup">
				<div class="scene on" ng-click="sendCmd('AlfredCommand', 'clean')">Clean</div>
			</div>
			<div class="sceneGroup">
				<div class="scene on2" ng-click="sendCmd('AlfredCommand', 'pause')">Pause</div>
			</div>
			<div class="sceneGroup">
				<div class="scene on2" ng-click="sendCmd('AlfredCommand', 'resume')">Resume</div>
			</div>
			<div class="sceneGroup">
				<div class="scene on2" ng-click="sendCmd('AlfredCommand', 'stop')">Stop</div>
			</div>
		</div>
		<div class="bottom">
			<div class="sceneGroup">
				<div class="scene on2" ng-class="{true: 'disabled'}[itemValue('AlfredDockHasBeenSeen')!='Ja']" ng-click="sendCmd('AlfredCommand', 'dock')">Andocken</div>
			</div>
			<div class="sceneGroup">
				<div class="scene on" ng-click="sendCmd('Alfred_GetNewMap', 'ON')">Update Map</div>
			</div>
		</div>
		<div class="bottom">
			<div class="error">{{itemValue('AlfredError')}}</div>
		</div>
	</div>

	<div class="bigDash">
	<div class="description">Aktion - Aktuell</div>
		<div class="top">
			<div class="icon on"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#battery-connected"></use></svg></div>
			<div class="value">
				<div class="main">{{itemValue('AlfredBattery')}}</div>
				<div class="sub">%</div>
			</div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#buildings-03"></use></svg></div>
			<div class="value">In Station</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredIsDocked')}}</div></div>
				<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#buildings-29"></use></svg></div>
			<div class="value">Station erkannt</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredDockHasBeenSeen')}}</div></div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#charging-1"></use></svg></div>
			<div class="value">Am Laden</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredIsCharging')}}</div></div>
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#event-date"></use></svg></div>
			<div class="value">Zeitplan</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredIsScheduled')}}</div></div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#buildings-28"></use></svg></div>
			<div class="value">Kategorie</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredCategory')}}</div></div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#buildings-38"></use></svg></div>
			<div class="value">Reinigungs-Modus</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredMode')}}</div></div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#buildings-37"></use></svg></div>
			<div class="value">Reinigungs-Art</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredModifier')}}</div></div>
		</div>
		<div class="bottom">
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#close-arrows"></use></svg></div>
			<div class="value">Spot-Länge</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredSpotHeight')}}</div></div>
			<div class="icon off"><svg viewBox="0 0 48 48"><use xlink:href="/static/matrix-theme/smarthome.svg#close-arrows-4"></use></svg></div>
			<div class="value">Spot-Breite</div>
			<div class="valueGroup"><div class="value">{{itemValue('AlfredSpotWidth')}}</div></div>
		</div>
	</div>

	<div class="bigDash">
		<div class="description">Reinigungs-Karte</div>
		<div class="graph">
			<img width="300" height="300" src="/static/neato-botvac/botvac-last-map.png" />
			<div class="legend">{{itemValue('Alfred_GetNewMap')}} x {{itemValue('Alfred_GetNewMap_exit')}} x {{itemValue('Alfred_GetNewMap_last')}}</div>
		</div>
	</div>
</div>

items / neato.items:

Group GNeato
//String AlfredName                   "Name [%s]"                                         (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:name"}
Number AlfredBattery                "Akku Status [%.0f %%]"                 <battery>   (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:battery-level"}
String AlfredState                  "Status [MAP(neato.map):%s]"                        (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:state"}
String AlfredError                  "Fehler [%s]"                           <error>     (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:error"}
//String AlfredVersion                "Version [%s]"                                      (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:version"}
//String AlfredModel                  "Model [%s]"                                        (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:model-name"}
//String AlfredFirmware               "Firmware [%s]"                                     (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:firmware"}
String AlfredAction                 "Aktuelle Aktion [MAP(neato.map):%s]"               (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:action"}
Switch AlfredDockHasBeenSeen        "Ladestation erkannt [MAP(neato.map):%s]"                   <presence>  (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:dock-has-been-seen"}
Switch AlfredIsDocked               "In Ladestation [MAP(neato.map):%s]"                        <presence>  (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:is-docked"}
Switch AlfredIsScheduled            "Zeitplan [MAP(neato.map):%s]"                              <time>      (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:is-scheduled"}
Switch AlfredIsCharging             "Am Laden [MAP(neato.map):%s]"                              <heating>   (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:is-charging"}
String AlfredCategory               "Reinigung [MAP(neato.map):%s]"                     (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-category"}
String AlfredMode                   "Reinigungs Modus [MAP(neato.map):%s]"              (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-mode"}
String AlfredModifier               "Reinigungs Art [MAP(neato.map):%s]"                (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-modifier"}
Number AlfredSpotWidth              "Spot-Breite [%.0f]"                    <niveau>    (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-spotwidth"}
Number AlfredSpotHeight             "Spot-Höhe [%.0f]"                      <niveau>    (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-spotheight"}
//String AlfredNavigationMode         "Nav Mode"                (GNeato)    {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:cleaning-navigation-mode"}
String AlfredCommand                "Befehl"                                <movecontrol>           {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:command"}

String AlfredMapId "Map ID" {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:map-id"}
String AlfredMapUrl "Map Url" {channel="neato:vacuumcleaner:OPSXXX-YOUR_ROBOT_SERIAL:map-url"}

//Alexa Dummy Switch - see .rules
Switch AlfredAlexa "Alfred" ["Switchable"]

Switch Alfred_GetLastMapTrigger "TEST %s"

// state of the execution, is running or finished
Switch Alfred_GetNewMap {channel="exec:command:neato_new_map:run"}
// Arguments to be placed for '%2$s' in command line
String Alfred_GetNewMap_Args {channel="exec:command:neato_new_map:input"}
// Output of command line execution 
String Alfred_GetNewMap_out {channel="exec:command:neato_new_map:output"}
Number Alfred_GetNewMap_exit {channel="exec:command:neato_new_map:exit"}
DateTime Alfred_GetNewMap_last {channel="exec:command:neato_new_map:lastexecution"}

rules / neato.rules:

rule "Getting last map for neato botvac connected"
	when
	   Item Alfred_GetLastMapTrigger received command ON
	then
		//Alfred_GetLastMapRunning.postUpdate("Download ...")
		executeCommandLine("/usr/bin/python /etc/openhab2/scripts/neato-botvac-getlastmap.py", 5000)
        logInfo("Your command exec", "Result:" + Alfred_GetLastMapTrigger.state )
		//Alfred_GetLastMapRunning.postUpdate("-")
		Alfred_GetLastMapTrigger.postUpdate(OFF)
end

transform / neato.map

ui_alert_busy_charging=beschäftigt mit Aufladen
ui_error_navigation_noprogress=Navigationsproblem
ui_alert_recovering_location=Standort wiederherstellen
ui_error_brush_overload=Bürsten überlastet
ui_error_dust_bin_full=Staubbehälter voll
ui_error_dust_bin_emptied=Staubbehälter entleert
ui_error_brush_stuck=verstopfte Bürsten

// Curent state of the vacuum cleaner
INVALID=Chillen
IDLE=Idle
BUSY=Busy
PAUSED=Pause
ERROR=Error

ON=Ja
OFF=Nein

// Current action of the vacuum cleaner
HOUSE_CLEANING=Haus Reinigung
SPOT_CLEANING=Spot Reinigung
MANUAL_CLEANING=Manuelle Reinigung
DOCKING=Angedockt
USER\ MENU\ ACTIVE=Aktives Benutzermenü
SUSPENDED\ CLEANING=Reinigung abgebrochen
UPDATING=Aktualisieren
COPYING\ LOGS=Logs kopieren
RECOVERING_LOCATION=zum Ausgangspunkt zurückkehren
IEC\ TEST=Iec Test

// Current or last cleaning mode
CLEAN-MODE-ECO=Eco
CLEAN-MODE-TURBO=Turbo

// Modifier of current or last cleaning
CLEAN-MODIFIER-NORMAL=Normal
CLEAN-MODIFIER-DOUBLE=Doppelt

// Current or Last category of the cleaning
CLEAN-CATEGORY-HOUSE=Haus Reinigung
CLEAN-CATEGORY-SPOT=Spot Reinigung
CLEAN-CATEGORY-MANUAL=Manuelle Reinigung

CLEAN-NAVIGATIONMODE-NORMAL=Normal
CLEAN-NAVIGATIONMODE-EXTRACARE=Extra Care

NULL=NULL
-=NA
=NA

UNRECOGNIZED=unerkannt
NORMAL=Normal
TURBO=Turbo
HOUSE=House Reinigung
ECO=Eco

Because the binding doesnt support maps right now… I used these following tools to get the cleaning map thing to work.

you need to install pybotvac → GitHub - stianaske/pybotvac: Python module for interacting with Neato Botvac Connected vacuum robots.

additional for image crop and format:
you need to install PIL Pillow → GitHub - python-pillow/Pillow: Python Imaging Library (Fork)
and
you need to install numpy → GitHub - numpy/numpy: The fundamental package for scientific computing with Python.

scripts / neato-botvac-getlastmap.py

#!/usr/bin/python
from pybotvac import Account
from os       import system
from PIL import Image
import numpy as np

email    = 'YOUR_NEATO_EMAIL'
password = 'YOUR_NEATO_PASSWORD'
serial   = 'OPS49416-884AEAF3DA74'
path_tmp = '/etc/openhab2/html/neato-botvac/tmp/'
path_dst = '/etc/openhab2/html/neato-botvac/'
map_file = 'botvac-last-map.png'

account  = Account(email, password)
link     = account.maps[serial]['maps'][0]['url']

account.get_map_image(link, path_tmp)

system('mv ' + path_tmp + '*-user-map.png ' + path_dst + map_file)

img = Image.open(path_dst + map_file).convert('RGB')
area = (73, 214, 713, 854)
cropped_img = img.crop(area)
cropped_img.save(path_dst + map_file)
orig_color = (51,61,74)
replacement_color = (38, 49, 29)
img3 = Image.open(path_dst + map_file).convert('RGB')
data = np.array(img3)
data[(data == orig_color).all(axis = -1)] = replacement_color
img2 = Image.fromarray(data, mode='RGB')
img2.save(path_dst + map_file)

I would be very interested to see that. Looks great! Which binding are you using?

Sure… as I said i need some time to clean up the code…

I’m running on the latest openHABian snapshot builds with the original Neato Binding included…

Will this work on both the botvac and the D3-D7 models?

Of course it should… but i only tested with the D5

Nice, I have a D7 and a D5. I also have the old Botvac 85e that I added MQTT to. I tried to get the items work as close as possible to the Neato Binding, maybe I can get that to work as well.

Hi Holger,
I ordered today my D3, so I want to ask, if you can share your code with us? The widget looks amazing!!

Thanks
Tobias

How do you get the map into openhab?

Really nice work!

What do you think, when you have cleaned up your code, to share us the widget?
Would be really nice!

Thanks a lot!
Christoph

@kugelsicha Is this widget reality or just to tease us :slight_smile:

Hi all and sorry for the late reply… but i havn’t had time the last weeks…

I’ve updated the first post and added the code i’m using…

the neato binding doesnt support maps right now, so i needed to install additional libs to get the map image working…

The best way would be to include the map part in the binding, but I cant do it on my own… so maybe some dev is willing to help here…

If someone has improvements for the code above, please let me know…

cheers
/Holger

Hi Kigelsicha,
I tried the code above, expecially the map. Therefore I created the getlastmap.py file, but when executing

from pybotvac import Account

I get the error message:

python /etc/openhab2/scripts/neato-botvac-getlastmap.py
Traceback (most recent call last):
  File "/etc/openhab2/scripts/neato-botvac-getlastmap.py", line 2, in <module>
    from pybotvac import Account
  File "/usr/local/lib/python2.7/dist-packages/pybotvac/__init__.py", line 1, in <module>
    from .account import Account
  File "/usr/local/lib/python2.7/dist-packages/pybotvac/account.py", line 13, in <module>
    from .robot import Robot
  File "/usr/local/lib/python2.7/dist-packages/pybotvac/robot.py", line 7, in <module>
    from datetime import datetime, timezone
ImportError: cannot import name timezone

pybotvac is installed. Do you have the same issue?

Merry Christmas
Tobias

Hi

it’s a bit late, I was facing the same issue a few moments ago. for me the problem was solved when using python v3.7. python v2 was constantly giving me the same error…

Cheers

Martin

I used your script before (on OH2.5), and on OH3 I wanted to revive it.

So I installed pybotvac and imported the robot.

But when I run get last map I get:

openhabian@homer:~/pybotvac $ python3 /etc/openhab/scripts/neato-botvac-getlastmap.py
Traceback (most recent call last):
  File "/etc/openhab/scripts/neato-botvac-getlastmap.py", line 12, in <module>
    account  = Account(email, password)
TypeError: __init__() takes 2 positional arguments but 3 were given

The script looks like (and worked in the past):

#!/usr/bin/python
from pybotvac import Account
from os       import system
email    = 'xxx'
password = 'xxx'
serial   = 'OPSxxx-xxx'
path_tmp = '/etc/openhab/html/tmp/'
path_dst = '/etc/openhab/html/neato-botvac/'
map_file = 'botvac-last-map.png'
account  = Account(email, password)
link     = account.maps[serial]['maps'][0]['url']
account.get_map_image(link, path_tmp)
system('mv ' + path_tmp + '*-user-map.png ' + path_dst + map_file)

Got it!
Just needed to import more than just Account:
from pybotvac import Account, Neato, OAuthSession, PasswordlessSession, PasswordSession, Vorwerk

I don’t know why, though (just changing from OH2.5 to OH3…
Might be related to changes in the installed python package!?

Hi there,

here are some adjustments to get this running. I am using openhab 3.

Rule:
The executeCommandLine has changed, so I had to use: executeCommandLine(Duration.ofSeconds(5000), "/usr/bin/python", "/etc/openhab/scripts/neato-botvac-getlastmap.py")

Script:

  1. I needed to install sudo apt-get install libatlas-base-dev on my Pi to get numpy running
  2. Got the same error as NCO and fixed it like this:
    password_session = PasswordSession(email=email, password=password, vendor=Neato()) account = Account(password_session)

Hope this helps someone else. :slight_smile: