https://www.sensibledefaults.io/blog/reverse-engineering-cat-feeder/index SensibleDefaults BlogResourcesTagsAbout Blog Resources Tags About Published on October 8, 2022 Reverse Engineering a Cat Feeder to Boost Productivity Authors + []avataravatar Name John Partee Twitter @_JohnPartee overview I've been laughing about it this weekend and had to write it up - After some late-night Amazon'ing I got a PetKit FreshElement Solo. I had two problems it could solve: low side project motivation, and loving Target's Dark Chocolate Sea Salt Almonds way too much. I'm a codemonkey - Why not feed my monkey brain when I push code? I couldn't find a USB candy dispenser (I even checked thinkgeek, nothing close!), so I figured I'd try a cat feeder. This thing rocks, and I'll show you how to replicate what I did. Teaser Code: https://github.com/morganpartee/pyPetKit/blob/master/run.py To run yourself, you'll have to set your username and password here: https://github.com/morganpartee/pyPetKit/blob/master/settings.py An Overview REST API's are basically the language of the internet. Most products use them to talk from devices to servers. I saw some old Python code that had the authentication flow worked out already, so I had somewhere to start! I snooped for the API request that did a manual feeding, replicated that in Python with Requests, and tied that script to a button on my streamdeck, which also sends a ctl+enter to send the commit! There are other (better) ways to do this - AWS, webhooks, git hooks (git hooks probably being the best bet), but I wanted this to work locally only, so I'm not dispensing candy while I'm out of town. No need to overcomplicate! Worst case I forget to use the button and eat less candy. The Hardware I really can't overstate - This thing is perfect for dispensing snacks. The food is portioned by this neat little silicone auger system, with silicone fingers that limit the amount that can pass into the dispenser. It's clever, and gentle on the food. And food safe! Auger Top View Auger Side View And this was just cute: Cute They include a sizing chart for food that says it should be under 12mm: Sizing But it filters and portions food like: via GIPHY So if it is 12mm tall sideways, it'll work fine. I plan to test jerky chunks and cheez balls in the future. It includes a desiccant packet in the lid to keep stuff fresh... So I expect it's pretty safe to get wild. As of writing they're about 70$, but they work great and are built well: Buy Here App The app is pretty good. It's a little weird if you don't have a pet... I had to add myself as a dog and pick my "breed": Breed Picker But after that I was able to use the API. After testing the serving size with the app a few times! 1/10th of a cup is one half of the rotor - Which averages out to about five almonds. Snooping I used Packet Capture for Android while I ran a manual feeding: Packet And found this request: Packet There's no real silver bullet here, I had to look through a few groups of requests to find the right one. But we can see it hits the /latest/d4/saveDailyFeed endpoint, which gives us a big hint! The URLENCODED section tells us that amount, time, and deviceId are included in the URL... So we can replicate it locally! Andy and I have covered how to do this on desktop Here! Coding Code is available here! I started with geeks4hire's fork of PyPetKit. It had the auth flow worked out for me, I just had to add a way to hit the API. I knew the endpoint I had to hit from the packet above, so I added a generic function to send requests to the API to my fork of the package: def send_api_request(self, path, method="POST", params=None, json=None): """ Sends an API request. """ custom_headers = { "X-Session": self._access_token, "User-Agent": "PETKIT/7.26.1 (iPhone; iOS 14.7.1; Scale/3.00)", "X-Timezone": f"{round(self._tzone._utcoffset.seconds/60/60)}.0", "X-Api-Version": "7.26.1", "X-Img-Version": "1", "X-TimezoneId": self._tzone.zone, "X-Client": "ios(14.7.1;iPhone13,4)", "X-Locale": self._locale.replace("-", "_"), } return requests.request( method, self._apiServerBaseURL + path, headers=custom_headers, params=params, json=json, ).json() This looks complicated, but the headers were copied from someone doing exactly what I did from an iphone. This uses requests (requests.request) to make an HTTP request, with whatever method. This is exactly what a browser does, but with Python! We cover this in detail in the scraping talk mentioned above - If you can copy the headers, you can do a lot! Then getting those sweet sweet almonds is as easy as: from pypetkit import PetKitAPI from settings import ( API_USERNAME, API_PASSWORD, API_COUNTRY_CODE, API_LOCALE_CODE, API_TIMEZONE, ) from pprint import pprint petkit_api = PetKitAPI( API_USERNAME, API_PASSWORD, API_COUNTRY_CODE, API_LOCALE_CODE, API_TIMEZONE ) #! Sign in petkit_api.request_token() print(f"Authorized: {petkit_api.is_authorized}") #! Send the actual feeding request pprint( petkit_api.send_api_request( "d4/saveDailyFeed", params={"deviceId": 10019856, "amount": 10, "time": -1} ) ) We leverage the existing code's authentication flow, then send our request. The parameters match the request we found when we did a manual feeding from the app! I did try a smaller serving size, but 10 seems to be the smallest amount it can dispense. The -1 seems to make it feed immediately. You can schedule feedings in the app, I'd bet you can do that with dates too. If it works, you'll see an output like this from run.py: It works! And a little beep, followed by the beautiful sound of a reward hitting your (don't think about it too much, it's only slightly degrading) bowl: via GIPHY "Deploying" Deploying is a strong word, but that's what I'd call it. I'm running this locally so I don't dispense food when I'm away from home. I have an Elgato Stream Deck that I love, that I used to make the hotkey. Hotkey The System:Hotkey sends Ctl+Enter to send the commit when I'm done writing the message. The System:Open key opens python c:// run.py, which feeds me sweet chocolatey reinforcement for my good coding habits! You could 100% do this "better", but there's no need for cloud stuff or whatever for a goofy little project. It's just part of the fun! Conclusion Huge thanks to everyone that had reverse-engineered this gear before me. All that was left was finding the endpoint and making the request, which was too easy! Who knows if it'll be a real coding reinforcement, but I hadn't seen someone do it before, and I'm glad I gave it a shot. Troubleshooting CI problems has gotten a lot sweeter. Security Note You'll see that the data for these products is sent in plaintext to and from their servers... No HTTPS... That's just sketchy. They send your complete location too, which is further sketchy. I'd avoid using the app as much as you can, and use a one time password for when it gets breached... Because it really might happen at some point. Resources PETKIT Fresh Element Solo Github Repo Packet Capture for Android Elgato Stream Deck Target's Dark Chocolate Sea Salt Almonds (Seriously, they rock) Follow Me On Twitter Discuss on Twitter * View on GitHub Tags PythonREST-API Previous Article Using CircleCI with AWS SAM - Back to the blog mailgithubtwitter John Partee * (c) 2022 * SensibleDefaults.io Tailwind Nextjs Theme