natekspencer / pylitterbot

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

Litter-Robot 4 Additional Features #22

Closed jfarley248 closed 2 years ago

jfarley248 commented 2 years ago

Hello again!

Making this issue as I just did a very quick poke of my LR4 through burp suite + iOS device and your library (but not with a good enough understanding of the codebase and the previous LR3 to make a pull request that would be very good) So feel free to immediately close this issue as you see fit since it's not really an issue. (Also most of the questions posed below are rhetorical, more for your reference if you don't already know the answer) I hope this can be of some help!

Retrieve cat weight

Implementing something like the following can get the cats last weight:

    @property
    def last_pet_weight(self) -> float:
        """Return the last recorded pet weight in pounds."""
        return self._data.get("catWeight", 0)

IMG_5380

I don't actually pick up my new cat until a couple weeks, so this is a plant pot. The LR4 scale seems decently accurate, as when placed on a food scale it also measured at 3.8 pounds. On a very brief look through the app settings it does not seem like there's options to change what unit of weight is being used, perhaps its regional, or it's just always pounds?

Anyway, here's the raw requests that you already have implemented, but just an example of it and the response for completeness:

Request

POST /graphql HTTP/2
Host: lr4.iothings.site
Accept: */*
Content-Type: application/json
Authorization: Bearer REDACTED_BEARERTOKEN
Content-Length: 1863
X-Amz-Date: 20220806T171512Z
Accept-Language: en-US,en;q=0.9
User-Agent: amplify-flutter/0.5.1 amplify-iOS/1.23.0 iOS/15.4 en_US
Accept-Encoding: gzip, deflate

{"query":"    query getLitterRobot4BySerial($serial: String!) {\n      getLitterRobot4BySerial(serial: $serial) {\n        name\n        serial\n        unitId\n        unitPowerType\n        unitPowerStatus\n        robotStatus\n        unitTimezone\n        unitPowerStatus\n        odometerCleanCycles\n        DFINumberOfCycles\n        isOnboarded\n        isProvisioned\n        sessionId\n        lastSeen\n        setupDateTime\n        weightSensor\n        cleanCycleWaitTime\n        isKeypadLockout\n        nightLightBrightness\n        nightLightMode\n        litterLevel\n        DFILevelPercent\n        globeMotorFaultStatus\n        catWeight\n        isBonnetRemoved\n        isDFIFull\n        isOnline\n        espFirmware\n        picFirmwareVersion\n        laserBoardFirmwareVersion\n        isFirmwareUpdateTriggered\n        sleepStatus\n        catDetect\n        robotCycleState\n        robotCycleStatus\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  ","variables":{"serial":"REDACTED_SERIAL"}}

Response

HTTP/2 200 OK
Content-Type: application/json;charset=UTF-8
Vary: Accept-Encoding
Date: Sat, 06 Aug 2022 17:25:39 GMT
X-Amzn-Requestid: X
X-Amzn-Appsync-Tokensconsumed: 1
X-Amzn-Trace-Id: Root=X
X-Cache: Miss from cloudfront
Via: 1.1 844f1d5f6c5723bfa87f9a3a73f6fd58.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: BOS50-C1
X-Amz-Cf-Id: X

{
    "data": {
        "getLitterRobot4BySerial": {
            "name": "Litter Robot 4",
            "serial": "REDACTED_SERIAL",
            "unitId": "REDACTED_UNITID",
            "unitPowerType": "AC",
            "unitPowerStatus": "ON",
            "robotStatus": "ROBOT_CAT_DETECT",
            "unitTimezone": "America/New_York",
            "odometerCleanCycles": 3,
            "DFINumberOfCycles": 3,
            "isOnboarded": true,
            "isProvisioned": true,
            "sessionId": "REDACTED_SESSIONID",
            "lastSeen": "2022-08-06T16:29:59.084Z",
            "setupDateTime": "2022-08-06T16:26:45.172Z",
            "weightSensor": 0.9,
            "cleanCycleWaitTime": 7,
            "isKeypadLockout": false,
            "nightLightBrightness": 255,
            "nightLightMode": "AUTO",
            "litterLevel": 500,
            "DFILevelPercent": 0,
            "globeMotorFaultStatus": "FAULT_CLEAR",
            "catWeight": 3.8,
            "isBonnetRemoved": false,
            "isDFIFull": false,
            "isOnline": true,
            "espFirmware": "1.1.50",
            "picFirmwareVersion": "10512.2560.2.51",
            "laserBoardFirmwareVersion": "255.0.255.255",
            "isFirmwareUpdateTriggered": false,
            "sleepStatus": "WAKE",
            "catDetect": "CAT_DETECT_SCALE_CLEAR",
            "robotCycleState": "CYCLE_STATE_WAIT_ON",
            "robotCycleStatus": "CYCLE_IDLE",
            "weekdaySleepModeEnabled": {
                "Sunday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Monday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Tuesday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Wednesday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Thursday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Friday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                },
                "Saturday": {
                    "sleepTime": 0,
                    "wakeTime": 0,
                    "isEnabled": false
                }
            }
        }
    }
}

Retrieve Activity Listing

This one I was unsure since I am not familiar with how the LR3 responses format the activity listing. A very half "implementation" to get the activity data could start out to look something like this, which you may already be aware:

    async def _retrieve_activity(self) -> None:
        """Retrieves activity history listing from the API"""
        data = await self._post(
            json={
                "query": f"""
                    query GetLR4($serial: String!) {{
                        getLitterRobot4Activity(serial: $serial) {{
                        value
                        timestamp
                        measure
                        actionValue
                        }}
                    }}
                    """,
                "variables": {"serial": self.serial},
            },
        )
        assert isinstance(data, dict)

Like before here are the raw requests and responses:

Request

POST /graphql HTTP/2
Host: lr4.iothings.site
Accept: */*
Content-Type: application/json
Authorization: Bearer REDACTED_BEARERTOKEN
Content-Length: 283
X-Amz-Date: 20220806T172309Z
Accept-Language: en-US,en;q=0.9
User-Agent: amplify-flutter/0.5.1 amplify-iOS/1.23.0 iOS/15.4 en_US
Accept-Encoding: gzip, deflate

{"query":"    query GetLR4($serial: String!, $consumer: String) {\n      getLitterRobot4Activity(serial: $serial, consumer: $consumer) {\n        value\n        timestamp\n        measure\n        actionValue\n      }\n    }\n  ","variables":{"serial":"REDACTED_SERIAL","consumer":"app"}}

Response

HTTP/2 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 708
Date: Sat, 06 Aug 2022 18:20:46 GMT
X-Amzn-Requestid: X
X-Amzn-Appsync-Tokensconsumed: 1
X-Cache: Miss from cloudfront
Via: 1.1 980d2a1c9c4f90ad69118c6357f92882.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: X
X-Amz-Cf-Id: X

{
    "data": {
        "getLitterRobot4Activity": [
            {
                "value": "robotCycleStatusIdle",
                "timestamp": "2022-08-06 17:31:59.000000000",
                "measure": "action",
                "actionValue": ""
            },
            {
                "value": "robotCycleStatusDump",
                "timestamp": "2022-08-06 17:30:06.000000000",
                "measure": "action",
                "actionValue": ""
            },
            {
                "value": "catWeight",
                "timestamp": "2022-08-06 17:23:08.000000000",
                "measure": "action",
                "actionValue": "3.8"
            },
            {
                "value": "robotStatusCatDetect",
                "timestamp": "2022-08-06 17:21:51.000000000",
                "measure": "action",
                "actionValue": ""
            },
            {
                "value": "robotCycleStatusIdle",
                "timestamp": "2022-08-06 16:30:27.000000000",
                "measure": "action",
                "actionValue": ""
            },
            {
                "value": "robotCycleStatusDump",
                "timestamp": "2022-08-06 16:28:34.000000000",
                "measure": "action",
                "actionValue": ""
            }
        ]
    }
}

Interestingly, using the python function above to retrieve the activity listing returned much more data ( I did not investigate very much into the cause of this, probably something with the GQL query I copy pasted without fully understanding it :upside_down_face: ):

BIXM8mLmJ2

Litter Level

Didn't do basically anything into this section, and again unsure if the LR3 had this capability. But if not this may be a fun one to have included at some point, especially in the HA integration! But when it's completely empty it shows the litterLevel value as 500, and in the app it's at the lowest level. After placing the plant pot inside and then removing it, the litterLevel did appear to change to 497, however the app still reports the litter level at its lowest (granted it's a not meant to be an super accurate bar chart since it's animated slightly). Note that I don't have actual litter yet so apologies I can't accurately test this. The plant pot was an attempt to but the LR4 thought it was a cat which I guess is more accurate :smile:

IMG_5381

natekspencer commented 2 years ago

Thanks for the super detailed information from this. I've never used nor heard of burp suite before, so I may have to check that out. Instead, I had installed mitmproxy on a rpi3 and intercepted the requests that way, but it's always good to know other options.

Retrieve cat weight

Implementing something like the following can get the cats last weight:

    @property
    def last_pet_weight(self) -> float:
        """Return the last recorded pet weight in pounds."""
        return self._data.get("catWeight", 0)

I had already added the cat weight property locally, but not committed it yet (got sidetracked with work and other projects). But last_pet_weight is a better name for it anyway since it more closely matches the app. ^_^

Retrieve Activity Listing

The GraphQL query supporting this retrieval has a limit field that can be used to decrease the amount of responses that come back. There's a lot of information here to unpack and decide how to interpret as far as activity history. And since the HA integration doesn't use it right now, I opted to not implement it right away and figured I'd get back to it later.

Litter Level

This one I would like to get added, but I'm still trying to correlate the data returned and the representation of it in the app. Hopefully with more debug information in the future, we can grasp what it all means. For reference, my physical litter level in mine is currently right on the line in most places and a little below it in the front, the app shows a completely full bar with "optimal" and the data reports "litterLevel": 451.

natekspencer commented 2 years ago

These should all now be implemented with 2022.9.4. Thanks for this!