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.
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.