Low Cost (Amazon Dash Button) Washing Machine Timer

Hi together,

I am watching and observing the washing machine state machine thread (What a tongue twister :grin:) for a while now.
I definetely want to connect my washing machine in the same way later,
but currently i dont want to buy an extra switch that is able to measure.
Anyway i wanted to get noticed from my machine, since its located on another floor in this house.

So i was figuring out a low cost way, until i will switch my local setup to a state machine too.
Maybe this is a good “temporary” solution for someone else or in another usecase,
so i decided to share it with you.

Simple solution:

  1. An Amazon Dash Button which costs 4.99€ (that will get refunded at the first button-order)
  2. A little Rule
  3. A Telegram/Pushover Action

I ordered a Dash Button, bought some washing powder and got this button theoretically “for free”. :slight_smile:

The Rule is based on the preferred washing programms in our home.
Basically I could break the logic down to three different times, we have measured.

  1. The standard programm with about 1h and 43min duration
  2. The extended programm with about 2h and 23min duration
  3. The spin dry programm with 12min duration

I implemented this behavior with four Dash Button press-triggers.
Three for the different programs and one to reset all timers.

washingmachine.items:

// Washing Machine-Items
Switch      rWashingMachineTimer_active             "Waschmaschinen-Timer Aktiv"                        <clock>    (wmTimerStatus)
DateTime    rWashingMachineTimer_TimerEnd           "Waschmaschinen-Timer [%1$tH:%1$tM Uhr]"            <washingmachine>    (wmTimerStatus)
Number      rWashingMachineTimer_TimerDuration      "Waschmaschinen-Timer Restzeit [%d min]"            <washingmachine>    (wmTimerStatus)
Number      rWashingMachineTimer_choosenProgramm    "Gewähltes Programm: [MAP(washingMachine.map):%s]"  <washingmachine>    (wmTimerStatus)

washingmachine.rules:

// Imports
import org.joda.time.*

// Variables
var Timer tWashingMachine		// Timer for the alarm itself
var Timer tProgramFeedback		// Timer for feedback of the choosen program
var DateTime startTimeStorage	// Time stored at the first button-press-trigger
var int buttonPressCounter 		// Counter for the amount of button-press-trigger-events

// Read-Only values
val String filename = "washingMachine.rules"

// Timer presets
val int standardProgramDuration = 110	// 1 hour and 43 minutes
val int extendedProgramDuration = 150	// 2 hour and 23 minutes
val int spinDryProgramDuration = 15		// 12 minutes
val int feedbackAfterDuration = 5		// 5 minutes

rule "Washing Machine Timer Init"
when
    System started
then

	if (rWashingMachineTimer_active.state == NULL) rWashingMachineTimer_active.postUpdate(OFF)

end

rule "Washing Machine Duration Update"
when
    Time cron "0 * * * * ?"	// Cron every minute
then

	// Check if a timer is running
    if (rWashingMachineTimer_active.state == ON) {
        logInfo(filename, "updating timer duration")

		var DateTime timeEnd = null;
		logInfo(filename, "rWashingMachineTimer_TimerEnd.state " + rWashingMachineTimer_TimerEnd.state.toString)

		if(rWashingMachineTimer_TimerEnd.state == NULL){

			logInfo(filename, "updating first time")

			// Create timeEnd in case of a timer end NULL value
			timeEnd = new DateTime()

			logInfo(filename, "updating first time completed")

		}
		else{

			logInfo(filename, "updating again")

			// Update timeEnd to the timer end value
			timeEnd = new DateTime(DateTime.parse(rWashingMachineTimer_TimerEnd.state.toString()))
			logInfo(filename, "updating again completed")

		}

        logInfo(filename, "Deadline: " + timeEnd)

		// Calculate difference between now and timer end to get the duration
        var Number timeDuration = ((((timeEnd.millis - now.millis) / 1000) + 30) / 60).intValue()
		
		// Keep duration on a minimum of 0, since timer end can be in the past
		if(timeDuration <= 0){
			timeDuration = 0
		}

        logInfo(filename, "Duration: " + timeDuration)

		// Update duration item
        rWashingMachineTimer_TimerDuration.postUpdate(timeDuration)
    }
	else{

		// Keep duration on 0 minutes while timer is inactive
		rWashingMachineTimer_TimerDuration.postUpdate(0)

	}
end

rule "Washing Machine Dash button pressed"
    when
        Channel "amazondashbutton:dashbutton:bio:press" triggered
    then
        
        // No Timer active yet
        if(tWashingMachine == null){        	
        	
        	// Store the first button-press-action-time and initalize press-counter
        	startTimeStorage = now()

			//Initalize first press trigger in the counter variable
        	buttonPressCounter = 1

			// Update programm item to "standard programm"
			rWashingMachineTimer_choosenProgramm.postUpdate(1)

			// Set timer active item to ON
			rWashingMachineTimer_active.postUpdate(ON)

        	logInfo("washingMachineDashButtonEvent", "Waschmaschinentimer Startzeitpunkt: " + startTimeStorage.toString("H:m:s"))

			// Update timer end to preset 1
			var preset = startTimeStorage.plusMinutes(standardProgramDuration)
			rWashingMachineTimer_TimerEnd.postUpdate(new DateTimeType (preset.toCalendar(null)))

        	// Start timer with first preset
        	tWashingMachine = 
        		createTimer(startTimeStorage.plusMinutes(standardProgramDuration))[

        			logInfo("washingMachineDashButtonEvent","\"Standardprogramm fertig\" Meldung ausgelöst.")
					pushover("Waschmaschine Standardprogramm fertig")

					// Reset main timer
					tWashingMachine.cancel()
					tWashingMachine = null

					// Update programm item to "no timer choosen"
					rWashingMachineTimer_choosenProgramm.postUpdate(0)

					// Set timer active item to OFF
					rWashingMachineTimer_active.postUpdate(OFF)

				]
				
        	logInfo("washingMachineDashButtonEvent", "Standard Timer (" + standardProgramDuration.toString + " min) gestartet.")
        }
        // A timer is already started
        else if (tWashingMachine != null){
        	
			// Increase button counter
    	    buttonPressCounter++
    	    logInfo("washingMachineDashButtonEvent", "Button wurde - " + buttonPressCounter + " - mal gedrückt - Startzeitpunkt war: " + startTimeStorage.toString("H:m:s") + ".")
    	    
    	    // Start timer with second preset
    	    if(buttonPressCounter == 2){    	    	
    	    	
	    		logInfo("washingMachineDashButtonEvent", "Ändere Timer auf Langes Waschprogramm.")

				// Cancel currently running timer
	    		tWashingMachine.cancel()

				// Update programm item to "extended programm"
				rWashingMachineTimer_choosenProgramm.postUpdate(2)

				// Update timer end to preset 2
				var preset2 = startTimeStorage.plusMinutes(extendedProgramDuration)
				rWashingMachineTimer_TimerEnd.postUpdate(new DateTimeType (preset2.toCalendar(null)))

				// Set up new timer for the now choosen programm
	        	tWashingMachine = 
	        		createTimer(startTimeStorage.plusMinutes(extendedProgramDuration))[

	        			logInfo("washingMachineDashButtonEvent", "\"Langes Waschprogramm fertig\" Pushmeldung ausgelöst.")
						pushover("Langes Waschprogramm fertig")

						// Reset main timer
						tWashingMachine.cancel()
						tWashingMachine = null

						// Update programm item to "no timer choosen"
						rWashingMachineTimer_choosenProgramm.postUpdate(0)

						// Set timer active item to OFF
						rWashingMachineTimer_active.postUpdate(OFF)

	        		]
	        	logInfo("washingMachineDashButtonEvent", "Langes Waschprogramm Timer (" + extendedProgramDuration.toString + " min) gestartet.")
	        	
    	    }
    	    
    	    // Start timer with third preset
    	    if(buttonPressCounter == 3){    	    	
    	    	
	    		logInfo("washingMachineDashButtonEvent", "Ändere Timer auf Schleuderprogramm.")

				// Cancel currently running timer
	    		tWashingMachine.cancel()

				// Update programm item to "spin drying programm"
				rWashingMachineTimer_choosenProgramm.postUpdate(3)

				// Update timer end to preset 3
				var preset3 = startTimeStorage.plusMinutes(spinDryProgramDuration)
				rWashingMachineTimer_TimerEnd.postUpdate(new DateTimeType (preset3.toCalendar(null)))

				// Set up new timer for the now choosen programm
	        	tWashingMachine = 
	        		createTimer(startTimeStorage.plusMinutes(spinDryProgramDuration))[

	        			logInfo("washingMachineDashButtonEvent", "\"Schleuderprogramm fertig\" Pushmeldung ausgelöst.")
						pushover("Schleuderprogramm fertig")

						// Reset main timer
						tWashingMachine.cancel()
						tWashingMachine = null

						// Update programm item to "no timer choosen"
						rWashingMachineTimer_choosenProgramm.postUpdate(0)

						// Set timer active item to OFF
						rWashingMachineTimer_active.postUpdate(OFF)

	        		]
	        	logInfo("washingMachineDashButtonEvent", "Schleuderprogramm Timer (" + spinDryProgramDuration.toString + " min) gestartet.")
	        	
    	    }
    	    
    	    // reset timer    	    
    	    if(buttonPressCounter == 4){
    	    	    	    	
	    		logInfo("washingMachineDashButtonEvent", "Setze Waschmaschinen-Timer zurück...")

				// Reset main timer
	    		tWashingMachine.cancel()
				tWashingMachine = null

				// Update programm item to "everything cancelled"
				rWashingMachineTimer_choosenProgramm.postUpdate(4)

				// Set timer active item to OFF
				rWashingMachineTimer_active.postUpdate(OFF)

				// Set timer end to now, for cronjob
				rWashingMachineTimer_TimerEnd.postUpdate(new DateTimeType (now()))

	    		logInfo("washingMachineDashButtonEvent", "Alle Waschmaschinen Timer zurückgesetzt.")
    	    	
    	    }
        	
        }
end

rule "Washing Machine Programm Feedback"
when
	Item rWashingMachineTimer_choosenProgramm changed
then

	// Reset Feedback-Timer in case of programm change
	if(tProgramFeedback != null)
		tProgramFeedback.cancel()

	if(rWashingMachineTimer_choosenProgramm.state != NULL && rWashingMachineTimer_choosenProgramm.state != 0){

		logInfo(filename, "Feedback pushover timer started")

		// Start Feedback Timer for choosen Program
		// the finally choosen Timer will be pushed in "feedbackAfterDuration" minutes after the first button-press-event
		tProgramFeedback=
			createTimer(now.plusMinutes(feedbackAfterDuration))[
				logInfo(filename, "Feedback Pushover Switch fired")
				switch rWashingMachineTimer_choosenProgramm.state {
					case 1: pushover("[Waschmaschinentimer] Standardprogramm gestartet")
					case 2: pushover("[Waschmaschinentimer] Langes Programm gestartet")
					case 3: pushover("[Waschmaschinentimer] Schleuderprogramm gestartet")
					case 4: pushover("[Waschmaschinentimer] Alle Timer manuell resettet")
				}

			]
	}
        	
end

To keep the times as exactly as possible, the rule stores the exact time of the first button trigger event.
The second and third trigger event are using that datetime as startpoint.
The fourth trigger resets all active timers and the timers reset itself of course after firing the notification event.

Pro:

  • Low Cost
  • Low effort needed

Contra:

  • Only a workaround compared to a holistic approach like the state machine
  • You have to wait for the dash button to complete its trigger, until you can press again

Conclusion (for me):
Of course this is a very simple approach (no rocket science included this time :smiley:),
but maybe it can help someone and it works fine as my current workaround here.
(I am reading and learning from the community for a while now, so i thought it would be nice to give at least something small back to the com.)

I am open to feedback or improvement suggestions for sure. :slight_smile:

6 Likes

I very much like it. That’s a great example for in just how many ways one problem can be solved (in openHAB). :sparkles:

Also thanks for contributing this well written tutorial (yes I’ve recognized your user name ;))

Thanks @ThomDietrich. :slight_smile:

I’ll add an improvement suggest to myself here:
1.

Since the button has to be pressed more than one time in some cases, it would be nice to get a quick feedback push (maybe after XY seconds) , which timer is finally running now.

Also it would be nice to have a Countdown somewhere (maybe within the sitemap).

I will evaluate those two topics and update the tutorial, when i found a reliable solution for me.

I’ve got something similar to (2.) here. Feel free to snoop :wink:

I am figuring out some problems, with the cron rule, but wasn’t able to test it finally.
Seems there is somethign Special with the DateType thing.
But thanks 4 sharing it anyway.

Some Days ago, my Dash buttons quit work and until today i was only able to get one back to work/discovery in OH2
and i have no idea why they stopped working.

I can hopefully tell a bit more in some days.

For the feedback push i have found a (in my eyes) solid working solution with another timer.
I will share it in some days, when i (hopefully) get all dash buttons discovered again.

I’ve done some serious rewrite of the addons readme the other day: https://github.com/openhab/openhab2-addons/blob/master/addons/binding/org.openhab.binding.amazondashbutton/README.md

One piece of info I’ve added is that the lifetime is shortened with the internet connection blocked. Your button should blink red if that is the reason.

I have edited the inital post with a “big” update.
Thanks again to @ThomDietrich for sharing the display timer and duration stuff.

Improvements:

  • I have changed to pushover in general.
  • I have added a feedback pushover after X minutes, to confirm the finally choosen program.
  • I have added some items to display the timer activity, end time, program and duration within my sitemap
  • I have added some more comments to the rule, since i want to share it as tutorial here
1 Like

Sorry for the off topic message, but are you using the old kodi binding? It doesnt seem to match with the OH2 binding.

I do, simply because the new one was not around when building my rules. Why are you asking. If you’d like to help we can look into transforming my set of rules for the OH2 binding together.

Yes, i was interested in the OH2 equivalent. I couldnt find the OH2 item for this:

Switch KodiSystemState "Kodi State" <kodi> (gKodi) {xbmc="<[#livingRoom|System.State]"}

Without any testing I’d bet you should look into the actual Thing Status. Could you please open a new thread?