Dishwasher Cycle Template Help

I am trying to detect the drying cycle on my dishwasher using a template sensor and power monitoring, but the sensor seems to be stuck in a loop and never actually triggers the drying state. When I test the template in Develper tools, it resolves to Drying correctly using the correct values

Here is the sensor yaml

- sensor:
  - name: Dishwasher Cycle
    unique_id: dishwasher_cycle
    state: >
          {% set power=states('sensor.dishwasher_16_1min') | float %}
          {% set timecurrent=as_timestamp(now()) %}
          {% set timechanged=as_timestamp(states.sensor.dishwasher_cycle.last_changed) %}
          {% if timecurrent - timechanged >= 180 and 1 <= power < 80 %}
          Drying
          {% elif power >=80 %}
          Washing 
          {% elif 1 <= power < 80 %}
          Idle/Rinse
          {% else %}
          Off
          {% endif %}

And here is a screenshot of what my history looks like while the dishwasher is running.

That period where it looks like the dishwasher reverts to idle every 4 minutes is supposed to be the drying cycle. I can’t figure out why it resets itself every four minutes like that.

Also, I realize that I can accomplish this with an automation by creating a trigger based on power staying in that range for longer than 5 minutes, but I want this sensor to display the cycle in lovelace.

the reason it’s not working is because you’ve created a bit of a heisenbug.

first of all, for the 4 minutes of idle that you said should be the drying cycle, if all works as you kinda wrote (except for the bug below), it would report idle/rinsing for 3 minutes and then try to turn into drying at the 180th second, right? because your first “if” clause would not make it say “drying” for < 180 seconds.

so here’s your bug. the instant 180 hits, you’re going to report “drying” right? it will change from idle/rinse to “drying”. so you are changing the state of this sensor. which means last_changed now gets updated to now(). which means at the immediate next evaluation of state, it goes right back to idle/rinse because you just forced last_changed to now() so timecurrent - timechanged == (virutally) 0.

what you may want to do something like the below… i took liberty to change the time math to a format i personally prefer… note that i free hand coded this. didn’t test it, so i may have some issue here or there, but i think i’ve got your core breaking issue, and the framework for how to solve it.

- sensor:
  - name: Dishwasher Cycle
    unique_id: dishwasher_cycle
    state: >
          {% set power=states('sensor.dishwasher_16_1min') | float %}
          {% if power >=80 %}
            Washing 
          {% elif 1 <= power < 80 %}
              {% if (now() - states.sensor.dishwasher_cycle.last_changed >= timedelta(minutes=3)) and states('sensor.dishwasher_cycle')  != 'Drying' %}
                Drying
              {% else %}
                Idle/Rinse
              {% endif %}
          {% else %}
            Off
          {% endif %}

the reason why it works for you when you put it in template is because when you put it in template, you are not changing last_changed… when you put it in your sensor definition, you are changing last_changed…

I have a dumb washing machine and I build an overly complex MQTT entity with a script and an automation to get the exact moment my washing machine ends.

It’s not easy to understand and it took quite a bit of development because it captures all different washing machine cycle types.

I’ve only had it fail once in the past 3 years, and I built a way to reset it.

image

I only use it for notifications though. And I don’t display the active cycle, although that would be pretty easy with how I set it up.

You can attempt to use this method and I’ll try to help you set it up. It will require MQTT, an automation, and 3 scripts.

MQTT discovery and state updates script.

mqtt_automated_states:
  alias: Publish State and Attributes
  mode: parallel
  fields: 
    <<: &mqtt-fields
      domain:
        description: The entities domain
        selector:
          text:
            type: text
      unique_id:
        description: The entities unique_id
        selector:
          text:
            type: text
      object_id:
        description: The entities object_id
        selector:
          text:
            type: text
    state:
      description: The entities state
      selector:
        text:
          type: text
    attributes:
      description: The entities attributes
      example: A dictionary {} in yaml
      selector:
        object:
  variables: 
    <<: &mqtt-variables
      root: "homeassistant"
      topic_root: >
        {%- if domain is not defined or unique_id is not defined %}
          {{- [ root, 'error'] | join('/') }}
        {%- else %}
          {{- [ root, domain, unique_id ] | join('/') }}
        {%- endif %}
    service_data: >
      {{ {
        'topic': topic_root ~ '/state',
        'payload': '' ~ { 'state': state, 'attributes': attributes | default({}) } | tojson,
        'retain': retain | default(True)
      } }}

  sequence:
  - service: mqtt.publish
    data: "{{ service_data }}"

mqtt_automated_config:
  alias: Publish Discovery
  mode: parallel
  fields: 
    <<: *mqtt-fields
    device_class: 
      description: The entities device class
      selector:
        text:
          type: text
  variables:
    name: >
      {% if object_id is defined %}
        {{ object_id | default('') | replace('_', ' ') | title }}
      {% elif unique_id is defined %}
        {{ unique_id | default('') | replace('_', ' ') | title }}
      {% else %}
        Unknown Entity
      {% endif %}
    <<: *mqtt-variables
    service_data: >
      {%- set items = [
        ("name", name),
        ("unique_id", unique_id | default(none)),
        ("object_id", object_id | default(none)),
        ("state_topic", topic_root ~ "/state"),
        ("value_template", "{{ value_json.state }}"),
        ("json_attributes_topic", topic_root ~ "/state"),
        ("json_attributes_template", "{{ value_json.attributes | tojson }}"),
        ("device_class", device_class | default(none) ),
        ("unit_of_measurement", unit_of_measurement | default(none) ),
        ("state_class", state_class | default(none)),
        ("device", device | default(none))
      ] %}
      {% set payload = dict.from_keys(items | rejectattr('1', 'none') | list) %}
      {{ {
        'topic': topic_root ~ '/config',
        'payload': '' ~ payload | tojson,
      } }}
  sequence:
  - service: mqtt.publish
    data: "{{ service_data }}"

2 laundry scripts, 1 to reset if it breaks and the cycle counter that does the cycle calculation based on time

reset_washer:
  # You're feeling lazy right now, you'll need to add a way to actually reset this in the future
  # To do this, set an on/off sequence that matches the test_washer's configuration.  You'll
  # need to setup a loop of services to run to reset it based on the current state
  # of the actual washer.  Again, you can only be mad at yourself when you do this in the future
  # because you were a lazy ass and didn't feel like doing it today.
  alias: Washer - Reset the washer state
  mode: single
  variables:
    current: "{{ states('input_number.test_washer') | int }}"
    filtered: "{{ range(80) | reject('eq', current) | list }}"
    next_value: "{{ filtered | random }}"
  sequence:
  - service: input_number.set_value
    target:
      entity_id: input_number.test_washer
    data:
      value: "{{ next_value }}"

calculate_washer_cycle:
  alias: Washer - Calculate Current Cycle
  mode: parallel
  fields:
    prefix:
      description: (Required) start part of the unique_id
      example: foo
      required: True
      default: washer_dryer_
      selector:
        select:
          options:
          - washer_dryer_
    source: 
      description: (Required) Source of the state change
      example: binary_sensor.washer_status
      required: True
      default: binary_sensor.washer_status
      selector:
        entity:
          include_entities:
          - binary_sensor.washer_status
    next_phase:
      description: (Required) State of the next phase
      example: 'on'
      required: True
      default: 'off'
      selector:
        select:
          options:
          - label: "On"
            value: 'on'
          - label: "Off"
            value: 'off'
    reset:
      description: (Optional) Reset the cycle.
      example: True
      default: False
      selector:
        boolean:

  variables:

    # MQTT SETUP
    root: homeassistant
    topic_root: binary_sensor
    config: &config
      input_boolean.test_washer:
        final_spin:
          duration: 10
          window: 5
      binary_sensor.washer_status:
        object_id: washer
        final_spin:
          duration: 600
          window: 60

    # SETTING THE BINARY SENSOR STATE
    current: >
      {{ config.get(source) }}
    have_current: >
      {{ current is not none }}
    object_id: >
      {{ current.object_id if current.object_id is defined and have_current else source.split('.')[-1] }}
    entity: >
      {{ topic_root }}.{{ object_id }}
    unique_id: >
      {{ prefix }}{{ object_id }}
    phase_timestamp: >
      {{ now().isoformat() }}
    idle:
      phase: 'off'
      start: "{{ phase_timestamp }}"
      action: 'idle'
    current_cycles: >
      {{ state_attr(entity or 'dump', 'cycles') or [] }}
    current_cycle: >
      {{ (current_cycles or []) | last | default(none) }}
    current_phase: >
      {{ current_cycle.phase if current_cycle is not none else none }}
    next_cycle: >
      {% if current_cycle is not none %}
        {% if current_phase != next_phase %}
          {{ {'phase': next_phase, 'start': phase_timestamp} }}
        {% else %}
          {{ current_cycle }}
        {% endif %}
      {% else %}
        {{ {'phase': next_phase if next_phase == 'on' else 'off', 'start': phase_timestamp} }}
      {% endif %}
    cycles: >
      {% set rollup = current_cycles + [ next_cycle ] %}
      {% set ns = namespace(ret=[], attrs=[]) %}

      {# setup durations for phases #}
      {% if reset is not defined or not reset %}
        {% for i in range(rollup | length) %}
          {% set attrs = rollup[i] %}
          {% set next_attrs = rollup[i + 1] if i + 1 < rollup | length else {} %}
          {% set ns.attrs = attrs.items() | rejectattr('0','in',['duration', 'action']) | list %}
          {% set duration = (next_attrs.start | as_datetime - attrs.start | as_datetime).seconds if next_attrs else 0.0 %}
          {% set ns.attrs = ns.attrs + [('duration', duration)] %}
  
          {% if attrs.phase == 'on' %}
  
            {# on state, try to find phase #}
            {% set l, u = current.final_spin.duration - current.final_spin.window, current.final_spin.duration + current.final_spin.window %}
            {% if l <= duration <= u %}
              {% set ns.attrs = ns.attrs + [('action', "finish")] %}
            {% else %}
              {% set ns.attrs = ns.attrs + [('action', "spinning")] %} 
            {% endif %}
          {% else %}
            {# off state, do not collect phase #}
            {% set ns.attrs = ns.attrs + [('action', "idle")] %}
          {% endif %}
          {% set ns.ret = ns.ret + [dict(ns.attrs)] %}
        {% endfor %}
        {% set last = ns.ret | list | last | default(idle) %}
        {% if ns.ret | length == 1 and last.phase in ['off', 'idle'] %}
          []
        {% else %}
          {{ ns.ret if 'finish' not in ns.ret | map(attribute='action') else [] }}
        {% endif %}
      {% else %}
        []
      {% endif %}
    next_state: >
      O{{ 'N' if cycles | length > 0 else 'FF' }}

  sequence:
  - service: script.mqtt_automated_states
    data:
      <<: &mqtt
        domain: binary_sensor
      unique_id: "{{ unique_id }}"
      state: "{{ next_state }}"
      attributes: "{{ {'source': source, 'cycles': cycles} }}"

lastly the automation that counts it all

- alias: Cycle Counter
  id: washer_and_dryer_cycle_counter
  mode: parallel
  trigger:
  - platform: state
    entity_id:
    - input_boolean.test_washer
    - binary_sensor.washer_status
  variables:

    <<: &prefix
      prefix: washer_dryer_

    valid: >
      {{ trigger | default(none) is not none and trigger.to_state is defined and trigger.from_state is defined }}
    continue: >
      {{ trigger.to_state.state != trigger.from_state.state if valid else False }}
    source: >
      {{ trigger.entity_id }}
    next_phase: >
      {{ trigger.to_state.state if valid else none }}

  condition:
  - condition: template
    value_template: "{{ continue }}"
  action:
  - service: script.calculate_washer_cycle
    data:
      prefix: "{{ prefix }}"
      source: "{{ source }}"
      next_phase: "{{ next_phase }}"

- alias: MQTT Discovery
  id: 3e5e2ffb-d811-4818-a6bd-1f1386c6dbae
  trigger:
  - platform: homeassistant
    event: start
  action:
  - variables:
      <<: *prefix
      binary_sensors:
      - test_washer
      - washer
  - repeat:
      count: "{{ binary_sensors | length }}"
      sequence:
      - service: script.mqtt_automated_config
        data:
          domain: binary_sensor
          unique_id: "{{ prefix }}{{ binary_sensors[repeat.index - 1] }}"
          object_id: "{{ binary_sensors[repeat.index - 1] }}"
          device_class: running

This will create a binary sensor that holds the running/not_running state. It also creates attributes that have the current cycles as an object. I can help you build a template sensor from that.

Unfortunately even with this modification, the sensor is still stuck in a loop where it changes the state every 4 minutes while trying to change its own state. Maybe I need to use a separate helper entity.

can you share your current exact code?

- sensor:
  - name: Dishwasher Cycle
    unique_id: dishwasher_cycle
    state: >
          {% set power=states('sensor.dishwasher_16_1min') | float %}
          {% if power >=80 %}
            Washing 
          {% elif 1 <= power < 80 %}
              {% if (now() - states.sensor.dishwasher_cycle.last_changed >= timedelta(minutes=3)) and states('sensor.dishwasher_cycle')  != 'Drying' %}
                Drying
              {% else %}
                Idle/Rinse
              {% endif %}
          {% else %}
            Off
          {% endif %}

i’m sorry, i think i had a logic bug in my prior. try this. i also converted over to use this.state…just for code beauty’s sake…that part’s purely cosmetic. i think i’ve got that right, but if not, revret back to states(‘sensor.dishwasher_cycle’)

- sensor:
  - name: Dishwasher Cycle
    unique_id: dishwasher_cycle
    state: >
          {% set power=states('sensor.dishwasher_16_1min') | float %}
          {% if power >=80 %}
            Washing 
          {% elif 1 <= power < 80 %}
              {% if (now() - states.sensor.dishwasher_cycle.last_changed >= timedelta(minutes=3)) or this.state == 'Drying' %}
                Drying
              {% else %}
                Idle/Rinse
              {% endif %}
          {% else %}
            Off
          {% endif %}

No worries, we’re all victims of logic bugs today, and that conversion renders correctly in template. I’ll report back once we run our daily cycle, thanks again for taking the time.

So I think we’re getting somewhere, but we’ve gone too far in the other direction, as this script has now prevented me from achieving the Idle/Rinse state. Now, any time power is between 1 and 80W, it is always Drying, regardless of how much time has passed in between Washing cycles with power greater than 80. That being said, the 30 minute period of drying at the end of the cycle is now correctly identified as drying. Sorry to keep coming back to you with this, I appreciate your help, you’ve gotten me much further with debugging than I would have by myself.

it goes straight to drying? skips idle/rinse?
i presume drying should always comes after idle/rinse?
could you post the new equivalent of this:


?

if my assumption above is right, then try this… definitely close!:

- sensor:
  - name: Dishwasher Cycle
    unique_id: dishwasher_cycle
    state: >
          {% set power=states('sensor.dishwasher_16_1min') | float %}
          {% if power >=80 %}
            Washing 
          {% elif 1 <= power < 80 %}
              {% if ((now() - states.sensor.dishwasher_cycle.last_changed >= timedelta(minutes=3)) and (this.state == 'Idle/Rinse')) or this.state == 'Drying' %}
                Drying
              {% else %}
                Idle/Rinse
              {% endif %}
          {% else %}
            Off
          {% endif %}

Your assumption is correct, see below:

I’ll try your updated code tonight and let you know. To quote my wife, I’m not allowed to “waste water running extra cycles to test something only you are going to look at” lol

think of it as an investment… :slight_smile:

At last we have success, thanks again for your help!

1 Like

hurray!! :slight_smile: