natekspencer / pylitterbot

Python package for controlling a Whisker connected self-cleaning litter boxes and feeders
MIT License
87 stars 11 forks source link

New Feature: Litter Hopper #247

Open malodie opened 1 month ago

malodie commented 1 month ago

LitterRobot has added a new feature to the LR4, the litter hopper. It would be excellent to add this as a feature in pylitterbot to then extend that to Home Assistant. The app does not display the litter hopper information until you physically click into the Litter Robot section and the hopper itself.

I'm hoping to contribute and add this feature, but I am unsure of how to use the non-public API to see if this information is available. Due to its availability on the app, I am assuming it is available in the API. I will need to spend some time discovering how this works, or if you have any shortcuts available (e.g. a way to list endpoints) or some useful documentation around accessing and reading non-public graphql that would be really helpful.

natekspencer commented 1 month ago

On my iPad, I've had success with Proxyman. On Android I've had to use a mitm proxy to find the endpoints the app is hitting. I see a couple of fields in the existing LR4 endpoint for hopperStatus with values like enabled, disabled, empty, but guessing when you click in there is more information than that.

malodie commented 1 month ago

Thanks for the response. I primarily work on windows at home (sad) but there is a proxyman windows app. If that drives me nuts I'll switch to ubuntu. I will play with it and see what I can find!

malodie commented 1 month ago

I tried to set up the proxy with my android phone (via my PC). It grabs the traffic but I can't actually see any of the requests fully, and it just seems to have a single endpoint that loads all of the data. I'm not seeing any of the direct API requests. I think it's having issues with the cert, which I set up following the instructions. I'll have to try either emulating it on my Windows PC, or come up with another idea, or at worst use some sort of proxy intercept on my phone.

Tell me this is going to be the one time I really wish I was in the mac ecosystem...

They explicitly state that this is harder with android.

natekspencer commented 1 month ago

I tried to set up the proxy with my android phone (via my PC). It grabs the traffic but I can't actually see any of the requests fully, and it just seems to have a single endpoint that loads all of the data. I'm not seeing any of the direct API requests. I think it's having issues with the cert, which I set up following the instructions. I'll have to try either emulating it on my Windows PC, or come up with another idea, or at worst use some sort of proxy intercept on my phone.

Tell me this is going to be the one time I really wish I was in the mac ecosystem...

They explicitly state that this is harder with android.

If you can share the full endpoint, I can check if I'm able to see a few things.

malodie commented 1 month ago

I only get so far as to see the api.onesignal.com and api.exponea.com in the list. But, it gives me an internal error. According to the troubleshooting, I can't actually get that information on Android because I do not own the application.

image

That might mean this is a nonstarter unless I can get my hands on an apple device.

From the docs: If you're trying to intercept Android apps that you're not an owner -> It isn't possible to intercept -> ❌ https://docs.proxyman.io/troubleshooting/get-ssl-error-from-https-request-and-response#android-device

I'll check out Http Toolkit and see if I have more success.

natekspencer commented 1 month ago

You'll probably be looking for an iothings.site url as that is what most of the other endpoints use.

malodie commented 1 month ago

Yeah, you're right. I was able to at least see that URL with HTTP Toolkit, but there's still the android device issue. image image

I don't think there's a way for me to emulate this app.

On the bright side, my best friend has an iPad she's not using and has agreed to lend it to me, so I can hopefully move forward next week.

Thanks for your help on this :)

jrhe commented 3 days ago

@malodie When I was doing some work on this to add pet profiles/weights I futzed around with a few options but ultimately settled on the following as it was easiest:

  1. Install Android Studio
  2. Create an android emulator with an Android Open Source Project (AOSP) version of android on the emulated device. This is important as if you chose a non-AOSP version you will not have root access to the device, which means you won't be able to install root certificates to intercept SSL.
  3. Install the root certificates of your chose proxy software. I used MITMProxy before with wireshark, but HTTP Toolkit is much more user friendly. Use the android with ADB connection method of connection, as scanning a QR code with an emulated device is a headache. AOSP android builds do not have the play store, so you will have to download the HTTP Toolkit APK from elsewhere. I used APK Pure (https://apkpure.com/http-toolkit/tech.httptoolkit.android.v1). You should see in the app both "User Trust Enabled" and "System Trust Enabled"
  4. Install the whisker app. (https://apkpure.com/whisker/com.whisker.android)
jrhe commented 3 days ago

Had a quick poke and can confirm the method above works for the whisker app. Below are endpoints, requests and responses for:

Hope this helps. I don't actually have a hopper so can't really do much more unfortunately.

Endpoint: POST https://lr4.iothings.site/graphql Request body:

{
  "operationName": null,
  "variables": {
    "serial": "<REDACTED>",
    "command": "disableHopper",
    "commandSource": "app"
  },
  "query": "mutation sendLitterRobot4Command($serial: String!, $command: String!, $value: String, $commandSource: String) {\n  __typename\n  sendLitterRobot4Command(input: {serial: $serial, command: $command, value: $value, commandSource: $commandSource})\n}"
}

Response body:

{
  "data": {
    "__typename": "Mutation",
    "sendLitterRobot4Command": "command \"disableHopper (0x020C0000)\" sent"
  }
}

Endpoint: POST https://lr4.iothings.site/graphql Request body:

{
  "operationName": null,
  "variables": {
    "serial": "<REDACTED>",
    "command": "enableHopper",
    "commandSource": "app"
  },
  "query": "mutation sendLitterRobot4Command($serial: String!, $command: String!, $value: String, $commandSource: String) {\n  __typename\n  sendLitterRobot4Command(input: {serial: $serial, command: $command, value: $value, commandSource: $commandSource})\n}"
}

Response body:

{
  "data": {
    "__typename": "Mutation",
    "sendLitterRobot4Command": "command \"enableHopper (0x020C0001)\" sent"
  }
}

Endpoint: POST https://lr4.iothings.site/graphql Request body:

{
  "variables": {
    "serial": "<REDACTED>",
    "consumer": "app",
    "limit": 10,
    "activityTypes": [
      "litterHopperDispensed"
    ]
  },
  "query": "    query GetLR4($serial: String!, $consumer: String, $startTimestamp: String, $endTimestamp: String, $limit: Int, $activityTypes: [String]) {\n      getLitterRobot4Activity(serial: $serial, consumer: $consumer, startTimestamp: $startTimestamp, endTimestamp: $endTimestamp, limit: $limit, activityTypes: $activityTypes) {\n        value\n        timestamp\n        measure\n        actionValue\n      }\n    }\n  "
}

Response body:

{
  "data": {
    "getLitterRobot4Activity": []
  }
}

Also got this over the websocket wss://lr4.iothings.site/graphql/realtime?header=<REDACTED - JWT Token>&payload=e30%3D

{
  "id": "<REDACTED - uuid v4 id string>",
  "type": "start",
  "payload": {
    "data": "{\"variables\":{\"userId\":\"<REDACTED - integer>\"},\"query\":\"    subscription GetLR4($userId: String!) {\\n      litterRobot4StateSubscriptionByUser(userId: $userId) {\\n      robots {\\n        name\\n        serial\\n        unitId\\n        unitPowerType\\n        unitPowerStatus\\n        robotStatus\\n        unitTimezone\\n        unitPowerStatus\\n        isOnboarded\\n        setupDateTime\\n        cleanCycleWaitTime\\n        isKeypadLockout\\n        nightLightBrightness\\n        nightLightMode\\n        litterLevel\\n        DFILevelPercent\\n        globeMotorFaultStatus\\n        catWeight\\n        isBonnetRemoved\\n        isDFIFull\\n        wifiRssi\\n        isOnline\\n        espFirmware\\n        picFirmwareVersion\\n        laserBoardFirmwareVersion\\n        isFirmwareUpdateTriggered\\n        sleepStatus\\n        catDetect\\n        robotCycleState\\n        robotCycleStatus\\n        isLaserDirty\\n        panelBrightnessHigh\\n        smartWeightEnabled\\n        surfaceType\\n        hopperStatus\\n        scoopsSavedCount\\n        isHopperRemoved\\n        litterLevelPercentage\\n        litterLevelState\\n        weekdaySleepModeEnabled {\\n            Sunday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Monday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Tuesday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Wednesday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Thursday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Friday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n            Saturday {\\n                sleepTime\\n                wakeTime\\n                isEnabled\\n            }\\n        }\\n        }\\n      }\\n    }\\n  \"}",
    "extensions": {
      "authorization": {
        "Accept": "application/json, text/javascript",
        "Content-Encoding": "amz-1.0",
        "Content-Type": "application/json; charset=utf-8",
        "Authorization": "Bearer <REDACTED - JWT token>",
        "Host": "lr4.iothings.site"
      }
    }
  }
} 
natekspencer commented 2 days ago

Thanks for those @jrhe. I saw the hopperStatus and isHopperRemoved fields before, but wasn't sure what would come back in hopperStatus. Initially I thought there might be a different endpoint to get more details about the hopper accessory, but looking at the information here and on Whisker's website, I don't think there is much more info about the hopper than these couple of fields.

jrhe commented 2 days ago

I think the API just provides those two vars and then allows you to query dispensing history (litterHopperDispensed activity type).

hopperStatus should be something like full/running low/empty based on the screenshots I have seen.

I think there is a distinction between the hopper being enabled, and hopper being removed. When I tried to enable the hopper without actually having one installed I got a "LitterHopper Error: Motor fault di... - There's an issue with the LitterHopper's motor connection. [...]".

The hopper itself seems to just have a single barrel jack, that presumably provides power, with no additional sensors. I think any data provided is calculated based on the resistance of the motor (missing, spinning, spinning freely/empty), the LR's litter level sensors, and how many times the hopper has run since being refilled.

Hopefully @malodie can check out the exact values!

malodie commented 2 days ago

Unfortunately I still don't have an iPad to check this :(

jrhe commented 2 days ago

You should be able to install the android emulator as above if you want to do it without an ipad