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 -> https://github.com/stianaske/pybotvac

additional for image crop and format:
you need to install PIL Pillow -> https://github.com/python-pillow/Pillow
and
you need to install numpy -> https://github.com/numpy/numpy

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)
9 Likes

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…

2 Likes

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

1 Like

How do you get the map into openhab?

1 Like

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

1 Like

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

1 Like

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

1 Like

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:

1 Like