webdjoe / pyvesync

pyvesync is a python library to manage Etekcity & Levoit smart devices
MIT License
169 stars 80 forks source link

Add support for Levoit LV_600S Humidifier #100

Closed jddayley closed 2 years ago

jddayley commented 2 years ago

I would like to request supporter the Levoit LV_600S Humidifier. I install a MITM proxy and captured the following dumps. Let me know if you any additional payloads.

{ "code": 0, "msg": "request success", "result": { "cidFwInfoList": [ { "code": 0, "configModule": "WFON_AHM_LUH-A602S-WUS_US", "connectionType": "WiFi+BTOnboarding+BTNotify", "deviceCid": "vsaq7588f6f84290868639af8c171175", "deviceImg": "https://image.vesync.com/defaultImages/LV_600S_Series/icon_lv600s_humidifier_160.png", "deviceName": "Humidifier ", "deviceRegion": "US", "firmUpdateInfos": [ { "currentVersion": "1.0.42", "isMainFw": false, "latestVersion": "1.0.00", "latestVersionUrl": "http://firm-testonline.vesync.com:4005/firm/amazon/WFON_AHM_LUH-A602S-WUS_US/v1.0.00/", "partFirmwareVersionUrl": "", "pluginName": "mcuFw", "priority": 0, "releaseNotes": "First Version.", "upgradeLevel": 0, "upgradeTimeoutInSec": 120 }, { "currentVersion": "1.0.07", "isMainFw": true, "latestVersion": null, "latestVersionUrl": null, "partFirmwareVersionUrl": null, "pluginName": "mainFw", "priority": 1, "releaseNotes": null, "upgradeLevel": 0, "upgradeTimeoutInSec": 120 } ], "macID": "24:d7:eb:01:af:02", "msg": null, "uuid": "cb5c8675-a00e-40a6-bc70-55b37b2ab7da" } ], "macIDFwInfoList": null }, "traceId": "1639054148013"

{ "code": 0, "msg": "request success", "result": { "code": 0, "result": { "automatic_stop_reach_target": true, "configuration": { "auto_target_humidity": 50, "display": true }, "display": true, "enabled": true, "extension": { "schedule_count": 0, "timer_remain": 0 }, "humidity": 52, "humidity_high": false, "mist_level": 3, "mist_virtual_level": 9, "mode": "humidity", "warm_enabled": true, "warm_level": 3, "water_lacks": false, "water_tank_lifted": false }, "traceId": "1639054152557" }, "traceId": "1639054152557"

webdjoe commented 2 years ago

@jddayley Thank you for the captures. Can you please capture the device list and all functions with a description of what the function is by each capture? I'll add this on when you send them over

jddayley commented 2 years ago

Thanks for the quick response. I wrote a python script that performs a few functions, turn on/off, set target humidity and get details. You'll easily see the payloads within each function. Please note the user agent is important because they seem to be filtering on it.

from os import pardir import requests, json, time, datetime, io, hashlib from random import randint from datetime import datetime from urllib.request import urlopen import urllib3

urllib3.disable_warnings() BASE_URL = "https://smartapi.vesync.com" ts = time.time() st = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') token = "" accountID = "" import logging

logging.basicConfig( filename='vesync.log', filemode='a', format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%H:%M:%S', level=logging.DEBUG)

class VesyncApi: def init(self, username, password): global token global accountID

    payload = json.dumps({
        "account":
        username,
        "devToken":
        "",
        "password":
        hashlib.md5(password.encode('utf-8')).hexdigest()
    })
    account = requests.post(BASE_URL + "/vold/user/login",
                            verify=False,
                            data=payload).json()
    if "error" in account:
        raise RuntimeError("Invalid username or password")
    else:
        self._account = account
        #logging.info (account)
        token = account['tk']
        #logging.info (token)
        accountID = account['accountID']
        #logging.info (accountID)
        logging.info('Account Connection Successful')

    self._devices = []

def get_detail(self, id):
    global token
    global accountID
    payload = {
        "acceptLanguage": "en",
        "accountID": accountID,
        "appVersion": "VeSync 3.1.54 build10",
        "cid": id,
        "configModule": "WFON_AHM_LUH-A602S-WUS_US",
        "deviceRegion": "US",
        "method": "bypassV2",
        "payload": {
            "data": {},
            "method": "getHumidifierStatus",
            "source": "APP"
        },
        "phoneBrand": "iPhone",
        "phoneOS": "iOS 15.1.1",
        "timeZone": "America/New_York",
        "token": token,
        "traceId": "1639257589508",
        "userCountryCode": "US"
    }

    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
    r = requests.post(BASE_URL + '/cloud/v2/deviceManaged/bypassV2',
                      headers=headers,
                      json=payload)
    return r

def get_devices(self, id):
    global token
    global accountID
    payload = {
        "acceptLanguage": "en",
        "accountID": accountID,
        "appVersion": "VeSync 3.1.54 build10",
        "cid": id,
        "method": "deviceInfo",
        "phoneBrand": "iPhone",
        "phoneOS": "iOS 15.1.1",
        "subDeviceNo": 0,
        "timeZone": "America/New_York",
        "token": token,
        "traceId": "1639257579506",
        "userCountryCode": "US"
    }
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
    r = requests.post(BASE_URL + '/cloud/v1/deviceManaged/deviceInfo',
                      headers=headers,
                      json=payload)
    return r

def turn_off(self, id):
    payload = {
        "acceptLanguage": "en",
        "accountID": accountID,
        "appVersion": "VeSync 3.1.54 build10",
        "cid": id,
        "configModule": "WFON_AHM_LUH-A602S-WUS_US",
        "debugMode": False,
        "deviceRegion": "US",
        "method": "bypassV2",
        "payload": {
            "data": {
                "enabled": False,
                "id": 0
            },
            "method": "setSwitch",
            "source": "APP"
        },
        "phoneBrand": "iPhone",
        "phoneOS": "iOS 15.1.1",
        "timeZone": "America/New_York",
        "token": token,
        "traceId": "1639267601802",
        "userCountryCode": "US"
    }
    headers = {
        'Content-Type':
        'application/json',
        'Accept':
        '*/*',
        'user-agent':
        'VeSync/3.1.54 (com.etekcity.vesyncPlatform; build:10; iOS 15.1.1) Alamofire/5.2.1'
    }
    r = requests.put(BASE_URL + '/cloud/v2/deviceManaged/bypassV2',
                     headers=headers,
                     json=payload)
    logging.info(r.text)

def turn_on(self, id):
    payload = {
        "acceptLanguage": "en",
        "accountID": accountID,
        "appVersion": "VeSync 3.1.54 build10",
        "cid": id,
        "configModule": "WFON_AHM_LUH-A602S-WUS_US",
        "debugMode": False,
        "deviceRegion": "US",
        "method": "bypassV2",
        "payload": {
            "data": {
                "enabled": True,
                "id": 0
            },
            "method": "setSwitch",
            "source": "APP"
        },
        "phoneBrand": "iPhone",
        "phoneOS": "iOS 15.1.1",
        "timeZone": "America/New_York",
        "token": token,
        "traceId": "1639267601802",
        "userCountryCode": "US"
    }
    headers = {
        'Content-Type':
        'application/json',
        'Accept':
        '*/*',
        'user-agent':
        'VeSync/3.1.54 (com.etekcity.vesyncPlatform; build:10; iOS 15.1.1) Alamofire/5.2.1'
    }
    r = requests.put(BASE_URL + '/cloud/v2/deviceManaged/bypassV2',
                     headers=headers,
                     json=payload)
    logging.info(r.text)
def set_target(self, id, target):
    payload = {
        "acceptLanguage": "en",
        "accountID": accountID,
        "appVersion": "VeSync 3.1.54 build10",
        "cid": id,
        "configModule": "WFON_AHM_LUH-A602S-WUS_US",
        "debugMode": False,
        "deviceRegion": "US",
        "method": "bypassV2",
        "payload": {
            "data": {
                "target_humidity": target
            },
            "method": "setTargetHumidity",
            "source": "APP"
        },
        "phoneBrand": "iPhone",
        "phoneOS": "iOS 15.1.1",
        "timeZone": "America/New_York",
        "token": token,
        "traceId": "1639267601802",
        "userCountryCode": "US"
    }
    headers = {
        'Content-Type':
        'application/json',
        'Accept':
        '*/*',
        'user-agent':
        'VeSync/3.1.54 (com.etekcity.vesyncPlatform; build:10; iOS 15.1.1) Alamofire/5.2.1'
    }
    r = requests.put(BASE_URL + '/cloud/v2/deviceManaged/bypassV2',
                     headers=headers,
                     json=payload)
    logging.info(r.text)
webdjoe commented 2 years ago

@jddayley How did you do original capture? It looks like VeSync started to use certificate pinning

jddayley commented 2 years ago

I'm using https://mitmproxy.org with IOS. I am able to see the traffic as opposed to other apps that have certificate pinning.

anoblet commented 2 years ago

I just bought one of these. I have Packet Capture installed on my phone.

This would be my first time intercepting/recording requests, I work in technology though and wouldn't necessarily be drowning in confusion :)

anoblet commented 2 years ago

Any update on this?

dag81 commented 2 years ago

Any chance anyone could capture the url's and payloads used to set warm_mode to enabled as well as the warm_level for the 600S?

h0jeZvgoxFepBQ2C commented 2 years ago

Please integrate this, waiting for this on my home assistant server :D ❤️

brianhealey commented 2 years ago

I added a PR to add my 600s. If you all want to take a look and try it out, it should be straight forward. https://github.com/webdjoe/pyvesync/pull/110

brianhealey commented 2 years ago

Any chance anyone could capture the url's and payloads used to set warm_mode to enabled as well as the warm_level for the 600S?

I have a 600s and can capture packets. Where in the app is the warm level? I am not seeing it in mine.

anoblet commented 2 years ago

@brianhealey This issue is for the LV_600S humidifier, not the Core600S air purifier.

brianhealey commented 2 years ago

Ah! Fair Enough. :)

brianhealey commented 2 years ago

For what its worth, I have an LV_600S on order and will be able to put a PR together for that once I have it.

brianhealey commented 2 years ago

I added a PR: https://github.com/webdjoe/pyvesync/pull/112

brianhealey commented 2 years ago

Any chance anyone could capture the url's and payloads used to set warm_mode to enabled as well as the warm_level for the 600S?

FYI, enabled and disabled is automatically set based upon level being 0 or higher.

brianhealey commented 2 years ago

@anoblet Would you mind playing with my PR and seeing if it does was you want? @dag81 . I hope this PR has what you are looking for.

anoblet commented 2 years ago

Sure! What's the best way for me to override pyvesync? Do I just clone it inside of custom_components?

brianhealey commented 2 years ago

I haven't got that far. :) I tested it using a locally written cli, but I don't have it integrated with other tools.

anoblet commented 2 years ago

I was able to use the CLI and got this response:

pprint(vars(fan))

{'cid': 'XXXXX',
 'config': {'auto_target_humidity': 45,
            'automatic_stop': True,
            'display': False},
 'config_module': 'WFON_AHM_LUH-A602S-WUS_US',
 'connection_status': 'online',
 'connection_type': 'WiFi+BTOnboarding+BTNotify',
 'current_firm_version': None,
 'details': {'automatic_stop_reach_target': False,
             'display': False,
             'humidity': 42,
             'humidity_high': False,
             'mist_level': 2,
             'mist_virtual_level': 5,
             'mode': 'humidity',
             'warm_enabled': False,
             'warm_level': 0,
             'water_lacks': False,
             'water_tank_lifted': False},
 'device_image': 'https://image.vesync.com/defaultImages/LV_600S_Series/icon_lv600s_humidifier_160.png',
 'device_name': 'Bedroom Humidifier',
 'device_status': 'on',
 'device_type': 'LUH-A602S-WUS',
 'enabled': True,
 'extension': None,
 'mac_id': 'XXXXX',
 'manager': <pyvesync.vesync.VeSync object at 0x000001BD4F03AB90>,
 'mode': None,
 'night_light': False,
 'speed': None,
 'sub_device_no': None,
 'type': 'wifi-air',
 'uuid': 'XXXXX',
 'warm': True}

print(fan.displayJSON())

 {
    "Device Name": "Bedroom Humidifier",
    "Model": "LUH-A602S-WUS",
    "Subdevice No": "None",
    "Status": "on",
    "Online": "online",
    "Type": "wifi-air",
    "CID": "XXXXX",
    "Mode": "humidity",
    "Humidity": "44",
    "Mist Virtual Level": "5",
    "Mist Level": "2",
    "Water Lacks": false,
    "Humidity High": false,
    "Water Tank Lifted": false,
    "Display": false,
    "Automatic Stop Reach Target": false,
    "Auto Target Humidity": "45",
    "Automatic Stop": true,
    "Mist Warm Enabled": false,
    "Mist Warm Level": 0
}

Are the properties on the device object supposed to be in sync with those from details? I ask because my humidifier is set to warm = 0 as reflected in the details attribute though on the device object it's set to true. Speed, nightlight, and mode either don't exist for this device, or don't seem to be in sync with the device state.

brianhealey commented 2 years ago

:) What you are seeing on the device is a "feature flag". The code uses this feature flag to determine if it should report and allow changes to the warmth setting. This is how nightlight works with other humidifiers.

I am not sure I follow your comments. I see mode listed.

"Mode": "humidity", "Automatic Stop Reach Target": false, "Auto Target Humidity": "45", "Mist Virtual Level": "5", "Mist Level": "2", "Display": false,

The mode can be set using mode_toggle however I do see that it doesn't support humidity which I can fix. I imagine by "speed" you are referring to the mist level? That should be able to be toggled using the API.

I don't see a nightlight mode in the app, all that I see is "on/off" for the display. That can be set using set_display.

webdjoe commented 2 years ago

Sorry for delay - I refactored the library to make it easier to add devices if you can please test - https://github.com/webdjoe/pyvesync/tree/air-refactor

webdjoe commented 2 years ago

Can anyone give this PR #121 a test for the LV600S?

webdjoe commented 2 years ago

This should be fixed in #121 Please make a new issue if you are still having trouble.