My two Canadian Sphynx cat use the Litter Robot toilet. It is one of the most useful things a cat owner can have as it saves me loads of time cleaning. Most of the time it functions perfectly, but on couple of occasions it got stuck half way through the cleaning cycle, leaving my cats worried - “WTF has happened to the toilet?” One can buy a Wifi module and connect the Litter Robot to the app and be able to get notifications, but it costs 100 USD while pretty much the same functionality can be achieved using the vibration sensor and some rule logic in OH.
The idea is that the sensor mounted on top of the container would detect motion once the cleaning cycle start and will begin sending the XYZ coordinates of its location, so we can identify the position of the container and detect the current cycle ( cleaning, filling and ready cycle)
mqtt-vibro.things
Thing topic sens_vibr02 "Vibration02" @ "LitterRobot" {
Channels:
Type number : batt "Battery" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.battery" ]
Type number : volt "Voltage" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.voltage" ]
Type number : link "Link" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.linkquality" ]
Type number : angle "Angle" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle" ]
Type number : angle_x "Angle_x" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle_x" ]
Type number : angle_y "Angle_y" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle_y" ]
Type number : angle_z "Angle_z" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle_z" ]
Type number : angle_x_absolute "Angle_x_abs" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle_x_absolute" ]
Type number : angle_y_absolute "Angle_y_abs" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.angle_y_absolute" ]
Type string : sensitivity "Sensitivity" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.sensitivity" ]
Type number : strength "Strength" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.strength" ]
Type string : action "Action" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="REGEX:(.*action.*)∩JSONPATH:$.action" ]
Type number : lastseen_epoch "LastSeen" [ stateTopic="zigbee2mqtt/sens_vibr02", transformationPattern="JSONPATH:$.last_seen" ]
}
mqtt-vibro.items
Number sens_vibr02_batt "Vibr02 Battery [%.1f %%]" <battery> (gPers_Change_Day, gZB_bat) {channel="mqtt:topic:mymosquitto:sens_vibr02:batt"}
Number sens_vibr02_volt "Vibr02 Volt [%d mV]" <energy> {channel="mqtt:topic:mymosquitto:sens_vibr02:volt"}
Number sens_vibr02_link "Vibr02 Link [%s]" <qualityofservice> (gPers_Change_Day) {channel="mqtt:topic:mymosquitto:sens_vibr02:link"}
Number sens_vibr02_angle "Vibr02 Angle [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle"}
Number sens_vibr02_angle_x "Vibr02 Angle X [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle_x"}
Number sens_vibr02_angle_y "Vibr02 Angle Y [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle_y"}
Number sens_vibr02_angle_z "Vibr02 Angle Z [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle_z"}
Number sens_vibr02_angle_x_abs "Vibr02 Absolute X [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle_x_absolute"}
Number sens_vibr02_angle_y_abs "Vibr02 Absolute Y [%d]" <incline> {channel="mqtt:topic:mymosquitto:sens_vibr02:angle_y_absolute"}
String sens_vibr02_sensitivity "Vibr02 Sensitivity [%s]" {channel="mqtt:topic:mymosquitto:sens_vibr02:sensitivity"}
Number sens_vibr02_strength "Vibr02 Strength [%d]" <heating> (gPers_Change_Day) {channel="mqtt:topic:mymosquitto:sens_vibr02:strength"}
String sens_vibr02_action "Vibr02 Action [%s]" <movecontrol> (gPers_Change_Day) {channel="mqtt:topic:mymosquitto:sens_vibr02:action"}
Number sens_vibr02_lastseen_epoch "Vibr02 [%s]" (gZB_lastseen) {channel="mqtt:topic:mymosquitto:sens_vibr02:lastseen_epoch"}
DateTime sens_vibr02_lastseen_datetime "Last Seen: [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]"
Some specific items holding informatiob on the current status, time of last update, number of clean cycles the toilet has made and .how many times a day it got stuck.
litterrobot.items
String litterrobot_status "LitterRobot status [%s]" <toilet> (gPers_Change_Day)
String litterrobot_lastupdate "LitterRobot last update [%1$ta, %1$ty-%1$tm-%1$td %1$tH:%1$tM:%1$tS]"
Switch litterrobot_timer {expire="120s, command=OFF"} // Timer
Number litterrobot_day_cycles "Daily cycles [%d]" <pressure> (gPers_Change_Day)
Number litterrobot_day_stuck "Daily stuck [%d]" <error> (gPers_Change_Day)
First rule determines the current cleaning phase, but first you have to record the measurement of the sensor at three points, when the toilet is empty, when the toilet is filled and when the toilet is ready.
I use the angle_x, angle_y, angle_z for this. Also it makes sense not to glue the sensor to the toilet but use velcro instead, so you can remove the sensor when you need to clean the container. I would expect the you would need to “recalibrate” the angle_XYZ after you reattach the sensor.
The second rule waits 2 minutes after the completion of the last phase and checks the location. This extra delay is needed as cats can sometime stop the cleaning cycle by applying pressure on the toilet during rotation.
The third rule checks number of cycles the toiltet had run for and resets the daily counter.
zb_vibro-litterrobot.rules
rule "LitterRobot status"
when
Item sens_vibr02_angle_x received update or
Item sens_vibr02_angle_y received update or
Item sens_vibr02_angle_z received update
then
//Ph1 - Empty
var ph1_angle_x_target = 38
var ph1_angle_x_upper = ph1_angle_x_target + 5
var ph1_angle_x_lower = ph1_angle_x_target - 5
var ph1_angle_y_target = 50
var ph1_angle_y_upper = ph1_angle_y_target + 5
var ph1_angle_y_lower = ph1_angle_y_target - 5
var ph1_angle_z_target = -11
var ph1_angle_z_upper = ph1_angle_z_target + 5
var ph1_angle_z_lower = ph1_angle_z_target - 5
//Ph2 - Fill
var ph2_angle_x_target = 27
var ph2_angle_x_upper = ph2_angle_x_target + 5
var ph2_angle_x_lower = ph2_angle_x_target - 5
var ph2_angle_y_target = -7
var ph2_angle_y_upper = ph2_angle_y_target + 5
var ph2_angle_y_lower = ph2_angle_y_target - 5
var ph2_angle_z_target = 62
var ph2_angle_z_upper = ph2_angle_z_target + 5
var ph2_angle_z_lower = ph2_angle_z_target - 5
//Ph3 - Ready
var ph3_angle_x_target = 2
var ph3_angle_x_upper = ph3_angle_x_target + 5
var ph3_angle_x_lower = ph3_angle_x_target - 5
var ph3_angle_y_target = -10
var ph3_angle_y_upper = ph3_angle_y_target + 5
var ph3_angle_y_lower = ph3_angle_y_target - 5
var ph3_angle_z_target = 80
var ph3_angle_z_upper = ph3_angle_z_target + 5
var ph3_angle_z_lower = ph3_angle_z_target - 5
// logInfo("LitterRobot DEBUG", "X: " + sens_vibr02_angle_x.state + " Y: " + sens_vibr02_angle_y.state + " Z: " + sens_vibr02_angle_z.state + " TILT: " + sens_vibr02_action.state)
if ( sens_vibr02_angle_x.state >= ph1_angle_x_lower && sens_vibr02_angle_x.state <= ph1_angle_x_upper &&
sens_vibr02_angle_y.state >= ph1_angle_y_lower && sens_vibr02_angle_y.state <= ph1_angle_y_upper &&
sens_vibr02_angle_z.state >= ph1_angle_z_lower && sens_vibr02_angle_z.state <= ph1_angle_z_upper &&
sens_vibr02_action.state == "tilt") {
// logInfo("LitterRobot Ph1 ", "X: " + sens_vibr02_angle_x.state + " Y: " + sens_vibr02_angle_y.state + " Z: " + sens_vibr02_angle_z.state + " TILT: " + sens_vibr02_action.state)
postUpdate(litterrobot_status, "Ph1 - Empty")
postUpdate(litterrobot_lastupdate,new DateTimeType())
postUpdate(litterrobot_timer, "ON")
}
if ( sens_vibr02_angle_x.state >= ph2_angle_x_lower && sens_vibr02_angle_x.state <= ph2_angle_x_upper &&
sens_vibr02_angle_y.state >= ph2_angle_y_lower && sens_vibr02_angle_y.state <= ph2_angle_y_upper &&
sens_vibr02_angle_z.state >= ph2_angle_z_lower && sens_vibr02_angle_z.state <= ph2_angle_z_upper &&
sens_vibr02_action.state == "tilt") {
// logInfo("LitterRobot Ph2 ", "X: " + sens_vibr02_angle_x.state + " Y: " + sens_vibr02_angle_y.state + " Z: " + sens_vibr02_angle_z.state + " TILT: " + sens_vibr02_action.state)
postUpdate(litterrobot_status, "Ph2 - Fill")
postUpdate(litterrobot_lastupdate,new DateTimeType())
postUpdate(litterrobot_timer, "ON")
}
if ( sens_vibr02_angle_x.state >= ph3_angle_x_lower && sens_vibr02_angle_x.state <= ph3_angle_x_upper &&
sens_vibr02_angle_y.state >= ph3_angle_y_lower && sens_vibr02_angle_y.state <= ph3_angle_y_upper &&
sens_vibr02_angle_z.state >= ph3_angle_z_lower && sens_vibr02_angle_z.state <= ph3_angle_z_upper &&
sens_vibr02_action.state == "tilt") {
// logInfo("LitterRobot Ph3 ", "X: " + sens_vibr02_angle_x.state + " Y: " + sens_vibr02_angle_y.state + " Z: " + sens_vibr02_angle_z.state + " TILT: " + sens_vibr02_action.state)
postUpdate(litterrobot_status, "Ph3 - Ready")
postUpdate(litterrobot_lastupdate,new DateTimeType())
postUpdate(litterrobot_timer, "ON")
postUpdate(sens_vibr02_action, "") //Need to erase action, so that hourly updates do not trigger the Phase3 while toilet is stationary
}
end
rule "LitterRobot timer OFF"
when
Item litterrobot_timer received update "OFF"
then
// Initialise variables if NULL
if ( litterrobot_day_cycles.state == NULL ) {
logInfo("VARIABLE: ", "uninitialised " + litterrobot_day_cycles)
postUpdate(litterrobot_day_cycles,0)
Thread::sleep(10)
}
if ( litterrobot_day_stuck.state == NULL ) {
logInfo("VARIABLE: ", "uninitialised " + litterrobot_day_stuck)
postUpdate(litterrobot_day_stuck,0)
Thread::sleep(10)
}
// update counters and send notifications
if ( litterrobot_status.state != "Ph3 - Ready" ) {
logInfo("LitterRobot ", "Stuck after " + litterrobot_status.state )
sendPushoverMessage(pushoverBuilder("LitterRobot: STUCK after " + litterrobot_status.state).withEmergencyPriority())
postUpdate(litterrobot_status, "STUCK")
postUpdate(litterrobot_day_stuck,litterrobot_day_stuck.state as DecimalType + 1)
Thread::sleep(10)
}
else {
logInfo("LitterRobot ", "Cycled and ready")
sendPushoverMessage(pushoverBuilder("LitterRobot cycled and ready"))
postUpdate(litterrobot_day_cycles,litterrobot_day_cycles.state as DecimalType + 1)
Thread::sleep(10)
}
end
rule "Check usage and reset counter"
when
Time cron "0 0 0 * * ?"
then
if ( litterrobot_day_cycles.state == 0 ) {
logInfo("LitterRobot ", "Didn't cycle in the last 24hr. Please investigate.")
}
postUpdate(litterrobot_day_cycles,0)
postUpdate(litterrobot_day_stuck,0)
end
Finally the .sitemap portion
Frame label="Litter Robot" {
Text item=litterrobot_status
Text item=litterrobot_lastupdate
Text item=litterrobot_day_cycles
Text item=litterrobot_day_stuck
}