Set ventilation time based on toilet visit duration

I had a default ventilation time of 15 minutes when somebody visited the toilet. But in some cases this is not enough :stuck_out_tongue_winking_eye:

I came up with the following code and I would like to get some feedback or other ideas.

When my pir sensor (ESP easy + mqtt) gets triggered I store the timestamp it was first triggered. I then block updating of this timestamp till the duration is calculated. This is because the pir sensor keeps being triggered when the toilet is occupied.

When the light switches off (180 sec after last movement) I get the latest timestamp from the pir sensor, calculate the difference, turn the fan on for 15 min if the visit was less then 5 minutes or 30 min if the visit was longer.

Items

Number		pir_toilet			"Toilet PIR [MAP(pir.map):%s]"			<motion>	(gToilet, gPIR, gSensorsStatuses)	{channel="mqtt:topic:toilet:pir", expire="1m,state=0"}
DateTime	pir_toilet_lastChanged		"Toilet PIR [%1$td-%1$tm-%1$tY, %1$tR]"		<time>		(gToilet, gPIR, gSensorsStatuses)					{channel="mqtt:topic:toilet:pir" [profile="timestamp-change"]}
DateTime	StartToiletVisit			"Start measurement toilet visit"
DateTime	StopToiletVisit				"Stop measurement toilet visit"
Switch		VentilatieToilet		"Ventilatie Toilet (15 min)"		<switch>		(gVentilation)		{expire="15m,command=OFF"}
Switch		VentilatieToiletLang		"Ventilatie Toilet (30 min)"		<switch>		(gVentilation)		{expire="30m,command=OFF"}
Dimmer		Milight_ID0x2_G4_Level		"Toiletlamp"					(gToiletLightsLevel)		{channel="espmilighthub:rgb_cct:milighthub_beneden:0x24:level"}

Rule

var boolean measurementRunning1 = false

rule "Start Detect toilet visit duration"
when
    Item pir_toilet changed from 0 to 1
then
logInfo("Start Toilet Visit Measurement", "measurementRunning1 = " + measurementRunning1)
    if (!measurementRunning1) {
        measurementRunning1 = true
        StartToiletVisit.sendCommand(pir_toilet_lastChanged.state.toString)
        logInfo("Start Toilet Visit Measurement", "StartToiletVisit = " + StartToiletVisit.state.toString)
    }
end

rule "Stop Detect toilet visit duration"
when
    Item Milight_ID0x2_G4_Level changed to 0
then
        StopToiletVisit.sendCommand(pir_toilet_lastChanged.state.toString)
        logInfo("Stop Toilet Visit Measurement", "StopToiletVisit = " + StopToiletVisit.state.toString)
        measurementRunning1 = false
    
    Thread::sleep(500) // Give items time to update

    val Number MyEpochFromDateTimeTypeItem_VariantB1 = new DateTime(StartToiletVisit.state.toString).millis
    val Number MyEpochFromDateTimeTypeItem_VariantB2 = new DateTime(StopToiletVisit.state.toString).millis

    logInfo("Calculate duration Toilet Visit", "MyEpochFromDateTimeTypeItem_VariantB1 = " + MyEpochFromDateTimeTypeItem_VariantB1)
    logInfo("Calculate duration Toilet Visit", "MyEpochFromDateTimeTypeItem_VariantB2 = " + MyEpochFromDateTimeTypeItem_VariantB2)

    if (MyEpochFromDateTimeTypeItem_VariantB1 === null || MyEpochFromDateTimeTypeItem_VariantB2 === null || MyEpochFromDateTimeTypeItem_VariantB1 == MyEpochFromDateTimeTypeItem_VariantB2) {
        logWarn("Total duration toilet visit", "1 MyEpochFromDateTimeTypeItem_Variant is null or equal.")
        return;
    }
        
        val Number toiletvisitduration = ((MyEpochFromDateTimeTypeItem_VariantB2 - MyEpochFromDateTimeTypeItem_VariantB1)/1000)
        logInfo("Total duration toilet visit", "toiletvisitduration = " + toiletvisitduration + " seconds.")

        if ( toiletvisitduration <= 300) {
            logInfo("Total duration toilet visit", "Total duration toilet visit was less then 5 min, turn fan on for 15 min")
            VentilatieToilet.sendCommand(ON)
        } else {
            logInfo("Total duration toilet visit", "Total duration toilet visit was longer then 5 min, turn fan on for 30 min")
            if (VentilatieToilet.state == ON) {
                VentilatieToilet.sendCommand(OFF)
            }
            VentilatieToiletLang.sendCommand(ON)
        }
end
2 Likes

Very nice touch.
I use something similar, however during the night the fan will stay off to prevent the noise.

I do that in my main ventilation rule. This rule switches on a virtual item, and my main ventilation rule switches on the actual fan.

var DateTime until = null

rule "Ventilatie Algemeen"
when
    Member of gVentilation changed or //received command or
    Item VentilatieAanwezigheid changed //received command
then
    // 1a. Check to see if this rule is already running.
    if(until !== null && until.isAfter(now)) return; // skip this event if the timer exists
        //logInfo("Ventilation Function", "0a. until is " + until) 
        until = now.plusSeconds(10)
        //logInfo("Ventilation Function", "0b. until is " + until)

    // 1b. Check to see if the Rule has to run at all, if not exit.
    if(gVentilation.members.filter[ i | i.state == NULL ].size != 0) {
        val StringBuilder sb = new StringBuilder
        gVentilation.members.filter[ i | i.state == NULL].forEach[i | sb.append(i.name + ", ") ]
        logWarn("Ventilatie Algemeen", "Atleast one of the Ventilation Items is NULL:" + sb)
        // send alert using you preferred service
        return;
    }
    
    // 2. Calculate what needs to be done.
    var mediumState = OFF // default to sending the OFF command
    var highState = OFF // default to sending the OFF command

    if (Vacation_Mode.state == ON) {
        mediumState = OFF
        highState = OFF
    } else if (VentilatieAanwezigheid.state == ON && gVentilation.state != ON && (new Interval(now.withTime(9,0,0,0),now.withTime(21,30,0,0)).contains(now))) {
        logInfo("Ventilation Function", "1. Ventilation demand (presence) is " + VentilatieAanwezigheid.state.toString + " and ventilation = " + Wallplug_Zolder_MV1.state.toString + " / " + Wallplug_Zolder_MV2.state.toString + ". Turning ventilation ON in MEDIUM mode.")
        mediumState = ON
    } else if (gVentilation.state == OFF && (Wallplug_Zolder_MV1.state != ON && Wallplug_Zolder_MV2.state != ON)) {
        logInfo("Ventilation Function", "2. Ventilation demand is " + gVentilation.state.toString + " and ventilation system = " + Wallplug_Zolder_MV1.state.toString + " / " + Wallplug_Zolder_MV2.state.toString + ". No action needed.")
        return;
    } else if (gVentilation.state == ON && now.getHourOfDay < 9) {
        logInfo("Ventilation Function", "4. Ventilation demand is " + gVentilation.state.toString + " and ventilation = " + Wallplug_Zolder_MV1.state.toString + " / " + Wallplug_Zolder_MV2.state.toString + ". Turning ventilation ON in MEDIUM mode.")
        mediumState = ON
        highState = OFF
    } else if (gVentilation.state == ON && (new Interval(now.withTime(9,0,0,0),now.withTime(21,30,0,0)).contains(now))) {
        logInfo("Ventilation Function", "5. Ventilation demand is " + gVentilation.state.toString + " and ventilation = " + Wallplug_Zolder_MV1.state.toString + " / " + Wallplug_Zolder_MV2.state.toString + ". Turning ventilation ON in HIGH mode.")
        mediumState = OFF
        highState = ON
    } else {
        mediumState = OFF
        highState = OFF        
    }

    // 3. Do it. Only do actions like sendCommand in one place at the end of the Rule.
    logInfo("Ventilation Function", "6. gVentilation = " + gVentilation.state.toString + ", VentilatieAanwezigheid = " + VentilatieAanwezigheid.state.toString + ", VentilatieBadkamer = " + VentilatieBadkamer.state.toString + 
                          ", VentilatieHandmatig = " + VentilatieHandmatig.state.toString + ", VentilatieToilet = " + VentilatieToilet.state.toString +
                          ". Sending " + mediumState.toString + " " + highState.toString + " to Ventilation Unit.")
    if (Wallplug_Zolder_MV1.state == mediumState) {
        logInfo("Ventilation Function", "7. Wallplug_Zolder_MV1 is already " + mediumState + ". Nothing to do")
    } else {
        logInfo("Ventilation Function", "8. Wallplug_Zolder_MV1 is " + Wallplug_Zolder_MV1.state + ". Switching " + mediumState)
        Wallplug_Zolder_MV1.sendCommand(mediumState)
    }
        
    if (Wallplug_Zolder_MV2.state == highState) {
        logInfo("Ventilation Function", "9. Wallplug_Zolder_MV2 is already " + highState + ". Nothing to do")
    } else {
        logInfo("Ventilation Function", "10. Wallplug_Zolder_MV2 is " + Wallplug_Zolder_MV2.state + ". Switching " + highState)
        Wallplug_Zolder_MV2.sendCommand(highState)
    }

end
1 Like