Some cleanups and add support for browsing maps

master
Michael Zanetti 2021-04-08 00:58:33 +02:00
parent 6d21e49cd5
commit 4eb24160a6
4 changed files with 147 additions and 96 deletions

View File

@ -48,6 +48,8 @@
"name": "robot", "name": "robot",
"displayName": "Neato robot", "displayName": "Neato robot",
"createMethods": ["auto"], "createMethods": ["auto"],
"interfaces":["cleaningrobot", "battery", "connectable"],
"browsable": true,
"paramTypes": [ "paramTypes": [
{ {
"id": "def9a4bb-7a7e-4e3a-a63c-c55a105abb5e", "id": "def9a4bb-7a7e-4e3a-a63c-c55a105abb5e",
@ -60,16 +62,7 @@
"name": "secret", "name": "secret",
"displayName": "Secret", "displayName": "Secret",
"type": "QString" "type": "QString"
},
{
"id": "141f0d98-1806-432c-aaac-c0d3a89a8e58",
"name": "mapId",
"displayName": "Map ID",
"type": "QString"
} }
],
"interfaces":[
], ],
"settingsTypes": [ "settingsTypes": [
{ {
@ -95,14 +88,22 @@
} }
], ],
"stateTypes":[ "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", "id": "dce4f7f3-a0a6-46bb-9216-c9089d9e9b0d",
"name": "cleaning", "name": "cleaning",
"displayName": "Cleaning", "displayName": "Cleaning",
"displayNameEvent": "Cleaning yes/no", "displayNameEvent": "Cleaning yes/no",
"type": "bool", "type": "bool",
"defaultValue": false, "defaultValue": false
"cached": false
}, },
{ {
"id": "0f925abf-396c-437e-b259-2fed7eafe7f4", "id": "0f925abf-396c-437e-b259-2fed7eafe7f4",
@ -110,8 +111,7 @@
"displayName": "Paused", "displayName": "Paused",
"displayNameEvent": "Cleaning paused yes/no", "displayNameEvent": "Cleaning paused yes/no",
"type": "bool", "type": "bool",
"defaultValue": false, "defaultValue": false
"cached": false
}, },
{ {
"id": "1b8abd35-8276-44ba-8c75-a647877b2e11", "id": "1b8abd35-8276-44ba-8c75-a647877b2e11",
@ -119,8 +119,7 @@
"displayName": "Charging", "displayName": "Charging",
"displayNameEvent": "Robot charging yes/no", "displayNameEvent": "Robot charging yes/no",
"type": "bool", "type": "bool",
"defaultValue": true, "defaultValue": true
"cached": false
}, },
{ {
"id": "805175ec-c2e4-4fbe-9505-282750ef1467", "id": "805175ec-c2e4-4fbe-9505-282750ef1467",
@ -128,8 +127,15 @@
"displayName": "Docked", "displayName": "Docked",
"displayNameEvent": "Robot docked yes/no", "displayNameEvent": "Robot docked yes/no",
"type": "bool", "type": "bool",
"defaultValue": true, "defaultValue": true
"cached": false },
{
"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", "id": "20ed8767-806f-4ec2-8626-842cd398f9df",
@ -137,10 +143,10 @@
"displayName": "Battery level", "displayName": "Battery level",
"displayNameEvent": "Battery level percentage", "displayNameEvent": "Battery level percentage",
"type": "int", "type": "int",
"unit": "Percentage",
"defaultValue": 0, "defaultValue": 0,
"minValue": 0, "minValue": 0,
"maxValue": 100, "maxValue": 100
"cached": false
} }
], ],
"actionTypes": [ "actionTypes": [
@ -151,8 +157,8 @@
}, },
{ {
"id": "5178a803-5696-4ee1-80a4-2c7c20a5043a", "id": "5178a803-5696-4ee1-80a4-2c7c20a5043a",
"name": "goToBase", "name": "returnToBase",
"displayName": "Go to base" "displayName": "Return to base"
}, },
{ {
"id": "30775042-55a7-4f1b-9042-a9bdeadc4b0d", "id": "30775042-55a7-4f1b-9042-a9bdeadc4b0d",

View File

@ -6,8 +6,9 @@ import json
# pybotvac library: https://github.com/stianaske/pybotvac # pybotvac library: https://github.com/stianaske/pybotvac
thingsAndRobots = {}
oauthSessions = {} oauthSessions = {}
accountsMap = {}
thingsAndRobots = {}
pollTimer = None pollTimer = None
@ -30,9 +31,12 @@ def confirmPairing(info, username, secret):
del oauthSessions[info.transactionId] del oauthSessions[info.transactionId]
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
def setupThing(info): def setupThing(info):
# Setup for the account # Setup for the account
if info.thing.thingClassId == accountThingClassId: if info.thing.thingClassId == accountThingClassId:
logger.log("SetupThing for account:", info.thing.name)
pluginStorage().beginGroup(info.thing.id) pluginStorage().beginGroup(info.thing.id)
token = json.loads(pluginStorage().value("token")) token = json.loads(pluginStorage().value("token"))
logger.log("setup", token) logger.log("setup", token)
@ -53,102 +57,83 @@ def setupThing(info):
# Create an account session on the session to get info about the login # Create an account session on the session to get info about the login
account = Account(oAuthSession) account = Account(oAuthSession)
accountsMap[info.thing] = account
# List all robots associated with account # List all robots associated with account
logger.log("account created. Robots:", account.robots); 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 = [] thingDescriptors = []
for robot in account.robots: for robot in account.robots:
logger.log(robot) logger.log("Found new robot:", robot.serial)
# Check if this robot is already added in nymea # Check if this robot is already added in nymea
found = False found = False
for thing in myThings(): 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 # Yep, already here... skip it
found = True found = True
continue break
if found: if found:
continue continue
thingDescriptor = nymea.ThingDescriptor(robotThingClassId, robot.name) logger.log("Adding new robot to the system with parent", info.thing.id)
logger.log("MapID for Serial: ", robot.serial, mapDict[robot.serial]) thingDescriptor = nymea.ThingDescriptor(robotThingClassId, robot.name, parentId=info.thing.id)
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)
thingDescriptor.params = [ thingDescriptor.params = [
nymea.Param(robotThingSerialParamTypeId, robot.serial), nymea.Param(robotThingSerialParamTypeId, robot.serial),
nymea.Param(robotThingSecretParamTypeId, robot.secret), nymea.Param(robotThingSecretParamTypeId, robot.secret)
nymea.Param(robotThingMapIdParamTypeId, mapId)
] ]
thingDescriptors.append(thingDescriptor) thingDescriptors.append(thingDescriptor)
# And let nymea know about all the users robots # And let nymea know about all the users robots
autoThingsAppeared(thingDescriptors) autoThingsAppeared(thingDescriptors)
# return
# If no poll timer is set up yet, start it now # If no poll timer is set up yet, start it now
logger.log("Creating polltimer") logger.log("Creating polltimer")
global pollTimer global pollTimer
if pollTimer is not None:
pollTimer = threading.Timer(5, pollService) pollTimer = threading.Timer(5, pollService)
pollTimer.start() pollTimer.start()
return
return
# Setup for the robots # Setup for the robots
if info.thing.thingClassId == robotThingClassId: if info.thing.thingClassId == robotThingClassId:
logger.log("SetupThing for robot:", info.thing.name)
serial = info.thing.paramValue(robotThingSerialParamTypeId) serial = info.thing.paramValue(robotThingSerialParamTypeId)
secret = info.thing.paramValue(robotThingSecretParamTypeId) secret = info.thing.paramValue(robotThingSecretParamTypeId)
robot = Robot(serial, secret, info.thing.name) robot = Robot(serial, secret, info.thing.name)
thingsAndRobots[info.thing] = robot; 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()) logger.log(robot.get_robot_state())
info.thing.setStateValue(robotConnectedStateTypeId, True)
# set up polling for robot status # set up polling for robot status
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
return return
def refreshRobot(thing):
def pollService():
logger.log("pollService!!!")
# Poll all robots we know
for thing in myThings():
if thing.thingClassId == robotThingClassId:
robot = thingsAndRobots[thing] robot = thingsAndRobots[thing]
logger.log("polling robot:", robot) logger.log("Refreshing robot:", robot)
# Get robot state # Get robot state
rbtState = thingsAndRobots[thing].get_robot_state() rbtState = thingsAndRobots[thing].get_robot_state()
rbtStateJson = rbtState.json() rbtStateJson = rbtState.json()
logger.log("Robot state for %s: %s" % (thing.name, rbtStateJson))
# Set robot docked/charging state # Set robot docked/charging state
rbtStateDetails = rbtStateJson['details'] rbtStateDetails = rbtStateJson['details']
rbtCharging = rbtStateDetails['isCharging'] thing.setStateValue(robotChargingStateTypeId, rbtStateDetails['isCharging'])
rbtDocked = rbtStateDetails['isDocked'] thing.setStateValue(robotDockedStateTypeId, rbtStateDetails['isDocked'])
rbtStateOfCharge = rbtStateDetails['charge'] thing.setStateValue(robotBatteryLevelStateTypeId, 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 # Set robot cleaning/paused state
rbtStateCommands = rbtStateJson['availableCommands'] rbtStateCommands = rbtStateJson['availableCommands']
@ -156,18 +141,25 @@ def pollService():
rbtPauseAv = rbtStateCommands['pause'] rbtPauseAv = rbtStateCommands['pause']
rbtResumeAv = rbtStateCommands['resume'] rbtResumeAv = rbtStateCommands['resume']
if rbtStartAv == True: if rbtStartAv == True:
logger.log("Updating thing", thing.name, "Cleaning: False")
thing.setStateValue(robotCleaningStateTypeId, False) thing.setStateValue(robotCleaningStateTypeId, False)
thing.setStateValue(robotPausedStateTypeId, False) thing.setStateValue(robotPausedStateTypeId, False)
elif rbtPauseAv == True: elif rbtPauseAv == True:
logger.log("Updating thing", thing.name, "Cleaning: True")
thing.setStateValue(robotCleaningStateTypeId, True) thing.setStateValue(robotCleaningStateTypeId, True)
thing.setStateValue(robotPausedStateTypeId, False) thing.setStateValue(robotPausedStateTypeId, False)
elif rbtResumeAv == True: elif rbtResumeAv == True:
logger.log("Updating thing", thing.name, "Paused: True")
thing.setStateValue(robotCleaningStateTypeId, True) thing.setStateValue(robotCleaningStateTypeId, True)
thing.setStateValue(robotPausedStateTypeId, True) thing.setStateValue(robotPausedStateTypeId, True)
def pollService():
# Poll all robots we know
for thing in myThings():
if thing.thingClassId == robotThingClassId:
try:
refreshRobot(thing)
except:
logger.warn("Error refreshing robot state")
# restart the timer for next poll # restart the timer for next poll
global pollTimer global pollTimer
pollTimer = threading.Timer(60, pollService) pollTimer = threading.Timer(60, pollService)
@ -190,18 +182,18 @@ def executeAction(info):
thingsAndRobots[info.thing].pause_cleaning() thingsAndRobots[info.thing].pause_cleaning()
elif rbtResumeAv == True: elif rbtResumeAv == True:
thingsAndRobots[info.thing].resume_cleaning() thingsAndRobots[info.thing].resume_cleaning()
threading.Timer(5, pollService).start() refreshRobot(info.thing)
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
return return
if info.actionTypeId == robotGoToBaseActionTypeId: if info.actionTypeId == robotReturnToBaseActionTypeId:
thingsAndRobots[info.thing].send_to_base() thingsAndRobots[info.thing].send_to_base()
threading.Timer(5, pollService).start() refreshRobot(info.thing)
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
if info.actionTypeId == robotStopCleaningActionTypeId: if info.actionTypeId == robotStopCleaningActionTypeId:
thingsAndRobots[info.thing].stop_cleaning() thingsAndRobots[info.thing].stop_cleaning()
threading.Timer(5, pollService).start() refreshRobot(info.thing)
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
# To do: get available boundaries to use with start_cleaning action # To do: get available boundaries to use with start_cleaning action
@ -211,7 +203,54 @@ def executeAction(info):
logger.log("rbtMapBound Type: ", type(rbtMapBound), "rbtMapBound Contents: ", rbtMapBound) logger.log("rbtMapBound Type: ", type(rbtMapBound), "rbtMapBound Contents: ", rbtMapBound)
rbtBoundData = rbtMapBound.text rbtBoundData = rbtMapBound.text
logger.log("rbtBoundData Type: ", type(rbtBoundData), "rbtBoundData Contents: ", rbtBoundData) 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) info.finish(nymea.ThingErrorNoError)

View File

@ -0,0 +1,5 @@
TEMPLATE = aux
OTHER_FILES = integrationpluginneatobotvac.json \
integrationpluginneatobotvac.py

View File

@ -33,6 +33,7 @@ PLUGIN_DIRS = \
lifx \ lifx \
mailnotification \ mailnotification \
mqttclient \ mqttclient \
neatobotvac \
nanoleaf \ nanoleaf \
netatmo \ netatmo \
networkdetector \ networkdetector \