diff --git a/debian/control b/debian/control index 32d75ec9..6c0ed8cc 100644 --- a/debian/control +++ b/debian/control @@ -480,6 +480,22 @@ Description: nymea.io plugin for a generic MQTT client This package will install a generic MQTT client plugin for nymea.io +Package: nymea-plugin-neatobotvac +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, + python3-pip, +Description: nymea.io plugin for neato + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for Neato Botvac robots. + + Package: nymea-plugin-netatmo Architecture: any Depends: ${shlibs:Depends}, @@ -1163,6 +1179,7 @@ Depends: nymea-plugin-anel, nymea-plugin-texasinstruments, nymea-plugin-telegram, nymea-plugin-nanoleaf, + nymea-plugin-neatobotvac, nymea-plugin-netatmo, nymea-plugin-networkdetector, nymea-plugin-nuki, diff --git a/debian/nymea-plugin-neatobotvac.install.in b/debian/nymea-plugin-neatobotvac.install.in new file mode 100644 index 00000000..76a6c8b3 --- /dev/null +++ b/debian/nymea-plugin-neatobotvac.install.in @@ -0,0 +1,3 @@ +neatobotvac/integrationpluginneatobotvac.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/neatobotvac/ +neatobotvac/integrationpluginneatobotvac.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/neatobotvac/ +neatobotvac/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/neatobotvac/ diff --git a/neatobotvac/README.md b/neatobotvac/README.md new file mode 100644 index 00000000..4cab2e18 --- /dev/null +++ b/neatobotvac/README.md @@ -0,0 +1,17 @@ +# Neato Botvac + +This plugin allows to interact with your Neato Botvac cleaning robots. Each robot linkd to your account will appear automatically in the system, once the account is added to nymea. + +## Supported Things + +* Neato D7 (Tested) +* Other Neato Dx should also work, but haven't been tested + +## Requirements + +* The robot must be added to a personal Neato account, that can be created via the Neato Robotics website. +* The package “nymea-plugin-neatobotvac” must be installed + +## More + + [Neato Robotics](https://neatorobotics.com/) diff --git a/neatobotvac/integrationpluginneatobotvac.json b/neatobotvac/integrationpluginneatobotvac.json new file mode 100644 index 00000000..c69deb9c --- /dev/null +++ b/neatobotvac/integrationpluginneatobotvac.json @@ -0,0 +1,170 @@ +{ + "name": "neato", + "displayName": "Neato", + "id": "4f6ecb6f-a7fe-4fdb-b8d8-45b1f235110c", + "apiKeys": ["neato"], + "vendors": [ + { + "name": "neato", + "displayName": "Neato Robotics", + "id": "d2a234a5-0aeb-4c04-98d5-6428cd266433", + "thingClasses": [ + { + "id": "fe594fb0-b712-4f23-8267-649eb459747b", + "name": "account", + "displayName": "Neato account", + "createMethods": ["User"], + "interfaces": ["account"], + "setupMethod": "oauth", + "settingsTypes": [ + + ], + "stateTypes":[ + { + "id": "e8f47781-e3fd-416f-a9ac-51ef942d0573", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected/disconnected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "b0db7079-49f0-444a-9c55-4bb4c764f3cb", + "name": "loggedIn", + "displayName": "Logged in", + "displayNameEvent": "Logged in or out", + "type": "bool", + "defaultValue": false + } + ], + "actionTypes": [ + + ] + }, + { + "id": "b924c87a-f783-4f45-a3af-929684c24aea", + "name": "robot", + "displayName": "Neato robot", + "createMethods": ["auto"], + "interfaces":["cleaningrobot", "battery", "connectable"], + "browsable": true, + "paramTypes": [ + { + "id": "def9a4bb-7a7e-4e3a-a63c-c55a105abb5e", + "name": "serial", + "displayName": "Robot Serial", + "type": "QString" + }, + { + "id": "3793e48b-043e-43cb-b672-7c1e2e90bc8e", + "name": "secret", + "displayName": "Secret", + "type": "QString" + } + ], + "settingsTypes": [ + { + "id": "dabaafd3-908f-4f06-8039-5a7a729346da", + "name": "eco", + "displayName": "Eco", + "type": "bool", + "defaultValue": true + }, + { + "id": "86694abb-5633-4e62-bd6c-325eb246c683", + "name": "care", + "displayName": "Extra Care", + "type": "bool", + "defaultValue": false + }, + { + "id": "f72bcfbd-a262-44b3-ad75-9bb094aa2bb1", + "name": "noGoLines", + "displayName": "No-go Lines Enabled", + "type": "bool", + "defaultValue": true + } + ], + "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": "robotState", + "displayName": "Cleaning state", + "displayNameEvent": "Cleaning state changed", + "type": "QString", + "possibleValues": ["docked", "cleaning", "paused", "traveling", "stopped", "error"], + "defaultValue": "docked" + }, + { + "id": "cb22b48c-1c21-4d52-bde6-847287435685", + "name": "errorMessage", + "displayName": "Error message", + "displayNameEvent": "Error message changes", + "type": "QString", + "defaultValue": "no error" + }, + { + "id": "1b8abd35-8276-44ba-8c75-a647877b2e11", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Started or stopped charging", + "type": "bool", + "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", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level changed", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + } + ], + "actionTypes": [ + { + "id": "1f774998-5fa7-4e3b-8ab0-a8402dd561bb", + "name": "startCleaning", + "displayName": "Start cleaning" + }, + { + "id": "e731faa6-88c9-406d-b505-f89b5f0868b0", + "name": "pauseCleaning", + "displayName": "Pause/resume cleaning" + }, + { + "id": "5178a803-5696-4ee1-80a4-2c7c20a5043a", + "name": "returnToBase", + "displayName": "Return to base" + }, + { + "id": "30775042-55a7-4f1b-9042-a9bdeadc4b0d", + "name": "stopCleaning", + "displayName": "Stop cleaning" + } + ] + } + ] + } + ] +} diff --git a/neatobotvac/integrationpluginneatobotvac.py b/neatobotvac/integrationpluginneatobotvac.py new file mode 100644 index 00000000..ef033245 --- /dev/null +++ b/neatobotvac/integrationpluginneatobotvac.py @@ -0,0 +1,328 @@ +import nymea +import time +import threading +from pybotvac import Account, Neato, OAuthSession, PasswordlessSession, PasswordSession, Vorwerk, Robot +import json + +# pybotvac library: https://github.com/stianaske/pybotvac + +oauthSessions = {} +accountsMap = {} +thingsAndRobots = {} + +pollTimer = None + +def startPairing(info): + # Start OAuth2 session + apiKey = apiKeyStorage().requestKey("neato") + oauthSession = OAuthSession(client_id=apiKey.data("clientId"), client_secret=apiKey.data("clientSecret"), redirect_uri="https://127.0.0.1:8888", vendor=Neato()) + oauthSessions[info.transactionId] = oauthSession; + authorizationUrl = oauthSession.get_authorization_url() + info.oAuthUrl = authorizationUrl + info.finish(nymea.ThingErrorNoError) + + +def confirmPairing(info, username, secret): + # The user has successfully logged in at neato. Obtain the token from the OAuth session + token = oauthSessions[info.transactionId].fetch_token(secret) + pluginStorage().beginGroup(info.thingId) + pluginStorage().setValue("token", json.dumps(token)) + pluginStorage().endGroup(); + 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) + pluginStorage().endGroup(); + + try: + oAuthSession = OAuthSession(token=token) + # Login went well, finish the setup + info.finish(nymea.ThingErrorNoError) + except Exception as e: + # Login error + logger.warn("Error setting up neato account:", str(e)) + info.finish(nymea.ThingErrorAuthenticationFailure, str(e)) + return + + # Mark the account as logged-in and connected + info.thing.setStateValue(accountLoggedInStateTypeId, True) + info.thing.setStateValue(accountConnectedStateTypeId, True) + + # 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); + + thingDescriptors = [] + for robot in account.robots: + logger.log("Found new robot:", robot.serial) + # Check if this robot is already added in nymea + found = False + for thing in myThings(): + 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 + break + if found: + continue + + 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) + ] + thingDescriptors.append(thingDescriptor) + + # And let nymea know about all the users robots + autoThingsAppeared(thingDescriptors) + + # If no poll timer is set up yet, start it now + logger.log("Creating polltimer") + global pollTimer + if pollTimer is 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) + try: + robot = Robot(serial, secret, info.thing.name) + thingsAndRobots[info.thing] = robot; + refreshRobot(info.thing) + except Exception as e: + logger.warn("Error setting up robot:", e) + info.finish(nymea.ThingErrorHardwareFailure, str(e)) + return + + 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(robotBatteryLevelStateTypeId, rbtStateDetails['charge']) + + # Set robot cleaning/paused state + rbtStateCommands = rbtStateJson['availableCommands'] + rbtStartAv = rbtStateCommands['start'] + rbtPauseAv = rbtStateCommands['pause'] + rbtResumeAv = rbtStateCommands['resume'] + if rbtStateJson['error'] == None: + rbtError = "no error" + else: + rbtError = rbtStateJson['error'] + # alert state hasn't been implemented yet (not sure what would trigger an alert, haven't seen one yet) + if rbtStateJson['alert'] == None: + rbtAlert = "no alert" + else: + rbtAlert = rbtStateJson['alert'] + logger.log("error: %s: -- alert: %s" % (rbtError, rbtAlert)) + if rbtStateDetails['isDocked'] == True: + thing.setStateValue(robotRobotStateStateTypeId, "docked") + elif rbtPauseAv == True: + thing.setStateValue(robotRobotStateStateTypeId, "cleaning") + elif rbtResumeAv == True: + thing.setStateValue(robotRobotStateStateTypeId, "paused") + elif rbtStartAv == True: + thing.setStateValue(robotRobotStateStateTypeId, "stopped") + else: + thing.setStateValue(robotRobotStateStateTypeId, "error") + thing.setStateValue(robotErrorMessageStateTypeId, rbtError) + + +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 + global pollTimer + pollTimer = threading.Timer(60, pollService) + pollTimer.start() + + +def executeAction(info): + if info.actionTypeId == robotStartCleaningActionTypeId: + refreshRobot(info.thing) + if info.thing.stateValue(robotRobotStateStateTypeId) == "paused": + thingsAndRobots[info.thing].resume_cleaning() + else: + cleanWithRobot(info.thing, None, None) + + refreshRobot(info.thing) + info.finish(nymea.ThingErrorNoError) + return + + if info.actionTypeId == robotPauseCleaningActionTypeId: + refreshRobot(info.thing) + if info.thing.stateValue(robotRobotStateStateTypeId) == "paused": + thingsAndRobots[info.thing].resume_cleaning() + else: + thingsAndRobots[info.thing].pause_cleaning() + refreshRobot(info.thing) + info.finish(nymea.ThingErrorNoError) + return + + if info.actionTypeId == robotReturnToBaseActionTypeId: + thingsAndRobots[info.thing].send_to_base() + refreshRobot(info.thing) + info.finish(nymea.ThingErrorNoError) + return + + if info.actionTypeId == robotStopCleaningActionTypeId: + thingsAndRobots[info.thing].stop_cleaning() + refreshRobot(info.thing) + info.finish(nymea.ThingErrorNoError) + return + +def cleanWithRobot(robotThing, mapID, boundaryID): + # To do: add a parameter to the start action which takes a zone id --> this should now be represented by mapID & boundaryID + robot = thingsAndRobots[robotThing] + logger.log("Cleaning with robot:", robot, robotThing) + boolEco = robotThing.setting(robotSettingsEcoParamTypeId) + boolCare = robotThing.setting(robotSettingsCareParamTypeId) + boolNogo = robotThing.setting(robotSettingsNoGoLinesParamTypeId) + if boolEco == False: + intEco = 2 + else: + intEco = 1 + if boolCare == False: + intCare = 1 + else: + intCare = 2 + if boolNogo == False: + intNogo = 2 + else: + intNogo = 4 + logger.log("Settings: Eco:", boolEco, "Care:", boolCare, "Enable nogo:", boolNogo, "mapID:", mapID, "boundaryID:", boundaryID) + thingsAndRobots[robotThing].start_cleaning(mode=intEco, navigation_mode=intCare, category=intNogo, boundary_id=boundaryID, map_id=mapID) + refreshRobot(robotThing) + +def cleanWithRobot(robotThing, mapID, boundaryID): + # To do: add a parameter to the start action which takes a zone id --> this should now be represented by mapID & boundaryID + robot = thingsAndRobots[robotThing] + logger.log("Cleaning with robot:", robot, robotThing, mapID, boundaryID) + boolEco = robotThing.setting(robotSettingsEcoParamTypeId) + boolCare = robotThing.setting(robotSettingsCareParamTypeId) + boolNogo = robotThing.setting(robotSettingsNoGoLinesParamTypeId) + if boolEco == False: + intEco = 2 + else: + intEco = 1 + if boolCare == False: + intCare = 1 + else: + intCare = 2 + if boolNogo == False: + intNogo = 2 + else: + intNogo = 4 + logger.log("Settings: Eco:", boolEco, "Care:", boolCare, "Enable nogo:", boolNogo, "mapID:", mapID, "boundaryID:", boundaryID) + thingsAndRobots[robotThing].start_cleaning(mode=intEco, navigation_mode=intCare, category=intNogo, boundary_id=boundaryID, map_id=mapID) + + +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 == "" or browseResult.itemId == "maps": + maps = account.persistent_maps + + logger.log("maps", type(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:] + + try: + boundaries = robot.get_map_boundaries(mapId) + except Exception as e: + logger.warn("Error fetching boundaries from robot:", e) + info.finish(nymea.ThingErrorHardwareFailure, "Unable to fetch boundaries from robot.") + + logger.log("boundaries", type(boundaries), boundaries.json()) + for boundary in boundaries.json()["data"]["boundaries"]: + if boundary["type"] == "polygon": + logger.log("vertices:", boundary) + browseResult.addItem(nymea.BrowserItem("boundary-" + mapId + ";" + boundary["id"], boundary["name"], json.dumps(boundary), executable=True, disabled=not boundary["enabled"], icon=nymea.BrowserIconFavorites)) + + browseResult.finish(nymea.ThingErrorNoError) + return + + +def executeBrowserItem(info): + logger.log("Browser item clicked:", info.itemId) + if info.itemId.startswith("boundary-"): + ids = info.itemId[9:]; + logger.log("IDS:", ids) + mapId = ids.split(";")[0] + boundaryId = ids.split(";")[1] + logger.log("Cleaning boundary:", mapId, boundaryId) + cleanWithRobot(info.thing, mapId, boundaryId) + refreshRobot(info.thing) + info.finish(nymea.ThingErrorNoError) + return + + logger.warn("Can't execute browser item:", info.itemId) + info.finish(nymea.ThingErrorItemNotExecutable) + + +def deinit(): + global pollTimer + # If we started a poll timer, cancel it on shutdown. + if pollTimer is not None: + pollTimer.cancel() diff --git a/neatobotvac/meta.json b/neatobotvac/meta.json new file mode 100644 index 00000000..b7589f29 --- /dev/null +++ b/neatobotvac/meta.json @@ -0,0 +1,14 @@ +{ + "title": "Neato Botvac", + "tagline": "Connect to and control your Neato Botvac connected cleaning robots.", + "icon": "neato.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "account", + "cleaningrobot" + ] +} diff --git a/neatobotvac/neato.png b/neatobotvac/neato.png new file mode 100644 index 00000000..391c570f Binary files /dev/null and b/neatobotvac/neato.png differ 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/neatobotvac/requirements.txt b/neatobotvac/requirements.txt new file mode 100644 index 00000000..238cc1b9 --- /dev/null +++ b/neatobotvac/requirements.txt @@ -0,0 +1,138 @@ +pybotvac==0.0.20 \ + --hash=sha256:d4d9d348ee2f3b7472b78bd80f9653406af6e351aa0362ac191055a304930b33 \ + --hash=sha256:e49a3258f251da0d56764797efb01ba730537b6344ff03b57542142f0640b273 +setuptools==54.1.1 \ + --hash=sha256:1ce82798848a978696465866bb3aaab356003c42d6143e1111fcf069ac838274 \ + --hash=sha256:75c5c4479f4961f1ffdb597c98aa4e4077e6813685025e8bdebf7598aa84e859 +requests-oauthlib==1.3.0 \ + --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \ + --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a \ + --hash=sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc +oauthlib==3.1.0 \ + --hash=sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889 \ + --hash=sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea +requests==2.25.1 \ + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e +voluptuous==0.12.1 \ + --hash=sha256:663572419281ddfaf4b4197fd4942d181630120fb39b333e3adad70aeb56444b \ + --hash=sha256:8ace33fcf9e6b1f59406bfaf6b8ec7bcc44266a9f29080b4deb4fe6ff2492386 +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 +certifi==2020.12.5 \ + --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \ + --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 +launchpadlib==1.10.13 \ + --hash=sha256:5804d68ec93247194449d17d187e949086da0a4d044f12155fad269ef8515435 \ + --hash=sha256:f61a591aff60a9315da09131eeb000bfb8287b788d1899a161e727ca22b1989f +httplib2==0.19.0 \ + --hash=sha256:749c32603f9bf16c1277f59531d502e8f1c2ca19901ae653b49c4ed698f0820e \ + --hash=sha256:e0d428dad43c72dbce7d163b7753ffc7a39c097e6788ef10f4198db69b92f08e +keyring==23.0.0 \ + --hash=sha256:237ff44888ba9b3918a7dcb55c8f1db909c95b6f071bfb46c6918f33f453a68a \ + --hash=sha256:29f407fd5509c014a6086f17338c70215c8d1ab42d5d49e0254273bc0a64bbfc +lazr.uri==1.0.5 \ + --hash=sha256:f36e7e40d5f8f2cf20ff2c81784a14a546e6c19c216d40a6617ebe0c96c92c49 \ + --hash=sha256:71f2faf04b148cf63d78da08ee5d8d6a7a7dbda8c9016b389a16f790d080c06f +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +testresources==2.0.1 \ + --hash=sha256:67a361c3a2412231963b91ab04192209aa91a1aa052f0ab87245dbea889d1282 \ + --hash=sha256:ee9d1982154a1e212d4e4bac6b610800bfb558e4fb853572a827bc14a96e4417 +wadllib==1.3.5 \ + --hash=sha256:84fecbaec2fef5ae2d7717a8115d271f18c6b5441eac861c58be8ca57f63c1d3 \ + --hash=sha256:67d3102b40eefdd6c3007cfbcc4c07f6948fec0666ba5c17d703eab21f054692 +lazr.restfulclient==0.14.3 \ + --hash=sha256:9f28bbb7c00374159376bd4ce36b4dacde7c6b86a0af625aa5e3ae214651a690 \ + --hash=sha256:2320e6d132c9a5148895e85be03274bc9305e4605439b03541ee3a618e00fb94 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b +jeepney==0.6.0 \ + --hash=sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657 \ + --hash=sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae +importlib-metadata==3.7.0 \ + --hash=sha256:24499ffde1b80be08284100393955842be4a59c7c16bbf2738aad0e464a8e0aa \ + --hash=sha256:c6af5dbf1126cd959c4a8d8efd61d4d3c83bddb0459a17e554284a077574b614 +pbr==5.5.1 \ + --hash=sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9 \ + --hash=sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00 +SecretStorage==3.3.1 \ + --hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \ + --hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195 +distro==1.5.0 \ + --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \ + --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799 +zipp==3.4.1 \ + --hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 \ + --hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 +cffi==1.14.5 \ + --hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \ + --hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \ + --hash=sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea \ + --hash=sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee \ + --hash=sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396 \ + --hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \ + --hash=sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315 \ + --hash=sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1 \ + --hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \ + --hash=sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892 \ + --hash=sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482 \ + --hash=sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058 \ + --hash=sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5 \ + --hash=sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53 \ + --hash=sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045 \ + --hash=sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3 \ + --hash=sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5 \ + --hash=sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e \ + --hash=sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c \ + --hash=sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369 \ + --hash=sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827 \ + --hash=sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053 \ + --hash=sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa \ + --hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \ + --hash=sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322 \ + --hash=sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132 \ + --hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \ + --hash=sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa \ + --hash=sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0 \ + --hash=sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396 \ + --hash=sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e \ + --hash=sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991 \ + --hash=sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6 \ + --hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \ + --hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \ + --hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \ + --hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c \ + --hash=sha256:9338beed13d880320450d95c9e07ccf839faa3ea7b75d788f4ed46d845044a71 +pycparser==2.20 \ + --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \ + --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 +idna==2.5 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 +typing-extensions==3.7.4.3 \ + --hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \ + --hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c \ + --hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f +cryptography==3.3 \ + --hash=sha256:1366e6fff96bb1d320e3ef3c531b0428cb780c517b6059ffe8820e2a30bf5858 \ + --hash=sha256:2c28e69e8c2620869425420e466b224d351d5dc242d3e3293062cffa7fcdd276 \ + --hash=sha256:402273e7f78e01f5c42452acef56bd52fa73fa0f312e4160db7ad29bbc90335d \ + --hash=sha256:41892759f13c7dfc329573cabd3a513f1e7b5d309ca55c931ffefc3b2f304899 \ + --hash=sha256:4935d0603b118dc036c477917e5e8a020f7309bc11c363a4d572407dcbd53c80 \ + --hash=sha256:55574bd84ff551fd6f7617e5eeda0b2d129f84788340ab57904f0c7f13f8f149 \ + --hash=sha256:651eff09297e4518287f711b4e28523567cbde7beaa794d06d0b35ac9adc1172 \ + --hash=sha256:66f41aa7642f97d35976750a2c0a6d5915733ba6c9ca7a0f327e56f037c1236e \ + --hash=sha256:9518da854136181407d946b971dd27a0e941d5d07f87f87a44c598b3da7a77d2 \ + --hash=sha256:ad8dc319f876273b474a59797344d5986beaaaf18f7cc0ab9255155da057d979 \ + --hash=sha256:d9f1e520f2ee08c5a88e1ae0b31159bdb13da40a486bea3e9f7d338564850ea6 \ + --hash=sha256:ee0084f6d62f083316b08111b122dc4fbe16e534059b92b5ddc3d73dc52dd39c \ + --hash=sha256:f43d6e72e3cdf983b5e5e65938c0519ce4eeb42b6af766f714b1414ae7b9f8ef \ + --hash=sha256:f95ca692fafea80f1815bbfecf57c6833c0b21432d17026a077341debee76e79 \ + --hash=sha256:19c2cbff0434b4e345d08737a7c706d0ba3c2aeecb89936525481e866fd71cea diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 96691bcb..e32bd80e 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -33,6 +33,7 @@ PLUGIN_DIRS = \ lifx \ mailnotification \ mqttclient \ + neatobotvac \ nanoleaf \ netatmo \ networkdetector \