Skip to content

Support fetch, update of device record history (Litter Box)#233

Open
sovanyio wants to merge 30 commits into
Jezza34000:mainfrom
sovanyio:main
Open

Support fetch, update of device record history (Litter Box)#233
sovanyio wants to merge 30 commits into
Jezza34000:mainfrom
sovanyio:main

Conversation

@sovanyio

@sovanyio sovanyio commented May 10, 2026

Copy link
Copy Markdown

📝 Proposed Change / Description

This PR adds support for fetching a litter box's device record state as well as update it if incorrect.

I have 2 cats with pretty similar weights so Petkit often fails to properly identify them and I would otherwise like to have their (correct) weight trends and usage easily seen in HA, without the manual daily checking and correction that I often don't do through the stock app. Basically a DIY implementation of the "ai" camera on the more expensive models, when combined with Frigate and custom model classification.

I have this working locally along with a Frigate automation that checks for agreement of the Petkit record guess with Frigate's classification and updates Petkit's api (including weight, similar to the app). Otherwise useful for folks that find the log useful as a general monitoring tool and want to have the info centralized and not have to touch that bloated app trying to sell you more stuff.

I wasn't sure what the weight threshold was for the app to trigger the weight update/confirmation prompt so this currently has a hardcoded check to see if it is off by 500g and then updates if so. Definitely room to improve this or implement differently, if anyone happens to know what the app is doing from the decompilation--this was based on my sniffing of the app traffic. Perhaps having a HA-notification asking for update confirmation before doing it when there is weight disagreement, but I'm not really sure when an incorrect record update would be wanted without updating the weight associated.

The frigate detection and correction automation uses two helpers:

  • Input text: input_text.last_cat_seen_on_cat_camera
  • Input date/time: input_datetime.time_cat_seen_on_cat_camera
frigate detection automation
alias: "PetKit: Auto-Correct Litter Box Records via Frigate"
triggers:
  - entity_id: sensor.petkit_puramax_2_device_records
    trigger: state
    attribute: records
    not_from:
      - unknown
      - unavailable
conditions:
  - condition: template # run on new usage record
    value_template: >
      {% set records = trigger.to_state.attributes.records | default([]) %}
      {{ records | length > 0 and records[0].eventType == 10 }}
actions:
  - variables:
      latest_record: "{{ trigger.to_state.attributes.records[0] }}"
      frigate_cat_id: "{{ states('input_text.last_cat_seen_on_cat_camera') }}"
      frigate_time: "{{ as_timestamp(states('input_datetime.time_cat_seen_on_cat_camera')) }}"
      petkit_cat_id: "{{ latest_record.petId | string }}"
      names: # grab your mapping via debug logging
        "123456789": "Pet 1"
        "987654321": "Pet 2"
  - condition: template # this may be unnecessary due to the state filtering, but was having some empty notifications in earlier testing
    value_template: "{{ frigate_cat_id in names.keys() }}"
  - condition: template
    value_template: "{{ petkit_cat_id != frigate_cat_id }}"
  - condition: template
    # frigate detection is within 5 minutes of usage record
    value_template: >
      {{ (latest_record.content.timeOut - frigate_time) | abs < 300 }}
  - action: petkit.update_litter_box_usage_record
    data:
      device_id: "{{ latest_record.deviceId }}"
      old_pet_id: "{{ latest_record.petId }}"
      new_pet_id: "{{ frigate_cat_id }}"
      timestamp: "{{ latest_record.content.timeOut }}"
  - action: notify.notify
    data:
      title: "PetKit Correction 🐈" # debug notification
      message: >
        {% set cat_name = names.get(frigate_cat_id, 'Unknown Cat') %}
        PetKit identified the cat as {{ latest_record.petName | default('Unknown') }}, but Frigate confirmed it was {{ cat_name }}.
        The record has been updated to {{ cat_name }}.
      data:
        tag: litter-log
        channel: Litterbox Log
        notification_icon: mdi:toilet
mode: single

🔖 Type of change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 🔧 Refactor / code cleanup (no functional change)
  • 📚 Documentation update
  • 🔒 Security fix
  • ⬆️ Dependency update

🐾 Affected devices

  • 🌐 All devices

Specific type:

  • Litter boxes : T4, I'm assuming all litter boxes report usage and action logging
  • Feeders : ()
  • Fountains : ()
  • Purifiers : ()

Specific category :

  • Cameras devices : I don't have a camera device, but I assume the camera boxes provide identical logging, though camera boxes probably don't have this problem; in-HA log still useful here
  • Non camera devices : T4, +?
  • BLE only devices : ()

If your device is not listed, please specify it here:


🧪 How has this been tested?

  • Tested locally with a real PetKit device
  • Tested with Home Assistant version: 2026.5.x
  • No testing required (docs, CI config, etc.)

Device(s) tested on:

PetKit Pura Max 2

Other devices you own when the test was done:


✅ Checklist

  • Code follows project style : pre-commit run -a hooks and all checks pass
  • No new linting/type errors introduced
  • Documentation updated (if needed)
  • Branch is up-to-date with main

📸 Screenshots / Logs (optional)

Today's weights aren't that close, but they usually are!

image
log table
type: custom:flex-table-card
entities:
  - entity: sensor.petkit_puramax_2_device_records
css:
  thead: "display: none;"
  tbody tr:hover: "background-color: var(--secondary-background-color) !important;"
  tbody tr:first-of-type td:first-of-type: "border-radius: .5rem 0 0 0; overflow: hidden;"
  tbody tr:first-of-type td:last-of-type: "border-radius: 0 .5rem 0 0; overflow: hidden;"
  tbody tr:last-of-type td:first-of-type: "border-radius: 0 0 0 .5rem; overflow: hidden;"
  tbody tr:last-of-type td:last-of-type: "border-radius: 0 0 .5rem 0; overflow: hidden;"
  td:nth-child(1): "vertical-align: top; padding-top: 1rem; width: 5rem;"
  td:nth-child(2): "vertical-align: top; padding-top: 1rem; padding-bottom: 1rem;"
  td:nth-child(3): "vertical-align: top; padding-top: .75rem; width: 2.5rem;"
columns:
  - name: Time
    data: records
    modify: >
      (new Date(x.timestamp * 1000)).toLocaleTimeString('en-US', {hour:
      'numeric', minute:'2-digit', hour12: true}).toLowerCase()
    align: right
  - name: Activity
    data: records
    modify: |
      let text = '';

      // 1. Process the main event
      if (x.eventType == 10) {
        let dur = x.content.timeOut - x.content.timeIn;
        let lbs = ((x.content.petWeight / 1000) * 2.20462).toFixed(1);
        if (x.petId == -1 || !x.petName) {
          text = 'Occupied by your pet for ' + dur + 's, weighing record ' + lbs + 'lbs.<br><span style="color: #999; font-size: 0.9em;">Unknown pet, please match your pet &gt;</span>';
        } else {
          text = '<span style="color: #00bcd4; font-weight: bold;">' + x.petName + '</span> just spent ' + dur + 's in the litter box, weighing record ' + lbs + 'lbs.';
        }
      } else if (x.eventType == 8) {
        text = 'Periodic deodorizing done';
      } else if (x.eventType == 7) {
        text = 'Maintenance mode completed';
      } else if (x.eventType == 5) {
        if (x.content && x.content.result == 2) {
          let errStr = x.content.err == 'hallT' ? "the litter box's upper cover is not placed properly" : (x.content.err || "unknown error");
          text = 'Automatic cleaning failure, ' + errStr + ', please check.';
        } else {
          text = 'Automatic cleaning done';
        }
      } else {
        text = x.enumEventType || 'Unknown event';
      }

      // 2. Process nested sub-events (Auto cleaning / Deodorizing)
      if (x.subContent && x.subContent.length > 0) {
        let subHtml = '<ul style="list-style-type: none; padding-left: 15px; margin: 10px 0 0 10px; border-left: 1px dashed #ccc;">';
        x.subContent.forEach(sub => {
          let t = new Date(sub.timestamp * 1000);
          let timeStr = t.toLocaleTimeString('en-US', {hour: 'numeric', minute:'2-digit', hour12: true}).toLowerCase()
          let subText = '';
          
          if (sub.eventType == 5) {
            if (sub.content && sub.content.result == 2) {
              let errStr = sub.content.err == 'hallT' ? "the litter box's upper cover is not placed properly" : (sub.content.err || "unknown error");
              subText = 'Automatic cleaning failure, ' + errStr + ', please check.';
            } else {
              subText = 'Auto cleaning done';
            }
          } else if (sub.eventType == 8) {
            if (sub.content && sub.content.result == 6) {
              subText = 'Auto deodorization failure. Check the battery level of the AIR PURIFIER.';
            } else {
              subText = 'Auto deodorizing complete.';
            }
          } else {
            subText = sub.enumEventType;
          }
          
          // Render dashed timeline line and circle node
          subHtml += '<li style="position: relative; color: #999; font-size: 0.9em; margin-bottom: 8px;">';
          subHtml += '<span style="position: absolute; left: -19px; top: 6px; width: 6px; height: 6px; border: 1px solid #ccc; border-radius: 50%; background: var(--card-background-color, white);"></span>';
          subHtml += timeStr + ' - ' + subText;
          subHtml += '</li>';
        });
        subHtml += '</ul>';
        text += subHtml;
      }
      text;
  - name: ""
    data: records
    modify: |
      if (x.avatar) {
        '<img src="' + x.avatar + '" style="width: 1.75rem; height: 1.75rem; border-radius: 10%; object-fit: cover;">'
      } else if (x.eventType == 10) {
        '<ha-icon icon="mdi:cat" style="color: #ccc;"></ha-icon>'
      } else if (x.eventType == 8) {
        '<ha-icon icon="mdi:leaf" style="color: #64b5f6;"></ha-icon>'
      } else if (x.eventType == 7) {
        '<ha-icon icon="mdi:tools" style="color: #4db6ac;"></ha-icon>'
      } else {
        '<ha-icon icon="mdi:information-outline" style="color: #4db6ac;"></ha-icon>'
      }
    align: center
strict: true

ℹ️ Additional context


⚠️ Breaking Changes

  • No breaking changes.
  • Warning: This PR introduces breaking changes.

Description of changes:


Thanks for the helping paw! 🐾

@sovanyio sovanyio requested a review from Jezza34000 as a code owner May 10, 2026 16:38
Comment thread custom_components/petkit/translations/pl.json
Comment thread custom_components/petkit/sensor.py Outdated
Comment thread custom_components/petkit/__init__.py Outdated
Comment thread custom_components/petkit/__init__.py Outdated
Comment thread custom_components/petkit/__init__.py Outdated
Comment thread custom_components/petkit/__init__.py Outdated
Comment thread custom_components/petkit/sensor.py Outdated
Comment thread custom_components/petkit/services.yaml Outdated
mode: box
old_pet_id:
name: Old Pet ID
description: The original Pet ID that the device recorded (use "-1" or "0" if it was unknown/unrecognized).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is also missing the "-2" : I've forgotten what this "-2" means...
I'm also worried that the user won't understand what to put here, it's not very clear, and I admit I don't really know how to make it clearer.

@Jezza34000

Copy link
Copy Markdown
Owner

Hey @sovanyio, great idea thanks for your contribution

I took the time to do a full code review and left some comments with a few things worth fixing before merging. Nothing major, mostly code quality and one small bug in the Polish translation.

Feel free to reach out if you want to discuss any of the points above
happy to help!

@sovanyio

sovanyio commented May 14, 2026

Copy link
Copy Markdown
Author

Pushed most of those changes, working on the blueprint but not going to finish that one today.

Much cleaner: Jezza34000/py-petkit-api#75 :)

@sovanyio

Copy link
Copy Markdown
Author

@Jezza34000 blueprint added, tested the correction half, will update tomorrow for verification on the mqtt trigger for it--my cats aren't playing along tonight

@Jezza34000

Copy link
Copy Markdown
Owner

Hi, thanks for submitting the changes. I'm away from home for a few days, I'll look at it early next week.

@sovanyio

sovanyio commented May 17, 2026

Copy link
Copy Markdown
Author

FYI I think my combined blueprint might need some more work on the frigate mqtt trigger--will let you know when that's ironed.

@sovanyio

Copy link
Copy Markdown
Author

Blueprint is ready to go

@Jezza34000

Copy link
Copy Markdown
Owner

Hi @sovanyio
I'm a bit busy at the moment
I take a look at this, as soon as I have free time.
There is still some check in the CI not passing can you fix it ?
Thank you

@sonarqubecloud

Copy link
Copy Markdown

@sovanyio sovanyio requested a review from Jezza34000 May 30, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants