Smart Litter Box (or Smart Cats)

OMG I thought this forum was perhaps one place that disproved the theory that the internet was designed solely for the purpose of disseminating cute cat pictures.

Clearly I am deluded and should just accept the inevitable: cat world domination.

5 Likes

Sorry :grinning:

3 Likes

That’s genius! Will implement myself right away.

1 Like

I just wrote an AppDaemon App for my version, which is triggered by alexa for querying and resetting - PIR is on order :slight_smile: Its a little more complex because I was picky about exactly what I wanted Alexa to say. Also loving the spreadsheet idea - I might add that next!

import appdaemon.appapi as appapi
import shelve
import datetime
import globals
import calendar

#
# App to track Jack's visits to his litter tray
#
# Args:
#
# motion - motion sensor to use
# reset_daily - whether to reset the count daily or not
# file - file to use for DB
# log - log all visits
# notify - notify all visits
# limit - number of visits before alert
# timeout - mimimum time in seconds between visits
# 
#
# Release Notes
#
# Version 1.0:
#   Initial Version

class JackLitter(appapi.AppDaemon):
    def initialize(self):
        self.litter_db = shelve.open(self.args["file"])

        if self.litter_db is None:
            self.litter_db = {}

        if "visits" not in self.litter_db:
            self.litter_db["visits"] = 0

        if "last_reset" not in self.litter_db:
            yesterday = self.datetime() - datetime.timedelta(days=2)
            self.litter_db["last_reset"] = yesterday

        if "last_visit" not in self.litter_db:
            yesterday = self.datetime() - datetime.timedelta(days=2)
            self.litter_db["last_visit"] = yesterday
        self.log("Restarting. Visits = {}, Last Visit = {}, Last Reset = {}".format(self.litter_db["visits"], self.litter_db["last_visit"], self.litter_db["last_reset"]))

        self.listen_state(self.motion, self.args["motion"])
        self.run_daily(self.midnight, datetime.time(0, 0, 0))

    def since(self):
        if "reset_daily" in self.args and self.args["reset_daily"] == 1:
            since = "today"
        else:
            lr = self.litter_db["last_reset"]
            if lr.date() == self.date():
                since = "since {} today".format(lr.time().strftime("%H:%M"))
            elif lr.date() == self.date() - datetime.timedelta(days=1):
                since = "since yesterday at {}".format(lr.time().strftime("%H:%M"))
            elif self.date() - lr.date() >= datetime.timedelta(days=7):
                self.log(self.date() - lr.date())
                since = "since {} at {}".format(lr.date(), lr.time().strftime("%H:%M"))
            else:
                since = "since {} at {}".format(calendar.day_name[lr.weekday()], lr.time().strftime("%H:%M"))

        return (since)

    def motion(self, entity, attribute, old, new, kwargs):
        if new == "on":
            elapsed = self.datetime() - self.litter_db["last_visit"]
            if elapsed.total_seconds() > self.args["timeout"]:
                self.litter_db["visits"] += 1
                self.litter_db["last_visit"] = self.datetime()
                ordinal = lambda n: "%d%s" % (n, "tsnrhtdd"[(n / 10 % 10 != 1) * (n % 10 < 4) * n % 10::4])
                self.log_notify(
                    "Jack is visiting his litter tray for the {} time {}".format(ordinal(self.litter_db["visits"]), self.since()))
                if self.litter_db["visits"] >= self.args["limit"]:
                    self.log_notify("Time to clean his tray out!")

    def midnight(self, kwargs):
        if "reset_daily" in self.args and self.args["reset_daily"] == 1:
            self.reset()

    def reset(self):
        self.litter_db["visits"] = 0
        self.litter_db["last_reset"] = self.datetime()
        n = "Jack's litter tray tracking has been reset"
        self.log_notify(n)
        return n

    def query(self):
        if self.litter_db["visits"] == 1:
            times = "once"
        elif self.litter_db["visits"] == 2:
            times = "twice"
        else:
            times = "{} times".format(self.litter_db["visits"])
        n = "Jack has visited his litter tray {} {}".format(times, self.since())
        self.log_notify(n)
        return n

    def log_notify(self, message, level="INFO"):
        if "log" in self.args:
            self.log(message)
        if "notify" in self.args:
            self.notify(message, service=globals.notify, name=globals.notify)

1 Like

This is great! ehehe always have been thinking in a way of makin my pets life ‘smart’ :stuck_out_tongue:
Gonna definitely gonna give it a go since we got 4 litter box.

this is Nina and Charlie on the behind, two of our 5 cats

and this is Charlie

3 Likes

As a gratitude for this great idea, here’s a pic of my cat:

5 Likes

All the cute kitties :heart_eyes:

See what you started??

2 Likes

The inevitable overtaking of the community by cats?

3 Likes

Looks like it, yes :slight_smile:

Anacleta (Eclectus female), Dudu (Sun Conure male), Yoshi (Indian RingNeck male)

and our newest member Scarlet the Scotish fold female

5 Likes

Actually Scarlet isn’t our last family member!!
Almost forgot I bought yesterday a Venus flytrap (Dionaea muscipula)

Already busy making her soil moisture sensor, temperature, and lux :smiley:

1 Like

This is a great little project! I have two cats and decided to give this a try. I’m using a spare esp8266 and pir sensor with MQTT. I figured out a way to update the dummy sensor directly from configuration.yaml using mqtt.publish service rather than using external python scripts. It increments the count and posts a card to the Web page each time the sensor trips. I just run the reset automation to zero out the count for now. I plan to add a reset button to the esp at some point.

Posting in case anyone is interested.

  - alias: Count visits to litter box
    trigger:
      - platform: state
        entity_id: sensor.pir
        to: '1'
    condition: 
    - condition: template
      value_template: >
        {% if states.automation.count_visits_to_litter_box.last_triggered is not none %}
          {% if as_timestamp(now()) | int   -  as_timestamp(states.automation.count_visits_to_litter_box.attributes.last_triggered) | int > 300 %} true {% else %} false
          {% endif %}
        {% else %}
        false
        {% endif %}
    action:
      - service: persistent_notification.create
        data:
          title: "Litter Box"
          message: ' {{ now().strftime("%I:%M %p") }} Litter Box Visit! '
      - service: mqtt.publish
        data:
          topic: "dummy/litterbox/count"
          payload_template: '{{ states.sensor.litter_box_visits.state |int +1  }}'
  - alias: Set Litter Box Sensor To Zero On Start
    trigger:
      platform: homeassistant
      event: start
    action:
      - service: mqtt.publish
        data:
          topic: "dummy/litterbox/count"
          payload: 0
6 Likes

My situation was a little bit different. We have one litter tray in our house, and want to check/empty it after every visit to minimise odour.

The tray is conveniently in the same room as my Raspberry Pi, though. So I was able to build my own sensor, with a PIR attached to an Arduino, plug it in to the Pi, and check its state with a serial sensor.

Every second, the Arduino sends a number. 0 means the tray hasn’t been visited. Greater than 0 is the time in seconds since last visit. The sensor also has a button attached - after cleaning the tray you hit the button, and the sensor is reset back to an unvisited state. This makes it a lot easier to track visits.

I have three sensors: One is just the raw readings from the serial device. One parses that reading from seconds to a friendly string. And one uses that string and a custom icon for use in the frontend.

sensor:
  - platform: serial
    name: raw poopmon
    serial_port: /dev/ttyS3
  - platform: template
    sensors:
      poopmon_time:
        friendly_name: "Time when litter tray was last used"
        value_template: >-
          {% set time = states.sensor.raw_poopmon.state | int %}
          {% set minutes = ((time % 3600) / 60) | int %}
          {% set hours = ((time % 86400) / 3600) | int %}
          {%- if time < 60 -%}
            less than a minute
          {%- else -%}
            {%- if hours > 0 -%}
              {% if hours == 1 -%}
                1 hour
              {%- else -%}
                {{ hours }} hours
              {%- endif -%}
            {%- endif -%}
            {%- if minutes > 0 -%}
              {%- if hours > 0 -%}
                {{ ', ' }}
              {%- endif -%}
              {%- if minutes == 1 -%}
                1 minute
              {%- else -%}
                {{ minutes }} minutes
              {%- endif -%}
            {%- endif -%}
          {%- endif -%}
      litter_tray:
        friendly_name: "Eli's litter tray"
        value_template: >-
          {% if states.sensor.raw_poopmon.state|int == 0 %}
            Clean
          {% else %}
            Last used {{ states.sensor.poopmon_time.state }} ago
          {% endif %}
        icon_template: >-
          {% if states.sensor.raw_poopmon.state|int == 0 %}
            mdi:cat
          {% else %}
            mdi:emoticon-poop
          {% endif %}

Automations are done by just looking at the raw readings:

automation:
  - id: 'cat poop notification'
    alias: cat poop notification
    trigger:
      platform: numeric_state
      entity_id: sensor.raw_poopmon
      above: 0
    action:
      service: notify.html5_notifier
      data_template:
        title: "\uD83D\uDCA9 alert"
        message: Litter box has been visited

  - id: 'clean tray notification'
    alias: clean tray notification
    trigger:
      platform: numeric_state
      entity_id: sensor.raw_poopmon
      below: 1
      for:
        minutes: 3
    action:
      service: notify.html5_notifier
      data_template:
        title: "\uD83D\uDE3B alert"
        message: Litter box has been cleaned

I haven’t checked yet, but it should be fairly easy to count visits by just looking for raw_poopmon changing state from 0 to >0.

4 Likes

This needs https://home-assistant.io/components/xiaomi_aqara/ no?

Do you also need the “Xiaomi gateway” for this to work? Just asking because you didn’t mention it specifically, a bit confused.

You need the gateway if you don’t have any other way of getting the sensors in hass. For example, Homey and Smartthings can both take the Xiaomi sensors without the gateway, and I think you can also use the Conbee usb stick.

1 Like

Thats Thinking out side the Litter box LOL
Like the Logic :slight_smile: :beer:

1 Like

I need this :smiley_cat:
Since we have the litterbox where people passes I’m wondering if a internal pir would do the trick? Just have to figure out how to get the electricity close enough…
Where did you all put your motion detectors? :slight_smile:

The litter boxes are kind of hidden away, so that’s not a problem with people passing here.

Ok, thanks for the inspiration. Got it to work beautifully and the daughter gets a hangout message after a few visits. I get a hangout message when someone claims it to be cleaned :smiley:

1 Like