Some cleanups and add support for browsing maps
parent
6d21e49cd5
commit
4eb24160a6
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
TEMPLATE = aux
|
||||||
|
|
||||||
|
OTHER_FILES = integrationpluginneatobotvac.json \
|
||||||
|
integrationpluginneatobotvac.py
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ PLUGIN_DIRS = \
|
||||||
lifx \
|
lifx \
|
||||||
mailnotification \
|
mailnotification \
|
||||||
mqttclient \
|
mqttclient \
|
||||||
|
neatobotvac \
|
||||||
nanoleaf \
|
nanoleaf \
|
||||||
netatmo \
|
netatmo \
|
||||||
networkdetector \
|
networkdetector \
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue