diff --git a/daikinairco/README.md b/daikinairco/README.md new file mode 100644 index 00000000..51185b27 --- /dev/null +++ b/daikinairco/README.md @@ -0,0 +1,16 @@ +# Daikin Airco + +This plugin allows discovery and simple control via Nymea of Daikin airconditioning devices. + +## Supported Things + +* Devices with the Daikin Residential Controller + +## Requirements + +* nymea and the airconditioning device must be in the same local area network. +* The package "nymea-plugin-daikinairco" must be installed. + +## More + + [Daikin Residential Controller](https://www.daikin.eu/en_us/product-group/control-systems/daikin-online-controller.html) diff --git a/daikinairco/daikin.jpg b/daikinairco/daikin.jpg new file mode 100644 index 00000000..289fc375 Binary files /dev/null and b/daikinairco/daikin.jpg differ diff --git a/daikinairco/integrationplugindaikinairco.json b/daikinairco/integrationplugindaikinairco.json new file mode 100644 index 00000000..c40b6b0b --- /dev/null +++ b/daikinairco/integrationplugindaikinairco.json @@ -0,0 +1,131 @@ +{ + "displayName": "Daikin Airco", + "name": "Daikinairco", + "id": "b5cf475e-707d-46a5-9684-296fb1a14886", + "vendors": [ + { + "id": "88f6421c-cc7b-4006-b077-421fc384939c", + "displayName": "Daikin", + "name": "Daikin", + "thingClasses": [ + { + "id": "e6d0bfe6-5923-4c29-840d-ef8b0a17fad9", + "name": "airco", + "displayName": "Daikin Airco", + "createMethods": ["discovery"], + "interfaces": ["power", "thermostat", "wirelessconnectable"], + "paramTypes": [ + { + "id": "758cad5c-994a-4136-b83d-80fe2f07346c", + "name": "deviceId", + "displayName": "Device ID", + "type" : "QString", + "readOnly": true + }, + { + "id": "1fefe7df-ef8e-4434-bb51-0c521553f750", + "name": "deviceName", + "displayName": "Device name", + "type" : "QString", + "readOnly": true + } + ], + "settingsTypes": [ + + ], + "stateTypes": [ + { + "id": "71c5b083-6c95-44f0-b54c-b98f0ac459d8", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool", + "cached": false + }, + { + "id": "bd0ff439-6ab5-414a-ba6e-9ecef0f7c3f4", + "name": "url", + "displayName": "Device Address", + "displayNameEvent": "Device IP changed", + "defaultValue": "0.0.0.0", + "type" : "QString" + }, + { + "displayName": "Power", + "id": "6bb42aa0-2280-408d-89b5-400410abdd73", + "name": "power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "2261311a-4302-4a03-bd67-50e92044fe59", + "name": "targetTemperature", + "displayName": "Target temperature", + "displayNameEvent": "Target temperature changed", + "displayNameAction": "Set target temperature", + "type": "double", + "unit": "DegreeCelsius", + "minValue": "10", + "maxValue": "30", + "defaultValue": "20", + "writable": true + }, + { + "id": "a2b7a3aa-641c-45b8-a13b-e845324969ca", + "name": "temperature", + "displayName": "Home temperature", + "displayNameEvent": "Home temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": "0" + }, + { + "id": "b99b7182-51e6-456c-b7bc-f39ea2397500", + "name": "outsideTemperature", + "displayName": "Outside temperature", + "displayNameEvent": "Outside temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": "0" + }, + { + "id": "9b614a01-4984-4e73-8cbc-95389c484c3b", + "name": "humidity", + "displayName": "Humidity", + "displayNameEvent": "Humidity changed", + "type": "double", + "unit": "Percentage", + "minValue": "0", + "maxValue": "100", + "defaultValue": "0" + }, + { + "id": "59da03a6-1b61-4777-91e1-51e518beecbc", + "name": "Mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "displayNameAction": "Set mode", + "type": "QString", + "writable": true, + "possibleValues": [ + "Automatic", + "Cooling", + "Heating", + "Dehumidifier", + "Fan" + ], + "defaultValue": "Automatic" + } + ], + "actionTypes": [ + + ] + } + ] + } + ] +} diff --git a/daikinairco/integrationplugindaikinairco.py b/daikinairco/integrationplugindaikinairco.py new file mode 100644 index 00000000..e65a755d --- /dev/null +++ b/daikinairco/integrationplugindaikinairco.py @@ -0,0 +1,387 @@ +import nymea +import time +import threading +import json +import requests +import random +from socket import * +import sys + +pollTimer = None +pollFrequency = 30 + +def discoverThings(info): + logger.log("Discovery started for", info.thingClassId) + discoveredIps = findIps() + + for i in range(0, len(discoveredIps)): + deviceIp = discoveredIps[i] + # get basic info /common/basic_info + # response example: + # ret=OK,type=aircon,reg=eu,dst=1,ver=1_14_58,rev=83B3526,pow=0,err=0,location=0, + # name=name-in-%hex,icon=0,method=polling,port=30050,id=uuid, + # pw=,lpw_flag=0,adp_kind=3,pv=3.30,cpv=3,cpv_minor=20,led=1,en_setzone=1,mac=mac,adp_mode=run,en_hol=0, + # ssid1=ssid,radio1=-43,ssid=DaikinAP14937,grp_name=,en_grp=0 + + # get model info /aircon/get_model_info --> (how) can we use this? + # response example: + # ret=OK,model=0FC3,type=N,pv=3.30,cpv=3,cpv_minor=20,mid=NA,humd=0,s_humd=0,acled=0,land=0,elec=1,temp=1, + # temp_rng=0,m_dtct=1,ac_dst=--,disp_dry=0,dmnd=1,en_scdltmr=1,en_frate=1,en_fdir=1,s_fdir=3,en_rtemp_a=0, + # en_spmode=5,en_ipw_sep=1,en_mompow=1,hmlmt_l=10.0 + + aUrl = 'http://' + deviceIp + '/common/basic_info' + headers = {'Accept': '*/*'} + rr = requests.get(aUrl, headers=headers) + pollResponse = rr.text + if rr.status_code == requests.codes.ok: + systemType = 'none' + splitResponse = pollResponse.split(",") + for j in range(0, len(splitResponse)): + splitItem = splitResponse[j].split("=") + if splitItem[0] == 'type': + systemType = splitItem[1] + elif splitItem[0] == 'id': + systemId = splitItem[1] + elif splitItem[0] == 'name': + hexArray = splitItem[1].split("%") + deviceName = "" + for k in range(0, len(hexArray)): + deviceName+=bytes.fromhex(hexArray[k]).decode('utf-8') + if systemType == 'aircon': + logger.log("Device with IP " + deviceIp + " is a Daikin airco unit.") + logger.log("Device ID:", systemId) + logger.log("Device Name:", deviceName) + # check if device already known + exists = False + for thing in myThings(): + logger.log("Comparing to existing units: is %s an airco unit?" % (thing.name)) + if thing.thingClassId == aircoThingClassId: + logger.log("Yes, %s is an airco unit." % (thing.name)) + if thing.paramValue(aircoThingDeviceIdParamTypeId) == systemId: + logger.log("Already have unit with serial number %s in the system: %s" % (systemId, thing.name)) + exists = True + else: + logger.log("Thing %s doesn't match with found unit with serial number %s" % (thing.name, systemId)) + if exists == False: # unit doesn't exist yet, so add it + thingDescriptor = nymea.ThingDescriptor(aircoThingClassId, deviceName) + thingDescriptor.params = [ + nymea.Param(aircoThingDeviceIdParamTypeId, systemId), + nymea.Param(aircoThingDeviceNameParamTypeId, deviceName) + ] + info.addDescriptor(thingDescriptor) + else: # unit already exists, so show it to allow reconfiguration + thingDescriptor = nymea.ThingDescriptor(aircoThingClassId, deviceName, thingId=thing.id) + thingDescriptor.params = [ + nymea.Param(aircoThingDeviceIdParamTypeId, systemId), + nymea.Param(aircoThingDeviceNameParamTypeId, deviceName) + ] + info.addDescriptor(thingDescriptor) + else: + logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin airco unit.") + else: + logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin airco unit.") + info.finish(nymea.ThingErrorNoError) + return + +def findIps(): + discoveredIps = [] + + # Create a UDP socket + sock = socket(AF_INET, SOCK_DGRAM) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + sock.settimeout(20) + + server_address = ('255.255.255.255', 30050) + message = 'DAIKIN_UDP/common/basic_info' + + try: + while True: + # Send data + logger.log('sending: ' + message) + sent = sock.sendto(message.encode(), server_address) + + # Receive response + logger.log('waiting to receive') + data, server = sock.recvfrom(4096) + if data.decode('UTF-8')[0:18] == 'ret=OK,type=aircon': + print('Received confirmation; server ip: ' + str(server[0]) ) + discoveredIps.append(str(server[0])) + break + else: + print('Verification failed') + + print('Trying again...') + finally: + sock.close() + return discoveredIps + +def setupThing(info): + searchSystemId = info.thing.paramValue(aircoThingDeviceIdParamTypeId) + logger.log("setupThing called for", info.thing.name, searchSystemId) + + discoveredIps = findIps() + found = False + + for i in range(0, len(discoveredIps)): + deviceIp = discoveredIps[i] + aUrl = 'http://' + deviceIp + '/common/basic_info' + headers = {'Accept': '*/*'} + rr = requests.get(aUrl, headers=headers) + pollResponse = rr.text + if rr.status_code == requests.codes.ok: + logger.log("Device with IP " + deviceIp + " is a supported Daikin Airco.") + # get device info + splitResponse = pollResponse.split(",") + for j in range(0, len(splitResponse)): + splitItem = splitResponse[j].split("=") + if splitItem[0] == 'id': + systemId = splitItem[1] + logger.log("Device ID:", systemId) + # check if this is the device with the serial number we're looking for + if systemId == searchSystemId: + logger.log("Device with IP " + deviceIp + " is the existing device.") + found = True + info.thing.setStateValue(aircoUrlStateTypeId, deviceIp) + else: + logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin Airco.") + if found == True: + info.thing.setStateValue(aircoConnectedStateTypeId, True) + pollAirco(info.thing) + info.finish(nymea.ThingErrorNoError) + else: + info.thing.setStateValue(aircoConnectedStateTypeId, False) + info.finish(nymea.ThingErrorHardwareFailure, "Error connecting to the device in the network.") + + logger.log("Airco added:", info.thing.name) + + # If no poll timer is set up yet, start it now + logger.log("Creating pollService") + global pollTimer + global pollFrequency + if pollTimer == None: + logger.log("Starting timer @ setupThing") + pollTimer = nymea.PluginTimer(pollFrequency, pollService) + logger.log("timer interval @ setupThing", pollTimer.interval) + else: + logger.log("Timer already exists @ setupThing") + + info.finish(nymea.ThingErrorNoError) + return + +def pollAirco(info): + global pollFrequency + deviceIp = info.stateValue(aircoUrlStateTypeId) + logger.log("polling airco", deviceIp, info.name) + airco = info + headers = {'Accept': '*/*'} + # get sensor info + # response example: + # ret=OK,htemp=25.0,hhum=30,otemp=2.0,err=0,cmpfreq=0,mompow=1 + aUrl = 'http://' + deviceIp + '/aircon/get_sensor_info' + pr = requests.get(aUrl, headers=headers) + pollResponse = pr.text + if pr.status_code == requests.codes.ok: + airco.setStateValue(aircoConnectedStateTypeId, True) + splitResponse = pollResponse.split(",") + for i in range(0, len(splitResponse)): + splitItem = splitResponse[i].split("=") + if splitItem[0] == 'htemp': + homeTemp = float(splitItem[1]) + airco.setStateValue(aircoTemperatureStateTypeId, homeTemp) + elif splitItem[0] == 'hhum': + homeHumidity = float(splitItem[1]) + airco.setStateValue(aircoHumidityStateTypeId, homeHumidity) + elif splitItem[0] == 'otemp': + outTemp = float(splitItem[1]) + airco.setStateValue(aircoOutsideTemperatureStateTypeId, outTemp) + else: + airco.setStateValue(aircoConnectedStateTypeId, False) + + # get control info + # response example: + # ret=OK,pow=0,mode=4,adv=,stemp=21.0,shum=0,dt1=22.0,dt2=M,dt3=20.0,dt4=21.0,dt5=21.0,dt7=22.0, + # dh1=0,dh2=0,dh3=0,dh4=0,dh5=0,dh7=0,dhh=50,b_mode=4,b_stemp=21.0,b_shum=0,alert=255, + # f_rate=A,f_dir=0,b_f_rate=A,b_f_dir=0,dfr1=A,dfr2=A,dfr3=A,dfr4=A,dfr5=A,dfr6=A,dfr7=A,dfrh=5, + # dfd1=0,dfd2=0,dfd3=0,dfd4=0,dfd5=0,dfd6=0,dfd7=0,dfdh=0,dmnd_run=0,en_demand=1 + aUrl = 'http://' + deviceIp + '/aircon/get_control_info' + pr = requests.get(aUrl, headers=headers) + pollResponse = pr.text + if pr.status_code == requests.codes.ok: + airco.setStateValue(aircoConnectedStateTypeId, True) + splitResponse = pollResponse.split(",") + for i in range(0, len(splitResponse)): + splitItem = splitResponse[i].split("=") + # get power state + if splitItem[0] == 'pow': + power = int(splitItem[1]) + logger.log("Power", power) + airco.setStateValue(aircoPowerStateTypeId, power) + elif splitItem[0] == 'mode': + mode = int(splitItem[1]) + if mode == 2: # 2 = dehumidifier mode + logger.log("Airco mode: dehumidifier", mode) + airco.setStateValue(aircoModeStateTypeId, "Dehumidifier") + elif mode == 3: # 3 = cooling mode + logger.log("Airco mode: cooling", mode) + airco.setStateValue(aircoModeStateTypeId, "Cooling") + elif mode == 4: # 4 = heating mode + logger.log("Airco mode: heating", mode) + airco.setStateValue(aircoModeStateTypeId, "Heating") + elif mode == 6: # 6 = fan mode + logger.log("Airco mode: fan", mode) + airco.setStateValue(aircoModeStateTypeId, "Fan") + else: # 0-1-7 = automatic mode + logger.log("Airco mode: automatic", mode) + airco.setStateValue(aircoModeStateTypeId, "Automatic") + elif splitItem[0] == 'stemp': + try: + targetTemp = float(splitItem[1]) + logger.log("Target temperature", targetTemp) + airco.setStateValue(aircoTargetTemperatureStateTypeId, targetTemp) + except: + logger.log("Target temperature parameter is not a number:", splitItem[1]) + for j in range(0, len(splitResponse)): + splitItem = splitResponse[j].split("=") + if splitItem[0] == 'dt1': + targetTemp = float(splitItem[1]) + logger.log("Using target temperature from automatic mode", targetTemp) + airco.setStateValue(aircoTargetTemperatureStateTypeId, targetTemp) + else: + airco.setStateValue(aircoConnectedStateTypeId, False) + +def pollService(): + logger.log("pollTimer triggered") + global pollTimer + global pollFrequency + pollTimer.interval = pollFrequency + # Poll all airco units we know + for thing in myThings(): + if thing.thingClassId == aircoThingClassId: + pollAirco(thing) + +def executeAction(info): + deviceIp = info.thing.stateValue(aircoUrlStateTypeId) + logger.log("executeAction called for thing", info.thing.name, deviceIp, info.actionTypeId, info.params) + headers = {'Accept': '*/*'} + aUrl = 'http://' + deviceIp + '/aircon/get_control_info' + pr = requests.get(aUrl, headers=headers) + # get control info, to obtain target temperature per mode, to set the correct target temperature (stemp) when changing mode + pollResponse = pr.text + logger.log("response", pr.text) + if pr.status_code == requests.codes.ok: + info.thing.setStateValue(aircoConnectedStateTypeId, True) + splitResponse = pollResponse.split(",") + for i in range(0, len(splitResponse)): + splitItem = splitResponse[i].split("=") + if splitItem[0] == 'pow': + power = int(splitItem[1]) + elif splitItem[0] == 'mode': + mode = int(splitItem[1]) + elif splitItem[0] == 'stemp': + targetTemp = splitItem[1] + elif splitItem[0] == 'shum': + targetHum = splitItem[1] + elif splitItem[0] == 'f_rate': + f_rate = splitItem[1] + elif splitItem[0] == 'f_dir': + f_dir = splitItem[1] + elif splitItem[0] == 'dt1': + dt1 = splitItem[1] + elif splitItem[0] == 'dt2': + dt2 = splitItem[1] + elif splitItem[0] == 'dt3': + dt3 = splitItem[1] + elif splitItem[0] == 'dt4': + dt4 = splitItem[1] + elif splitItem[0] == 'dt5': + dt5 = splitItem[1] + elif splitItem[0] == 'dt7': + dt7 = splitItem[1] + elif splitItem[0] == 'dh1': + dh1 = splitItem[1] + elif splitItem[0] == 'dh2': + dh2 = splitItem[1] + elif splitItem[0] == 'dh3': + dh3 = splitItem[1] + elif splitItem[0] == 'dh4': + dh4 = splitItem[1] + elif splitItem[0] == 'dh5': + dh5 = splitItem[1] + elif splitItem[0] == 'dh7': + dh7 = splitItem[1] + else: + info.thing.setStateValue(aircoConnectedStateTypeId, False) + info.finish(nymea.ThingErrorNoError) + + if info.actionTypeId == aircoPowerActionTypeId: + power = info.paramValue(aircoPowerActionPowerParamTypeId) + if power == True: + power = 1 + elif power == False: + power = 0 + logger.log("new power value", power) + elif info.actionTypeId == aircoTargetTemperatureActionTypeId: + targetTemp = str(info.paramValue(aircoTargetTemperatureActionTargetTemperatureParamTypeId)) + logger.log("new target temperature", targetTemp) + elif info.actionTypeId == aircoModeActionTypeId: + targetMode = info.paramValue(aircoModeActionModeParamTypeId) + if targetMode == "Automatic": + mode = 0 + targetTemp = dt1 + targetHum = dh1 + elif targetMode == "Cooling": + mode = 3 + targetTemp = dt3 + targetHum = dh3 + elif targetMode == "Heating": + mode = 4 + targetTemp = dt4 + targetHum = dh4 + elif targetMode == "Dehumidifier": + mode = 2 + targetTemp = dt2 + targetHum = dh2 + elif targetMode == "Fan": + mode = 6 + logger.log("new target mode:", targetMode, mode) + + # mandatory parameters: pow, mode, stemp, shum, f_rate, f_dir (f_rate & f_dir can't be set from the plugin yet, so we always use the values obtained above) + param = "pow="+str(power)+"&mode="+str(mode)+"&stemp="+targetTemp+"&shum="+targetHum+"&f_rate="+f_rate+"&f_dir="+f_dir + logger.log("parameters", param) + aUrl = "http://" + deviceIp + "/aircon/set_control_info?"+param + pr = requests.post(aUrl, headers=headers) + time.sleep(0.5) + pollAirco(info.thing) + info.finish(nymea.ThingErrorNoError) + return + + # Control Info Examples + # Switched Off + # ret=OK,pow=0,mode=7,adv=,stemp=24.0,shum=0,dt1=24.0,dt2=M,dt3=25.0,dt4=25.0,dt5=25.0,dt7=24.0,dh1=0,dh2=50,dh3=0,dh4=0,dh5=0,dh7=0, + # dhh=50,b_mode=7,b_stemp=24.0,b_shum=0,alert=255,f_rate=4,f_dir=0,b_f_rate=4,b_f_dir=0,dfr1=4,dfr2=5,dfr3=7,dfr4=5,dfr5=5,dfr6=5, + # dfr7=4,dfrh=5,dfd1=0,dfd2=0,dfd3=3,dfd4=0,dfd5=0,dfd6=0,dfd7=0,dfdh=0 + # Auto 25C ( CONFORT AIR ) ( INTELLIGENT EYE ) + # ret=OK,pow=1,mode=7,adv=,stemp=25.0,shum=0,dt1=25.0,dt2=M,dt3=22.0,dt4=25.0,dt5=25.0,dt7=25.0,dh1=0,dh2=50,dh3=0,dh4=0,dh5=0,dh7=0, + # dhh=50,b_mode=7,b_stemp=25.0,b_shum=0,alert=255,f_rate=A,f_dir=0,b_f_rate=4,b_f_dir=0,dfr1=4,dfr2=5,dfr3=4,dfr4=5,dfr5=5,dfr6=5, + # dfr7=4,dfrh=5,dfd1=0,dfd2=0,dfd3=0,dfd4=0,dfd5=0,dfd6=0,dfd7=0,dfdh=0 + # ret=OK,pow=1&dh2=50&dfd4=0&b_stemp=25.0&alert=255&f_dir=0&b_shum=0&dh4=0&dfd3=0&dh3=0&dfd2=0&dfr2=5&dfr7=B&dfr4=5&dfd7=0& + # dfrh=5&dt3=25.0&dfdh=0&adv=&dh5=0&dh1=0&dfr6=5&dt5=25.0&dfr1=B&stemp=25.0&shum=0&dfd6=0&f_rate=A&b_f_dir=0&dt1=25.0&dhh=50& + # dfd1=0&dfr3=5&dh7=0&mode=1&dfd5=0&b_mode=7&dt4=25.0&b_f_rate=A&dt7=25.0&dt2=M&dfr5=5 + # Hot 25c ( AIR silence ) + # ret=OK,pow=1,mode=4,adv=,stemp=25.0,shum=0,dt1=25.0,dt2=M,dt3=22.0,dt4=25.0,dt5=25.0,dt7=25.0,dh1=0,dh2=50,dh3=0,dh4=0,dh5=0,dh7=0, + # dhh=50,b_mode=4,b_stemp=25.0,b_shum=0,alert=255,f_rate=B,f_dir=0,b_f_rate=B,b_f_dir=0,dfr1=B,dfr2=B,dfr3=B,dfr4=B,dfr5=B,dfr6=B, + # dfr7=B,dfrh=5,dfd1=0,dfd2=0,dfd3=0,dfd4=0,dfd5=0,dfd6=0,dfd7=0,dfdh=0 + +def deinit(): + global pollTimer + # If we started a poll timer, cancel it on shutdown. + if pollTimer is not None: + pollTimer = None + +def thingRemoved(thing): + global pollTimer + logger.log("removeThing called for", thing.name) + # Clean up all data related to this thing + if len(myThings()) == 0 and pollTimer is not None: + pollTimer = None \ No newline at end of file diff --git a/daikinairco/meta.json b/daikinairco/meta.json new file mode 100644 index 00000000..ef6256f5 --- /dev/null +++ b/daikinairco/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Daikin Airco", + "tagline": "Discover and control Daikin airconditioning devices", + "icon": "daikin.jpg", + "stability": "testing", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + ] +} diff --git a/daikinairco/requirements.txt b/daikinairco/requirements.txt new file mode 100644 index 00000000..2a65daa0 --- /dev/null +++ b/daikinairco/requirements.txt @@ -0,0 +1,61 @@ +testresources==2.0.1 \ + --hash=sha256:67a361c3a2412231963b91ab04192209aa91a1aa052f0ab87245dbea889d1282 \ + --hash=sha256:ee9d1982154a1e212d4e4bac6b610800bfb558e4fb853572a827bc14a96e4417 +requests==2.25.1 \ + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 +lazr.uri==1.0.5 \ + --hash=sha256:f36e7e40d5f8f2cf20ff2c81784a14a546e6c19c216d40a6617ebe0c96c92c49 \ + --hash=sha256:71f2faf04b148cf63d78da08ee5d8d6a7a7dbda8c9016b389a16f790d080c06f +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +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 +idna==2.5 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 +certifi==2020.12.5 \ + --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \ + --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 +setuptools==54.1.1 \ + --hash=sha256:1ce82798848a978696465866bb3aaab356003c42d6143e1111fcf069ac838274 \ + --hash=sha256:75c5c4479f4961f1ffdb597c98aa4e4077e6813685025e8bdebf7598aa84e859 +pbr==5.5.1 \ + --hash=sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9 \ + --hash=sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00 +distro==1.5.0 \ + --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \ + --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799 +oauthlib==3.1.0 \ + --hash=sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889 \ + --hash=sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea +requests-oauthlib==1.3.0 \ + --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \ + --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a \ + --hash=sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 +httplib2==0.19.0 \ + --hash=sha256:749c32603f9bf16c1277f59531d502e8f1c2ca19901ae653b49c4ed698f0820e \ + --hash=sha256:e0d428dad43c72dbce7d163b7753ffc7a39c097e6788ef10f4198db69b92f08e +ifaddr==0.1.7 \ + --hash=sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94 \ + --hash=sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3