diff --git a/README.md b/README.md
index 72a7450..dc7fa5e 100644
--- a/README.md
+++ b/README.md
@@ -16,19 +16,20 @@ and should prevent most any random attacker on your network from being able to
## Setup
+To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
```
-pip3 -r requirements.txt
+python3 -m venv venv
+source venv/bin/activate
+git clone https://github.com/osresearch/hcpy
+cd hcpy
+pip3 install -r requirements.txt
```
Install the Python dependencies; the `sslpsk` one is a little weird
and we might need to revisit it later.
-### For Mac Users
-To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
-1. python3 -m venv venv
-1. source venv/bin/activate
-1. clone this repo
+### For Mac Users
Installing `sslpsk` needs some extra steps:
1. The openssl package installed via brew: `brew install openssl`, and
@@ -39,7 +40,7 @@ Installing `sslpsk` needs some extra steps:

-```
+```bash
hc-login $USERNAME $PASSWORD > config.json
```
@@ -55,10 +56,9 @@ the resulting configuration JSON file *should* be sufficient to
connect to the devices on your local network, assuming that
your mDNS or DNS server resolves the names correctly.
-
## Home Connect to MQTT
-```
+```bash
hc2mqtt config.json
```
@@ -82,89 +82,89 @@ library.
Example message published to `homeconnect/dishwasher`:
-```
+```json
{
- "state": "Run",
- "door": "Closed",
- "remaining": "2:49",
- "power": true,
- "lowwaterpressure": false,
- "aquastop": false,
- "error": false,
- "remainingseconds": 10140
+ "state": "Run",
+ "door": "Closed",
+ "remaining": "2:49",
+ "power": true,
+ "lowwaterpressure": false,
+ "aquastop": false,
+ "error": false,
+ "remainingseconds": 10140
}
```
Full state information
-```
+```json
{
- 'AllowBackendConnection': False,
- 'BackendConnected': False,
- 'RemoteControlLevel': 'ManualRemoteStart',
- 'SoftwareUpdateAvailable': 'Off',
- 'ConfirmPermanentRemoteStart': 'Off',
- 'ActiveProgram': 0,
- 'SelectedProgram': 8192,
- 'RemoteControlStartAllowed': False,
- '520': '2022-02-21T16:48:54',
- 'RemoteControlActive': True,
- 'AquaStopOccured': 'Off',
- 'DoorState': 'Open',
- 'PowerState': 'Off',
- 'ProgramFinished': 'Off',
- 'ProgramProgress': 100,
- 'LowWaterPressure': 'Off',
- 'RemainingProgramTime': 0,
- 'ProgramAborted': 'Off',
- '547': False,
- 'RemainingProgramTimeIsEstimated': True,
- 'OperationState': 'Inactive',
- 'StartInRelative': 0,
- 'EnergyForecast': 82,
- 'WaterForecast': 70,
- 'ConnectLocalWiFi': 'Off',
- 'SoftwareUpdateTransactionID': 0,
- 'SoftwareDownloadAvailable': 'Off',
- 'SoftwareUpdateSuccessful': 'Off',
- 'ProgramPhase': 'Drying',
- 'SilenceOnDemandRemainingTime': 0,
- 'EcoDryActive': False,
- 'RinseAid': 'R04',
- 'SensitivityTurbidity': 'Standard',
- 'ExtraDry': False,
- 'HotWater': 'ColdWater',
- 'TimeLight': 'On',
- 'EcoAsDefault': 'LastProgram',
- 'SoundLevelSignal': 'Off',
- 'SoundLevelKey': 'Medium',
- 'WaterHardness': 'H04',
- 'DryingAssistantAllPrograms': 'AllPrograms',
- 'SilenceOnDemandDefaultTime': 1800,
- 'SpeedOnDemand': False,
- 'InternalError': 'Off',
- 'CheckFilterSystem': 'Off',
- 'DrainingNotPossible': 'Off',
- 'DrainPumpBlocked': 'Off',
- 'WaterheaterCalcified': 'Off',
- 'LowVoltage': 'Off',
- 'SaltLack': 'Off',
- 'RinseAidLack': 'Off',
- 'SaltNearlyEmpty': 'Off',
- 'RinseAidNearlyEmpty': 'Off',
- 'MachineCareReminder': 'Off',
- '5121': False,
- 'HalfLoad': False,
- 'IntensivZone': False,
- 'VarioSpeedPlus': False,
- '5131': False,
- '5134': True,
- 'SilenceOnDemand': False
+ 'AllowBackendConnection': False,
+ 'BackendConnected': False,
+ 'RemoteControlLevel': 'ManualRemoteStart',
+ 'SoftwareUpdateAvailable': 'Off',
+ 'ConfirmPermanentRemoteStart': 'Off',
+ 'ActiveProgram': 0,
+ 'SelectedProgram': 8192,
+ 'RemoteControlStartAllowed': False,
+ '520': '2022-02-21T16:48:54',
+ 'RemoteControlActive': True,
+ 'AquaStopOccured': 'Off',
+ 'DoorState': 'Open',
+ 'PowerState': 'Off',
+ 'ProgramFinished': 'Off',
+ 'ProgramProgress': 100,
+ 'LowWaterPressure': 'Off',
+ 'RemainingProgramTime': 0,
+ 'ProgramAborted': 'Off',
+ '547': False,
+ 'RemainingProgramTimeIsEstimated': True,
+ 'OperationState': 'Inactive',
+ 'StartInRelative': 0,
+ 'EnergyForecast': 82,
+ 'WaterForecast': 70,
+ 'ConnectLocalWiFi': 'Off',
+ 'SoftwareUpdateTransactionID': 0,
+ 'SoftwareDownloadAvailable': 'Off',
+ 'SoftwareUpdateSuccessful': 'Off',
+ 'ProgramPhase': 'Drying',
+ 'SilenceOnDemandRemainingTime': 0,
+ 'EcoDryActive': False,
+ 'RinseAid': 'R04',
+ 'SensitivityTurbidity': 'Standard',
+ 'ExtraDry': False,
+ 'HotWater': 'ColdWater',
+ 'TimeLight': 'On',
+ 'EcoAsDefault': 'LastProgram',
+ 'SoundLevelSignal': 'Off',
+ 'SoundLevelKey': 'Medium',
+ 'WaterHardness': 'H04',
+ 'DryingAssistantAllPrograms': 'AllPrograms',
+ 'SilenceOnDemandDefaultTime': 1800,
+ 'SpeedOnDemand': False,
+ 'InternalError': 'Off',
+ 'CheckFilterSystem': 'Off',
+ 'DrainingNotPossible': 'Off',
+ 'DrainPumpBlocked': 'Off',
+ 'WaterheaterCalcified': 'Off',
+ 'LowVoltage': 'Off',
+ 'SaltLack': 'Off',
+ 'RinseAidLack': 'Off',
+ 'SaltNearlyEmpty': 'Off',
+ 'RinseAidNearlyEmpty': 'Off',
+ 'MachineCareReminder': 'Off',
+ '5121': False,
+ 'HalfLoad': False,
+ 'IntensivZone': False,
+ 'VarioSpeedPlus': False,
+ '5131': False,
+ '5134': True,
+ 'SilenceOnDemand': False
}
```
-
+
### Clothes washer
@@ -178,86 +178,86 @@ binary data over the websocket (type 0x82).
Example message published to `homeconnect/washer`:
-```
+```json
{
- "state": "Ready",
- "door": "Closed",
- "remaining": "3:48",
- "power": true,
- "lowwaterpressure": false,
- "aquastop": false,
- "error": false,
- "remainingseconds": 13680
+ "state": "Ready",
+ "door": "Closed",
+ "remaining": "3:48",
+ "power": true,
+ "lowwaterpressure": false,
+ "aquastop": false,
+ "error": false,
+ "remainingseconds": 13680
}
```
Full state information
-```
+```json
{
- 'BackendConnected': False,
- 'CustomerEnergyManagerPaired': False,
- 'CustomerServiceConnectionAllowed': False,
- 'DoorState': 'Open',
- 'FlexStart': 'Disabled',
- 'LocalControlActive': False,
- 'OperationState': 'Ready',
- 'RemoteControlActive': True,
- 'RemoteControlStartAllowed': False,
- 'WiFiSignalStrength': -50,
- 'LoadInformation': 0,
- 'AquaStopOccured': 'Off',
- 'CustomerServiceRequest': 'Off',
- 'LowWaterPressure': 'Off',
- 'ProgramFinished': 'Off',
- 'SoftwareUpdateAvailable': 'Off',
- 'WaterLevelTooHigh': 'Off',
- 'DoorNotLockable': 'Off',
- 'DoorNotUnlockable': 'Off',
- 'DoorOpen': 'Off',
- 'FatalErrorOccured': 'Off',
- 'FoamDetection': 'Off',
- 'DrumCleanReminder': 'Off',
- 'PumpError': 'Off',
- 'ReleaseRinseHoldPending': 'Off',
- 'EnergyForecast': 20,
- 'EstimatedTotalProgramTime': 13680,
- 'FinishInRelative': 13680,
- 'FlexFinishInRelative': 0,
- 'ProgramProgress': 0,
- 'RemainingProgramTime': 13680,
- 'RemainingProgramTimeIsEstimated': True,
- 'WaterForecast': 40,
- 'LoadRecommendation': 10000,
- 'ProcessPhase': 4,
- 'ReferToProgram': 0,
- 'LessIroning': False,
- 'Prewash': False,
- 'RinseHold': False,
- 'RinsePlus': 0,
- 'SilentWash': False,
- 'Soak': False,
- 'SpeedPerfect': False,
- 'SpinSpeed': 160,
- 'Stains': 0,
- 'Temperature': 254,
- 'WaterPlus': False,
- 'AllowBackendConnection': False,
- 'AllowEnergyManagement': False,
- 'AllowFlexStart': False,
- 'ChildLock': False,
- 'Language': 'En',
- 'PowerState': 'On',
- 'EndSignalVolume': 'Medium',
- 'KeySignalVolume': 'Loud',
- 'EnableDrumCleanReminder': True,
- 'ActiveProgram': 0,
- 'SelectedProgram': 28718
+ 'BackendConnected': False,
+ 'CustomerEnergyManagerPaired': False,
+ 'CustomerServiceConnectionAllowed': False,
+ 'DoorState': 'Open',
+ 'FlexStart': 'Disabled',
+ 'LocalControlActive': False,
+ 'OperationState': 'Ready',
+ 'RemoteControlActive': True,
+ 'RemoteControlStartAllowed': False,
+ 'WiFiSignalStrength': -50,
+ 'LoadInformation': 0,
+ 'AquaStopOccured': 'Off',
+ 'CustomerServiceRequest': 'Off',
+ 'LowWaterPressure': 'Off',
+ 'ProgramFinished': 'Off',
+ 'SoftwareUpdateAvailable': 'Off',
+ 'WaterLevelTooHigh': 'Off',
+ 'DoorNotLockable': 'Off',
+ 'DoorNotUnlockable': 'Off',
+ 'DoorOpen': 'Off',
+ 'FatalErrorOccured': 'Off',
+ 'FoamDetection': 'Off',
+ 'DrumCleanReminder': 'Off',
+ 'PumpError': 'Off',
+ 'ReleaseRinseHoldPending': 'Off',
+ 'EnergyForecast': 20,
+ 'EstimatedTotalProgramTime': 13680,
+ 'FinishInRelative': 13680,
+ 'FlexFinishInRelative': 0,
+ 'ProgramProgress': 0,
+ 'RemainingProgramTime': 13680,
+ 'RemainingProgramTimeIsEstimated': True,
+ 'WaterForecast': 40,
+ 'LoadRecommendation': 10000,
+ 'ProcessPhase': 4,
+ 'ReferToProgram': 0,
+ 'LessIroning': False,
+ 'Prewash': False,
+ 'RinseHold': False,
+ 'RinsePlus': 0,
+ 'SilentWash': False,
+ 'Soak': False,
+ 'SpeedPerfect': False,
+ 'SpinSpeed': 160,
+ 'Stains': 0,
+ 'Temperature': 254,
+ 'WaterPlus': False,
+ 'AllowBackendConnection': False,
+ 'AllowEnergyManagement': False,
+ 'AllowFlexStart': False,
+ 'ChildLock': False,
+ 'Language': 'En',
+ 'PowerState': 'On',
+ 'EndSignalVolume': 'Medium',
+ 'KeySignalVolume': 'Loud',
+ 'EnableDrumCleanReminder': True,
+ 'ActiveProgram': 0,
+ 'SelectedProgram': 28718
}
```
-
+
### Coffee Machine
@@ -268,101 +268,101 @@ The coffee machine needs a better mapping to MQTT messages.
Full state information
-```
+```json
{
- 'LastSelectedBeverage': 8217,
- 'LocalControlActive': False,
- 'PowerSupplyError': 'Off',
- 'DripTrayNotInserted': 'Off',
- 'DripTrayFull': 'Off',
- 'WaterFilterShouldBeChanged': 'Off',
- 'WaterTankEmpty': 'Off',
- 'WaterTankNearlyEmpty': 'Off',
- 'BrewingUnitIsMissing': 'Off',
- 'SelectedProgram': 0,
- 'MacchiatoPause': '5Sec',
- 'ActiveProgram': 0,
- 'BeverageCountdownWaterfilter': 48,
- 'BeverageCountdownCalcNClean': 153,
- 'RemoteControlStartAllowed': True,
- 'EmptyDripTray': 'Off',
- 'BeverageCountdownDescaling': 153,
- 'EmptyDripTrayRemoveContainer': 'Off',
- 'BeverageCounterRistrettoEspresso': 177,
- 'AllowBackendConnection': True,
- 'BeverageCounterHotWater': 37351,
- 'RemindForMilkAfter': 'Off',
- 'BeverageCounterFrothyMilk': 22,
- 'BeverageCounterCoffeeAndMilk': 1077,
- 'CustomerServiceRequest': 'Off',
- '4645': 0,
- 'CoffeeMilkOrder': 'FirstCoffee',
- 'BackendConnected': True,
- 'BeverageCounterCoffee': 21,
- 'Enjoy': 'Off',
- 'UserMode': 'Barista',
- 'PlaceEmptyGlassUnderOutlet': 'Off',
- 'WaterTankNotInserted': 'Off',
- 'PlaylistRunning': False,
- 'BeverageCounterPowderCoffee': 9,
- 'DemoModeActive': False,
- 'CleanBrewingUnit': 'Off',
- 'WaterHardness': 'Medium',
- 'CloseDoor': 'Off',
- 'EmptyMilkTank': 'Off',
- 'SpecialRinsing': 'Off',
- 'AllowConsumerInsights': False,
- 'SwitchOffAfter': '01Hours15Minutes',
- '4681': 0,
- 'LastSelectedCoffeeWorldBeverage': 20514,
- 'BrightnessDisplay': 7,
- 'CleanMilkTank': 'Off',
- 'NotEnoughWaterForThisKindOfBeverage': 'Off',
- 'ChildLock': False,
- '4666': 0,
- 'Language': 'De',
- 'MilkContainerConnected': 'Off',
- 'SoftwareUpdateAvailable': 'Off',
- 'LeaveProfilesAutomatically': True,
- 'RemoveWaterFilter': 'Off',
- 'OperationState': 'Inactive',
- 'BeverageCounterHotMilk': 9,
- '4362': 0,
- 'MilkTubeRemoved': 'Off',
- 'DeviceIsToCold4C': 'Off',
- 'SystemHasRunDry': 'Off',
- 'DeviceShouldBeDescaled': 'Off',
- 'PowerState': 'Standby',
- 'DeviceShouldBeCleaned': 'Off',
- 'DeviceShouldBeCalcNCleaned': 'Off',
- 'BeanContainerEmpty': 'Off',
- 'MilkStillOK': 'Off',
- 'CoffeeOutletMissing': 'Off',
- 'MilkReminder': 'Off',
- 'RefillEmptyWaterTank': 'Off',
- 'RefillEmptyBeanContainer': 'Off',
- 'UnderOverVoltage': 'Off',
- 'NotEnoughPomaceCapacityForThisKindOfBeverage': 'Off',
- 'AdjustGrindSetting': 'Off',
- 'InsertWaterFilter': 'Off',
- 'FillDescaler': 'Off',
- 'CleanFillWaterTank': 'Off',
- 'PlaceContainerUnderOutlet': 'Off',
- 'SwitchOffPower30sekBackOn': 'Off',
- 'ThrowCleaningDiscInTheDrawer': 'Off',
- 'RemoveMilkContainer': 'Off',
- 'RemoveContainerUnderOutlet': 'Off',
- 'MilkContainerRemoved': 'Off',
- 'ServiceProgramFinished': 'Off',
- 'DeviceDescalingOverdue': 'Off',
- 'DeviceDescalingBlockage': 'Off',
- 'CustomerServiceConnectionAllowed': False,
- 'BeverageCountdownCleaning': 38,
- 'ProcessPhase': 'None'
+ 'LastSelectedBeverage': 8217,
+ 'LocalControlActive': False,
+ 'PowerSupplyError': 'Off',
+ 'DripTrayNotInserted': 'Off',
+ 'DripTrayFull': 'Off',
+ 'WaterFilterShouldBeChanged': 'Off',
+ 'WaterTankEmpty': 'Off',
+ 'WaterTankNearlyEmpty': 'Off',
+ 'BrewingUnitIsMissing': 'Off',
+ 'SelectedProgram': 0,
+ 'MacchiatoPause': '5Sec',
+ 'ActiveProgram': 0,
+ 'BeverageCountdownWaterfilter': 48,
+ 'BeverageCountdownCalcNClean': 153,
+ 'RemoteControlStartAllowed': True,
+ 'EmptyDripTray': 'Off',
+ 'BeverageCountdownDescaling': 153,
+ 'EmptyDripTrayRemoveContainer': 'Off',
+ 'BeverageCounterRistrettoEspresso': 177,
+ 'AllowBackendConnection': True,
+ 'BeverageCounterHotWater': 37351,
+ 'RemindForMilkAfter': 'Off',
+ 'BeverageCounterFrothyMilk': 22,
+ 'BeverageCounterCoffeeAndMilk': 1077,
+ 'CustomerServiceRequest': 'Off',
+ '4645': 0,
+ 'CoffeeMilkOrder': 'FirstCoffee',
+ 'BackendConnected': True,
+ 'BeverageCounterCoffee': 21,
+ 'Enjoy': 'Off',
+ 'UserMode': 'Barista',
+ 'PlaceEmptyGlassUnderOutlet': 'Off',
+ 'WaterTankNotInserted': 'Off',
+ 'PlaylistRunning': False,
+ 'BeverageCounterPowderCoffee': 9,
+ 'DemoModeActive': False,
+ 'CleanBrewingUnit': 'Off',
+ 'WaterHardness': 'Medium',
+ 'CloseDoor': 'Off',
+ 'EmptyMilkTank': 'Off',
+ 'SpecialRinsing': 'Off',
+ 'AllowConsumerInsights': False,
+ 'SwitchOffAfter': '01Hours15Minutes',
+ '4681': 0,
+ 'LastSelectedCoffeeWorldBeverage': 20514,
+ 'BrightnessDisplay': 7,
+ 'CleanMilkTank': 'Off',
+ 'NotEnoughWaterForThisKindOfBeverage': 'Off',
+ 'ChildLock': False,
+ '4666': 0,
+ 'Language': 'De',
+ 'MilkContainerConnected': 'Off',
+ 'SoftwareUpdateAvailable': 'Off',
+ 'LeaveProfilesAutomatically': True,
+ 'RemoveWaterFilter': 'Off',
+ 'OperationState': 'Inactive',
+ 'BeverageCounterHotMilk': 9,
+ '4362': 0,
+ 'MilkTubeRemoved': 'Off',
+ 'DeviceIsToCold4C': 'Off',
+ 'SystemHasRunDry': 'Off',
+ 'DeviceShouldBeDescaled': 'Off',
+ 'PowerState': 'Standby',
+ 'DeviceShouldBeCleaned': 'Off',
+ 'DeviceShouldBeCalcNCleaned': 'Off',
+ 'BeanContainerEmpty': 'Off',
+ 'MilkStillOK': 'Off',
+ 'CoffeeOutletMissing': 'Off',
+ 'MilkReminder': 'Off',
+ 'RefillEmptyWaterTank': 'Off',
+ 'RefillEmptyBeanContainer': 'Off',
+ 'UnderOverVoltage': 'Off',
+ 'NotEnoughPomaceCapacityForThisKindOfBeverage': 'Off',
+ 'AdjustGrindSetting': 'Off',
+ 'InsertWaterFilter': 'Off',
+ 'FillDescaler': 'Off',
+ 'CleanFillWaterTank': 'Off',
+ 'PlaceContainerUnderOutlet': 'Off',
+ 'SwitchOffPower30sekBackOn': 'Off',
+ 'ThrowCleaningDiscInTheDrawer': 'Off',
+ 'RemoveMilkContainer': 'Off',
+ 'RemoveContainerUnderOutlet': 'Off',
+ 'MilkContainerRemoved': 'Off',
+ 'ServiceProgramFinished': 'Off',
+ 'DeviceDescalingOverdue': 'Off',
+ 'DeviceDescalingBlockage': 'Off',
+ 'CustomerServiceConnectionAllowed': False,
+ 'BeverageCountdownCleaning': 38,
+ 'ProcessPhase': 'None'
}
```
-
+
## FRIDA tools
diff --git a/hc-login b/hc-login
index 985c474..9add37a 100755
--- a/hc-login
+++ b/hc-login
@@ -4,7 +4,7 @@
# A really nice walk through of how it works is:
# https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce
import requests
-from urllib.parse import urlparse, parse_qs, urlencode
+from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
from lxml import html
import io
import re
@@ -13,6 +13,7 @@ import json
from time import time
from base64 import b64decode as base64_decode
from base64 import urlsafe_b64encode as base64url_encode
+from bs4 import BeautifulSoup
from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA256
from zipfile import ZipFile
@@ -124,44 +125,40 @@ while True:
r = session.get(preauth_url, allow_redirects=False)
if r.status_code == 200:
break
- if r.status_code == 302 or r.status_code == 301:
+ if r.status_code > 300 and r.status_code < 400:
preauth_url = r.headers["location"]
+ # Make relative locations absolute
+ if not bool(urlparse(preauth_url).netloc):
+ preauth_url = singlekey_host + preauth_url
continue
print(f"2: {preauth_url=}: failed to fetch {r} {r.text}", file=sys.stderr)
exit(1)
# get the ReturnUrl from the response
query = parse_qs(urlparse(preauth_url).query)
-return_url = query["ReturnUrl"][0]
+return_url = query["returnUrl"][0]
debug(f"{return_url=}")
-headers["RequestVerificationToken"] = r.cookies["X-CSRF-FORM-TOKEN"]
+if "X-CSRF-FORM-TOKEN" in r.cookies:
+ headers["RequestVerificationToken"] = r.cookies["X-CSRF-FORM-TOKEN"]
session.headers.update(headers)
debug("--------")
-valid_url = singlekey_host + '/auth/api/v1/authentication/UserExists'
-auth_url = singlekey_host + '/auth/api/v1/authentication/login'
+soup = BeautifulSoup(r.text, 'html.parser')
+requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
+r = session.post(preauth_url, data={"UserIdentifierInput.EmailInput.StringValue": email, "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
-r = session.post(valid_url, json={"username": email})
-debug(f"{valid_url=}: {r} {r.text}")
+password_url = r.headers['location']
+if not bool(urlparse(password_url).netloc):
+ password_url = singlekey_host + password_url
+r = session.get(password_url, allow_redirects=False)
+soup = BeautifulSoup(r.text, 'html.parser')
+requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
-login_fields = {
- "username": email,
- "password": password,
- "keepMeSignedIn": False,
- "returnUrl": return_url,
-}
-
-r = session.post(auth_url, json=login_fields, allow_redirects=False)
+r = session.post(password_url, data={"Password": password, "RememberMe": "false", "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
-if r.status_code != 200:
- debug(f"auth failed: {auth_url=}, {login_fields=} {r} {r.text}")
- exit(-1)
-
-debug(f"{auth_url=}, {r} {r.text}")
-return_url = json.loads(r.text)["returnUrl"]
if return_url.startswith("/"):
return_url = singlekey_host + return_url
diff --git a/requirements.txt b/requirements.txt
index f3fecf0..ff8a012 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,5 @@
+bs4
+requests
pycryptodome
websocket-client
sslpsk