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: ![laptop in a clothes washer with a display DoorState:Closed](images/doorclose.jpg) -``` +```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