diff --git a/neatobotvac/integrationpluginneatobotvac.json b/neatobotvac/integrationpluginneatobotvac.json index 6554dfdf..01e515d1 100644 --- a/neatobotvac/integrationpluginneatobotvac.json +++ b/neatobotvac/integrationpluginneatobotvac.json @@ -48,6 +48,8 @@ "name": "robot", "displayName": "Neato robot", "createMethods": ["auto"], + "interfaces":["cleaningrobot", "battery", "connectable"], + "browsable": true, "paramTypes": [ { "id": "def9a4bb-7a7e-4e3a-a63c-c55a105abb5e", @@ -60,16 +62,7 @@ "name": "secret", "displayName": "Secret", "type": "QString" - }, - { - "id": "141f0d98-1806-432c-aaac-c0d3a89a8e58", - "name": "mapId", - "displayName": "Map ID", - "type": "QString" } - ], - "interfaces":[ - ], "settingsTypes": [ { @@ -95,14 +88,22 @@ } ], "stateTypes":[ + { + "id": "4c319e4b-9206-48ed-9d24-56a536c58d61", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, { "id": "dce4f7f3-a0a6-46bb-9216-c9089d9e9b0d", "name": "cleaning", "displayName": "Cleaning", "displayNameEvent": "Cleaning yes/no", "type": "bool", - "defaultValue": false, - "cached": false + "defaultValue": false }, { "id": "0f925abf-396c-437e-b259-2fed7eafe7f4", @@ -110,8 +111,7 @@ "displayName": "Paused", "displayNameEvent": "Cleaning paused yes/no", "type": "bool", - "defaultValue": false, - "cached": false + "defaultValue": false }, { "id": "1b8abd35-8276-44ba-8c75-a647877b2e11", @@ -119,8 +119,7 @@ "displayName": "Charging", "displayNameEvent": "Robot charging yes/no", "type": "bool", - "defaultValue": true, - "cached": false + "defaultValue": true }, { "id": "805175ec-c2e4-4fbe-9505-282750ef1467", @@ -128,8 +127,15 @@ "displayName": "Docked", "displayNameEvent": "Robot docked yes/no", "type": "bool", - "defaultValue": true, - "cached": false + "defaultValue": true + }, + { + "id": "1985ce98-f387-47e0-a5f3-9b807f532ca1", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery entered or left critical state", + "type": "bool", + "defaultValue": false }, { "id": "20ed8767-806f-4ec2-8626-842cd398f9df", @@ -137,10 +143,10 @@ "displayName": "Battery level", "displayNameEvent": "Battery level percentage", "type": "int", + "unit": "Percentage", "defaultValue": 0, "minValue": 0, - "maxValue": 100, - "cached": false + "maxValue": 100 } ], "actionTypes": [ @@ -151,8 +157,8 @@ }, { "id": "5178a803-5696-4ee1-80a4-2c7c20a5043a", - "name": "goToBase", - "displayName": "Go to base" + "name": "returnToBase", + "displayName": "Return to base" }, { "id": "30775042-55a7-4f1b-9042-a9bdeadc4b0d", diff --git a/neatobotvac/integrationpluginneatobotvac.py b/neatobotvac/integrationpluginneatobotvac.py index 915ac221..fc1852cb 100644 --- a/neatobotvac/integrationpluginneatobotvac.py +++ b/neatobotvac/integrationpluginneatobotvac.py @@ -6,8 +6,9 @@ import json # pybotvac library: https://github.com/stianaske/pybotvac -thingsAndRobots = {} oauthSessions = {} +accountsMap = {} +thingsAndRobots = {} pollTimer = None @@ -30,9 +31,12 @@ def confirmPairing(info, username, secret): del oauthSessions[info.transactionId] info.finish(nymea.ThingErrorNoError) + def setupThing(info): # Setup for the account if info.thing.thingClassId == accountThingClassId: + logger.log("SetupThing for account:", info.thing.name) + pluginStorage().beginGroup(info.thing.id) token = json.loads(pluginStorage().value("token")) logger.log("setup", token) @@ -53,120 +57,108 @@ def setupThing(info): # Create an account session on the session to get info about the login account = Account(oAuthSession) + accountsMap[info.thing] = account # List all robots associated with account logger.log("account created. Robots:", account.robots); - logger.log("Persistent maps: ", account.persistent_maps) - mapDict = account.persistent_maps - mapKeys = mapDict.keys() - logger.log("Keys: ", mapKeys) - mapValues = mapDict.values() - logger.log("Values: ", mapValues) - thingDescriptors = [] for robot in account.robots: - logger.log(robot) + logger.log("Found new robot:", robot.serial) # Check if this robot is already added in nymea found = False for thing in myThings(): - if thing.paramValue(robotThingSerialParamTypeId) == robot.serial: + logger.log("Comparing to existing robot:", thing.name) + if thing.thingClassId == robotThingClassId and thing.paramValue(robotThingSerialParamTypeId) == robot.serial: + logger.log("Already have this robot in the system") # Yep, already here... skip it found = True - continue + break if found: continue - thingDescriptor = nymea.ThingDescriptor(robotThingClassId, robot.name) - logger.log("MapID for Serial: ", robot.serial, mapDict[robot.serial]) - mapInfo = mapDict[robot.serial] - logger.log("Type mapInfo: ", type(mapInfo)) - logger.log("Contents mapInfo: ", mapInfo) - mapInfo2 = mapInfo[0] - logger.log("MapInfo2 type: ", type(mapInfo2), " MapInfo2 contents: ", mapInfo2) - mapId = mapInfo2['id'] - logger.log("MapId type: ", type(mapId), " MapId contents: ", mapId) - mapName = mapInfo2['name'] - logger.log("MapName type: ", type(mapName), " MapName contents: ", mapName) - rbtMapBound = robot.get_map_boundaries(mapId) - logger.log("rbtMapBound Type: ", type(rbtMapBound), "rbtMapBound Contents: ", rbtMapBound) - rbtBoundData = rbtMapBound.text - logger.log("rbtBoundData Type: ", type(rbtBoundData), "rbtBoundData Contents: ", rbtBoundData) + logger.log("Adding new robot to the system with parent", info.thing.id) + thingDescriptor = nymea.ThingDescriptor(robotThingClassId, robot.name, parentId=info.thing.id) thingDescriptor.params = [ nymea.Param(robotThingSerialParamTypeId, robot.serial), - nymea.Param(robotThingSecretParamTypeId, robot.secret), - nymea.Param(robotThingMapIdParamTypeId, mapId) + nymea.Param(robotThingSecretParamTypeId, robot.secret) ] thingDescriptors.append(thingDescriptor) # And let nymea know about all the users robots autoThingsAppeared(thingDescriptors) - # return # If no poll timer is set up yet, start it now logger.log("Creating polltimer") global pollTimer - pollTimer = threading.Timer(5, pollService) - pollTimer.start() - return + if pollTimer is not None: + pollTimer = threading.Timer(5, pollService) + pollTimer.start() + return # Setup for the robots if info.thing.thingClassId == robotThingClassId: + logger.log("SetupThing for robot:", info.thing.name) serial = info.thing.paramValue(robotThingSerialParamTypeId) secret = info.thing.paramValue(robotThingSecretParamTypeId) robot = Robot(serial, secret, info.thing.name) thingsAndRobots[info.thing] = robot; + try: + refreshRobot(info.thing) + except: + logger.warn("Error getting robot state"); + info.finish(nymea.ThingErrorHardwareFailure, "Unable to connect to neato API.") + return; + logger.log(robot.get_robot_state()) + info.thing.setStateValue(robotConnectedStateTypeId, True) # set up polling for robot status info.finish(nymea.ThingErrorNoError) return +def refreshRobot(thing): + robot = thingsAndRobots[thing] + logger.log("Refreshing robot:", robot) + + # Get robot state + rbtState = thingsAndRobots[thing].get_robot_state() + rbtStateJson = rbtState.json() + + logger.log("Robot state for %s: %s" % (thing.name, rbtStateJson)) + + # Set robot docked/charging state + rbtStateDetails = rbtStateJson['details'] + thing.setStateValue(robotChargingStateTypeId, rbtStateDetails['isCharging']) + thing.setStateValue(robotDockedStateTypeId, rbtStateDetails['isDocked']) + thing.setStateValue(robotBatteryLevelStateTypeId, rbtStateDetails['charge']) + + # Set robot cleaning/paused state + rbtStateCommands = rbtStateJson['availableCommands'] + rbtStartAv = rbtStateCommands['start'] + rbtPauseAv = rbtStateCommands['pause'] + rbtResumeAv = rbtStateCommands['resume'] + if rbtStartAv == True: + thing.setStateValue(robotCleaningStateTypeId, False) + thing.setStateValue(robotPausedStateTypeId, False) + elif rbtPauseAv == True: + thing.setStateValue(robotCleaningStateTypeId, True) + thing.setStateValue(robotPausedStateTypeId, False) + elif rbtResumeAv == True: + thing.setStateValue(robotCleaningStateTypeId, True) + thing.setStateValue(robotPausedStateTypeId, True) + def pollService(): - logger.log("pollService!!!") - # Poll all robots we know for thing in myThings(): if thing.thingClassId == robotThingClassId: - robot = thingsAndRobots[thing] - logger.log("polling robot:", robot) - - # Get robot state - rbtState = thingsAndRobots[thing].get_robot_state() - rbtStateJson = rbtState.json() - - # Set robot docked/charging state - rbtStateDetails = rbtStateJson['details'] - rbtCharging = rbtStateDetails['isCharging'] - rbtDocked = rbtStateDetails['isDocked'] - rbtStateOfCharge = rbtStateDetails['charge'] - logger.log("Updating thing", thing.name, "Charging", rbtCharging) - thing.setStateValue(robotChargingStateTypeId, rbtCharging) - logger.log("Updating thing", thing.name, "Docked", rbtDocked) - thing.setStateValue(robotDockedStateTypeId, rbtDocked) - logger.log("Updating thing", thing.name, "Battery Charge Level", rbtStateOfCharge) - thing.setStateValue(robotBatteryLevelStateTypeId, rbtStateOfCharge) - - # Set robot cleaning/paused state - rbtStateCommands = rbtStateJson['availableCommands'] - rbtStartAv = rbtStateCommands['start'] - rbtPauseAv = rbtStateCommands['pause'] - rbtResumeAv = rbtStateCommands['resume'] - if rbtStartAv == True: - logger.log("Updating thing", thing.name, "Cleaning: False") - thing.setStateValue(robotCleaningStateTypeId, False) - thing.setStateValue(robotPausedStateTypeId, False) - elif rbtPauseAv == True: - logger.log("Updating thing", thing.name, "Cleaning: True") - thing.setStateValue(robotCleaningStateTypeId, True) - thing.setStateValue(robotPausedStateTypeId, False) - elif rbtResumeAv == True: - logger.log("Updating thing", thing.name, "Paused: True") - thing.setStateValue(robotCleaningStateTypeId, True) - thing.setStateValue(robotPausedStateTypeId, True) + try: + refreshRobot(thing) + except: + logger.warn("Error refreshing robot state") # restart the timer for next poll global pollTimer @@ -190,18 +182,18 @@ def executeAction(info): thingsAndRobots[info.thing].pause_cleaning() elif rbtResumeAv == True: thingsAndRobots[info.thing].resume_cleaning() - threading.Timer(5, pollService).start() + refreshRobot(info.thing) info.finish(nymea.ThingErrorNoError) return - if info.actionTypeId == robotGoToBaseActionTypeId: + if info.actionTypeId == robotReturnToBaseActionTypeId: thingsAndRobots[info.thing].send_to_base() - threading.Timer(5, pollService).start() + refreshRobot(info.thing) info.finish(nymea.ThingErrorNoError) if info.actionTypeId == robotStopCleaningActionTypeId: thingsAndRobots[info.thing].stop_cleaning() - threading.Timer(5, pollService).start() + refreshRobot(info.thing) info.finish(nymea.ThingErrorNoError) # To do: get available boundaries to use with start_cleaning action @@ -211,10 +203,57 @@ def executeAction(info): logger.log("rbtMapBound Type: ", type(rbtMapBound), "rbtMapBound Contents: ", rbtMapBound) rbtBoundData = rbtMapBound.text logger.log("rbtBoundData Type: ", type(rbtBoundData), "rbtBoundData Contents: ", rbtBoundData) - threading.Timer(5, pollService).start() + refreshRobot(info.thing) info.finish(nymea.ThingErrorNoError) +def browseThing(browseResult): + robotThing = browseResult.thing + robot = thingsAndRobots[browseResult.thing] + account = None + for thing in myThings(): + logger.log("checking thing", thing.name, thing.id, robotThing.parentId) + if thing.id == robotThing.parentId: + account = accountsMap[thing] + break + if account is None: + logger.warn("Cannot find account for robot", robotThing.name) + browseResult.finish(nymea.ThingErrorAuthenticationFailure) + return; + + + # Top level entries -> return maps + if browseResult.itemId == "": + maps = account.persistent_maps + + logger.log("maps", tpye(maps), maps) + for map in maps[robot.serial]: + logger.log("Type mapInfo: ", type(map)) + logger.log("map:", map) + browseResult.addItem(nymea.BrowserItem("map-" + map["id"], map["name"], browsable=True, thumbnail=map["url"])) + + browseResult.finish(nymea.ThingErrorNoError) + return + + # browsing boundaries for a map + if browseResult.itemId.startswith("map-"): + mapId = browseResult.itemId[4:] + boundaries = robot.get_map_boundaries(mapId) + + logger.log("boundaries", type(boundaries), boundaries.json()) + for boundary in boundaries.json()["data"]["boundaries"]: + browseResult.addItem(nymea.BrowserItem(boundary["id"], boundary["name"], boundary["type"], executable=True, disabled=not boundary["enabled"], icon=nymea.BrowserIconFavorites)) + + browseResult.finish(nymea.ThingErrorNoError) + return + + +def executeBrowserItem(info): + # TODO: An item in the browser has been clicked. + logger.log("Browser item clicked:", info.itemId) + info.finish(nymea.ThingErrorNoError) + + def deinit(): global pollTimer # If we started a poll timer, cancel it on shutdown. diff --git a/neatobotvac/neatobotvac.pro b/neatobotvac/neatobotvac.pro new file mode 100644 index 00000000..5334cad8 --- /dev/null +++ b/neatobotvac/neatobotvac.pro @@ -0,0 +1,5 @@ +TEMPLATE = aux + +OTHER_FILES = integrationpluginneatobotvac.json \ + integrationpluginneatobotvac.py + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index eb037d01..fba11d49 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -33,6 +33,7 @@ PLUGIN_DIRS = \ lifx \ mailnotification \ mqttclient \ + neatobotvac \ nanoleaf \ netatmo \ networkdetector \