diff --git a/debian/control b/debian/control index db1cc724..ce906773 100644 --- a/debian/control +++ b/debian/control @@ -480,6 +480,21 @@ 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, +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}, 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/integrationpluginneatobotvac.json b/neatobotvac/integrationpluginneatobotvac.json new file mode 100644 index 00000000..6554dfdf --- /dev/null +++ b/neatobotvac/integrationpluginneatobotvac.json @@ -0,0 +1,175 @@ +{ + "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", + "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": "a4b5f07f-e71a-4c3a-8d6b-50162a455159", + "name": "getMaps", + "displayName": "Get available maps" + } + ] + }, + { + "id": "b924c87a-f783-4f45-a3af-929684c24aea", + "name": "robot", + "displayName": "Neato robot", + "createMethods": ["auto"], + "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" + }, + { + "id": "141f0d98-1806-432c-aaac-c0d3a89a8e58", + "name": "mapId", + "displayName": "Map ID", + "type": "QString" + } + ], + "interfaces":[ + + ], + "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": "dce4f7f3-a0a6-46bb-9216-c9089d9e9b0d", + "name": "cleaning", + "displayName": "Cleaning", + "displayNameEvent": "Cleaning yes/no", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "0f925abf-396c-437e-b259-2fed7eafe7f4", + "name": "paused", + "displayName": "Paused", + "displayNameEvent": "Cleaning paused yes/no", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "1b8abd35-8276-44ba-8c75-a647877b2e11", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Robot charging yes/no", + "type": "bool", + "defaultValue": true, + "cached": false + }, + { + "id": "805175ec-c2e4-4fbe-9505-282750ef1467", + "name": "docked", + "displayName": "Docked", + "displayNameEvent": "Robot docked yes/no", + "type": "bool", + "defaultValue": true, + "cached": false + }, + { + "id": "20ed8767-806f-4ec2-8626-842cd398f9df", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level percentage", + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100, + "cached": false + } + ], + "actionTypes": [ + { + "id": "1f774998-5fa7-4e3b-8ab0-a8402dd561bb", + "name": "startCleaning", + "displayName": "Start/pause cleaning" + }, + { + "id": "5178a803-5696-4ee1-80a4-2c7c20a5043a", + "name": "goToBase", + "displayName": "Go to base" + }, + { + "id": "30775042-55a7-4f1b-9042-a9bdeadc4b0d", + "name": "stopCleaning", + "displayName": "Stop cleaning" + }, + { + "id": "95ba515b-0023-4a98-a867-ca8286512a4e", + "name": "getBoundaries", + "displayName": "Get No-go Lines" + } + ] + } + ] + } + ] +} + + + diff --git a/neatobotvac/integrationpluginneatobotvac.py b/neatobotvac/integrationpluginneatobotvac.py new file mode 100644 index 00000000..d12411b6 --- /dev/null +++ b/neatobotvac/integrationpluginneatobotvac.py @@ -0,0 +1,217 @@ +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 + +thingsAndRobots = {} +oauthSessions = {} + +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: + 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: + # Login error + info.finish(nymea.ThingErrorAuthenticationFailure, "Login error") + 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) + + # 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) + # Check if this robot is already added in nymea + found = False + for thing in myThings(): + if thing.paramValue(robotThingSerialParamTypeId) == robot.serial: + # Yep, already here... skip it + found = True + continue + 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) + mapIDshort = mapId + thingDescriptor.params = [ + nymea.Param(robotThingSerialParamTypeId, robot.serial), + nymea.Param(robotThingSecretParamTypeId, robot.secret), + nymea.Param(robotThingMapIdParamTypeId, mapIDshort) + ] + 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 + + + # Setup for the robots + if info.thing.thingClassId == robotThingClassId: + + serial = info.thing.paramValue(robotThingSerialParamTypeId) + secret = info.thing.paramValue(robotThingSecretParamTypeId) + robot = Robot(serial, secret, info.thing.name) + thingsAndRobots[info.thing] = robot; + logger.log(robot.get_robot_state()) + # set up polling for robot status + info.finish(nymea.ThingErrorNoError) + return + + + +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) + + # restart the timer for next poll + global pollTimer + pollTimer = threading.Timer(60, pollService) + pollTimer.start() + + +def executeAction(info): + if info.actionTypeId == robotStartCleaningActionTypeId: + rbtState = thingsAndRobots[info.thing].get_robot_state() + rbtStateJson = rbtState.json() + rbtStateCommands = rbtStateJson['availableCommands'] + rbtStartAv = rbtStateCommands['start'] + rbtPauseAv = rbtStateCommands['pause'] + rbtResumeAv = rbtStateCommands['resume'] + if rbtStartAv == True: + logger.log("Start cleaning") + thingsAndRobots[info.thing].start_cleaning() + elif rbtPauseAv == True: + logger.log("Pause cleaning") + thingsAndRobots[info.thing].pause_cleaning() + elif rbtResumeAv == True: + thingsAndRobots[info.thing].resume_cleaning() + threading.Timer(5, pollService).start() + info.finish(nymea.ThingErrorNoError) + return + + if info.actionTypeId == robotGoToBaseActionTypeId: + thingsAndRobots[info.thing].send_to_base() + threading.Timer(5, pollService).start() + info.finish(nymea.ThingErrorNoError) + + if info.actionTypeId == robotStopCleaningActionTypeId: + thingsAndRobots[info.thing].stop_cleaning() + threading.Timer(5, pollService).start() + info.finish(nymea.ThingErrorNoError) + + # To do: get available boundaries to use with start_cleaning action + if info.actionTypeId == robotGetBoundariesActionTypeId: + # rbtMapBound = thingsAndRobots[info.thing].get_map_boundaries() + # rbtMapBoundJson = rbtMapBound.json() + # logger.log("Robot Map Boundaries", rbtMapBoundJson) + threading.Timer(5, pollService).start() + info.finish(nymea.ThingErrorNoError) + + +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/requirements.txt b/neatobotvac/requirements.txt new file mode 100644 index 00000000..ae7b9306 --- /dev/null +++ b/neatobotvac/requirements.txt @@ -0,0 +1,136 @@ +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 +cryptography==3.4.6 \ + --hash=sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b \ + --hash=sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336 \ + --hash=sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87 \ + --hash=sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7 \ + --hash=sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799 \ + --hash=sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b \ + --hash=sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df \ + --hash=sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0 \ + --hash=sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3 \ + --hash=sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724 \ + --hash=sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2 \ + --hash=sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964 \ + --hash=sha256:926ae3dfd160050158b9ca25d419fb7ee658974564b01aa10c059a75dffab7e8 +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