-
Logs and maps (part 5)
09/06/2022 at 17:33 • 0 commentsQuick update on the update
Writing live map image to buffer instead of file. Reduces load quite a bit.
img_buf = io.BytesIO() plt.savefig(img_buf, dpi=50, format='png') img_buf.seek(0) poseLogImageString = img_buf.read() poseLogImageByteArray = bytes(poseLogImageString) client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map",poseLogImageByteArray,0,False) img_buf.close()
It's still not fast enough for a real live feed though. Other small changes I made are: using "zorder" to make sure the scatter plot with dots is on top. Changing the size of the dots with s=120 and setting the resolution for both live-map and post-process-map with "dpi".
Updated python script is uploaded.
I guess, now it's time to do the deep dive into the accuracy of the positional data...
-
Logs and maps (part 5)
09/03/2022 at 10:22 • 0 commentsQuick update: better live mapping:
I noticed the live mapping would slow down and fail. This was because of the way I used the scatter plot function. I added a new layer with just one dot for each value.
Now I clear the plot and map out the complete path it took with a red dot on its last position. This works much better.
Updated python script is uploaded to files.
-
Logs and maps (part 4)
09/02/2022 at 10:01 • 0 commentsLive mapping and better maps!
I added a live mapping feature. While cleaning, every 10 position readings are added to a scatter plot. The image of the plot is then published by MQTT.
I tried omitting saving to file and using an io-buffer instead, but I can't get this to work. This means it's not fast enough to send an updated plot every 500ms (the update-rate of positional data). I have it set now to post every 10 datapoints. So an updated plot every 5 secs.
plt.scatter(poseJson['X'],poseJson['Y'], color='r') #-every 10 seconds records? liveFeedCounter=liveFeedCounter+1 if liveFeedCounter == 10: liveFeedCounter = 0 #img_buf = io.BytesIO() #plt.savefig(img_buf, format='png') #poseLogImageString = img_buf.read() plt.savefig("live.png",format='png') poseLogImageFile = open("live.png", "rb") poseLogImageString = poseLogImageFile.read() poseLogImageByteArray = bytes(poseLogImageString) client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map",poseLogImageByteArray,0,False) poseLogImageFile.close() #img_buf.close()
Also, the map created after a clean is much better. It's an overlay of a 2D histogram to show where the roomba has spent most of it's time. Then the positional points as scattered dots to indicate speed. And a line to get a sense of the completed track. This is all presented with equal axis to not distort the map and with a grid to get a sense of scale.
The plot should also be cleared in between cleaning cycles with plt.close()
plotData = np.array(plotData) x,y = plotData.T plt.close() plt.hist2d(x,y, bins=25, cmin=1, norm=col.LogNorm()) #plt.plot(x,y,linestyle='--') plt.scatter(x,y,marker='.',color='r') plt.plot(x,y,linestyle='-',color='b') #OutlineX = [] #OutlineX = [] #plt.plot(OutlineX,OutlineY) plt.axis('equal') plt.grid(True) #plt.xlim([-2000, 8000]) #plt.ylim([-5000, 5000]) plt.savefig(poseLogFilename + ".png", format='png') plt.close() plt.axis('equal') plt.grid(True) # - publish map poseLogImageFile = open(poseLogFilename+".png", "rb") poseLogImageString = poseLogImageFile.read() poseLogImageByteArray = bytes(poseLogImageString) client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map",poseLogImageByteArray,0,False)
Full python script is uploaded to files.
-
Logs and maps (part 3)
08/30/2022 at 18:01 • 0 commentsAaaand, it's done!
Using numpy and matplotlib. It's not pretty, but it works! Actually I wanted to use a heatmap. But this will work for now.
Full python code:
#!/usr/bin/env python # (c) 2022-08-30 S.E.Jansen. # MQTT-layer for Roomba logging # Listen for the start and stop of cleaning event # Log raw and positional data to file # Loop closing and error correction on positional data # Render map from positional data as a imagefile # Post imagefile to camera entity for Home Assistant import time import datetime import MQTT_Config import json import paho.mqtt.client as paho import csv import matplotlib.pyplot as plt import numpy as np #Roomba device info for Home Assistant autoconfig DeviceName = 'Roomba632' DeviceID = '01' DeviceManufacturer = 'iRobot' DeviceModel = '632' poseLogFilename = 'poseDummy' poseLogFile = open(poseLogFilename + ".csv", 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFilename = 'rawDummy' rawLogFile = open(rawLogFilename + ".csv", 'w') rawLogWriter = csv.writer(rawLogFile) #MQTT callback functions def on_message(client, userdata, message): data = message.payload global poseLogFilename global poseLogFile global poseLogWriter global rawLogFilename global rawLogFile global rawLogWriter if message.topic == MQTT_Config.HA_name + "/button/" + DeviceName + "_" + DeviceID + "/set": command=data.decode("utf-8") # start logging if command == "Clean": # open logfile print("Started cleaning cycle") poseLogFile.close() poseLogFilename = "./Roomba/Logs/" + datetime.datetime.now().isoformat('_', 'seconds') + "_pose" poseLogFile = open(poseLogFilename + ".csv", 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFile.close() rawLogFilename = "./Roomba/Logs/" + datetime.datetime.now().isoformat('_', 'seconds')+ "_raw" rawLogFile = open(rawLogFilename + ".csv", 'w') rawLogWriter = csv.writer(rawLogFile) if message.topic == MQTT_Config.HA_name + "/device_automation/" + DeviceName + "_" + DeviceID + "/event_DoneCleaning": event=data.decode("utf-8") # start logging if event == "done cleaning": # close log print("Cleaning cycle is done. Save logfile and start postprocess") poseLogFile.close() poseLogFile = open('poseDummy.csv', 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFile.close() rawLogFile = open('rawDummy.csv', 'w') rawLogWriter = csv.writer(rawLogFile) # plot image from CSV file. with open(poseLogFilename + ".csv", 'r') as plotDataFile: plotData = list(csv.reader(plotDataFile, delimiter=",",quoting=csv.QUOTE_NONNUMERIC)) plotData = np.array(plotData) x,y = plotData.T plt.scatter(x,y) #plt.imshow(plotData, cmap='hot', interpolation='nearest') plt.savefig(poseLogFilename + ".png", format='png') # - publish map poseLogImageFile = open(poseLogFilename+".png", "rb") poseLogImageString = poseLogImageFile.read() poseLogImageByteArray = bytes(poseLogImageString) client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map",poseLogImageByteArray,0,False) if message.topic == MQTT_Config.HA_name + "/device/roomba/raw": rawJson=json.loads(data.decode("utf-8")) # start logging #print(rawJson['mEL']) #print(rawJson['mER']) #print(rawJson['rTh']) #print(rawJson['Xacc']) #print(rawJson['Yacc']) #print(rawJson['Yaw']) if not rawLogFile.closed: rawLogData = [rawJson['mEL'],rawJson['mER'],rawJson['rTh'],rawJson['Xacc'],rawJson['Yacc'],rawJson['Yaw']] rawLogWriter.writerow(rawLogData) else: print("Raw logfile was closed") if message.topic == MQTT_Config.HA_name + "/device/roomba/pose": poseJson=json.loads(data.decode("utf-8")) # start logging #print(poseJson['X']) #print(poseJson['Y']) #print(poseJson['rTh']) if not poseLogFile.closed: #poseLogData = [poseJson['X'],poseJson['Y'],poseJson['rTh']] poseLogData = [poseJson['X'],poseJson['Y']] poseLogWriter.writerow(poseLogData) else: print("Pose logfile was closed") #call back function for MQTT connection def on_connect(client, userdata, flags, rc): if rc==0: client.connected_flag=True #set flag print("connected OK") # Send autoconfig messages and subscribe to command topics init() else: print("Bad connection Returned code=",rc) client.bad_connection_flag=True #call back function for disconnect MQTT connection to reconnect #def on_disconnect # MQTT Home Assistant autodiscover config and subscriptions def init(): # MQTT autoconfig device = {} device['identifiers'] = DeviceName + "_" + DeviceID device['name'] = DeviceName + "_" + DeviceID device['manufacturer'] = DeviceManufacturer #device['sw_version'] = "" device['model'] = DeviceModel # - Camera entity for maps data = {} data['name'] = "Map of last clean" data['topic'] = MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map" #data['availability_topic'] = MQTT_Config.HA_name + "/button/heos/" + HeosName + "_play/available" #data['payload_available'] = 'yes' #data['payload_available'] = 'no' data['unique_id'] = DeviceName + "_" + DeviceID + "_Map" data['icon'] = "mdi:map-outline" data['device'] = device client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/config",json.dumps(data),0,True) # MQTT subscribe to command topics to receive commands # - Start clean cycle: client.subscribe(MQTT_Config.HA_name + "/device_automation/" + DeviceName + "_" + DeviceID + "/event_DoneCleaning",0) # - Event for ending of clean cylce: client.subscribe(MQTT_Config.HA_name + "/button/" + DeviceName + "_" + DeviceID + "/set",0) # - Positional data calculated on roomba: client.subscribe(MQTT_Config.HA_name + "/device/roomba/pose",0) # - Raw sensordata for postprocess calculation of position client.subscribe(MQTT_Config.HA_name + "/device/roomba/raw",0) # MQTT details paho.Client.connected_flag=False #Create flag in class paho.Client.bad_connection_flag=False #another flag client= paho.Client() #create client object client.username_pw_set(MQTT_Config.user,MQTT_Config.password) client.on_connect=on_connect #bind call back function #on disconnect? client.on_message= on_message #attach function to callback client.connect(MQTT_Config.broker,MQTT_Config.port) #establish connection client.loop_start() #start network loop while not client.connected_flag and not client.bad_connection_flag: #wait in loop time.sleep(0.1) if client.bad_connection_flag: client.loop_stop() #Stop loop sys.exit() # Send autoconfig messages and subscribe to command topics init() # Main loop while True: try: #print("still running") # publish availability? # publishAvailability(Heos1Name) # publishAvailability(Heos2Name) # publishAvailability(Heos3Name) # publish 5x in expiration time time.sleep(60) except KeyboardInterrupt: client.loop_stop() #stop connection loop client.disconnect() #gracefully disconnect client.connected_flag=False #reset flag poseLogFile.close() rawLogFile.close() print("Ending program") exit()
This runs as a service. Once again, I'm not responsible for any spilled beer with the use of this code. Still quite proud though :)
-
Logs and maps (part 2)
08/29/2022 at 18:59 • 0 commentsCaptains log, stardate...
Nice! logging is done. I'll get csv-logfiles on every clean cycle.
poseLogFilename = 'poseDummy' poseLogFile = open(poseLogFilename + ".csv", 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFilename = 'rawDummy' rawLogFile = open(rawLogFilename + ".csv", 'w') rawLogWriter = csv.writer(rawLogFile) #MQTT callback functions def on_message(client, userdata, message): data = message.payload global poseLogFilename global poseLogFile global poseLogWriter global rawLogFilename global rawLogFile global rawLogWriter if message.topic == MQTT_Config.HA_name + "/button/" + DeviceName + "_" + DeviceID + "/set": command=data.decode("utf-8") # start logging if command == "Clean": # open logfile print("Started cleaning cycle") poseLogFile.close() poseLogFilename = "./Roomba/Logs/" + datetime.datetime.now().isoformat('_', 'seconds') + "_pose" poseLogFile = open(poseLogFilename + ".csv", 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFile.close() rawLogFilename = "./Roomba/Logs/" + datetime.datetime.now().isoformat('_', 'seconds')+ "_raw" rawLogFile = open(rawLogFilename + ".csv", 'w') rawLogWriter = csv.writer(rawLogFile) if message.topic == MQTT_Config.HA_name + "/device_automation/" + DeviceName + "_" + DeviceID + "/event_DoneCleaning": event=data.decode("utf-8") # start logging if event == "done cleaning": # close log print("Cleaning cycle is done. Save logfile and start postprocess") poseLogFile.close() poseLogFile = open('poseDummy.csv', 'w') poseLogWriter = csv.writer(poseLogFile) rawLogFile.close() rawLogFile = open('rawDummy.csv', 'w') rawLogWriter = csv.writer(rawLogFile) # plot image from CSV file. if message.topic == MQTT_Config.HA_name + "/device/roomba/raw": rawJson=json.loads(data.decode("utf-8")) # start logging #print(rawJson['mEL']) #print(rawJson['mER']) #print(rawJson['rTh']) #print(rawJson['Xacc']) #print(rawJson['Yacc']) #print(rawJson['Yaw']) if not rawLogFile.closed: rawLogData = [rawJson['mEL'],rawJson['mER'],rawJson['rTh'],rawJson['Xacc'],rawJson['Yacc'],rawJson['Yaw']] rawLogWriter.writerow(rawLogData) else: print("Raw logfile was closed") if message.topic == MQTT_Config.HA_name + "/device/roomba/pose": poseJson=json.loads(data.decode("utf-8")) # start logging #print(poseJson['X']) #print(poseJson['Y']) #print(poseJson['rTh']) if not poseLogFile.closed: poseLogData = [poseJson['X'],poseJson['Y'],poseJson['rTh']] poseLogWriter.writerow(poseLogData) else: print("Pose logfile was closed")
Only thing left now is to plot the pose-log to an image-file and post that with MQTT....
Anyone who knows how??
-
Logs and maps (part 1)
08/28/2022 at 19:43 • 0 commentsAlways fun when your wild ideas seem quite possible
I'm doing some experimenting with python on the Pi to log positioning info. I edited the sketch to output a stream of calculated pose data to construct a map with. And a stream with raw positional sensordata to do postprocessing, analysis and hopefully improve the accuracy of the system.
The python daemon will listen to these streams and make separate log-files. These files are opened/created and closed/saved on the start and end of a cleaning cycle.
After a cleaning cycle is done, the pose-log will be used to create a map-image. This image can be sent with MQTT and will display in Home Assistant. This seems to work brilliantly when testing with a dummy image:
I'm very curious if someone has a better way of getting something like a map in Home Assistant.
The MQTT autodiscovery and setup for the map and topics to subscribe to:
#Roomba device info for Home Assistant autoconfig DeviceName = 'Roomba632' DeviceID = '01' DeviceManufacturer = 'iRobot' DeviceModel = '632' # MQTT autoconfig device = {} device['identifiers'] = DeviceName + "_" + DeviceID device['name'] = DeviceName + "_" + DeviceID device['manufacturer'] = DeviceManufacturer #device['sw_version'] = "" device['model'] = DeviceModel # - Camera entity for maps data = {} data['name'] = "Map of last clean" data['topic'] = MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/map" #data['availability_topic'] = MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/available" #data['payload_available'] = 'yes' #data['payload_available'] = 'no' data['unique_id'] = DeviceName + "_" + DeviceID + "_Map" data['icon'] = "mdi:map-outline" data['device'] = device client.publish(MQTT_Config.HA_name + "/camera/" + DeviceName + "_" + DeviceID + "/config",json.dumps(data),0,True) # MQTT subscribe to command topics to receive commands # - Start clean cycle: client.subscribe(MQTT_Config.HA_name + "/device_automation/" + DeviceName + "_" + DeviceID + "/event_DoneCleaning",0) # - Event for ending of clean cylce: client.subscribe(MQTT_Config.HA_name + "/button/" + DeviceName + "_" + DeviceID + "/set",0) # - Positional data calculated on roomba: client.subscribe(MQTT_Config.HA_name + "/device/roomba/pose",0) # - Raw sensordata for postprocess calculation of position client.subscribe(MQTT_Config.HA_name + "/device/roomba/raw",0)
I'm able to listen to the pose and raw topics and can also deconstruct the JSON message. It shouldn't be that hard to log to file and plot something of a map... to be continued
-
Quick update
08/21/2022 at 18:44 • 0 commentsOut of office replies are great, no?
The roomba should have this too. I added "availability" to the button actions. See the buttons grey out?
Also added more tunes 8-)
- While docked, only the clean-button is available;
- While cleaning, both the dock- and music-button are available;
- While docking, both the clean- and music-button are available;
- While idle, all buttons are available;
- While playing music, none are available;
Docked:
Because of all the previous work, it was quite easy to add an event-trigger on "state-change" with callback in the library. Then add a function to check whether an action should be available or not, depending on the state of the roomba, and post this via MQTT. Add this availability to the MQTT-autodiscover, et voila! All latest files are uploaded:
https://hackaday.io/project/183524-controll-yer-roomba-600-series-with-esp8266#menu-files
I'm not sure how to display states in Home Assistant though. Should this be a sensor with a numeric value and templating to display it's corresponding state in text?
Or a "select" without the ability to actually select?
https://www.home-assistant.io/docs/mqtt/discovery/
If someone has any brilliant ideas, don't be shy.
I have decided how I want to handle the logging, correction and mapping of the positioning data:
On a seperate Pi (or the same, but outside of the HA-virtual enviroment) I will create a python script that runs as a service / daemon. This will subscribe to various MQTT-topics to check when a cleaning cycle (or callibration script) starts and ends. To start with, it will log (raw) sensordata to files. I can use these files to correct my positioning algorithms.
Later on, I will have this piece of software do loop closing / resolving and the creation of map-images.
These images can be posted with MQTT as "camera" to display the latest map.
-
Let it sing!
08/20/2022 at 22:53 • 0 commentsShowing progress is sometimes more important than making actual progress
There are a few things I want to accomplish before tackling the difficult maths. First off, The ability to do something that normal robot-vacuums can't. Just to make a point as to why I don't just buy the €100 more expensive model. Most of them can do scheduling and keep me posted on progress, but how many can play any tune I want?
Playing the macGyver theme song was one of the fist things I did with this roomba.
Back then, I was using a previous library and this was more a proof of concept. Which also showed I had full control and was master of all robots! Now we know better off course and welcome our robot overlords.
By now I have produced a library of my own, somewhat in secrect, and want to show off my mastery of this inanimate object. What better way than to let it sing for me.
The video shows the Home Assistant MQTT-autodiscover buttons and sensors. Also the notification in the end using the event trigger when the Roomba is docked again.
Some of the code I added:
void Roomba632::playMusic(uint8_t song[],int songNumberOfBytes){ //set music to be played p_songPointer = song; p_songNumberOfBytes = songNumberOfBytes; p_playMusicFlag = true; } //in the roomba state handler: case roombaIdle: //IDLE: not charging, driving, playing music or cleaning p_stopFlag = false; //Clear stop request flag if set //Keep alive //Check flags in appropriate order of prirority if(p_cleanFlag){ SendCleanCommand(); } else if(p_dockFlag){ SendDockCommand(); } else if(p_playMusicFlag){ SendPlayMusicCommand(); } if((millis() - p_undockedTimer) > DOCKEDDEBOUNCETIME){ if(docked){ roombaState = roombaHome; } } break; //The state machine for playing music: case roombaMusic: p_playMusicFlag = false; b_songPlaying = true; //time music or poll state static int songPointerIndex; static uint8_t maxIndex, i; static int musicState; static unsigned long commandTimerMusic, musicTimer, songTime; switch (musicState){ case musicInit: songPointerIndex = 0; musicState = musicWriteSongHeader; //Serial.write(OCPauseDatastream); //Serial.write(0); commandTimerMusic = millis(); break; case musicWriteSongHeader: if ((millis() - commandTimerMusic) > COMMANDTIMEOUT){ Serial.write(OCSong); Serial.write(1); songTime = 0; if ((p_songNumberOfBytes-songPointerIndex)<MAXSONGLENGTH){ maxIndex = (p_songNumberOfBytes-songPointerIndex); } else{ maxIndex = MAXSONGLENGTH; } Serial.write(maxIndex/2); //number of notes musicState = musicWriteNotes; i = 0; commandTimerMusic = millis(); } break; case musicWriteNotes: if ((millis() - commandTimerMusic) > COMMANDTIMEOUT){ if(i < maxIndex){ Serial.write(p_songPointer[songPointerIndex]); if(songPointerIndex % 2 != 0){ songTime += p_songPointer[songPointerIndex]; if (p_songPointer[songPointerIndex] != 0){ songTime -= 2; } } songPointerIndex++; i++; } else{ musicState = musicStartSong; } commandTimerMusic = millis(); } break; case musicStartSong: if ((millis() - commandTimerMusic) > COMMANDTIMEOUT){ musicTimer = millis(); songTime = (1000 * songTime)/64; musicState = musicPlaySong; Serial.write(OCPlaySong); Serial.write(1); } break; case musicPlaySong: if((millis() - musicTimer) > songTime){ if(songPointerIndex >= p_songNumberOfBytes){ musicState = musicInit; //Serial.println("done singing"); roombaState = roombaIdle; b_songPlaying = false; SendStopCommand(); //Serial.write(OCPauseDatastream); //Serial.write(1); } else { musicState = musicWriteSongHeader; //next song //Serial.println("next song"); } } //waiting while song plays break; } break; //And seperate function to start the music commands: void Roomba632::SendPlayMusicCommand(){ static int commandstate; static unsigned long commandTimer; switch (commandstate){ case startIO: Serial.write(OCstart); commandstate = setMode; commandTimer = millis(); break; case setMode: if ((millis() - commandTimer) > COMMANDMODETIMEOUT){ Serial.write(OCFullmode); commandstate = startIO; //reset state roombaState = roombaMusic; //sending music commands is handled in the music state } break; } }
A few of the problems I ran across:
- The robot seems to need some time to process the commands. It needs a bit of time between sending the different commands. In a fully async library this can't be done with delays() off course. So we need state-machines and timers;
- How to split up a long song in chunks of 16 notes? The roomba accepts songs with a maximum of 16 notes to be defined as numbered songs 0 to 4 which can then be played. How can we split up a song with undetermined and unrestricted length in the library?
- Using uint8_t and int (int16_t) together and in calculations, but also writing the correct data over Serial;
- Because the library is set up to be quite asynchronous, there's also a lot of stuff that can go wrong in parallel. This is sometimes very hard to diagnose. Maybe I need to clean up the code some more and stick to some best-practices more. For instance: thou shall never change state of a state-machine outside of said state machine;
- I still had the enum's for the notes not set up correctly. As far as I can tell, I now use C4 as "middle note", with MIDI #60. A4 is MIDI #69, 440Hz.
enum notes{ N_G1 = 31, * * N_B2 = 47, N_C3 = 48, N_C3S = 49, N_D3 = 50, N_D3S = 51, N_E3 = 52, N_F3 = 53, N_F3S = 54, N_G3 = 55, N_G3S = 56, N_A3 = 57, N_A3S = 58, N_B3 = 59, N_C4 = 60, N_C4S = 61, N_D4 = 62, N_D4S = 63, N_E4 = 64, N_F4 = 65, N_F4S = 66, N_G4 = 67, N_G4S = 68, N_A4 = 69, N_A4S = 70, N_B4 = 71, * * N_B7 = 107 };
- It has been some time last I checked in on the library in this depth. I still comment way too little :| I had to re-interpret what I wanted to do when last I wrote it. Not cool;
- There is still an audible "gap" between songs. This is due to timing of the commands being sent and the calculated waiting time. Maybe I can get these gaps closed;
For now, I consider this another feature added! The last version of this library and my sketch-files are uploaded. I want to do some more on the Home Assistant integration first.
- Use "availability" for the different action buttons in Home Assistant to differentiate between possible actions and divine interventions;
- Neat way to display states in Home Assistant. I think this can be done with templates. Somehow;
- How to get maps/cards??? I think I need to see results (easily) before I can try to improve accuracy;
-
Autoconfigure Home Assistant
08/15/2022 at 07:04 • 0 commentsBecause it's always easier when it says "auto"
Home Assistant has an option to discover MQTT-entities with their capabilities and features. The device needs tot send it's information in a proper format to a specific topic. The MQTT-autodiscovery service will see this and configure the corresponding devices and features.
https://www.home-assistant.io/docs/mqtt/discovery/
This way of configuring Home Assistant is necessary for using device triggers for example (the way I want to use events)
Now, there is an entity "vacuum" that should be perfect. I'm still missing some things though. It doesn't have event triggers the way I want to use them. Also, I'm missing a way to log positions for mapping and I want a bit more flexibility. Of course the way I set up the autoconfig messages here can be easily adapted to configure a "vacuum" entity.
All this means choosing my own "schema" and setting up a bunch of entities/attributes for a new device.
The device (roomba) will output a bunch of formatted messages to a config topic for each entity on startup. Entities are for instance:
- Event "done cleaning" (device trigger);
- Action "Clean" (button);
- Sensor with numeric value, temperature, battery voltage (sensor);
- Binary value for docked state (binary sensor);
These entities have their own topics for configuration, state, command etc. They can be given the same device information and are therefore seen as attributes of said device.
My schema will broadly look like this:
Device:
Name: Roomba632_01
Model: 632
Manufacturer: iRobot
Done cleaning event [trigger] (device_automation)
Start cleaning command [button] (action)
Return to dock command [button] (action)
Play music command [button] (action)
State info [sensor] (states)
Sensor vallues [sensor]I'm still trying to follow the "official" vacuum-entity and for now have the actions and event trigger configured.
I put everything in a single Autoconfigure function that runs when (re-) connecting to MQTT.
void MQTTAutoConfig() { //Event triggers mqttClient.publish( HANAME "/device_automation/" DEVICENAME "_" DEVICEID "/event_DoneCleaning/config", 0, true, "{" "\"automation_type\": \"trigger\", " "\"payload\": \"done cleaning\", " "\"unique_id\": \"" DEVICENAME "_" DEVICEID "\", " "\"topic\": \"" HANAME "/device_automation/" DEVICENAME "_" DEVICEID "/event_DoneCleaning" "\", " "\"type\": \"action\", " "\"subtype\": \"button_short_press\", " "\"icon\": \"mdi:vacuum\", " "\"device\": {" "\"identifiers\": [\"" DEVICENAME "_" DEVICEID "\"], " "\"name\": \"" DEVICENAME "_" DEVICEID "\", " "\"manufacturer\": \"" MANUFACTURER "\", " "\"sw_version\": \"" VERSION "\", " "\"model\": \"" MODEL "\"" "}" "}" ); //Actions //Start cleaning mqttClient.publish( HANAME "/button/" DEVICENAME "_" DEVICEID "/clean_button/config", 0, true, "{" "\"name\": \"Start cleaning\", " "\"command_topic\": \"" HANAME "/button/" DEVICENAME "_" DEVICEID "/set" "\", " "\"payload_press\": \"Clean\", " "\"unique_id\": \"" DEVICENAME "_" DEVICEID "_Clean\", " "\"icon\": \"mdi:vacuum-outline\", " "\"device\": {" "\"identifiers\": [\"" DEVICENAME "_" DEVICEID "\"], " "\"name\": \"" DEVICENAME "_" DEVICEID "\", " "\"manufacturer\": \"" MANUFACTURER "\", " "\"sw_version\": \"" VERSION "\", " "\"model\": \"" MODEL "\"" "}" "}" ); //Return to dock mqttClient.publish( HANAME "/button/" DEVICENAME "_" DEVICEID "/dock_button/config", 0, true, "{" "\"name\": \"Return to dock\", " "\"command_topic\": \"" HANAME "/button/" DEVICENAME "_" DEVICEID "/set" "\", " "\"payload_press\": \"Dock\", " "\"unique_id\": \"" DEVICENAME "_" DEVICEID "_Dock\", " "\"icon\": \"mdi:home-import-outline\", " "\"device\": {" "\"identifiers\": [\"" DEVICENAME "_" DEVICEID "\"], " "\"name\": \"" DEVICENAME "_" DEVICEID "\", " "\"manufacturer\": \"" MANUFACTURER "\", " "\"sw_version\": \"" VERSION "\", " "\"model\": \"" MODEL "\"" "}" "}" ); //Locate mqttClient.publish( HANAME "/button/" DEVICENAME "_" DEVICEID "/locate_button/config", 0, true, "{" "\"name\": \"Play music\", " "\"command_topic\": \"" HANAME "/button/" DEVICENAME "_" DEVICEID "/set" "\", " "\"payload_press\": \"Music\", " "\"unique_id\": \"" DEVICENAME "_" DEVICEID "_Locate\", " "\"icon\": \"mdi:music\", " "\"device\": {" "\"identifiers\": [\"" DEVICENAME "_" DEVICEID "\"], " "\"name\": \"" DEVICENAME "_" DEVICEID "\", " "\"manufacturer\": \"" MANUFACTURER "\", " "\"sw_version\": \"" VERSION "\", " "\"model\": \"" MODEL "\"" "}" "}" ); }
I'ts a bit ugly with all the defines, but I didn't want to mess around with character arrays and this works:
Now I don't need separate scripts and it feels much neater. I will add the sensordata and maybe more. The cool thing with this is you can add feedback and syncing. See my doorbell project for an example of this feedback: https://hackaday.io/project/183525-smart-classic-ac-doorbell
Switching the state in Home Assistant sends a command and updates a remote value to give feedback on success.
-
Life changing events
08/11/2022 at 20:06 • 0 commentsImportant events deserve to be noticed
To give the library some actual reason to be among us, it needs some practical benefits. I think all tinkerers and hackers know the "So what does it actually do?" question...
At the moment it can only clean on schedule. It would be nice to know when it is done cleaning for example.
For this, I wanted to use some type of event handling. The code in the library should decide when this event comes to pass, but the action should come from the sketch.
This meant using function callbacks in the library so it'll be independent of the sketch. Yaaaay, function pointers!
This is all new to me. It took me quite some googling... so for your benefit
Added in header file:
class Roomba632 { public: void setCallbackDoneCleaning(void (*callbackFuncDoneCleaning)()); private: void (*_callbackFuncDoneCleaning)(); void EventDoneCleaning(); };
Added in source file:
void Roomba632::setCallbackDoneCleaning(void (*callbackFuncDoneCleaning)()){ _callbackFuncDoneCleaning = callbackFuncDoneCleaning; } void Roomba632::EventDoneCleaning(){ if (_callbackFuncDoneCleaning){ _callbackFuncDoneCleaning(); } }
Whenever the roomba library decides it's done cleaning, it will call EventDoneCleaning() Wich in turn will call the function from the sketch that's been set as a callback.
In my sketch something like this:
void onDoneCleaning(){ mqttClient.publish(MQTT_PUB_TOPIC, 0, false, "done cleaning"); } void setup(){ roomba.setCallbackDoneCleaning(onDoneCleaning); }
Are we learning yet‽
I think this will come in handy later. Now I can set up Home Assistant to be triggered on these events and send me a notification with the results of the cleaning cycle for instance. Also, adding new features postpones the difficult maths :) So next up: Home Assistant MQTT-autoconfig setup.