Merge standalone neato botvac plugin

master
Michael Zanetti 2021-03-24 23:03:15 +01:00
parent 68ca8605f3
commit 115ce5168a
5 changed files with 546 additions and 0 deletions

15
debian/control vendored
View File

@ -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},

View File

@ -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/

View File

@ -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"
}
]
}
]
}
]
}

View File

@ -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()

View File

@ -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