-
-
Notifications
You must be signed in to change notification settings - Fork 16
New Feature: Litter Hopper #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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 |
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! |
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. |
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. That might mean this is a nonstarter unless I can get my hands on an apple device. From the docs: I'll check out Http Toolkit and see if I have more success. |
You'll probably be looking for an iothings.site url as that is what most of the other endpoints use. |
@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:
|
Had a quick poke and can confirm the method above works for the whisker app.
Hope this helps. I don't actually have a hopper so can't really do much more unfortunately. Endpoint: {
"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: {
"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: {
"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 {
"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"
}
}
}
} |
Thanks for those @jrhe. I saw the |
I think the API just provides those two vars and then allows you to query dispensing history (
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! |
Unfortunately I still don't have an iPad to check this :( |
You should be able to install the android emulator as above if you want to do it without an ipad |
@natekspencer @malodie Just checking to see if this additional functionality is coming. For me, I'd love to turn off the hopper at night because its a bit noisier than the LR4 itself. Let me know if I can help with testing, etc. |
I tried to do it with android and I can't capture the traffic. Sorry. If I get an ipad I will but I have no idea when that will be as I own no apple products. |
Hey gang, I have a LR4 and LitterHopper as well as Proxyman Pro and a physical iPhone/iOS device. Happy to contribute but sadly am unable to see any EDIT: I suspect that perhaps they're using certificate pinning? Or something else that prevents traditional MITM tools from sniffing the requests. I'll try to see if I can use Frida to bypass with the iOS Simulator later. |
I was able to dump the GraphQL schema via their introspection endpoint still being enabled in production. Full Schema: lr4.graphql.txt
Here is hopper-pertinent schema: enum HopperStatusEnum {
ENABLED
DISABLED
MOTOR_FAULT_SHORT
MOTOR_OT_AMPS
MOTOR_DISCONNECTED
EMPTY
}
type ToggleHopperOutput {
success: Boolean
}
type UnitDiagnostics {
# ...
hopperMotorAmperes: Int
# ...
}
type LitterRobot4 {
# ...
hopperStatus: HopperStatusEnum
isHopperRemoved: Boolean
# ...
}
type Mutation {
# ...
toggleHopper(serial: String!, isRemoved: Boolean!): ToggleHopperOutput
# ...
}
type Query {
# ...
getUnitDiagnosticsBySerial(serial: String!): UnitDiagnostics
getUnitDiagnosticsByUser(userId: String!): [UnitDiagnostics]
getLitterRobot4BySerial(serial: String!): LitterRobot4
getLitterRobot4ByUnitId(unitId: ID!): LitterRobot4
getLitterRobot4ByUser(userId: String!): [LitterRobot4]
# ...
} Obviously I'll be able to shed more light on any sort of LR4 hopper-related commands once I get a MITM setup going |
and thanks to @jrhe here are some real world examples for GraphQL query: query GetLitterRobot4Activity {
getLitterRobot4Activity(
serial: "REDACTED"
limit: 100
consumer: "app"
activityTypes: ["litterHopperDispensed"]
) {
serial
commandSource
consumer
stateString
valueString
originalHex
actionValue
value
timestamp
measure
}
} Response: {
"data": {
"getLitterRobot4Activity": [
{
"serial": "REDACTED",
"commandSource": null,
"consumer": "app",
"stateString": null,
"valueString": null,
"originalHex": null,
"actionValue": "85",
"value": "litterHopperDispensed",
"timestamp": "2025-04-21 00:23:26.000",
"measure": "action"
},
{
"serial": "REDACTED",
"commandSource": null,
"consumer": "app",
"stateString": null,
"valueString": null,
"originalHex": null,
"actionValue": "83",
"value": "litterHopperDispensed",
"timestamp": "2025-04-21 07:14:15.000",
"measure": "action"
},
{
"serial": "REDACTED",
"commandSource": null,
"consumer": "app",
"stateString": null,
"valueString": null,
"originalHex": null,
"actionValue": "84",
"value": "litterHopperDispensed",
"timestamp": "2025-04-22 10:11:14.000",
"measure": "action"
},
{
"serial": "REDACTED",
"commandSource": null,
"consumer": "app",
"stateString": null,
"valueString": null,
"originalHex": null,
"actionValue": "82",
"value": "litterHopperDispensed",
"timestamp": "2025-04-23 02:43:52.000",
"measure": "action"
},
{
"serial": "REDACTED",
"commandSource": null,
"consumer": "app",
"stateString": null,
"valueString": null,
"originalHex": null,
"actionValue": "87",
"value": "litterHopperDispensed",
"timestamp": "2025-04-24 04:20:58.000",
"measure": "action"
}
]
}
} Corresponding Whisker iOS App UI that appears to be powered by this: ![]() |
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.
The text was updated successfully, but these errors were encountered: