├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── complicated.py ├── example_start_roomba └── simple.py ├── openhab ├── html │ ├── roomba_map.html │ └── style.css ├── icons │ ├── angle.png │ ├── angle.svg │ ├── map.png │ ├── map.svg │ ├── msg.png │ ├── msg.svg │ ├── number.png │ ├── number.svg │ ├── roomba-charge.png │ ├── roomba-dock.png │ ├── roomba-drop.png │ ├── roomba-eco.png │ ├── roomba-error.png │ ├── roomba-old.png │ ├── roomba-pause.png │ ├── roomba-resume.png │ ├── roomba-run.png │ ├── roomba-start.png │ ├── roomba-stop.png │ ├── roomba-wifi.png │ ├── roomba.png │ ├── roombaerror-off.png │ ├── roombaerror-on.png │ ├── roombaerror.png │ ├── select.png │ ├── select.svg │ └── trashpresent.png ├── items │ └── roomba.items ├── sitemaps │ └── roomba.sitemap ├── start_openhab_roomba ├── things │ └── roomba.things └── transform │ ├── inverse_switch.map │ └── switch.map ├── requirements.txt ├── roomba ├── __init__.py ├── __main__.py ├── config_example.ini ├── getcloudpassword.py ├── getpassword.py ├── password.py ├── replay_log.py ├── res │ ├── app_map.png │ ├── binfull.png │ ├── first_floor.jpg │ ├── ground_floor.jpg │ ├── home.png │ ├── map.png │ ├── roomba-base.png │ ├── roomba-charge.png │ ├── roomba.png │ ├── roombacancelled.png │ ├── roombaerror.png │ ├── side_by_side_map.png │ ├── tanklow.png │ └── web_interface.png ├── roomba.py ├── roomba_direct.py ├── test_floorplan.png ├── views │ ├── css │ │ └── style.css │ ├── js │ │ └── map.js │ └── map.html └── web_server.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.ini 2 | *.pyc 3 | __pycache__ 4 | start_roomba 5 | .ropeproject 6 | .autoenv.zsh 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | *_map.html 11 | *.log* 12 | *.txt 13 | *.bak 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 NickWaterton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include roomba/res/*.png 2 | recursive-exclude * *.py[co] 3 | -------------------------------------------------------------------------------- /examples/complicated.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | from roomba import Roomba 5 | import paho.mqtt.client as mqtt 6 | import time 7 | import json 8 | import logging 9 | 10 | #Uncomment the following two lines to see logging output 11 | logging.basicConfig(level=logging.INFO, 12 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 13 | 14 | #put your own values here 15 | broker = 'localhost' #ip of mqtt broker 16 | user = 'user' #mqtt username 17 | password = 'password' #mqtt password 18 | #broker = None if not using local mqtt broker 19 | address = '192.168.1.181' 20 | blid = "38XXXXXXXX850" 21 | roombaPassword = ":1:1492XXX243:gOXXXXXXXD1xJ" 22 | 23 | loop = asyncio.get_event_loop() 24 | 25 | myroomba = Roomba(address) #minnimum required to connect on Linux Debian system, will read connection from config file 26 | #myroomba = Roomba(address, blid, roombaPassword) #setting things manually 27 | 28 | #all these are optional, if you don't include them, the defaults will work just fine 29 | #if you are using maps 30 | myroomba.enable_map(enable=True, mapSize="(800,1650,-300,-50,2,0)", mapPath="./", iconPath="./res") #enable live maps, class default is no maps 31 | if broker is not None: 32 | myroomba.setup_mqtt_client(broker, 1883, user, password, '/roomba/feedback') #if you want to publish Roomba data to your own mqtt broker (default is not to) if you have more than one roomba, and assign a roombaName, it is addded to this topic (ie /roomba/feedback/roombaName) 33 | #finally connect to Roomba - (required!) 34 | myroomba.connect() 35 | 36 | print(" to exit") 37 | print("Subscribe to /roomba/feedback/# to see published data") 38 | 39 | try: 40 | loop.run_forever() 41 | 42 | except (KeyboardInterrupt, SystemExit): 43 | print("System exit Received - Exiting program") 44 | myroomba.disconnect() -------------------------------------------------------------------------------- /examples/example_start_roomba: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # example start line - insert your own values 3 | 4 | # looks like 3012830851937810 5 | # looks like :1:1493919143:gOiaYpQ4LbfoD1yJ 6 | # -b is the ip of the mqtt broker to publish to, leave it out if you are not forwarding roomba data 7 | # -s is the size and shape of the map (if you are drawing maps) x,y, dock location x,y, map rotation, roomba rotation 8 | ./roomba.py -R -u -w -b localhost -s '(800,1650,-300,-50,2,0)' 9 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import json 5 | import logging 6 | from roomba import Roomba 7 | 8 | #Uncomment the following two lines to see logging output 9 | logging.basicConfig(level=logging.INFO, 10 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 11 | 12 | #uncomment the option you want to run, and replace address, blid and roombaPassword with your own values 13 | 14 | address = "192.168.1.181" 15 | blid = "3835850251647850" 16 | roombaPassword = ":1:1493319243:gOizXpQ4lcdSoD1xJ" 17 | 18 | #myroomba = Roomba(address, blid, roombaPassword) 19 | #or myroomba = Roomba(address) #if you have a config file - will attempt discovery if you don't 20 | myroomba = Roomba() 21 | async def test(): 22 | myroomba.connect() 23 | #myroomba.set_preference("carpetBoost", "true") 24 | #myroomba.set_preference("twoPass", "false") 25 | 26 | #myroomba.send_command("start") 27 | #myroomba.send_command("stop") 28 | #myroomba.send_command("dock") 29 | 30 | import json, time 31 | for i in range(10): 32 | print(json.dumps(myroomba.master_state, indent=2)) 33 | await asyncio.sleep(1) 34 | myroomba.disconnect() 35 | 36 | loop = asyncio.get_event_loop() 37 | loop.run_until_complete(test()) -------------------------------------------------------------------------------- /openhab/html/roomba_map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 33 | 34 | 35 | Roomba Map Live 36 | 37 | 38 | -------------------------------------------------------------------------------- /openhab/html/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | color: white; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | img,video { 8 | width: auto; 9 | max-height:100%; 10 | } 11 | -------------------------------------------------------------------------------- /openhab/icons/angle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/angle.png -------------------------------------------------------------------------------- /openhab/icons/angle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openhab/icons/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/map.png -------------------------------------------------------------------------------- /openhab/icons/map.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openhab/icons/msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/msg.png -------------------------------------------------------------------------------- /openhab/icons/msg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openhab/icons/number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/number.png -------------------------------------------------------------------------------- /openhab/icons/number.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openhab/icons/roomba-charge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-charge.png -------------------------------------------------------------------------------- /openhab/icons/roomba-dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-dock.png -------------------------------------------------------------------------------- /openhab/icons/roomba-drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-drop.png -------------------------------------------------------------------------------- /openhab/icons/roomba-eco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-eco.png -------------------------------------------------------------------------------- /openhab/icons/roomba-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-error.png -------------------------------------------------------------------------------- /openhab/icons/roomba-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-old.png -------------------------------------------------------------------------------- /openhab/icons/roomba-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-pause.png -------------------------------------------------------------------------------- /openhab/icons/roomba-resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-resume.png -------------------------------------------------------------------------------- /openhab/icons/roomba-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-run.png -------------------------------------------------------------------------------- /openhab/icons/roomba-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-start.png -------------------------------------------------------------------------------- /openhab/icons/roomba-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-stop.png -------------------------------------------------------------------------------- /openhab/icons/roomba-wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba-wifi.png -------------------------------------------------------------------------------- /openhab/icons/roomba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roomba.png -------------------------------------------------------------------------------- /openhab/icons/roombaerror-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roombaerror-off.png -------------------------------------------------------------------------------- /openhab/icons/roombaerror-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roombaerror-on.png -------------------------------------------------------------------------------- /openhab/icons/roombaerror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/roombaerror.png -------------------------------------------------------------------------------- /openhab/icons/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/select.png -------------------------------------------------------------------------------- /openhab/icons/select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 23 | 24 | 26 | Arrow icon set 27 | icons for toolbar buttons 28 | 29 | 30 | icon 31 | arrow 32 | navigation 33 | green 34 | red 35 | button 36 | 37 | 38 | 39 | 41 | Open Clip Art Library 42 | 43 | 44 | 45 | 46 | Jakub Jankiewicz 47 | 48 | 49 | 50 | 51 | Jakub Jankiewicz 52 | 53 | 54 | 55 | image/svg+xml 56 | 58 | 60 | pl 61 | 62 | 64 | 66 | 68 | 70 | 71 | 72 | 73 | 75 | 85 | 95 | 105 | 115 | 125 | 135 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 305 | 315 | 325 | 335 | 345 | 355 | 365 | 375 | 385 | 395 | 405 | 415 | 417 | 421 | 425 | 426 | 436 | 438 | 442 | 446 | 447 | 457 | 467 | 477 | 487 | 489 | 493 | 497 | 498 | 508 | 518 | 528 | 538 | 540 | 544 | 548 | 549 | 559 | 569 | 579 | 589 | 590 | 608 | 610 | 611 | 613 | image/svg+xml 614 | 616 | 617 | 618 | 619 | 624 | 628 | 631 | 640 | 644 | 653 | 663 | 664 | 668 | 677 | 687 | 688 | 692 | 701 | 711 | 712 | 716 | 725 | 735 | 736 | 745 | 749 | 758 | 768 | 769 | 770 | 772 | 777 | 782 | 783 | 784 | 785 | 786 | -------------------------------------------------------------------------------- /openhab/icons/trashpresent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/openhab/icons/trashpresent.png -------------------------------------------------------------------------------- /openhab/items/roomba.items: -------------------------------------------------------------------------------- 1 | /* Roomba items */ 2 | Group roomba_items "Roombas" 3 | Group downstairs_roomba_items "Downstairs Roomba" (roomba_items) 4 | Group upstairs_roomba_items "Upstairs Roomba" (roomba_items) 5 | Group mopster_roomba_items "Downstairs Mop" (roomba_items) 6 | 7 | /* Upstairs Roomba Commands */ 8 | String upstairs_roomba_command "Roomba" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_command" } 9 | /* Settings */ 10 | Switch upstairs_roomba_edgeClean "Edge Clean [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_edgeClean", autoupdate="false" } 11 | Switch upstairs_roomba_carpetBoost "Auto carpet Boost [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_carpetBoost", autoupdate="false" } 12 | Switch upstairs_roomba_vacHigh "Vacuum Boost [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_vacHigh", autoupdate="false" } 13 | Switch upstairs_roomba_noAutoPasses "Auto Passes [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_noAutoPasses", autoupdate="false" } 14 | Switch upstairs_roomba_twoPass "Two Passes [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_twoPass", autoupdate="false" } 15 | Switch upstairs_roomba_binPause "Always Complete (even if bin is full) [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_binPause", autoupdate="false" } 16 | /* Roomba Feedback */ 17 | String upstairs_roomba_softwareVer "Software Version [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_softwareVer" } 18 | Number upstairs_roomba_batPct "Battery [%d%%]" (upstairs_roomba_items, Battery) { channel="mqtt:topic:upstairs_roomba:roomba_batPct" } 19 | String upstairs_roomba_lastcommand "Last Command [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_lastcommand" } 20 | Switch upstairs_roomba_bin_present "Bin Present [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_bin_present" } 21 | Switch upstairs_roomba_full "Bin Full [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_full" } 22 | /* Mission values */ 23 | String upstairs_roomba_mission "Mission [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_mission" } 24 | Number upstairs_roomba_nMssn "Cleaning Mission Number [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_nMssn" } 25 | String upstairs_roomba_phase "Phase [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_phase" } 26 | String upstairs_roomba_initiator "Initiator [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_initiator" } 27 | Switch upstairs_roomba_error "Error [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_error" } 28 | String upstairs_roomba_errortext "Error Message [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_errortext" } 29 | Number upstairs_roomba_mssnM "Cleaning Elapsed Time [%d m]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_mssnM" } 30 | Number upstairs_roomba_sqft "Square Ft Cleaned [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_sqft" } 31 | Number upstairs_roomba_expireM "Mission Recharge Time [%d m]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_expireM" } 32 | Number upstairs_roomba_rechrgM "Remaining Time To Recharge [%d m]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_rechrgM" } 33 | String upstairs_roomba_status "Status [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_status" } 34 | Number upstairs_roomba_percent_complete "Mission % Completed [%d%%]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_percent_complete" } 35 | DateTime upstairs_roomba_lastmissioncompleted "Last Mission Completed [%1$ta %1$tR]" 36 | /* Schedule */ 37 | String upstairs_roomba_cycle "Day of Week [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_cycle" } 38 | String upstairs_roomba_cleanSchedule_h "Hour of Day [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_cleanSchedule_h" } 39 | String upstairs_roomba_cleanSchedule_m "Minute of Hour [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_cleanSchedule_m" } 40 | String upstairs_roomba_cleanSchedule2 "Schedule [%s]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_cleanSchedule2" } 41 | String upstairs_roomba_cleanSchedule "Schedule [%s]" (upstairs_roomba_items) 42 | /* General */ 43 | Switch upstairs_roomba_control "Upstairs Roomba ON/OFF [%s]" (upstairs_roomba_items) 44 | Number upstairs_roomba_theta "Theta [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_theta" } 45 | Number upstairs_roomba_x "X [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_x" } 46 | Number upstairs_roomba_y "Y [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_y" } 47 | Number upstairs_roomba_rssi "RSSI [%d]" (upstairs_roomba_items) { channel="mqtt:topic:upstairs_roomba:roomba_rssi" } 48 | DateTime upstairs_roomba_lastheardfrom "Last Update [%1$ta %1$tR]" { channel="mqtt:topic:upstairs_roomba:roomba_rssi" [profile="timestamp-update"] } 49 | 50 | /* Downstairs Roomba Commands */ 51 | String downstairs_roomba_command "Roomba" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_command" } 52 | /* Settings */ 53 | Switch downstairs_roomba_edgeClean "Edge Clean [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_edgeClean", autoupdate="false" } 54 | Switch downstairs_roomba_carpetBoost "Auto carpet Boost [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_carpetBoost", autoupdate="false" } 55 | Switch downstairs_roomba_vacHigh "Vacuum Boost [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_vacHigh", autoupdate="false" } 56 | Switch downstairs_roomba_noAutoPasses "Auto Passes [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_noAutoPasses", autoupdate="false" } 57 | Switch downstairs_roomba_twoPass "Two Passes [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_twoPass", autoupdate="false" } 58 | Switch downstairs_roomba_binPause "Always Complete (even if bin is full) [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_binPause", autoupdate="false" } 59 | /* Roomba Feedback */ 60 | String downstairs_roomba_softwareVer "Software Version [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_softwareVer" } 61 | Number downstairs_roomba_batPct "Battery [%d%%]" (downstairs_roomba_items, Battery) { channel="mqtt:topic:downstairs_roomba:roomba_batPct" } 62 | String downstairs_roomba_lastcommand "Last Command [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_lastcommand" } 63 | Switch downstairs_roomba_bin_present "Bin Present [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_bin_present" } 64 | Switch downstairs_roomba_full "Bin Full [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_full" } 65 | /* Mission values */ 66 | String downstairs_roomba_mission "Mission [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_mission" } 67 | Number downstairs_roomba_nMssn "Cleaning Mission Number [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_nMssn" } 68 | String downstairs_roomba_phase "Phase [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_phase" } 69 | String downstairs_roomba_initiator "Initiator [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_initiator" } 70 | Switch downstairs_roomba_error "Error [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_error" } 71 | String downstairs_roomba_errortext "Error Message [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_errortext" } 72 | Number downstairs_roomba_mssnM "Cleaning Elapsed Time [%d m]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_mssnM" } 73 | Number downstairs_roomba_sqft "Square Ft Cleaned [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_sqft" } 74 | Number downstairs_roomba_expireM "Mission Recharge Time [%d m]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_expireM" } 75 | Number downstairs_roomba_rechrgM "Remaining Time To Recharge [%d m]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_rechrgM" } 76 | String downstairs_roomba_status "Status [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_status" } 77 | Number downstairs_roomba_percent_complete "Mission % Completed [%d%%]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_percent_complete" } 78 | DateTime downstairs_roomba_lastmissioncompleted "Last Mission Completed [%1$ta %1$tR]" 79 | /* Schedule */ 80 | String downstairs_roomba_cycle "Day of Week [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_cycle" } 81 | String downstairs_roomba_cleanSchedule_h "Hour of Day [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_cleanSchedule_h" } 82 | String downstairs_roomba_cleanSchedule_m "Minute of Hour [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_cleanSchedule_m" } 83 | String downstairs_roomba_cleanSchedule2 "Schedule [%s]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_cleanSchedule2" } 84 | String downstairs_roomba_cleanSchedule "Schedule [%s]" (downstairs_roomba_items) 85 | /* General */ 86 | Switch downstairs_roomba_control "Downstairs Roomba ON/OFF [%s]" (downstairs_roomba_items) 87 | Number downstairs_roomba_theta "Theta [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_theta" } 88 | Number downstairs_roomba_x "X [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_x" } 89 | Number downstairs_roomba_y "Y [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_y" } 90 | Number downstairs_roomba_rssi "RSSI [%d]" (downstairs_roomba_items) { channel="mqtt:topic:downstairs_roomba:roomba_rssi" } 91 | DateTime downstairs_roomba_lastheardfrom "Last Update [%1$ta %1$tR]" { channel="mqtt:topic:downstairs_roomba:roomba_rssi" [profile="timestamp-update"] } 92 | 93 | /* Downstairs Braava Jet M6 Commands */ 94 | String mopster_roomba_command "Mopster" (roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_command" } 95 | /* Settings */ 96 | /* Mop Feedback */ 97 | String mopster_roomba_softwareVer "Software Version [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_softwareVer" } 98 | Number mopster_roomba_batPct "Battery [%d%%]" (mopster_roomba_items, Battery) { channel="mqtt:topic:downstairs_mop:roomba_batPct" } 99 | String mopster_roomba_lastcommand "Last Command [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_lastcommand" } 100 | String mopster_roomba_detectedPad "Detected Pad [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_detectedPad" } 101 | Switch mopster_roomba_lid_closed "Lid Closed [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_lid_closed" } 102 | Number mopster_roomba_padWetness_disposable "Disposable Pad Wetness [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_padWetness_disposable" } 103 | Number mopster_roomba_padWetness_reusable "Reusable Pad Wetness [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_padWetness_reusable" } 104 | Switch mopster_roomba_bin_present "Tank Present [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_bin_present" } 105 | Number mopster_roomba_tankLvl "Tank Level [%d%%]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_tankLvl" } 106 | /* Mission values */ 107 | String mopster_roomba_mission "Mission [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_mission" } 108 | Number mopster_roomba_nMssn "Cleaning Mission Number [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_nMssn" } 109 | String mopster_roomba_phase "Phase [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_phase" } 110 | String mopster_roomba_initiator "Initiator [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_initiator" } 111 | Switch mopster_roomba_error "Error [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_error" } 112 | String mopster_roomba_errortext "Error Message [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_errortext" } 113 | Number mopster_roomba_mssnM "Cleaning Elapsed Time [%d m]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_mssnM" } 114 | Number mopster_roomba_sqft "Square Ft Cleaned [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_sqft" } 115 | Number mopster_roomba_expireM "Mission Recharge Time [%d m]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_expireM" } 116 | Number mopster_roomba_rechrgM "Remaining Time To Recharge [%d m]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_rechrgM" } 117 | String mopster_roomba_status "Status [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_status" } 118 | Number mopster_roomba_percent_complete "Mission % Completed [%d%%]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_percent_complete" } 119 | DateTime mopster_roomba_lastmissioncompleted "Last Mission Completed [%1$ta %1$tR]" 120 | /* Schedule */ 121 | String mopster_roomba_cycle "Day of Week [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_cycle" } 122 | String mopster_roomba_cleanSchedule_h "Hour of Day [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_cleanSchedule_h" } 123 | String mopster_roomba_cleanSchedule_m "Minute of Hour [%s]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_cleanSchedule_m" } 124 | String mopster_roomba_cleanSchedule2 "Schedule [%s]" (mopster_roomba_items) { channel="mqtt:topic:mopster_roomba:roomba_cleanSchedule2" } 125 | String mopster_roomba_cleanSchedule "Schedule [%s]" (mopster_roomba_items) 126 | /* General */ 127 | Switch mopster_roomba_control "Mop ON/OFF [%s]" (mopster_roomba_items) 128 | Number mopster_roomba_theta "Theta [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_theta" } 129 | Number mopster_roomba_x "X [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_x" } 130 | Number mopster_roomba_y "Y [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_y" } 131 | Number mopster_roomba_rssi "RSSI [%d]" (mopster_roomba_items) { channel="mqtt:topic:downstairs_mop:roomba_rssi" } 132 | DateTime mopster_roomba_lastheardfrom "Last Update [%1$ta %1$tR]" { channel="mqtt:topic:downstairs_mop:roomba_rssi" [profile="timestamp-update"] } -------------------------------------------------------------------------------- /openhab/sitemaps/roomba.sitemap: -------------------------------------------------------------------------------- 1 | Group item=roomba_items { 2 | Group item=downstairs_roomba_items label="Downstairs Roomba" { 3 | Switch item=downstairs_roomba_command mappings=[start="Start",stop="Stop",pause="Pause",dock="Dock",resume="Resume",train="Train", reset="Reset"] 4 | Group item=downstairs_roomba_items label="Map" icon="map" { 5 | Frame label="Map" { 6 | Webview icon="map" url="http://your_OH_ip:port/static/roomba/Downstairsroomba_map.html" height=21 label="Map" 7 | } 8 | } 9 | Group item=downstairs_roomba_items label="Settings" icon="select"{ 10 | Text item=downstairs_roomba_cleanSchedule 11 | Switch item=downstairs_roomba_edgeClean 12 | Switch item=downstairs_roomba_carpetBoost 13 | Switch item=downstairs_roomba_vacHigh visibility=[downstairs_roomba_carpetBoost==OFF] 14 | Switch item=downstairs_roomba_noAutoPasses 15 | Switch item=downstairs_roomba_twoPass visibility=[downstairs_roomba_noAutoPasses==OFF] 16 | Switch item=downstairs_roomba_binPause 17 | } 18 | Frame item=downstairs_roomba_lastcommand label="Status [%s]" { 19 | Text item=downstairs_roomba_softwareVer 20 | Text item=downstairs_roomba_batPct 21 | Text item=downstairs_roomba_phase 22 | Text item=downstairs_roomba_lastcommand 23 | Switch item=downstairs_roomba_full mappings=[ON="FULL", OFF="Not Full"] 24 | Switch item=downstairs_roomba_bin_present mappings=[OFF="Removed", ON="Installed"] 25 | Text item=downstairs_roomba_rssi 26 | Text item=downstairs_roomba_lastheardfrom 27 | } 28 | Frame item=downstairs_roomba_status label="Mission [%s]" { 29 | Text item=downstairs_roomba_status 30 | Text item=downstairs_roomba_rechrgM visibility=[downstairs_roomba_status=="Recharging"] 31 | Text item=downstairs_roomba_mission 32 | Text item=downstairs_roomba_percent_complete 33 | Switch item=downstairs_roomba_error mappings=[ON="ERROR!", OFF="Normal"] 34 | Text item=downstairs_roomba_errortext 35 | Text item=downstairs_roomba_mssnM 36 | Text item=downstairs_roomba_sqft 37 | Text item=downstairs_roomba_nMssn 38 | Text item=downstairs_roomba_lastmissioncompleted 39 | Text item=downstairs_roomba_initiator 40 | } 41 | Frame label="Location" { 42 | Text item=downstairs_roomba_theta 43 | Text item=downstairs_roomba_x 44 | Text item=downstairs_roomba_y 45 | } 46 | } 47 | Group item=mopster_roomba_items label="Downstairs Mop" { 48 | Switch item=mopster_roomba_command mappings=[start="Start",stop="Stop",pause="Pause",dock="Dock",resume="Resume",train="Train"] 49 | Group item=mopster_roomba_items label="Map" icon="map" { 50 | Frame label="Map" { 51 | Webview icon="map" url="http://your_OH_ip:port/static/roomba/Mopsterroomba_map.html" height=21 label="Map" 52 | } 53 | } 54 | Group item=mopster_roomba_items label="Settings" icon="select"{ 55 | Text item=mopster_roomba_cleanSchedule 56 | } 57 | Frame item=mopster_roomba_lastcommand label="Status [%s]" { 58 | Text item=mopster_roomba_softwareVer 59 | Text item=mopster_roomba_batPct 60 | Text item=mopster_roomba_phase 61 | Text item=mopster_roomba_lastcommand 62 | Switch item=mopster_roomba_lid_closed mappings=[OFF="Open", ON="Closed"] 63 | Switch item=mopster_roomba_bin_present mappings=[OFF="Removed", ON="Installed"] 64 | Text item=mopster_roomba_tankLvl 65 | Text item=mopster_roomba_detectedPad 66 | Text item=mopster_roomba_padWetness_disposable 67 | Text item=mopster_roomba_padWetness_reusable 68 | Text item=mopster_roomba_rssi 69 | Text item=mopster_roomba_lastheardfrom 70 | } 71 | Frame item=mopster_roomba_status label="Mission [%s]" { 72 | Text item=mopster_roomba_status 73 | Text item=mopster_roomba_rechrgM visibility=[mopster_roomba_status=="Recharging"] 74 | Text item=mopster_roomba_mission 75 | Text item=mopster_roomba_percent_complete 76 | Switch item=mopster_roomba_error mappings=[ON="ERROR!", OFF="Normal"] 77 | Text item=mopster_roomba_errortext 78 | Text item=mopster_roomba_mssnM 79 | Text item=mopster_roomba_sqft 80 | Text item=mopster_roomba_nMssn 81 | Text item=mopster_roomba_lastmissioncompleted 82 | Text item=mopster_roomba_initiator 83 | } 84 | Frame label="Location" { 85 | Text item=mopster_roomba_theta 86 | Text item=mopster_roomba_x 87 | Text item=mopster_roomba_y 88 | } 89 | } 90 | Group item=upstairs_roomba_items label="Upstairs Roomba" { 91 | Switch item=upstairs_roomba_command mappings=[start="Start",stop="Stop",pause="Pause",dock="Dock",resume="Resume"] 92 | Group item=upstairs_roomba_items label="Map" icon="map" { 93 | Frame label="Map" { 94 | Webview icon="map" url="http://your_OH_ip:port/static/roomba/Upstairsroomba_map.html" height=21 label="Map" 95 | } 96 | } 97 | Group item=upstairs_roomba_items label="Settings" icon="select"{ 98 | Text item=upstairs_roomba_cleanSchedule 99 | Switch item=upstairs_roomba_edgeClean 100 | Switch item=upstairs_roomba_carpetBoost 101 | Switch item=upstairs_roomba_vacHigh visibility=[upstairs_roomba_carpetBoost==OFF] 102 | Switch item=upstairs_roomba_noAutoPasses 103 | Switch item=upstairs_roomba_twoPass visibility=[upstairs_roomba_noAutoPasses==OFF] 104 | Switch item=upstairs_roomba_binPause 105 | } 106 | Frame item=upstairs_roomba_lastcommand label="Status [%s]" { 107 | Text item=upstairs_roomba_softwareVer 108 | Text item=upstairs_roomba_batPct 109 | Text item=upstairs_roomba_phase 110 | Text item=upstairs_roomba_lastcommand 111 | Switch item=upstairs_roomba_full mappings=[ON="FULL", OFF="Not Full"] 112 | Switch item=upstairs_roomba_bin_present mappings=[OFF="Removed", ON="Installed"] 113 | Text item=upstairs_roomba_rssi 114 | Text item=upstairs_roomba_lastheardfrom 115 | } 116 | Frame item=upstairs_roomba_status label="Mission [%s]" { 117 | Text item=upstairs_roomba_status 118 | Text item=upstairs_roomba_rechrgM visibility=[upstairs_roomba_status=="Recharging"] 119 | Text item=upstairs_roomba_mission 120 | Text item=upstairs_roomba_percent_complete 121 | Switch item=upstairs_roomba_error mappings=[ON="ERROR!", OFF="Normal"] 122 | Text item=upstairs_roomba_errortext 123 | Text item=upstairs_roomba_mssnM 124 | Text item=upstairs_roomba_sqft 125 | Text item=upstairs_roomba_nMssn 126 | Text item=upstairs_roomba_lastmissioncompleted 127 | Text item=upstairs_roomba_initiator 128 | } 129 | Frame label="Location" { 130 | Text item=upstairs_roomba_theta 131 | Text item=upstairs_roomba_x 132 | Text item=upstairs_roomba_y 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /openhab/start_openhab_roomba: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | ./roomba.py -b localhost -l ./Roomba.log -M /etc/openhab2/html -s '(800,1650,-300,-50,2,0)' 3 | -------------------------------------------------------------------------------- /openhab/things/roomba.things: -------------------------------------------------------------------------------- 1 | Bridge mqtt:broker:proliant "Proliant" [ 2 | host="Your_MQTT_broker_IP", 3 | port="1883", 4 | secure=false, 5 | //retainMessages=false, 6 | clientID="Openhab2_mqtt2" 7 | ] 8 | 9 | //Roomba things 10 | Thing mqtt:topic:upstairs_roomba "Upstairs Roomba" (mqtt:broker:proliant) { 11 | Channels: 12 | /* Roomba Commands */ 13 | Type string : roomba_command "Roomba" [ commandTopic="/roomba/command/Upstairs" ] 14 | /* Settings */ 15 | Type switch : roomba_edgeClean "Edge Clean" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/openOnly", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="openOnly %s", on="false", off="true" ] 16 | Type switch : roomba_carpetBoost "Auto carpet Boost" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/carpetBoost", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="carpetBoost %s", on="false", off="true" ] 17 | Type switch : roomba_vacHigh "Vacuum Boost" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/vacHigh", transformationPattern="MAP:switch.map", formatBeforePublish="vacHigh %s", on="false", off="true" ] 18 | Type switch : roomba_noAutoPasses "Auto Passes" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/noAutoPasses", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="noAutoPasses %s", on="false", off="true" ] 19 | Type switch : roomba_twoPass "Two Passes" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/twoPass", transformationPattern="MAP:switch.map", formatBeforePublish="twoPass %s", on="false", off="true" ] 20 | Type switch : roomba_binPause "Always Complete (even if bin is full)" [ commandTopic="/roomba/setting/Upstairs", stateTopic="/roomba/feedback/Upstairs/binPause", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="binPause %s", on="false", off="true" ] 21 | /* Roomba Feedback */ 22 | Type string : roomba_softwareVer "Software Version" [ stateTopic="/roomba/feedback/Upstairs/softwareVer" ] 23 | Type number : roomba_batPct "Battery" [ stateTopic="/roomba/feedback/Upstairs/batPct" ] 24 | Type string : roomba_lastcommand "Last Command" [ stateTopic="/roomba/feedback/Upstairs/lastCommand_command" ] 25 | Type switch : roomba_bin_present "Bin Present" [ stateTopic="/roomba/feedback/Upstairs/bin_present", transformationPattern="MAP:switch.map" ] 26 | Type switch : roomba_full "Bin Full" [ stateTopic="/roomba/feedback/Upstairs/bin_full", transformationPattern="MAP:switch.map" ] 27 | /* Mission values */ 28 | Type string : roomba_mission "Mission" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_cycle" ] 29 | Type number : roomba_nMssn "Cleaning Mission number" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_nMssn" ] 30 | Type string : roomba_phase "Phase" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_phase" ] 31 | Type string : roomba_initiator "Initiator" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_initiator" ] 32 | Type switch : roomba_error "Error" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_error" ] 33 | Type string : roomba_errortext "Error Message" [ stateTopic="/roomba/feedback/Upstairs/error_message" ] 34 | Type number : roomba_mssnM "Cleaning Elapsed Time" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_mssnM" ] 35 | Type number : roomba_sqft "Square Ft Cleaned" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_sqft" ] 36 | Type number : roomba_percent_complete "Mission % complete" [ stateTopic="/roomba/feedback/Upstairs/roomba_percent_complete" ] 37 | Type number : roomba_expireM "Mission Recharge Time" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_expireM" ] 38 | Type number : roomba_rechrgM "Remaining Time To Recharge" [ stateTopic="/roomba/feedback/Upstairs/cleanMissionStatus_rechrgM" ] 39 | Type string : roomba_status "Status" [ stateTopic="/roomba/feedback/Upstairs/state" ] 40 | /* Schedule */ 41 | Type string : roomba_cycle "Day of Week" [ stateTopic="/roomba/feedback/Upstairs/cleanSchedule_cycle" ] 42 | Type string : roomba_cleanSchedule_h "Hour of Day" [ stateTopic="/roomba/feedback/Upstairs/cleanSchedule_h" ] 43 | Type string : roomba_cleanSchedule_m "Minute of Hour" [ stateTopic="/roomba/feedback/Upstairs/cleanSchedule_m" ] 44 | Type string : roomba_cleanSchedule2 "New Schedule" [ stateTopic="/roomba/feedback/Upstairs/cleanSchedule2" ] 45 | /* General */ 46 | Type number : roomba_theta "Theta" [ stateTopic="/roomba/feedback/Upstairs/pose_theta" ] 47 | Type number : roomba_x "X" [ stateTopic="/roomba/feedback/Upstairs/pose_point_x" ] 48 | Type number : roomba_y "Y" [ stateTopic="/roomba/feedback/Upstairs/pose_point_y" ] 49 | Type number : roomba_rssi "RSSI" [ stateTopic="/roomba/feedback/Upstairs/signal_rssi" ] 50 | } 51 | 52 | Thing mqtt:topic:downstairs_roomba "Downstairs Roomba" (mqtt:broker:proliant) { 53 | Channels: 54 | /* Roomba Commands */ 55 | Type string : roomba_command "Roomba" [ commandTopic="/roomba/command/Downstairs" ] 56 | /* Settings */ 57 | Type switch : roomba_edgeClean "Edge Clean" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/openOnly", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="openOnly %s", on="false", off="true" ] 58 | Type switch : roomba_carpetBoost "Auto carpet Boost" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/carpetBoost", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="carpetBoost %s", on="false", off="true" ] 59 | Type switch : roomba_vacHigh "Vacuum Boost" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/vacHigh", transformationPattern="MAP:switch.map", formatBeforePublish="vacHigh %s", on="false", off="true" ] 60 | Type switch : roomba_noAutoPasses "Auto Passes" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/noAutoPasses", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="noAutoPasses %s", on="false", off="true" ] 61 | Type switch : roomba_twoPass "Two Passes" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/twoPass", transformationPattern="MAP:switch.map", formatBeforePublish="twoPass %s", on="false", off="true" ] 62 | Type switch : roomba_binPause "Always Complete (even if bin is full)" [ commandTopic="/roomba/setting/Downstairs", stateTopic="/roomba/feedback/Downstairs/binPause", transformationPattern="MAP:inverse_switch.map", formatBeforePublish="binPause %s", on="false", off="true" ] 63 | /* Roomba Feedback */ 64 | Type string : roomba_softwareVer "Software Version" [ stateTopic="/roomba/feedback/Downstairs/softwareVer" ] 65 | Type number : roomba_batPct "Battery" [ stateTopic="/roomba/feedback/Downstairs/batPct" ] 66 | Type string : roomba_lastcommand "Last Command" [ stateTopic="/roomba/feedback/Downstairs/lastCommand_command" ] 67 | Type switch : roomba_bin_present "Bin Present" [ stateTopic="/roomba/feedback/Downstairs/bin_present", transformationPattern="MAP:switch.map" ] 68 | Type switch : roomba_full "Bin Full" [ stateTopic="/roomba/feedback/Downstairs/bin_full", transformationPattern="MAP:switch.map" ] 69 | /* Mission values */ 70 | Type string : roomba_mission "Mission" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_cycle" ] 71 | Type number : roomba_nMssn "Cleaning Mission number" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_nMssn" ] 72 | Type string : roomba_phase "Phase" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_phase" ] 73 | Type string : roomba_initiator "Initiator" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_initiator" ] 74 | Type switch : roomba_error "Error" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_error" ] 75 | Type string : roomba_errortext "Error Message" [ stateTopic="/roomba/feedback/Downstairs/error_message" ] 76 | Type number : roomba_mssnM "Cleaning Elapsed Time" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_mssnM" ] 77 | Type number : roomba_sqft "Square Ft Cleaned" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_sqft" ] 78 | Type number : roomba_percent_complete "Mission % complete" [ stateTopic="/roomba/feedback/Downstairs/roomba_percent_complete" ] 79 | Type number : roomba_expireM "Mission Recharge Time" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_expireM" ] 80 | Type number : roomba_rechrgM "Remaining Time To Recharge" [ stateTopic="/roomba/feedback/Downstairs/cleanMissionStatus_rechrgM" ] 81 | Type string : roomba_status "Status" [ stateTopic="/roomba/feedback/Downstairs/state" ] 82 | /* Schedule */ 83 | Type string : roomba_cycle "Day of Week" [ stateTopic="/roomba/feedback/Downstairs/cleanSchedule_cycle" ] 84 | Type string : roomba_cleanSchedule_h "Hour of Day" [ stateTopic="/roomba/feedback/Downstairs/cleanSchedule_h" ] 85 | Type string : roomba_cleanSchedule_m "Minute of Hour" [ stateTopic="/roomba/feedback/Downstairs/cleanSchedule_m" ] 86 | Type string : roomba_cleanSchedule2 "New Schedule" [ stateTopic="/roomba/feedback/Downstairs/cleanSchedule2" ] 87 | /* General */ 88 | Type number : roomba_theta "Theta" [ stateTopic="/roomba/feedback/Downstairs/pose_theta" ] 89 | Type number : roomba_x "X" [ stateTopic="/roomba/feedback/Downstairs/pose_point_x" ] 90 | Type number : roomba_y "Y" [ stateTopic="/roomba/feedback/Downstairs/pose_point_y" ] 91 | Type number : roomba_rssi "RSSI" [ stateTopic="/roomba/feedback/Downstairs/signal_rssi" ] 92 | } 93 | 94 | Thing mqtt:topic:downstairs_mop "Downstairs Braava Jet M6" (mqtt:broker:proliant) { 95 | Channels: 96 | /* Braava Commands */ 97 | Type string : roomba_command "Braava" [ commandTopic="/roomba/command/Mopster" ] 98 | /* Braava Feedback */ 99 | Type string : roomba_softwareVer "Software Version" [ stateTopic="/roomba/feedback/Mopster/softwareVer" ] 100 | Type number : roomba_batPct "Battery" [ stateTopic="/roomba/feedback/Mopster/batPct" ] 101 | Type string : roomba_lastcommand "Last Command" [ stateTopic="/roomba/feedback/Mopster/lastCommand_command" ] 102 | Type string : roomba_detectedPad "Detected Pad" [ stateTopic="/roomba/feedback/Mopster/detectedPad" ] 103 | Type switch : roomba_lid_closed "Lid Closed" [ stateTopic="/roomba/feedback/Mopster/mopReady_lidClosed", transformationPattern="MAP:switch.map" ] 104 | Type switch : roomba_bin_present "Bin Present" [ stateTopic="/roomba/feedback/Mopster/mopReady_tankPresent", transformationPattern="MAP:switch.map" ] 105 | Type number : roomba_tankLvl "Tank Level" [ stateTopic="/roomba/feedback/Mopster/tankLvl" ] 106 | Type number : roomba_padWetness_disposable "Disposable Pad Wetness" [ stateTopic="/roomba/feedback/Mopster/padWetness_disposable" ] 107 | Type number : roomba_padWetness_reusable "Reusable Pad Wetness" [ stateTopic="/roomba/feedback/Mopster/padWetness_reusable" ] 108 | /* Mission values */ 109 | Type string : roomba_mission "Mission" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_cycle" ] 110 | Type number : roomba_nMssn "Cleaning Mission number" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_nMssn" ] 111 | Type string : roomba_phase "Phase" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_phase" ] 112 | Type string : roomba_initiator "Initiator" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_initiator" ] 113 | Type switch : roomba_error "Error" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_error" ] 114 | Type string : roomba_errortext "Error Message" [ stateTopic="/roomba/feedback/Mopster/error_message" ] 115 | Type number : roomba_mssnM "Cleaning Elapsed Time" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_mssnM" ] 116 | Type number : roomba_sqft "Square Ft Cleaned" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_sqft" ] 117 | Type number : roomba_percent_complete "Mission % complete" [ stateTopic="/roomba/feedback/Mopster/roomba_percent_complete" ] 118 | Type number : roomba_expireM "Mission Recharge Time" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_expireM" ] 119 | Type number : roomba_rechrgM "Remaining Time To Recharge" [ stateTopic="/roomba/feedback/Mopster/cleanMissionStatus_rechrgM" ] 120 | Type string : roomba_status "Status" [ stateTopic="/roomba/feedback/Mopster/state" ] 121 | /* Schedule */ 122 | Type string : roomba_cycle "Day of Week" [ stateTopic="/roomba/feedback/Mopster/cleanSchedule_cycle" ] 123 | Type string : roomba_cleanSchedule_h "Hour of Day" [ stateTopic="/roomba/feedback/Mopster/cleanSchedule_h" ] 124 | Type string : roomba_cleanSchedule_m "Minute of Hour" [ stateTopic="/roomba/feedback/Mopster/cleanSchedule_m" ] 125 | Type string : roomba_cleanSchedule2 "New Schedule" [ stateTopic="/roomba/feedback/Mopster/cleanSchedule2" ] 126 | /* General */ 127 | Type number : roomba_theta "Theta" [ stateTopic="/roomba/feedback/Mopster/pose_theta" ] 128 | Type number : roomba_x "X" [ stateTopic="/roomba/feedback/Mopster/pose_point_x" ] 129 | Type number : roomba_y "Y" [ stateTopic="/roomba/feedback/Mopster/pose_point_y" ] 130 | Type number : roomba_rssi "RSSI" [ stateTopic="/roomba/feedback/Mopster/signal_rssi" ] 131 | } -------------------------------------------------------------------------------- /openhab/transform/inverse_switch.map: -------------------------------------------------------------------------------- 1 | ON=OFF 2 | OFF=ON 3 | 0=ON 4 | 1=OFF 5 | True=OFF 6 | False=ON 7 | true=OFF 8 | false=ON 9 | -=Unknown 10 | NULL=Unknown 11 | -------------------------------------------------------------------------------- /openhab/transform/switch.map: -------------------------------------------------------------------------------- 1 | ON=ON 2 | OFF=OFF 3 | 0=OFF 4 | 1=ON 5 | True=ON 6 | False=OFF 7 | true=ON 8 | false=OFF 9 | -=Unknown 10 | NULL=Unknown 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.12.1 2 | opencv-python>=3.2.0.7 3 | paho-mqtt>=1.5.1 4 | Pillow>=6.2.0 -------------------------------------------------------------------------------- /roomba/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .roomba import Roomba 3 | from .password import Password 4 | -------------------------------------------------------------------------------- /roomba/__main__.py: -------------------------------------------------------------------------------- 1 | from roomba_direct import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /roomba/config_example.ini: -------------------------------------------------------------------------------- 1 | [192.168.1.181] 2 | blid = 3117XXXXXXXXXXXXX 3 | password = :1:15XXXX571:3EXXXXXXXXXRnNX 4 | data = {'cap': {'binFullDetect': 1, 5 | 'carpetBoost': 1, 6 | 'eco': 1, 7 | 'edge': 1, 8 | 'langOta': 1, 9 | 'maps': 1, 10 | 'multiPass': 2, 11 | 'ota': 2, 12 | 'pose': 1, 13 | 'pp': 1, 14 | 'svcConf': 1}, 15 | 'hostname': 'Roomba-3117XXXXXXXXX', 16 | 'ip': '192.168.1.181', 17 | 'mac': 'F0:03:1C:13:64:5B', 18 | 'nc': 0, 19 | 'proto': 'mqtt', 20 | 'robotname': 'Upstairs', 21 | 'sku': 'R980020', 22 | 'sw': 'v2.4.6-3', 23 | 'ver': '3'} 24 | drawmap = True 25 | mapsize = (1700,750,-750,20,90,90) 26 | floorplan = ("res/first_floor.jpg",0,-25,0.85,90,0.2) 27 | room_outline = False 28 | max_sqft = 280 29 | iconpath = /etc/openhab2/icons/classic/ 30 | 31 | [192.168.1.206] 32 | blid = D07CXXXXXXXXXXXXXXXE1AEAD 33 | password = :1:1611XXXX203:MuXXXXXXXXXXXXXXxpu 34 | data = {'cap': {'5ghz': 1, 35 | 'area': 1, 36 | 'binFullDetect': 2, 37 | 'carpetBoost': 1, 38 | 'dockComm': 1, 39 | 'eco': 1, 40 | 'edge': 0, 41 | 'lang': 2, 42 | 'langOta': 0, 43 | 'log': 2, 44 | 'maps': 3, 45 | 'multiPass': 2, 46 | 'ota': 2, 47 | 'pmaps': 5, 48 | 'pose': 1, 49 | 'pp': 0, 50 | 'prov': 3, 51 | 'sched': 1, 52 | 'svcConf': 1, 53 | 'tLine': 2, 54 | 'team': 1, 55 | 'tileScan': 1}, 56 | 'hostname': 'iRobot-D07CXXXXXXXXXXXXXXXXXXXXE1AEAD', 57 | 'ip': '192.168.1.206', 58 | 'mac': '50:74:7A:72:6E:79', 59 | 'nc': 0, 60 | 'proto': 'mqtt', 61 | 'robotid': 'D07CXXXXXXXXXXXXXXXXE1AEAD', 62 | 'robotname': 'Downstairs', 63 | 'sku': 's955020', 64 | 'sw': 'soho+3.12.8+soho-release-420+12', 65 | 'ver': '3'} 66 | drawmap = True 67 | mapsize = (1700,750,-40,280,-90,-90) 68 | floorplan = ("res/ground_floor.jpg",0,0,0.85,-90,0.2) 69 | room_outline = False 70 | max_sqft = 400 71 | iconpath = /etc/openhab2/icons/classic/ 72 | 73 | [192.168.1.79] 74 | blid = 4F28XXXXXXXXXXXXXXXXXX563E 75 | password = :1:16XXXXXXX18:0XXXXXXXXXXXXXSgtg 76 | data = {'cap': {'5ghz': 1, 77 | 'area': 1, 78 | 'eco': 1, 79 | 'edge': 0, 80 | 'log': 2, 81 | 'maps': 3, 82 | 'multiPass': 2, 83 | 'ota': 2, 84 | 'pmaps': 4, 85 | 'pose': 1, 86 | 'pp': 0, 87 | 'prov': 3, 88 | 'sched': 1, 89 | 'svcConf': 1, 90 | 'tHold': 1, 91 | 'tLine': 2, 92 | 'team': 1, 93 | 'tileScan': 1}, 94 | 'hostname': 'iRobot-4F28XXXXXXXXXXXXXXX563E', 95 | 'ip': '192.168.1.79', 96 | 'mac': '50:84:79:29:B2:0D', 97 | 'nc': 0, 98 | 'proto': 'mqtt', 99 | 'robotid': '4F28XXXXXXXXXXXXXXXXXXX563E', 100 | 'robotname': 'Mopster', 101 | 'sku': 'm611220', 102 | 'sw': 'sanmarino+3.10.8+sanmarino-release-rt320+11', 103 | 'ver': '3'} 104 | drawmap = True 105 | mapsize = (1700,750,-220,-280,90,90) 106 | floorplan = ("res/ground_floor.jpg",0,0,0.85,90,0.2) 107 | room_outline = False 108 | max_sqft = 240 109 | iconpath = /etc/openhab2/icons/classic/ 110 | 111 | -------------------------------------------------------------------------------- /roomba/getcloudpassword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Matthew Garrett 4 | # 5 | # Portions Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All 6 | # Rights Reserved. 7 | # 8 | # This file is licensed under the Apache License, Version 2.0 (the 9 | # "License"). You may not use this file except in compliance with the 10 | # License. A copy of the License is located at 11 | # 12 | # http://aws.amazon.com/apache2.0/ 13 | # 14 | # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 15 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations under the License. 17 | 18 | import sys, os, base64, datetime, hashlib, hmac 19 | import random 20 | import requests 21 | import time 22 | import urllib.parse 23 | 24 | class awsRequest: 25 | def __init__(self, region, access_key, secret_key, session_token, service): 26 | self.region = region 27 | self.access_key = access_key 28 | self.secret_key = secret_key 29 | self.session_token = session_token 30 | self.service = service 31 | 32 | def sign(self, key, msg): 33 | return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() 34 | 35 | def getSignatureKey(self, key, dateStamp, regionName, serviceName): 36 | kDate = self.sign(('AWS4' + key).encode('utf-8'), dateStamp) 37 | kRegion = self.sign(kDate, regionName) 38 | kService = self.sign(kRegion, serviceName) 39 | kSigning = self.sign(kService, 'aws4_request') 40 | return kSigning 41 | 42 | def get(self, host, uri, query=""): 43 | method = "GET" 44 | 45 | # Create a date for headers and the credential string 46 | t = datetime.datetime.utcnow() 47 | amzdate = t.strftime('%Y%m%dT%H%M%SZ') 48 | datestamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 49 | 50 | canonical_uri = uri 51 | canonical_querystring = query 52 | canonical_headers = 'host:' + host + '\n' + 'x-amz-date:' + amzdate + '\n' + 'x-amz-security-token:' + self.session_token + '\n' 53 | signed_headers = 'host;x-amz-date;x-amz-security-token' 54 | payload_hash = hashlib.sha256(('').encode('utf-8')).hexdigest() 55 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 56 | 57 | algorithm = 'AWS4-HMAC-SHA256' 58 | credential_scope = datestamp + '/' + self.region + '/' + self.service + '/' + 'aws4_request' 59 | string_to_sign = algorithm + '\n' + amzdate + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 60 | 61 | signing_key = self.getSignatureKey(self.secret_key, datestamp, self.region, self.service) 62 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 63 | 64 | authorization_header = algorithm + ' ' + 'Credential=' + self.access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 65 | headers = {'x-amz-security-token': self.session_token, 'x-amz-date':amzdate, 'Authorization':authorization_header} 66 | 67 | req = "https://%s%s" % (host, uri) 68 | if query != "": 69 | req += "?%s" % query 70 | return requests.get(req, headers=headers) 71 | 72 | class irobotAuth: 73 | def __init__(self, username, password): 74 | self.username = username 75 | self.password = password 76 | 77 | def login(self): 78 | r = requests.get("https://disc-prod.iot.irobotapi.com/v1/discover/endpoints?country_code=US") 79 | response = r.json() 80 | deployment = response['deployments'][next(iter(response['deployments']))] 81 | self.httpBase = deployment['httpBase'] 82 | iotBase = deployment['httpBaseAuth'] 83 | iotUrl = urllib.parse.urlparse(iotBase) 84 | self.iotHost = iotUrl.netloc 85 | region = deployment['awsRegion'] 86 | 87 | self.apikey = response['gigya']['api_key'] 88 | self.gigyaBase = response['gigya']['datacenter_domain'] 89 | 90 | data = {"apiKey": self.apikey, 91 | "targetenv": "mobile", 92 | "loginID": self.username, 93 | "password": self.password, 94 | "format": "json", 95 | "targetEnv": "mobile", 96 | } 97 | 98 | r = requests.post("https://accounts.%s/accounts.login" % self.gigyaBase, data=data) 99 | 100 | response = r.json() 101 | ''' 102 | data = {"timestamp": int(time.time()), 103 | "nonce": "%d_%d" % (int(time.time()), random.randint(0, 2147483647)), 104 | "oauth_token": response.get('sessionInfo', {}).get('sessionToken', ''), 105 | "targetEnv": "mobile"} 106 | ''' 107 | uid = response['UID'] 108 | uidSig = response['UIDSignature'] 109 | sigTime = response['signatureTimestamp'] 110 | 111 | data = { 112 | "app_id": "ANDROID-C7FB240E-DF34-42D7-AE4E-A8C17079A294", 113 | "assume_robot_ownership": "0", 114 | "gigya": { 115 | "signature": uidSig, 116 | "timestamp": sigTime, 117 | "uid": uid, 118 | } 119 | } 120 | 121 | r = requests.post("%s/v2/login" % self.httpBase, json=data) 122 | 123 | response = r.json() 124 | access_key = response['credentials']['AccessKeyId'] 125 | secret_key = response['credentials']['SecretKey'] 126 | session_token = response['credentials']['SessionToken'] 127 | 128 | self.data = response 129 | 130 | self.amz = awsRequest(region, access_key, secret_key, session_token, "execute-api") 131 | 132 | def get_robots(self): 133 | return self.data['robots'] 134 | 135 | def get_maps(self, robot): 136 | return self.amz.get(self.iotHost, '/dev/v1/%s/pmaps' % robot, query="activeDetails=2").json() 137 | 138 | def get_newest_map(self, robot): 139 | maps = self.get_maps(robot) 140 | latest = "" 141 | latest_time = 0 142 | for map in maps: 143 | if map['create_time'] > latest_time: 144 | latest_time = map['create_time'] 145 | latest = map 146 | return latest 147 | 148 | def main(): 149 | import argparse, logging, json 150 | loglevel = logging.DEBUG 151 | LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s" 152 | LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" 153 | logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=loglevel) 154 | 155 | #-------- Command Line ----------------- 156 | parser = argparse.ArgumentParser( 157 | description='Get password and map data from iRobot aws cloud service') 158 | parser.add_argument( 159 | 'login', 160 | nargs='*', 161 | action='store', 162 | type=str, 163 | default=[], 164 | help='iRobot Account Login and Password (default: None)') 165 | parser.add_argument( 166 | '-m', '--maps', 167 | action='store_true', 168 | default = False, 169 | help='List maps (default: %(default)s)') 170 | 171 | arg = parser.parse_args() 172 | 173 | if len(arg.login) >= 2: 174 | irobot = irobotAuth(arg.login[0], arg.login[1]) 175 | irobot.login() 176 | robots = irobot.get_robots() 177 | logging.info("Robot ID and data: {}".format(json.dumps(robots, indent=2))) 178 | if arg.maps: 179 | for robot in robots.keys(): 180 | logging.info("Robot ID {}, MAPS: {}".format(robot, json.dumps(irobot.get_maps(robot), indent=2))) 181 | else: 182 | logging.error("Please enter iRobot account login and password") 183 | 184 | if __name__ == '__main__': 185 | main() 186 | -------------------------------------------------------------------------------- /roomba/getpassword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = "2.0" 5 | ''' 6 | Python 3.6 7 | Quick Program to get blid and password from roomba 8 | 9 | Nick Waterton 5th May 2017: V 1.0: Initial Release 10 | Nick Waterton 22nd Dec 2020: V2.0: now just calls password.main() 11 | ''' 12 | from password import main 13 | 14 | if __name__ == '__main__': 15 | main() 16 | -------------------------------------------------------------------------------- /roomba/password.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = "2.0a" 5 | ''' 6 | Python 3.6 7 | Quick Program to get blid and password from roomba 8 | 9 | Nick Waterton 5th May 2017: V 1.0: Initial Release 10 | Nick Waterton 22nd Dec 2020: V2.0: Updated for i and S Roomba versions, update to minimum python version 3.6 11 | ''' 12 | 13 | from pprint import pformat 14 | import json 15 | import logging 16 | import socket 17 | import ssl 18 | import sys 19 | import time 20 | from ast import literal_eval 21 | import configparser 22 | 23 | class Password(object): 24 | ''' 25 | Get Roomba blid and password - only V2 firmware supported 26 | if IP is not supplied, class will attempt to discover the Roomba IP first. 27 | Results are written to a config file, default ".\config.ini" 28 | V 1.2.3 NW 9/10/2018 added support for Roomba i7 29 | V 1.2.5 NW 7/10/2019 changed PROTOCOL_TLSv1 to PROTOCOL_TLS to fix i7 software connection problem 30 | V 1.2.6 NW 12/11/2019 add cipher to ssl to avoid dh_key_too_small issue 31 | V 2.0 NW 22nd Dec 2020 updated for S and i versions plus braava jet m6, min version of python 3.6 32 | V 2.1 NW 9th Dec 2021 Added getting password from aws cloud. 33 | ''' 34 | 35 | VERSION = __version__ = "2.1" 36 | 37 | config_dicts = ['data', 'mapsize', 'pmaps', 'regions'] 38 | 39 | def __init__(self, address='255.255.255.255', file=".\config.ini", login=[]): 40 | self.address = address 41 | self.file = file 42 | self.login = None 43 | self.password = None 44 | if len(login) >= 2: 45 | self.login = login[0] 46 | self.password = login[1] 47 | self.log = logging.getLogger('Roomba.{}'.format(__class__.__name__)) 48 | self.log.info("Using Password version {}".format(self.__version__)) 49 | 50 | def read_config_file(self): 51 | #read config file 52 | Config = configparser.ConfigParser() 53 | roombas = {} 54 | try: 55 | Config.read(self.file) 56 | self.log.info("reading/writing info from config file {}".format(self.file)) 57 | roombas = {s:{k:literal_eval(v) if k in self.config_dicts else v for k, v in Config.items(s)} for s in Config.sections()} 58 | #self.log.info('data read from {}: {}'.format(self.file, pformat(roombas))) 59 | except Exception as e: 60 | self.log.exception(e) 61 | return roombas 62 | 63 | def receive_udp(self): 64 | #set up UDP socket to receive data from robot 65 | port = 5678 66 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 67 | s.settimeout(10) 68 | if self.address == '255.255.255.255': 69 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 70 | s.bind(("", port)) #bind all interfaces to port 71 | self.log.info("waiting on port: {} for data".format(port)) 72 | message = 'irobotmcs' 73 | s.sendto(message.encode(), (self.address, port)) 74 | roomba_dict = {} 75 | while True: 76 | try: 77 | udp_data, addr = s.recvfrom(1024) #wait for udp data 78 | #self.log.debug('Received: Robot addr: {} Data: {}'.format(addr, udp_data)) 79 | if udp_data and udp_data.decode() != message: 80 | try: 81 | #if self.address != addr[0]: 82 | # self.log.warning( 83 | # "supplied address {} does not match " 84 | # "discovered address {}, using discovered " 85 | # "address...".format(self.address, addr[0])) 86 | 87 | parsedMsg = json.loads(udp_data.decode()) 88 | if addr[0] not in roomba_dict.keys(): 89 | s.sendto(message.encode(), (self.address, port)) 90 | roomba_dict[addr[0]]=parsedMsg 91 | self.log.info('Robot at IP: {} Data: {}'.format(addr[0], json.dumps(parsedMsg, indent=2))) 92 | except Exception as e: 93 | self.log.info("json decode error: {}".format(e)) 94 | self.log.info('RECEIVED: {}'.format(pformat(udp_data))) 95 | 96 | except socket.timeout: 97 | break 98 | s.close() 99 | return roomba_dict 100 | 101 | def add_cloud_data(self, cloud_data, roombas): 102 | for k, v in roombas.copy().items(): 103 | robotid = v.get('robotid', v.get("hostname", "").split('-')[1]) 104 | for id, data in cloud_data.items(): 105 | if robotid == id: 106 | roombas[k]["password"] = data.get('password') 107 | return roombas 108 | 109 | def get_password(self): 110 | #load roombas from config file 111 | file_roombas = self.read_config_file() 112 | cloud_roombas = {} 113 | #get roomba info 114 | roombas = self.receive_udp() 115 | if self.login and self.password: 116 | self.log.info("Getting Roomba information from iRobot aws cloud...") 117 | from getcloudpassword import irobotAuth 118 | iRobot = irobotAuth(self.login, self.password) 119 | iRobot.login() 120 | cloud_roombas = iRobot.get_robots() 121 | self.log.info("Got cloud info: {}".format(json.dumps(cloud_roombas, indent=2))) 122 | self.log.info("Found {} roombas defined in the cloud".format(len(cloud_roombas))) 123 | if len(cloud_roombas) > 0 and len(roombas) > 0: 124 | roombas = self.add_cloud_data(cloud_roombas, roombas) 125 | 126 | if len(roombas) == 0: 127 | self.log.warning("No Roombas found on network, try again...") 128 | return False 129 | 130 | self.log.info("{} robot(s) already defined in file{}, found {} robot(s) on network".format(len(file_roombas), self.file, len(roombas))) 131 | 132 | for addr, parsedMsg in roombas.items(): 133 | blid = parsedMsg.get('robotid', parsedMsg.get("hostname", "").split('-')[1]) 134 | robotname = parsedMsg.get('robotname', 'unknown') 135 | if int(parsedMsg.get("ver", "3")) < 2: 136 | self.log.info("Roombas at address: {} does not have the correct " 137 | "firmware version. Your version info is: {}".format(addr,json.dumps(parsedMsg, indent=2))) 138 | continue 139 | 140 | password = parsedMsg.get('password') 141 | if password is None: 142 | self.log.info("To add/update Your robot details," 143 | "make sure your robot ({}) at IP {} is on the Home Base and " 144 | "powered on (green lights on). Then press and hold the HOME " 145 | "button on your robot until it plays a series of tones " 146 | "(about 2 seconds). Release the button and your robot will " 147 | "flash WIFI light.".format(robotname, addr)) 148 | else: 149 | self.log.info("Configuring robot ({}) at IP {} from cloud data, blid: {}, password: {}".format(robotname, addr, blid, password)) 150 | if sys.stdout.isatty(): 151 | char = input("Press to continue...\r\ns to skip configuring this robot: ") 152 | if char == 's': 153 | self.log.info('Skipping') 154 | continue 155 | 156 | #self.log.info("Received: %s" % json.dumps(parsedMsg, indent=2)) 157 | 158 | if password is None: 159 | self.log.info("Roomba ({}) IP address is: {}".format(robotname, addr)) 160 | data = self.get_password_from_roomba(addr) 161 | 162 | if len(data) <= 7: 163 | self.log.error( 'Error getting password for robot {} at ip{}, received {} bytes. ' 164 | 'Follow the instructions and try again.'.format(robotname, addr, len(data))) 165 | continue 166 | # Convert password to str 167 | password = str(data[7:].decode().rstrip('\x00')) #for i7 - has null termination 168 | self.log.info("blid is: {}".format(blid)) 169 | self.log.info('Password=> {} <= Yes, all this string.'.format(password)) 170 | self.log.info('Use these credentials in roomba.py') 171 | 172 | file_roombas.setdefault(addr, {}) 173 | file_roombas[addr]['blid'] = blid 174 | file_roombas[addr]['password'] = password 175 | file_roombas[addr]['data'] = parsedMsg 176 | return self.save_config_file(file_roombas) 177 | 178 | def get_password_from_roomba(self, addr): 179 | ''' 180 | Send MQTT magic packet to addr 181 | this is 0xf0 (mqtt reserved) 0x05(data length) 0xefcc3b2900 (data) 182 | Should receive 37 bytes containing the password for roomba at addr 183 | This is is 0xf0 (mqtt RESERVED) length (0x23 = 35) 0xefcc3b2900 (magic packet), 184 | followed by 0xXXXX... (30 bytes of password). so 7 bytes, followed by 30 bytes of password 185 | total of 37 bytes 186 | Uses 10 second timeout for socket connection 187 | ''' 188 | data = b'' 189 | packet = bytes.fromhex('f005efcc3b2900') 190 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 191 | sock.settimeout(10) 192 | 193 | #context = ssl.SSLContext(ssl.PROTOCOL_TLS) 194 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 195 | context.check_hostname = False 196 | context.verify_mode = ssl.CERT_NONE 197 | #context.set_ciphers('DEFAULT@SECLEVEL=1:HIGH:!DH:!aNULL') 198 | wrappedSocket = context.wrap_socket(sock) 199 | 200 | try: 201 | wrappedSocket.connect((addr, 8883)) 202 | self.log.debug('Connection Successful') 203 | wrappedSocket.send(packet) 204 | self.log.debug('Waiting for data') 205 | 206 | while len(data) < 37: 207 | data_received = wrappedSocket.recv(1024) 208 | data+= data_received 209 | if len(data_received) == 0: 210 | self.log.info("socket closed") 211 | break 212 | 213 | wrappedSocket.close() 214 | return data 215 | 216 | except socket.timeout as e: 217 | self.log.error('Connection Timeout Error (for {}): {}'.format(addr, e)) 218 | except (ConnectionRefusedError, OSError) as e: 219 | if e.errno == 111: #errno.ECONNREFUSED 220 | self.log.error('Unable to Connect to roomba at ip {}, make sure nothing else is connected (app?), ' 221 | 'as only one connection at a time is allowed'.format(addr)) 222 | elif e.errno == 113: #errno.No Route to Host 223 | self.log.error('Unable to contact roomba on ip {} is the ip correct?'.format(addr)) 224 | else: 225 | self.log.error("Connection Error (for {}): {}".format(addr, e)) 226 | except Exception as e: 227 | self.log.exception(e) 228 | 229 | self.log.error('Unable to get password from roomba') 230 | return data 231 | 232 | def save_config_file(self, roomba): 233 | Config = configparser.ConfigParser() 234 | if roomba: 235 | for addr, data in roomba.items(): 236 | Config.add_section(addr) 237 | for k, v in data.items(): 238 | #self.log.info('saving K: {}, V: {}'.format(k, pformat(v) if k in self.config_dicts else v)) 239 | Config.set(addr,k, pformat(v) if k in self.config_dicts else v) 240 | # write config file 241 | with open(self.file, 'w') as cfgfile: 242 | Config.write(cfgfile) 243 | self.log.info('Configuration saved to {}'.format(self.file)) 244 | else: return False 245 | return True 246 | 247 | def get_roombas(self): 248 | roombas = self.read_config_file() 249 | if not roombas: 250 | self.log.warn("No roomba or config file defined, I will attempt to " 251 | "discover Roombas, please put the Roomba on the dock " 252 | "and follow the instructions:") 253 | self.get_password() 254 | return self.get_roombas() 255 | self.log.info("{} Roombas Found".format(len(roombas))) 256 | for ip in roombas.keys(): 257 | roombas[ip]["roomba_name"] = roombas[ip]['data']['robotname'] 258 | return roombas 259 | 260 | def main(): 261 | import argparse 262 | loglevel = logging.DEBUG 263 | LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s" 264 | LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" 265 | logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=loglevel) 266 | 267 | #-------- Command Line ----------------- 268 | parser = argparse.ArgumentParser( 269 | description='Get Robot passwords and update config file') 270 | parser.add_argument( 271 | 'login', 272 | nargs='*', 273 | action='store', 274 | type=str, 275 | default=[], 276 | help='iRobot Account Login and Password (default: None)') 277 | parser.add_argument( 278 | '-f', '--configfile', 279 | action='store', 280 | type=str, 281 | default="./config.ini", 282 | help='config file name, (default: %(default)s)') 283 | parser.add_argument( 284 | '-R','--roombaIP', 285 | action='store', 286 | type=str, 287 | default='255.255.255.255', 288 | help='ipaddress of Roomba (default: %(default)s)') 289 | 290 | arg = parser.parse_args() 291 | 292 | get_passwd = Password(arg.roombaIP, file=arg.configfile, login=arg.login) 293 | get_passwd.get_password() 294 | 295 | if __name__ == '__main__': 296 | main() 297 | 298 | -------------------------------------------------------------------------------- /roomba/replay_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Python 3.6 Program to test roomba mapping by replaying a log file 6 | This is for debugging only! use at your own risk... 7 | ''' 8 | import re 9 | from datetime import datetime 10 | import os 11 | import paho.mqtt.client as mqtt 12 | import time 13 | import argparse 14 | import logging as log 15 | 16 | end_mission = '{"state":{"reported":{"cleanMissionStatus":{"cycle":"none","phase":"charge","expireM":0,"rechrgM":0,"error":0,"notReady":0,"mssnM":0,"sqft":0,"initiator":"schedule","nMssn":0}}}}' 17 | 18 | def valid_datetime_type(arg_datetime_str): 19 | ''' 20 | custom argparse type for user datetime values given from the command line 21 | ''' 22 | if arg_datetime_str is None: 23 | return None 24 | try: 25 | return datetime.strptime(arg_datetime_str, "%Y-%m-%d %H:%M:%S") 26 | except ValueError: 27 | msg = "Given Datetime ({0}) not valid! Expected format, 'YYYY-MM-DD HH:mm:ss'! (use ' around the date/time)".format(arg_datetime_str) 28 | raise argparse.ArgumentTypeError(msg) 29 | 30 | def parse_args(): 31 | default_icon_path = os.path.join(os.path.dirname(__file__), 'res') 32 | #-------- Command Line ----------------- 33 | parser = argparse.ArgumentParser( 34 | description='Replay Roomba log to test mapping') 35 | parser.add_argument( 36 | '-n', '--roombaName', 37 | action='store', 38 | type=str, 39 | default="", help='optional Roomba name (default: "")') 40 | parser.add_argument( 41 | '-pn', '--pubroombaName', 42 | action='store', 43 | type=str, 44 | default="", help='optional Roomba name to publish to (default: "")') 45 | parser.add_argument( 46 | '-m', '--missionStart', 47 | action='store', 48 | type=valid_datetime_type, 49 | default=None, help='optional date/time to start parsing from, format is "2021-01-13 14:57:06" (default: None)') 50 | parser.add_argument( 51 | '-s', '--start_mission', 52 | action='store_true', 53 | default = False, 54 | help='Start Mission immediately (default: %(default)s)') 55 | parser.add_argument( 56 | '-C', '--brokerCommand', 57 | action='store', 58 | type=str, 59 | default="/roomba/simulate", 60 | help='Topic on broker to publish commands to (default: ' 61 | '/roomba/simulate)') 62 | parser.add_argument( 63 | '-b', '--broker', 64 | action='store', 65 | type=str, 66 | default=None, 67 | help='ipaddress of MQTT broker (default: None)') 68 | parser.add_argument( 69 | '-p', '--port', 70 | action='store', 71 | type=int, 72 | default=1883, 73 | help='MQTT broker port number (default: 1883)') 74 | parser.add_argument( 75 | '-U', '--user', 76 | action='store', 77 | type=str, 78 | default=None, 79 | help='MQTT broker user name (default: None)') 80 | parser.add_argument( 81 | '-P', '--password', 82 | action='store', 83 | type=str, 84 | default=None, 85 | help='MQTT broker password (default: None)') 86 | parser.add_argument( 87 | 'log', 88 | action='store', 89 | type=str, 90 | default=None, 91 | help='path/name of log file (default: None)') 92 | return parser.parse_args() 93 | 94 | def setup_client(user=None, password=None): 95 | client = mqtt.Client(protocol=mqtt.MQTTv311) 96 | # Assign event callbacks 97 | #client.on_message = on_message 98 | #client.on_connect = on_connect 99 | #client.on_publish = on_publish 100 | #client.on_subscribe = on_subscribe 101 | #client.on_disconnect = on_disconnect 102 | 103 | # Uncomment to enable debug messages 104 | #self.client.on_log = self.on_log 105 | 106 | if all([user, password]): 107 | self.client.username_pw_set(user, password) 108 | return client 109 | 110 | def publish(mqttc, topic, msg): 111 | if mqttc: 112 | log.info('publishing: {}: {}'.format(topic, msg)) 113 | mqttc.publish(topic, msg) 114 | 115 | def lines_from_file(filename, roomba_name='', startdate=None): 116 | ''' 117 | date format 2021-01-13 14:57:06 118 | ''' 119 | log.info('reading file: {}'.format(filename)) 120 | date = None 121 | with open(filename) as f: 122 | for line in f: 123 | if startdate: 124 | match = re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', line) 125 | if match: 126 | date = datetime.strptime(match.group(), '%Y-%m-%d %H:%M:%S') 127 | if startdate and date and date < startdate: 128 | continue 129 | #log.info('line: {}'.format(line)) 130 | if roomba_name: 131 | if 'Roomba.{}'.format(roomba_name) in line: 132 | yield line 133 | else: 134 | yield line 135 | 136 | def replay_data(gen, mission=False): 137 | for line in gen: 138 | if 'New Mission' in line: 139 | mission = True 140 | if mission: 141 | if 'reported' in line: 142 | data = line.find('{') 143 | if data: 144 | message = line[data:].replace("'","").rstrip() 145 | #log.info(message) 146 | yield message 147 | 148 | 149 | def main(): 150 | arg = parse_args() 151 | log.basicConfig(level=log.INFO) 152 | log.info("*******************") 153 | log.info("* Program Started *") 154 | log.info("*******************") 155 | 156 | if not os.path.isfile(arg.log): 157 | log.warning('File {} does not exist'.format(arg.log)) 158 | return 159 | 160 | if not arg.pubroombaName: 161 | arg.pubroombaName = arg.roombaName 162 | brokerCommand = '{}{}'.format(arg.brokerCommand, '/{}'.format(arg.pubroombaName) if arg.pubroombaName else '') 163 | 164 | log.info('reading file: {}, Roomba: {}, publish to {} Mission Date: {}'.format(arg.log, arg.roombaName, brokerCommand, arg.missionStart)) 165 | 166 | file_reader = lines_from_file(arg.log, arg.roombaName, arg.missionStart) 167 | data_gen = replay_data(file_reader, arg.start_mission) 168 | 169 | if arg.broker: 170 | mqttc = setup_client(arg.user, arg.password) 171 | mqttc.connect(arg.broker, arg.port, 60) 172 | mqttc.loop_start() 173 | else: 174 | mqttc = None 175 | 176 | try: 177 | for data in data_gen: 178 | publish(mqttc, brokerCommand, data) 179 | time.sleep(0.5) #do not make longer than 9 seconds!, and 0.5 is as low as you can go 180 | 181 | except KeyboardInterrupt: 182 | log.info('program exit') 183 | publish(mqttc, brokerCommand, end_mission) 184 | except Exception as e: 185 | log.error('program error: {}'.format(e)) 186 | finally: 187 | if mqttc: 188 | mqttc.loop_stop() 189 | 190 | if __name__ == '__main__': 191 | main() -------------------------------------------------------------------------------- /roomba/res/app_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/app_map.png -------------------------------------------------------------------------------- /roomba/res/binfull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/binfull.png -------------------------------------------------------------------------------- /roomba/res/first_floor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/first_floor.jpg -------------------------------------------------------------------------------- /roomba/res/ground_floor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/ground_floor.jpg -------------------------------------------------------------------------------- /roomba/res/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/home.png -------------------------------------------------------------------------------- /roomba/res/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/map.png -------------------------------------------------------------------------------- /roomba/res/roomba-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/roomba-base.png -------------------------------------------------------------------------------- /roomba/res/roomba-charge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/roomba-charge.png -------------------------------------------------------------------------------- /roomba/res/roomba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/roomba.png -------------------------------------------------------------------------------- /roomba/res/roombacancelled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/roombacancelled.png -------------------------------------------------------------------------------- /roomba/res/roombaerror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/roombaerror.png -------------------------------------------------------------------------------- /roomba/res/side_by_side_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/side_by_side_map.png -------------------------------------------------------------------------------- /roomba/res/tanklow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/tanklow.png -------------------------------------------------------------------------------- /roomba/res/web_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/res/web_interface.png -------------------------------------------------------------------------------- /roomba/roomba_direct.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # hacky fixes NW 30th Nov 2019 5 | # Pillow fix for V 7 __version__ replaced with __version__ 6 | # Jan 8th 2021 NW Complete re-write 7 | # Mar 11th 2021 NW V 2.0c added floorplan option, removed certificate option 8 | 9 | __version__ = "2.0c" 10 | 11 | import logging 12 | from logging.handlers import RotatingFileHandler 13 | import sys 14 | from roomba import Roomba 15 | from password import Password 16 | from ast import literal_eval 17 | import argparse 18 | import os 19 | import time 20 | import textwrap 21 | # Import trickery 22 | global HAVE_CV2 23 | global HAVE_MQTT 24 | global HAVE_PIL 25 | HAVE_CV2 = HAVE_MQTT = HAVE_PIL = False 26 | import configparser 27 | import asyncio 28 | 29 | try: 30 | import paho.mqtt.client as mqtt 31 | HAVE_MQTT = True 32 | except ImportError: 33 | print("paho mqtt client not found") 34 | try: 35 | import cv2 36 | HAVE_CV2 = True 37 | except ImportError: 38 | print("CV or numpy module not found, falling back to PIL") 39 | 40 | try: 41 | from PIL import Image 42 | HAVE_PIL = True 43 | except ImportError: 44 | print("PIL module not found, maps are disabled") 45 | 46 | import asyncio 47 | if sys.version_info < (3, 7): 48 | asyncio.get_running_loop = asyncio.get_event_loop 49 | 50 | def parse_args(): 51 | default_icon_path = os.path.join(os.path.dirname(__file__), 'res') 52 | #-------- Command Line ----------------- 53 | parser = argparse.ArgumentParser( 54 | description='Forward MQTT data from Roomba to local MQTT broker') 55 | parser.add_argument( 56 | '-f', '--configfile', 57 | action='store', 58 | type=str, 59 | default="./config.ini", 60 | help='config file name (default: %(default)s)') 61 | parser.add_argument( 62 | '-n', '--roomba_name', 63 | action='store', 64 | type=str, 65 | default="", help='optional Roomba name (default: "%(default)s")') 66 | parser.add_argument( 67 | '-t', '--topic', 68 | action='store', 69 | type=str, 70 | default="#", 71 | help='Roomba MQTT Topic to subscribe to (can use wildcards # and ' 72 | '+ default: %(default)s)') 73 | parser.add_argument( 74 | '-T', '--broker_feedback', 75 | action='store', 76 | type=str, 77 | default="/roomba/feedback", 78 | help='Topic on broker to publish feedback to (default: ' 79 | '%(default)s)') 80 | parser.add_argument( 81 | '-C', '--broker_command', 82 | action='store', 83 | type=str, 84 | default="/roomba/command", 85 | help='Topic on broker to publish commands to (default: ' 86 | '%(default)s)') 87 | parser.add_argument( 88 | '-S', '--broker_setting', 89 | action='store', 90 | type=str, 91 | default="/roomba/setting", 92 | help='Topic on broker to publish settings to (default: ' 93 | '%(default)s)') 94 | parser.add_argument( 95 | '-b', '--broker', 96 | action='store', 97 | type=str, 98 | default=None, 99 | help='ipaddress of MQTT broker (default: %(default)s)') 100 | parser.add_argument( 101 | '-p', '--port', 102 | action='store', 103 | type=int, 104 | default=1883, 105 | help='MQTT broker port number (default: %(default)s)') 106 | parser.add_argument( 107 | '-U', '--user', 108 | action='store', 109 | type=str, 110 | default=None, 111 | help='MQTT broker user name (default: %(default)s)') 112 | parser.add_argument( 113 | '-P', '--broker_password', 114 | action='store', 115 | type=str, 116 | default=None, 117 | help='MQTT broker password (default: %(default)s)') 118 | parser.add_argument( 119 | '-R', '--roomba_ip', 120 | action='store', 121 | type=str, 122 | default='255.255.255.255', 123 | help='ipaddress of Roomba (default: %(default)s)') 124 | parser.add_argument( 125 | '-u', '--blid', 126 | action='store', 127 | type=str, 128 | default=None, 129 | help='Roomba blid (default: %(default)s)') 130 | parser.add_argument( 131 | '-w', '--password', 132 | action='store', 133 | type=str, 134 | default=None, 135 | help='Roomba password (default: %(default)s)') 136 | parser.add_argument( 137 | '-wp', '--webport', 138 | action='store', 139 | type=int, 140 | default=None, 141 | help='Optional web server port number (default: %(default)s)') 142 | parser.add_argument( 143 | '-i', '--indent', 144 | action='store', 145 | type=int, 146 | default=0, 147 | help='Default indentation=auto') 148 | parser.add_argument( 149 | '-l', '--log', 150 | action='store', 151 | type=str, 152 | default="./roomba.log", 153 | help='path/name of log file (default: %(default)s)') 154 | parser.add_argument( 155 | '-e', '--echo', 156 | action='store_false', 157 | default = True, 158 | help='Echo to Console (default: %(default)s)') 159 | parser.add_argument( 160 | '-D', '--debug', 161 | action='store_true', 162 | default = False, 163 | help='debug mode') 164 | parser.add_argument( 165 | '-r', '--raw', 166 | action='store_true', 167 | default = False, 168 | help='Output raw data to mqtt, no decoding of json data (default: %(default)s)') 169 | parser.add_argument( 170 | '-j', '--pretty_print', 171 | action='store_true', 172 | default = False, 173 | help='pretty print json in logs (default: %(default)s)') 174 | parser.add_argument( 175 | '-m', '--drawmap', 176 | action='store_false', 177 | default = True, 178 | help='Draw Roomba cleaning map (default: %(default)s)') 179 | parser.add_argument( 180 | '-M', '--mappath', 181 | action='store', 182 | type=str, 183 | default=".", 184 | help='Location to store maps to (default: %(default)s)') 185 | parser.add_argument( 186 | '-sq', '--max_sqft', 187 | action='store', 188 | type=int, 189 | default=0, 190 | help='Max Square Feet of map (default: %(default)s)') 191 | parser.add_argument( 192 | '-s', '--mapsize', 193 | action='store', 194 | type=str, 195 | default="(800,1500,0,0,0,0)", 196 | help='Map Size, Dock offset and skew for the map.' 197 | '(800,1500) is the size, (0,0) is the dock location, ' 198 | 'in the center of the map, 0 is the rotation of the map, ' 199 | '0 is the rotation of the roomba. ' 200 | 'Use single quotes around the string. (default: ' 201 | '"%(default)s")') 202 | parser.add_argument( 203 | '-fp', '--floorplan', 204 | action='store', 205 | type=str, 206 | default=None, 207 | help='Floorplan for Map. eg ("res/first_floor.jpg",0,0,(1.0,1.0),0, 0.2)' 208 | '"res/first_floor.jpg" is the file name, ' 209 | '0,0 is the x,y offset, ' 210 | '(1.0, 1.0) is the (x,y) scale (or a single number eg 1.0 for both), ' 211 | '0 is the rotation of the floorplan, ' 212 | '0.2 is the transparency' 213 | 'Use single quotes around the string. (default: ' 214 | '%(default)s)') 215 | parser.add_argument( 216 | '-I', '--iconpath', 217 | action='store', 218 | type=str, 219 | default=default_icon_path, 220 | help='location of icons. (default: "%(default)s")') 221 | parser.add_argument( 222 | '-o', '--room_outline', 223 | action='store_false', 224 | default = True, 225 | help='Draw room outline (default: %(default)s)') 226 | parser.add_argument( 227 | '-x', '--exclude', 228 | action='store',type=str, default="", help='Exclude topics that have this in them (default: "%(default)s")') 229 | parser.add_argument( 230 | '--version', 231 | action='version', 232 | version="%(prog)s ({}) Roomba {}".format(__version__, Roomba.__version__), 233 | help='Display version of this program') 234 | return parser.parse_args() 235 | 236 | def main(): 237 | 238 | #----------- Local Routines ------------ 239 | 240 | def create_html(myroomba,mappath="."): 241 | ''' 242 | Create html files for live display of roomba maps - but only if they 243 | don't already exist 244 | NOTE add {{ for { in html where you need variable substitution 245 | ''' 246 | #default css and html 247 | css = '''\ 248 | body { 249 | background-color: white; 250 | margin: 0; 251 | color: white; 252 | padding: 0; 253 | } 254 | img,video { 255 | width: auto; 256 | max-height:100%; 257 | } 258 | ''' 259 | html = '''\ 260 | 261 | 262 | 263 | 264 | 265 | 292 | 293 | 294 | Roomba Map Live 295 | 296 | 297 | '''.format(myroomba.roombaName) 298 | 299 | def write_file(fname, data, mode=0o666): 300 | if not os.path.isfile(fname): 301 | log.warn("{} file not found, creating".format(fname)) 302 | try: 303 | with open(fname , "w") as fn: 304 | fn.write(textwrap.dedent(data)) 305 | os.chmod(fname, mode) 306 | except (IOError, PermissionError) as e: 307 | log.error("unable to create file {}, error: {}".format(fname, e)) 308 | 309 | #check if style.css exists, if not create it 310 | css_path = '{}/style.css'.format(mappath) 311 | write_file(css_path, css) 312 | 313 | #check if html exists, if not create it 314 | html_path = '{}/{}roomba_map.html'.format(mappath, myroomba.roombaName) 315 | write_file(html_path, html, 0o777) 316 | 317 | def setup_logger(logger_name, log_file, level=logging.DEBUG, console=False): 318 | try: 319 | l = logging.getLogger(logger_name) 320 | formatter = logging.Formatter('[%(asctime)s][%(levelname)5.5s](%(name)-20s) %(message)s') 321 | if log_file is not None: 322 | fileHandler = logging.handlers.RotatingFileHandler(log_file, mode='a', maxBytes=10000000, backupCount=10) 323 | fileHandler.setFormatter(formatter) 324 | if console == True: 325 | #formatter = logging.Formatter('[%(levelname)1.1s %(name)-20s] %(message)s') 326 | streamHandler = logging.StreamHandler() 327 | streamHandler.setFormatter(formatter) 328 | 329 | l.setLevel(level) 330 | if log_file is not None: 331 | l.addHandler(fileHandler) 332 | if console == True: 333 | l.addHandler(streamHandler) 334 | 335 | except Exception as e: 336 | print("Error in Logging setup: %s - do you have permission to write the log file??" % e) 337 | sys.exit(1) 338 | 339 | arg = parse_args() #note: all options can be included in the config file 340 | 341 | if arg.debug: 342 | log_level = logging.DEBUG 343 | else: 344 | log_level = logging.INFO 345 | 346 | #setup logging 347 | setup_logger('Roomba', arg.log, level=log_level,console=arg.echo) 348 | 349 | #log = logging.basicConfig(level=logging.DEBUG, 350 | # format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 351 | 352 | log = logging.getLogger('Roomba') 353 | 354 | log.info("*******************") 355 | log.info("* Program Started *") 356 | log.info("*******************") 357 | 358 | log.debug('Debug Mode') 359 | 360 | log.info("Roomba.py Version: %s" % Roomba.__version__) 361 | 362 | log.info("Python Version: %s" % sys.version.replace('\n','')) 363 | 364 | if HAVE_MQTT: 365 | import paho.mqtt 366 | log.info("Paho MQTT Version: %s" % paho.mqtt.__version__) 367 | 368 | if HAVE_CV2: 369 | log.info("CV Version: %s" % cv2.__version__) 370 | 371 | if HAVE_PIL: 372 | import PIL #bit of a kludge, just to get the version number 373 | log.info("PIL Version: %s" % PIL.__version__) 374 | 375 | log.debug("-- DEBUG Mode ON -") 376 | log.info(" to Exit") 377 | log.info("Roomba MQTT data Interface") 378 | 379 | group = None 380 | options = vars(arg) #use args as dict 381 | 382 | if arg.blid is None or arg.password is None: 383 | get_passwd = Password(arg.roomba_ip,file=arg.configfile) 384 | roombas = get_passwd.get_roombas() 385 | else: 386 | roombas = {arg.roomba_ip: {"blid": arg.blid, 387 | "password": arg.password, 388 | "roomba_name": arg.roomba_name}} 389 | 390 | roomba_list = [] 391 | for addr, info in roombas.items(): 392 | log.info("Creating Roomba object {}, {}".format(addr, info.get("roomba_name", addr))) 393 | #get options from config (if they exist) this overrides command line options. 394 | for opt, value in options.copy().items(): 395 | config_value = info.get(opt) 396 | if config_value is None: 397 | options[opt] = value 398 | elif value is None or isinstance(value, str): 399 | options[opt] = config_value 400 | else: 401 | options[opt] = literal_eval(str(config_value)) 402 | 403 | # minnimum required to connect on Linux Debian system 404 | # myroomba = Roomba(address, blid, roombaPassword) 405 | myroomba = Roomba(addr, 406 | blid=arg.blid, 407 | password=arg.password, 408 | topic=arg.topic, 409 | roombaName=arg.roomba_name, 410 | webport=arg.webport) 411 | 412 | if arg.webport: 413 | arg.webport+=1 414 | 415 | if arg.exclude: 416 | myroomba.exclude = arg.exclude 417 | 418 | #set various options 419 | myroomba.set_options(raw=arg.raw, 420 | indent=arg.indent, 421 | pretty_print=arg.pretty_print, 422 | max_sqft=arg.max_sqft) 423 | 424 | if arg.mappath and arg.mapsize and arg.drawmap: 425 | # auto create html files (if they don't exist) 426 | create_html(myroomba, arg.mappath) 427 | # enable live maps, class default is no maps 428 | myroomba.enable_map(enable=True, 429 | mapSize=arg.mapsize, 430 | mapPath=arg.mappath, 431 | iconPath=arg.iconpath, 432 | roomOutline=arg.room_outline, 433 | floorplan=arg.floorplan) 434 | 435 | if arg.broker is not None: 436 | # if you want to publish Roomba data to your own mqtt broker 437 | # (default is not to) if you have more than one roomba, and 438 | # assign a roombaName, it is addded to this topic 439 | # (ie brokerFeedback/roombaName) 440 | myroomba.setup_mqtt_client(arg.broker, 441 | arg.port, 442 | arg.user, 443 | arg.broker_password, 444 | arg.broker_feedback, 445 | arg.broker_command, 446 | arg.broker_setting) 447 | 448 | roomba_list.append(myroomba) 449 | 450 | 451 | loop = asyncio.get_event_loop() 452 | loop.set_debug(arg.debug) 453 | 454 | group = asyncio.gather(*[myroomba.async_connect() for myroomba in roomba_list]) 455 | 456 | if not group: 457 | for myroomba in roomba_list: 458 | myroomba.connect() #start each roomba connection individually 459 | 460 | try: 461 | loop.run_forever() 462 | 463 | except (KeyboardInterrupt, SystemExit): 464 | log.info("System exit Received - Exiting program") 465 | for myroomba in roomba_list: 466 | myroomba.disconnect() 467 | log.info('Program Exited') 468 | 469 | finally: 470 | pass 471 | 472 | 473 | if __name__ == '__main__': 474 | main() 475 | -------------------------------------------------------------------------------- /roomba/test_floorplan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickWaterton/Roomba980-Python/f782597852f7316cb02602ed11d20bf41603b628/roomba/test_floorplan.png -------------------------------------------------------------------------------- /roomba/views/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px; 3 | } 4 | 5 | .data { 6 | font-size: 15px; 7 | font-weight: 50 8 | } 9 | 10 | #path_layer, #robot_body_layer, #text_layer { 11 | max-width: 98vw; 12 | max-height: 84vh; 13 | } 14 | -------------------------------------------------------------------------------- /roomba/views/js/map.js: -------------------------------------------------------------------------------- 1 | /* global $ alert sizeX sizeY xOffset yOffset updateEvery */ 2 | /* eslint no-unused-vars: "off" */ 3 | /* eslint no-global-assign: "off" */ 4 | /* eslint no-native-reassign: "off" */ 5 | 6 | window.onload = getMapSize; 7 | 8 | var pathLayerContext; 9 | var robotBodyLayerContext; 10 | var textLayerContext; 11 | 12 | var pathLayer; 13 | var robotBodyLayer; 14 | var textLayer; 15 | 16 | var clearnew; 17 | 18 | var lastPhase = ''; 19 | var mapping = true; 20 | 21 | var Dockimg = new Image(); 22 | Dockimg.src = '/res/home.png' 23 | 24 | var Roombaimg = new Image(); 25 | Roombaimg.src = '/res/roomba.png' 26 | 27 | var outline = new Image(); 28 | var floorplan = new Image(); 29 | var floorplan_data = null 30 | var name = 'none'; 31 | 32 | var xOffset = 0; //dock location 33 | var yOffset = 0; //dock location 34 | var sizeX = 0; 35 | var sizeY = 0; 36 | var roombaangle = 0; 37 | var rotation = 0; 38 | var invert_x = 0; 39 | var invert_y = 0; 40 | var updateEvery = 3000; 41 | var lineWidth = 20; 42 | var maxDim = 0; 43 | var gXoff = 0; 44 | var gYoff = 0; 45 | var prevnMssn = null; 46 | 47 | //floorplan 48 | var FPfilename = null; 49 | var FPsizeX = 1.0; 50 | var FPsizeY = 1.0; 51 | var FPzoom = 0; 52 | var FPxOffset = 0; 53 | var FPyOffset = 0; 54 | var FProt = 0; 55 | var FPtrans = 0.2; 56 | 57 | var canvasStyle = document.styleSheets[0].cssRules.item(2).style 58 | var maxHeight = parseInt(canvasStyle.maxHeight, 10); 59 | var orgMaxheight = maxHeight; 60 | 61 | function getMapSize () { 62 | $.getJSON('/api/local/map/mapsize', function( data ) { 63 | sizeX = data.x; 64 | sizeY = data.y; 65 | xOffset = data.off_x; 66 | yOffset = data.off_y; 67 | rotation = data.angle; 68 | roombaangle = data.roomba_angle; 69 | updateEvery = data.update; 70 | if (data.invert_x) { 71 | invert_x = data.invert_x; 72 | } 73 | if (data.invert_y) { 74 | invert_y = data.invert_y; 75 | } 76 | }).always(function() { 77 | startApp(); 78 | }); 79 | } 80 | 81 | function getFloorPlanSize () { 82 | $.getJSON('/api/local/map/floorplansize', function( data ) { 83 | if (data) { 84 | FPfilename = data.fp_file; 85 | FPsizeX = data.x; 86 | FPsizeY = data.y; 87 | if (data.x == data.y) { 88 | FPzoom = data.x 89 | } 90 | else { 91 | FPzoom = 0; 92 | } 93 | FPxOffset = data.off_x; 94 | FPyOffset = data.off_y; 95 | FProt = data.angle; 96 | FPtrans = data.trans; 97 | UpdateFPvalues(); 98 | } else { 99 | var elem = document.getElementById('show_fpsize'); 100 | var elem1 = document.getElementById('label_fpsize'); 101 | if (elem !== null) { 102 | //remove floorplan checkbox if no floorplan data 103 | elem.parentNode.removeChild(elem); 104 | } 105 | if (elem1 !== null) { 106 | //remove floorplan checkbox label no floorplan data 107 | elem1.parentNode.removeChild(elem1); 108 | } 109 | } 110 | }); 111 | } 112 | 113 | function getRoombaName () { 114 | $.getJSON('/api/local/info/name', function( data ) { 115 | name = data.name; 116 | }).success(function() { 117 | $('#name').html(name); 118 | }); 119 | } 120 | 121 | function startApp () { 122 | pathLayer = document.getElementById('path_layer'); 123 | robotBodyLayer = document.getElementById('robot_body_layer'); 124 | textLayer = document.getElementById('text_layer'); 125 | 126 | clearnew = document.getElementsByName("clearnew"); 127 | 128 | toggleMapsize(); 129 | toggleFPsize(); 130 | 131 | Updatevalues(); 132 | getFloorPlanSize(); 133 | getDimentions(); 134 | getRoombaName(); 135 | 136 | pathLayer.width = maxDim; 137 | pathLayer.height = maxDim; 138 | 139 | robotBodyLayer.width = maxDim; 140 | robotBodyLayer.height = maxDim; 141 | 142 | textLayer.width = maxDim; 143 | textLayer.height = maxDim; 144 | 145 | rotation = NaN; //force rotation update 146 | 147 | pathLayerContext = pathLayer.getContext('2d'); 148 | robotBodyLayerContext = robotBodyLayer.getContext('2d'); 149 | textLayerContext = textLayer.getContext('2d'); 150 | 151 | UpdateCanvas(); 152 | getFloorplan(); 153 | clearMap(); 154 | startMissionLoop(); 155 | } 156 | 157 | function getDimentions () { 158 | maxDim = Math.max(sizeX, sizeY); 159 | gXoff = (maxDim - sizeX)/2; 160 | gYoff = (maxDim - sizeY)/2; 161 | } 162 | 163 | function getMapOutline () { 164 | $.get('/api/local/map/outline', function( data ) { 165 | var pngimg = data; 166 | console.log('got outline image, length: %d',pngimg.length); 167 | //console.log(pngimg) 168 | if (pngimg) { 169 | outline.src = "data:image/png;base64," + pngimg; 170 | textLayerContext.drawImage(outline, 0, (textLayer.height/2)-(outline.naturalHeight/2)); 171 | } else { 172 | var elem = document.getElementById('clearoutline'); 173 | if (elem !== null) { 174 | //remove clearoutline button if null 175 | elem.parentNode.removeChild(elem); 176 | } 177 | } 178 | }); 179 | } 180 | 181 | function getFloorplan () { 182 | $.get('/api/local/map/floorplan', function( data ) { 183 | floorplan_data = data; 184 | console.log('got floorplan image, length: %d',floorplan_data.length); 185 | //console.log(floorplan_data) 186 | if (floorplan_data) { 187 | floorplan.src = "data:image/png;base64," + floorplan_data; 188 | clearOutline(); 189 | } 190 | }); 191 | } 192 | 193 | function drawFloorplan () { 194 | if (floorplan_data) { 195 | textLayerContext.drawImage(floorplan, (textLayer.width/2)-(floorplan.naturalWidth/2), (textLayer.height/2)-(floorplan.naturalHeight/2)); 196 | } 197 | } 198 | 199 | function UpdateFPvalues () { 200 | $('#fpw').val(FPsizeX); 201 | $('#fph').val(FPsizeY); 202 | $('#fpzoom').val(FPzoom); 203 | 204 | $('#fpoffsetx').val(FPxOffset); 205 | $('#fpoffsety').val(FPyOffset); 206 | 207 | $('#fprot').val(FProt); 208 | $('#fptrans').val(FPtrans); 209 | 210 | console.log('updated FP values') 211 | } 212 | 213 | function Updatevalues () { 214 | $('#sizew').val(sizeX); 215 | $('#sizeh').val(sizeY); 216 | 217 | $('#offsetx').val(xOffset); 218 | $('#offsety').val(yOffset); 219 | 220 | $('#rotation').val(rotation); 221 | $('#roombaangle').val(roombaangle); 222 | 223 | $('#invert_x').val(invert_x); 224 | $('#invert_y').val(invert_y); 225 | 226 | $('#updateevery').val(updateEvery); 227 | $('#linewidth').val(lineWidth); 228 | 229 | $('#maxheight').val(maxHeight); 230 | 231 | $('#bin').html('no bin'); 232 | 233 | } 234 | 235 | function UpdateLineWidth () { 236 | lineWidth = getValue('#linewidth', lineWidth); 237 | pathLayerContext.lineWidth = lineWidth; 238 | if (lineWidth == 1) { 239 | pathLayerContext.strokeStyle = '#000000'; 240 | } else { 241 | pathLayerContext.strokeStyle = 'lawngreen'; 242 | } 243 | pathLayerContext.lineCap = 'round'; 244 | pathLayerContext.lineJoin = 'round'; 245 | } 246 | 247 | function startMissionLoop () { 248 | if (mapping) { 249 | $('#mapStatus').html('getting point...'); 250 | $.get('/api/local/info/mission', function (data) { 251 | messageHandler(data); 252 | setTimeout(startMissionLoop, updateEvery); 253 | }); 254 | } else { 255 | $('#mapStatus').html('stopped'); 256 | } 257 | } 258 | 259 | function messageHandler (msg) { 260 | if (msg.cleanMissionStatus) { 261 | // firmware version 2/3 262 | msg.ok = msg.cleanMissionStatus; 263 | msg.ok.pos = msg.pose; 264 | msg.ok.flags = msg.flags; 265 | msg.ok.batPct = msg.batPct; 266 | if (msg.bin) { 267 | $('#bin').html(msg.bin.present); 268 | } 269 | if (prevnMssn === null) { 270 | prevnMssn = msg.ok.nMssn; 271 | } 272 | } 273 | if (prevnMssn != msg.ok.nMssn) { 274 | //clear map on new mission if checkbox set 275 | ClearMapOnNew(); 276 | prevnMssn = msg.ok.nMssn; 277 | } 278 | var d = new Date(); 279 | msg.ok.time = new Date().toLocaleString().split(' ')[1]; 280 | $('#mapStatus').html('drawing...'); 281 | $('#last').html(msg.ok.time); 282 | $('#mission').html(msg.ok.mssnM); 283 | $('#nMssn').html(msg.ok.nMssn); 284 | $('#cycle').html(msg.ok.cycle); 285 | $('#phase').html(msg.ok.phase); 286 | $('#flags').html(msg.ok.flags); 287 | $('#batPct').html(msg.ok.batPct); 288 | $('#error').html(msg.ok.error); 289 | $('#sqft').html(msg.ok.sqft); 290 | $('#expireM').html(msg.ok.expireM); 291 | $('#rechrgM').html(msg.ok.rechrgM); 292 | $('#notReady').html(msg.ok.notReady); 293 | 294 | if (msg.ok.phase === 'charge') { 295 | //if we are charging, assume 0,0 location (dock) 296 | msg.ok.pos = {"theta": 180,"point": {"x": 0,"y": 0}}; 297 | } 298 | 299 | if (msg.ok.pos) { 300 | $('#theta').html(msg.ok.pos.theta); 301 | $('#x').html(msg.ok.pos.point.x); 302 | $('#y').html(msg.ok.pos.point.y); 303 | 304 | drawStep( 305 | msg.ok.pos.point.x, 306 | msg.ok.pos.point.y, 307 | msg.ok.pos.theta, 308 | msg.ok.cycle, 309 | msg.ok.phase 310 | ); 311 | } 312 | } 313 | 314 | function drawStep (x, y, theta, cycle, phase) { 315 | //offset is from the middle of the canvas 316 | var xoff = (pathLayer.width/2+xOffset); 317 | var yoff = (pathLayer.height/2+yOffset); 318 | x = parseInt(x, 10); 319 | y = parseInt(y, 10); 320 | var oldX = x; 321 | 322 | //x and y are reversed in pose... so swap 323 | x = y; 324 | y = oldX; 325 | if(invert_x == 1) { 326 | x = -x; 327 | } 328 | if(invert_y ==1) { 329 | y = -y; 330 | } 331 | 332 | 333 | x+=xoff; 334 | y+=yoff; 335 | 336 | console.log('x: %d, y:%d, xoff: %d, yoff: %d', x, y, xoff, yoff); 337 | 338 | drawRobotBody(x, y, theta); 339 | //draw charging base 340 | drawDock(); 341 | 342 | // draw changes in status with text. 343 | if (phase !== lastPhase) { 344 | textLayerContext.font = 'normal 12pt Calibri'; 345 | textLayerContext.fillStyle = 'blue'; 346 | textLayerContext.fillText(phase, x, y); 347 | getMapOutline(); 348 | lastPhase = phase; 349 | } 350 | pathLayerContext.lineTo(x, y); 351 | pathLayerContext.stroke(); 352 | } 353 | 354 | function drawDock () { 355 | drawRotatedImage(robotBodyLayerContext, Dockimg, (pathLayer.width/2+xOffset), (pathLayer.height/2+yOffset), (rotation+180)%360); 356 | } 357 | 358 | function drawRobotBody (x, y, theta) { 359 | theta = parseInt(theta, 10); 360 | //var radio = 15; //roomba radius 361 | clearContext(robotBodyLayerContext); 362 | theta = (theta -90 + roombaangle); 363 | drawRotatedImage(robotBodyLayerContext, Roombaimg, x-Roombaimg.naturalWidth/2, y-Roombaimg.naturalHeight/2, theta-rotation); 364 | /* 365 | robotBodyLayerContext.beginPath(); 366 | robotBodyLayerContext.arc(x, y, radio, 0, 2 * Math.PI, false); 367 | robotBodyLayerContext.fillStyle = 'limegreen'; 368 | robotBodyLayerContext.fill(); 369 | robotBodyLayerContext.lineWidth = 3; 370 | robotBodyLayerContext.strokeStyle = '#003300'; 371 | robotBodyLayerContext.stroke(); 372 | 373 | theta = (theta + roombaangle)%360; 374 | 375 | var outerX = x + radio * Math.cos(theta * (Math.PI / 180)); 376 | var outerY = y + radio * Math.sin(theta * (Math.PI / 180)); 377 | 378 | robotBodyLayerContext.beginPath(); 379 | robotBodyLayerContext.moveTo(x, y); 380 | robotBodyLayerContext.lineTo(outerX, outerY); 381 | robotBodyLayerContext.strokeStyle = 'red'; 382 | robotBodyLayerContext.lineWidth = 3; 383 | robotBodyLayerContext.stroke(); 384 | */ 385 | } 386 | 387 | function clearOutline () { 388 | $.get('/api/local/map/clear_outline'); 389 | lastPhase = ''; 390 | clearContext(textLayerContext); 391 | drawOutline(); 392 | } 393 | 394 | function ClearMapOnNew () { 395 | if (clearnew.checked) { 396 | clearMap(); 397 | } 398 | } 399 | 400 | function clearMap () { 401 | lastPhase = ''; 402 | clearContext(pathLayerContext); 403 | clearContext(robotBodyLayerContext); 404 | clearContext(textLayerContext); 405 | drawOutline(); 406 | pathLayerContext.beginPath(); 407 | } 408 | 409 | function clearContext (ctx) { 410 | console.log('clear context'); 411 | //clear a bigger area than you think, as rotated canvasses 412 | //leave remains inn the corners 413 | ctx.clearRect(0,0,ctx.canvas.width*2,ctx.canvas.height*2); 414 | } 415 | 416 | function drawRotatedText(ctx, mytext, x, y, deg) { 417 | //javascript is such a pain... 418 | var txtHeight = ctx.measureText(mytext).fontBoundingBoxAscent; 419 | ctx.textAlign = "center"; 420 | if (deg == 0) { 421 | ctx.fillText(mytext, x, y+txtHeight); 422 | } else { 423 | ctx.save(); 424 | ctx.translate(x, y); 425 | ctx.rotate(deg*Math.PI/180); 426 | ctx.fillText(mytext, 0, txtHeight); 427 | ctx.restore(); 428 | } 429 | } 430 | 431 | function drawRotatedImage(ctx, img, x, y, deg) { 432 | //javascript is such a pain... 433 | if (deg == 0) { 434 | ctx.drawImage(img, x-img.naturalWidth/2, y-img.naturalHeight/2); 435 | } else { 436 | ctx.save(); 437 | ctx.translate(x+img.naturalWidth/2, y+img.naturalHeight/2); 438 | ctx.rotate(deg*Math.PI/180); 439 | ctx.drawImage(img, -img.naturalWidth/2, -img.naturalHeight/2); 440 | ctx.restore(); 441 | } 442 | } 443 | 444 | function drawOutline () { 445 | //draws the rectangular bounds of the map 446 | console.log('DrawOutline sizeX: %d, sizeY: %d, gXoff: %d, gYoff: %d', sizeX, sizeY, gXoff, gYoff); 447 | drawFloorplan(); 448 | textLayerContext.beginPath(); 449 | textLayerContext.lineWidth = 5; 450 | textLayerContext.strokeStyle="#FF0000"; 451 | textLayerContext.strokeRect(gXoff, gYoff, sizeX, sizeY);//for white background 452 | textLayerContext.font = 'bold 40pt Calibri'; 453 | textLayerContext.fillStyle = 'red'; 454 | drawRotatedText(textLayerContext, '- W(x) +', textLayer.width/2, gYoff, 0); 455 | drawRotatedText(textLayerContext, '+ H(y) -', gXoff, textLayer.height/2, -90); 456 | textLayerContext.stroke(); 457 | getMapOutline(); 458 | drawDock(); 459 | } 460 | 461 | function toggleMapping () { 462 | mapping = !mapping; 463 | if (mapping) startMissionLoop(); 464 | } 465 | 466 | function toggleFPsize () { 467 | var show_fpsize = document.getElementById('show_fpsize'); 468 | var fpsize = document.getElementById('floorplan'); 469 | if (show_fpsize.checked) { 470 | fpsize.style.display = "inline"; 471 | } else { 472 | fpsize.style.display = "none"; 473 | } 474 | } 475 | 476 | function toggleMapsize () { 477 | var show_mapsize = document.getElementById('show_mapsize'); 478 | var mapsize = document.getElementById('mapsize'); 479 | if (show_mapsize.checked) { 480 | mapsize.style.display = "inline"; 481 | } else { 482 | mapsize.style.display = "none"; 483 | } 484 | } 485 | 486 | function getValue (name, actual) { 487 | var newValue = parseInt($(name).val(), 10); 488 | if (isNaN(newValue)) { 489 | alert('Invalid ' + name); 490 | $(name).val(actual); 491 | return actual; 492 | } 493 | return newValue; 494 | } 495 | 496 | function getFloat (name, actual) { 497 | var newValue = parseFloat($(name).val(), 10); 498 | if (isNaN(newValue)) { 499 | alert('Invalid ' + name); 500 | $(name).val(actual); 501 | return actual; 502 | } 503 | return newValue; 504 | } 505 | 506 | function downloadCanvas () { 507 | var bodyCanvas = document.getElementById('robot_body_layer'); 508 | var pathCanvas = document.getElementById('path_layer'); 509 | 510 | var bodyContext = bodyCanvas.getContext('2d'); 511 | bodyContext.drawImage(pathCanvas, 0, 0); 512 | 513 | document.getElementById('download').href = bodyCanvas.toDataURL(); 514 | document.getElementById('download').download = 'current_map.png'; 515 | } 516 | 517 | function shiftCanvas (ctx, x, y) { 518 | console.log('shifting: x: %f, y: %f', x, y); 519 | var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); 520 | clearContext(ctx); 521 | ctx.putImageData(imageData, x, y); 522 | } 523 | 524 | function resizeCanvas(ctx, w, h) { 525 | rotateCanvas(ctx, 0); //rotate to 0 as changing the size of a canvas clears it 526 | var imageData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height); 527 | ctx.canvas.width = w; //this will clear the canvas and set rotation to 0 528 | ctx.canvas.height = h; 529 | ctx.putImageData(imageData,0,0); 530 | //rotation = 0; //note rotation is now 0 531 | } 532 | 533 | function rotateCanvas (ctx, absrot){ 534 | rotation = rotation || 0; 535 | var rotdeg = (absrot-rotation); 536 | if (sizeY > sizeX) { 537 | rotdeg = 180-rotdeg 538 | } 539 | console.log('Abs Angle: %d, Rotate: %d deg', absrot, rotdeg); 540 | var radians=(rotdeg*Math.PI)/180; 541 | var absradians=(absrot*Math.PI)/180; 542 | // Create an second in-memory canvas: 543 | var mCanvas=document.createElement('canvas'); 544 | mCanvas.width=ctx.canvas.width; 545 | mCanvas.height=ctx.canvas.height; 546 | var mctx=mCanvas.getContext('2d'); 547 | //unrotate the image 548 | mctx.translate(mctx.canvas.width/2,mctx.canvas.height/2); 549 | mctx.rotate(radians-absradians); 550 | mctx.translate(-mctx.canvas.width/2,-mctx.canvas.height/2); 551 | // Draw your canvas onto the second canvas 552 | mctx.drawImage(ctx.canvas,0,0); 553 | //clear original canvas 554 | clearContext(ctx); 555 | //set rotation of canvas 556 | ctx.translate(ctx.canvas.width/2,ctx.canvas.height/2); 557 | ctx.rotate(radians); //note, this is cumulative 558 | ctx.translate(-ctx.canvas.width/2,-ctx.canvas.height/2); 559 | //Draw the second canvas back to the (now rotated) main canvas: 560 | ctx.drawImage(mCanvas, 0, 0); 561 | } 562 | 563 | function UpdateCanvas () { 564 | var w = getValue('#sizew', sizeX); 565 | var h = getValue('#sizeh', sizeY); 566 | var newXOffset = getValue('#offsetx', xOffset); 567 | var newYOffset = getValue('#offsety', yOffset); 568 | var newRotation = getValue('#rotation', rotation); 569 | roombaangle = getValue('#roombaangle', roombaangle); 570 | var newmaxHeight = getValue('#maxheight', maxHeight); 571 | var x; 572 | var y; 573 | var deg; 574 | invert_x = getValue('#invert_x', invert_x); 575 | invert_y = getValue('#invert_y', invert_y); 576 | 577 | if (sizeX !== w) { 578 | console.log('redrawing x'); 579 | if (sizeX == maxDim) { 580 | pathLayerContext.beginPath(); 581 | resizeCanvas(pathLayerContext, w, w); 582 | robotBodyLayer.width = w; 583 | textLayer.width = w; 584 | robotBodyLayer.height = w; 585 | textLayer.height = w; 586 | newXOffset = xOffset+(sizeX-w)/2; 587 | rotation = 0; //resizing a canvas sets rotation to 0 588 | } 589 | sizeX = w; 590 | } 591 | 592 | if (sizeY !== h) { 593 | console.log('redrawing y'); 594 | if (sizeY == maxDim) { 595 | resizeCanvas(pathLayerContext, h, h); 596 | robotBodyLayer.width = h; 597 | textLayer.width = h; 598 | robotBodyLayer.height = h; 599 | textLayer.height = h; 600 | newYOffset = yOffset+(sizeY-h)/2; 601 | rotation = 0; //resizing a canvas sets rotation to 0 602 | } 603 | sizeY = h; 604 | } 605 | 606 | if (newXOffset !== xOffset) { 607 | pathLayerContext.beginPath(); 608 | deg = rotation*Math.PI/180; 609 | x = Math.round(Math.cos(deg) * (newXOffset - xOffset)); 610 | y = Math.round(Math.sin(deg) * (newXOffset - xOffset)); 611 | shiftCanvas(pathLayerContext, x, y); 612 | xOffset = newXOffset; 613 | } 614 | 615 | if (newYOffset !== yOffset) { 616 | pathLayerContext.beginPath(); 617 | deg = rotation*Math.PI/180; 618 | x = Math.round(Math.cos(deg) * (newYOffset - yOffset)); 619 | y = Math.round(Math.sin(deg) * (newYOffset - yOffset)); 620 | shiftCanvas(pathLayerContext, x, y); 621 | yOffset = newYOffset; 622 | } 623 | 624 | if (newRotation !== rotation) { 625 | pathLayerContext.beginPath(); 626 | rotateCanvas(pathLayerContext, newRotation); 627 | rotateCanvas(textLayerContext, newRotation); 628 | rotateCanvas(robotBodyLayerContext, newRotation); 629 | rotation = newRotation; 630 | } 631 | 632 | if (newmaxHeight != maxHeight) { 633 | console.log('new max-height: %s', newmaxHeight); 634 | canvasStyle.maxHeight = `${newmaxHeight}vh`; 635 | maxHeight = newmaxHeight 636 | console.log('set new max-height: %s', maxHeight); 637 | } 638 | 639 | UpdateLineWidth(); 640 | getDimentions(); 641 | Updatevalues(); 642 | clearContext(textLayerContext); 643 | clearContext(robotBodyLayerContext); 644 | drawOutline(); 645 | console.log('updated canvas') 646 | } 647 | 648 | function saveValues () { 649 | var values = `mapsize = (${$('#sizew').val()}, 650 | ${$('#sizeh').val()}, 651 | ${$('#offsetx').val()}, 652 | ${$('#offsety').val()}, 653 | ${$('#rotation').val()}, 654 | ${$('#roombaangle').val()}, 655 | ${$('#invert_x').val()}, 656 | ${$('#invert_y').val()} 657 | )`; 658 | $.post('/map/display_values', values, function (data) { 659 | $('#apiresponse').html(data); 660 | }); 661 | } 662 | 663 | function saveFPValues () { 664 | var zoom; 665 | if (FPsizeX == FPsizeY) { 666 | zoom = `${$('#fpzoom').val()}`; 667 | } else { 668 | zoom = `(${$('#fpw').val()},${$('#fph').val()})`; 669 | } 670 | 671 | var values = `floorplan = ('${FPfilename}', 672 | ${$('#fpoffsetx').val()}, 673 | ${$('#fpoffsety').val()}, 674 | ${zoom}, 675 | ${$('#fprot').val()}, 676 | ${$('#fptrans').val()})`; 677 | $.post('/map/display_values', values, function (data) { 678 | $('#apiresponse').html(data); 679 | }); 680 | } 681 | 682 | $('.metrics').on('change', function () { 683 | UpdateCanvas(); 684 | }); 685 | 686 | $('.fpmasterzoom').on('change', function () { 687 | FPzoom = getFloat('#fpzoom', FPzoom); 688 | FPsizeX = FPsizeY = FPzoom; 689 | $('#fpw').val(FPzoom); 690 | $('#fph').val(FPzoom); 691 | console.log('updated floorplan X,Y Zoom: %s', FPzoom); 692 | postFPvalues(); 693 | }); 694 | 695 | $('.fpmetrics').on('change', function () { 696 | FPsizeX = getFloat('#fpw', FPsizeX); 697 | FPsizeY = getFloat('#fph', FPsizeY); 698 | postFPvalues(); 699 | }); 700 | 701 | function postFPvalues () { 702 | var values = {} 703 | values.filename = FPfilename; 704 | values.fpoffsetx = $('#fpoffsetx').val(); 705 | values.fpoffsety = $('#fpoffsety').val(); 706 | values.fpw = $('#fpw').val(); 707 | values.fph = $('#fph').val(); 708 | values.fprot = $('#fprot').val(); 709 | values.fptrans = $('#fptrans').val(); 710 | 711 | console.log('updating floorplan values: %s', values); 712 | $.post('/map/set_fp_values', values, function (data) { 713 | $('#apiresponse').html(JSON.stringify(data)); 714 | }); 715 | getFloorplan(); 716 | clearMap(); 717 | } 718 | 719 | $('.action').on('click', function () { 720 | var me = $(this); 721 | var path = me.data('action'); 722 | me.button('loading'); 723 | $.get(path, function (data) { 724 | me.button('reset'); 725 | $('#apiresponse').html(JSON.stringify(data)); 726 | }); 727 | }); 728 | 729 | function resetMaxHeight () { 730 | $('#maxheight').val(orgMaxheight); 731 | UpdateCanvas(); 732 | } 733 | 734 | $('#updateevery').on('change', function () { 735 | updateEvery = getValue('#updateevery', updateEvery); 736 | }); 737 | 738 | $('#linewidth').on('change', function () { 739 | UpdateLineWidth(); 740 | }); 741 | -------------------------------------------------------------------------------- /roomba/views/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Roomba map 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | Download Image 23 | 24 | 25 | 26 |
27 | Update every: ms. 28 | Line Width 29 | Zoom: 50% 200% 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | Map size: w: h: 39 | Dock: x: y: 40 | rot: R rot: 41 | inv x: inv y: 42 | 43 |
44 |
45 | 52 |
53 | 54 |
55 | Message: Center of map is (0,0), Roomba (0,0) is dock location. 56 |
57 | 58 |
59 | 60 | 61 | 62 |
63 |
64 | 65 |
66 | Name:
67 | Last point:
68 | Mission Time: min
69 | Mission:
70 | Bin present:
71 | Cycle:
72 | Phase:
73 | Flags:
74 | Battery: %
75 | Error:
76 | Sqft:
77 | expireM: min
78 | rechrgM: min
79 | notReady:
80 | theta:
81 | x:
82 | y:
83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /roomba/web_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Roomba api web server 6 | ''' 7 | import asyncio 8 | from aiohttp import web 9 | import base64 10 | import logging 11 | 12 | 13 | class webserver(): 14 | 15 | VERSION = __version__ = "2.0e" 16 | 17 | api_get = {'time' : 'utctime', 18 | 'bbrun' : 'bbrun', 19 | 'langs' : 'langs', 20 | 'sys' : ['bbrstinfo', 21 | 'cap', 22 | 'sku', 23 | 'batteryType', 24 | 'soundVer', 25 | 'uiSwVer', 26 | 'navSwVer', 27 | 'wifiSwVer', 28 | 'mobilityVer', 29 | 'bootloaderVer', 30 | 'umiVer', 31 | 'softwareVer', 32 | 'audio', 33 | 'bin'], 34 | 'lastwireless' : ['wifistat', 'wlcfg'], 35 | 'week' : ['cleanSchedule'], 36 | 'preferences' : ['cleanMissionStatus', 'cleanSchedule', 'name', 'vacHigh', 'signal'], 37 | 'state' : 'state', 38 | 'mission' : ['cleanMissionStatus', 'pose', 'bin', 'batPct', 'flags'], 39 | 'missionbasic' : ['cleanMissionStatus', 'bin', 'batPct'], 40 | 'wirelessconfig' : ['wlcfg', 'netinfo'], 41 | 'wireless' : ['wifistat', 'netinfo'], 42 | 'cloud' : ['cloudEnv'], 43 | 'sku' : 'sku', 44 | 'cleanRoom' : 'start', 45 | 'start' : 'start', 46 | 'clean' : 'clean', 47 | 'spot' : 'spot', 48 | 'pause' : 'pause', 49 | 'stop' : 'stop', 50 | 'resume' : 'resume', 51 | 'dock' : 'dock', 52 | 'evac' : 'evac', 53 | 'train' : 'train', 54 | 'find' : 'find', 55 | 'reset' : 'reset' 56 | } 57 | 58 | api_post = {'week' : 'cleanSchedule', 59 | 'preferences' : None, 60 | 'cleanRoom' : 'start', 61 | 'carpetboost/auto' : {'carpetBoost': True, 'vacHigh': False}, 62 | 'carpetboost/performance' : {'carpetBoost': False, 'vacHigh': True}, 63 | 'carpetboost/eco' : {'carpetBoost': False, 'vacHigh': False}, 64 | 'edgeclean/on' : {'openOnly': False}, 65 | 'edgeclean/off' : {'openOnly': True}, 66 | 'cleaningpasses/auto' : {'noAutoPasses': False, 'twoPass': False}, 67 | 'cleaningpasses/one' : {'noAutoPasses': True, 'twoPass': False}, 68 | 'cleaningpasses/two' : {'noAutoPasses': True, 'twoPass': True}, 69 | 'alwaysfinish/on' : {'binPause': False}, 70 | 'alwaysfinish/off' : {'binPause': True} 71 | } 72 | 73 | def __init__(self, roomba=None, webport=None, log=None): 74 | self.roomba = roomba if roomba else self.dummy_roomba() 75 | if log: 76 | self.log = log 77 | else: 78 | self.log = logging.getLogger("Roomba.{}.api".format(self.roomba.roombaName)) 79 | self.loop = asyncio.get_event_loop() 80 | self.webport = webport 81 | self.app = None 82 | self.web_task = None 83 | self.start_web() 84 | #except aiohttp.web_runner.GracefulExit: 85 | 86 | def start_web(self): 87 | routes = web.RouteTableDef() 88 | routes.static('/map', './views') 89 | routes.static('/js', './views/js') 90 | routes.static('/css', './views/css') 91 | routes.static('/res', './res') 92 | 93 | 94 | @routes.get('/api/local/map/{info}') 95 | async def map_info(request): 96 | item = request.match_info['info'] 97 | if item == 'enabled': 98 | return web.json_response(self.roomba.drawmap) 99 | elif item == 'mapsize': 100 | if self.roomba.mapSize: 101 | value = {'x': self.roomba.mapSize[0], 102 | 'y': self.roomba.mapSize[1], 103 | 'off_x': self.roomba.mapSize[2], 104 | 'off_y': self.roomba.mapSize[3], 105 | 'angle': self.roomba.mapSize[4], 106 | 'roomba_angle': self.roomba.mapSize[5], 107 | 'update': 3000} 108 | if len(self.roomba.mapSize) >= 7: 109 | value['invert_x'] = self.roomba.mapSize[6] 110 | if len(self.roomba.mapSize) >= 8: 111 | value['invert_y'] = self.roomba.mapSize[7] 112 | else: 113 | value = {'x': 2000, 114 | 'y': 2000, 115 | 'off_x': 0, 116 | 'off_y': 0, 117 | 'angle': 0, 118 | 'roomba_angle': 0, 119 | 'invert_x': 0, 120 | 'invert_y': 0, 121 | 'update': 3000} 122 | return web.json_response(value) 123 | elif item == 'floorplansize': 124 | if self.roomba.floorplan_size: 125 | scale = self.roomba.floorplan_size[3] 126 | if isinstance(scale, (int, float)): 127 | scale=(float(scale), float(scale)) 128 | value = {'fp_file': self.roomba.floorplan_size[0], 129 | 'x': scale[0], 130 | 'y': scale[1], 131 | 'off_x': self.roomba.floorplan_size[1], 132 | 'off_y': self.roomba.floorplan_size[2], 133 | 'angle': self.roomba.floorplan_size[4], 134 | 'trans': self.roomba.floorplan_size[5]} 135 | else: 136 | value = None 137 | return web.json_response(value) 138 | elif item == 'clear_outline': 139 | self.roomba.clear_outline() 140 | return web.Response(text="ok") 141 | elif item == 'outline': 142 | if not self.roomba.roomOutline: 143 | img = None 144 | else: 145 | img = self.roomba.img_to_png(self.roomba.room_outline) 146 | return web.Response(body=self.b64_encode(img)) 147 | elif item == 'floorplan': 148 | img = self.roomba.img_to_png(self.roomba.floorplan) 149 | return web.Response(body=self.b64_encode(img)) 150 | raise web.HTTPBadRequest(reason='bad api call {}'.format(str(request.rel_url))) 151 | 152 | @routes.get('/api/local/info/{info}') 153 | async def info(request): 154 | item = request.match_info['info'] 155 | items = self.get_items(item) 156 | value = await self.roomba.get_settings(items) 157 | if value: 158 | return web.json_response(value) 159 | raise web.HTTPBadRequest(reason='bad api call {}'.format(str(request.rel_url))) 160 | 161 | @routes.get('/api/local/action/{command}') 162 | async def action(request): 163 | command = request.match_info['command'] 164 | command = self.get_items(command) 165 | value = request.query 166 | if command and value: 167 | newcommand = {'command' : command} 168 | newcommand.update(value) 169 | self.log.info('received: {}'.format(newcommand)) 170 | self.roomba.send_region_command(newcommand) 171 | return web.Response(text="ok") 172 | await self.roomba.async_send_command(command) 173 | return web.Response(text="ok") 174 | 175 | @routes.get('/api/local/config/{config}') 176 | async def config(request): 177 | config = request.match_info['config'] 178 | config = self.get_items(config) 179 | value = await self.loop.run_in_executor(None, self.roomba.get_property, config) 180 | return web.json_response(value) 181 | 182 | @routes.get('/api/local/config/{config}/{setting}') 183 | async def set_config(request): 184 | config = request.match_info['config'] 185 | setting = request.match_info['setting'] 186 | settings = self.post_items(config, setting) 187 | for k, v in settings.items(): 188 | await self.roomba.async_set_preference(k, v) 189 | return web.Response(text="ok") 190 | 191 | @routes.post('/api/local/action/{command}') 192 | async def send_command(request): 193 | command = request.match_info['command'] 194 | value = {} 195 | if request.can_read_body: 196 | value = await request.json() 197 | self.log.info('received: {}'.format(value)) 198 | command = self.post_items(command) 199 | if command and value: 200 | value['command'] = command 201 | self.roomba.send_region_command(value) 202 | return web.Response(text="sent: {}".format(value)) 203 | raise web.HTTPBadRequest(reason='bad api call {}'.format(str(request.rel_url))) 204 | 205 | @routes.post('/map/{command}') 206 | async def map_values(request): 207 | command = request.match_info['command'] 208 | value = {} 209 | if request.can_read_body: 210 | value = await request.text() 211 | self.log.info('received: {}'.format(value)) 212 | if command == 'display_values': 213 | return web.Response(text="copy this to config.ini: {}".format(value)) 214 | elif command == 'set_fp_values': 215 | post = await request.post() 216 | for key in iter(post): 217 | self.log.info('received key: {} : value: {}'.format(key, post.get(key, None))) 218 | self.roomba.floorplan_size = (post.get('filename'), 219 | int(post.get('fpoffsetx')), 220 | int(post.get('fpoffsety')), 221 | (float(post.get('fpw')),float(post.get('fph'))), 222 | int(post.get('fprot')), 223 | float(post.get('fptrans'))) 224 | 225 | self.roomba.load_floorplan(self.roomba.floorplan_size[0], 226 | new_center=(self.roomba.floorplan_size[1], self.roomba.floorplan_size[2]), 227 | scale=self.roomba.floorplan_size[3], 228 | angle=self.roomba.floorplan_size[4], 229 | transparency=self.roomba.floorplan_size[5]) 230 | 231 | return web.Response(text="set fp values to: {}".format(value)) 232 | raise web.HTTPBadRequest(reason='bad api call {}'.format(str(request.rel_url))) 233 | 234 | self.app = web.Application() 235 | self.app.add_routes(routes) 236 | self.log.info('starting api WEB Server V{} on port {}'.format(self.__version__, self.webport)) 237 | self.web_task = self.loop.create_task(web._run_app(self.app, host='0.0.0.0', port=self.webport, print=None, access_log=self.log)) 238 | 239 | async def cancel(self): 240 | ''' 241 | shutdown web server 242 | ''' 243 | if self.app: 244 | await self.app.shutdown() 245 | await self.app.cleanup() 246 | if self.web_task and not self.web_task.done(): 247 | self.web_task.cancel() 248 | 249 | def get_items(self, request): 250 | return self.api_get.get(request, request) 251 | 252 | def post_items(self, setting, value=''): 253 | key = '/'.join([setting, value]) 254 | return self.api_post.get(key, {}) 255 | 256 | def b64_encode(self, img): 257 | b64img = None 258 | if img is not None: 259 | b64img = base64.b64encode(img) 260 | self.log.info('got: bytes len {}'.format(len(b64img))) 261 | return b64img 262 | 263 | 264 | class dummy_roomba(): 265 | ''' 266 | dummy roomba class for testing 267 | 268 | ''' 269 | def __init__(self): 270 | self.roombaName = 'Simulated' 271 | self.log = logging.getLogger("Roomba.{}.api".format(self.roombaName)) 272 | self.log.info('Simulating Roomba') 273 | self.response = 'Simulated Roomba' 274 | self.drawmap = False 275 | self.roomOutline = False 276 | self.room_outline = None 277 | self.floorplan = None 278 | self.floorplan_size = None 279 | self.mapSize = None 280 | 281 | def img_to_png(self, name): 282 | return None 283 | 284 | def clear_outline(self): 285 | return None 286 | 287 | async def get_settings(self, *args): 288 | return self.response 289 | 290 | async def async_send_command(self, *args): 291 | return None 292 | 293 | def get_property(self, *args): 294 | return self.response 295 | 296 | async def async_set_preference(self, *args): 297 | return None 298 | 299 | def main(): 300 | logging.basicConfig(level=logging.INFO, 301 | format= '[%(asctime)s][%(levelname)5.5s](%(name)-20s) %(message)s') 302 | try: 303 | log = logging.getLogger('Roomba.{}'.format(__name__)) 304 | loop = asyncio.get_event_loop() 305 | webs = webserver(webport=8200) 306 | loop.run_forever() 307 | 308 | except (KeyboardInterrupt, SystemExit): 309 | log.info("System exit Received - Exiting program") 310 | 311 | finally: 312 | pass 313 | 314 | if __name__ == '__main__': 315 | main() 316 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | setup( 5 | name='Roomba980-Python', 6 | version='2.0', 7 | license='MIT', 8 | description='Python program and library to control iRobot WiFi Roomba ' \ 9 | 'Vacuum Cleaner', 10 | author_email='nick.waterton@med.ge.com', 11 | url='https://github.com/NickWaterton/Roomba980-Python', 12 | packages=find_packages(), 13 | python_requires='>=3.6', 14 | install_requires=['paho-mqtt'], 15 | include_package_data=True, 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'roomba=roomba.__main__:main', 24 | 'roomba-getpassword=roomba.getpassword:main' 25 | ] 26 | } 27 | ) 28 | --------------------------------------------------------------------------------