Repository navigation
Expand file tree
/
Copy pathmaintenance_log.yaml
More file actions
374 lines (347 loc) · 14.8 KB
/
Copy pathmaintenance_log.yaml
File metadata and controls
374 lines (347 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Maintenance Logging - webhook ingest + graphable HA history
# Related Issue: 1816
# Stores water softener salt maintenance events in recorder-backed helpers.
# -------------------------------------------------------------------
# Video: https://youtu.be/_OqTv9i-jAs
# Blog: https://www.vcloudinfo.com/2026/07/home-assistant-water-softener-salt-automation.html
# Notes: Webhook id is bearclaw_maintenance_log_v1; internal maintenance_log events reuse the same queued processor.
# Notes: The water-softener NFC tag uses a short single-run guard to suppress duplicate phone scans.
# Notes: Duplicate event_id values are ignored to prevent double-count totals.
# Notes: Recent event history string format is "YYYY-MM-DD HH:MM:SS|amount|note||...".
# Notes: Long-term stats live on template sensors; helpers are internal state for restore/calculation.
######################################################################
input_number:
water_softener_salt_last_amount_lb:
name: "Softener salt last amount"
min: 0
max: 1000
step: 0.1
mode: box
unit_of_measurement: lb
icon: mdi:shaker
water_softener_salt_total_added_lb:
name: "Softener salt total added"
min: 0
max: 100000
step: 0.1
mode: box
unit_of_measurement: lb
icon: mdi:chart-line
counter:
water_softener_salt_event_count:
name: "Softener salt event count"
step: 1
icon: mdi:counter
input_datetime:
water_softener_salt_last_occurred_at:
name: "Softener salt last occurred at"
has_date: true
has_time: true
icon: mdi:calendar-clock
input_text:
water_softener_salt_last_note:
name: "Softener salt last note"
max: 255
icon: mdi:text-box-outline
water_softener_salt_recent_event_ids:
name: "Softener salt recent event ids"
max: 255
icon: mdi:identifier
water_softener_salt_recent_events:
name: "Softener salt recent events"
max: 255
icon: mdi:history
template:
- sensor:
- name: "Water Softener Salt Days Since Last Add"
unique_id: water_softener_salt_days_since_last_add
unit_of_measurement: d
availability: >-
{% set raw = states('input_datetime.water_softener_salt_last_occurred_at') %}
{% if raw in ['unknown', 'unavailable', 'none', ''] %}
false
{% else %}
{{ as_timestamp(as_local(as_datetime(raw)), default=none) is not none }}
{% endif %}
state: >-
{% set raw = states('input_datetime.water_softener_salt_last_occurred_at') %}
{% set event_ts = as_timestamp(as_local(as_datetime(raw)), default=none) %}
{% if event_ts is not none %}
{{ [((as_timestamp(now()) - event_ts) / 86400), 0] | max | round(1) }}
{% else %}
0
{% endif %}
- name: "Water Softener Salt Last Summary"
unique_id: water_softener_salt_last_summary
icon: mdi:clipboard-text-clock-outline
state: >-
{% set raw = states('input_datetime.water_softener_salt_last_occurred_at') %}
{% set amount = states('input_number.water_softener_salt_last_amount_lb') | float(0) %}
{% set note = states('input_text.water_softener_salt_last_note') %}
{% if raw in ['unknown', 'unavailable', 'none', ''] %}
No salt events logged yet.
{% else %}
{% set when = as_datetime(raw).astimezone().strftime('%a %b %d, %Y %I:%M %p') | replace(' 0', ' ') %}
{% if note in ['unknown', 'unavailable', 'none', ''] %}
{{ amount | round(1) }} lb on {{ when }}
{% else %}
{{ amount | round(1) }} lb on {{ when }} - {{ note }}
{% endif %}
{% endif %}
- name: "Water Softener Salt Average Days Between Refills"
unique_id: water_softener_salt_average_days_between_refills
unit_of_measurement: d
state_class: measurement
state: >-
{% set raw = states('input_text.water_softener_salt_recent_events') %}
{% if raw in ['unknown', 'unavailable', 'none', ''] %}
unknown
{% else %}
{% set entries = raw.split('||') %}
{% set ns = namespace(previous_ts=none, total_days=0, sample_count=0) %}
{% for entry in entries %}
{% set parts = entry.split('|') %}
{% set dt = as_datetime(parts[0] | default('', true) | trim, default=none) %}
{% if dt is not none %}
{% set ts = as_timestamp(dt, default=none) %}
{% if ts is not none %}
{% if ns.previous_ts is not none and ns.previous_ts > ts %}
{% set ns.total_days = ns.total_days + ((ns.previous_ts - ts) / 86400) %}
{% set ns.sample_count = ns.sample_count + 1 %}
{% endif %}
{% set ns.previous_ts = ts %}
{% endif %}
{% endif %}
{% endfor %}
{% if ns.sample_count > 0 %}
{{ (ns.total_days / ns.sample_count) | round(1) }}
{% else %}
unknown
{% endif %}
{% endif %}
- name: "Water Softener Salt Refill Count"
unique_id: water_softener_salt_refill_count
icon: mdi:counter
state_class: total
state: "{{ states('counter.water_softener_salt_event_count') | int(0) }}"
- name: "Water Softener Salt Total Added"
unique_id: water_softener_salt_total_added
unit_of_measurement: lb
device_class: weight
state_class: total
state: "{{ states('input_number.water_softener_salt_total_added_lb') | float(0) | round(1) }}"
automation:
- alias: "Maintenance Log - Water Softener Salt NFC"
id: water_softener_salt_nfc_refill
description: Log one 80 lb salt refill when the bound water-softener NFC sticker is scanned
mode: single
max_exceeded: silent
trigger:
- platform: tag
tag_id: d7d89d9a-5351-44fc-a26c-011f197e9433
action:
- event: maintenance_log
event_data:
item_key: water_softener_salt
action: add
amount_value: 80
amount_unit: lb
event_id: "nfc_water_softener_salt_{{ trigger.event.context.id }}"
occurred_at: "{{ now().isoformat() }}"
actor: home_assistant_nfc
raw_text: "Water softener salt refill NFC sticker scanned."
parse_confidence: deterministic
- delay: "00:00:10"
- alias: "Maintenance Log - Joanna Webhook Ingest"
id: 1c9fba4f-fef5-4da9-82d4-4049deff17cf
description: Process webhook and internal maintenance events through one deduplicated recorder-backed path
mode: queued
max: 30
trigger:
- platform: webhook
id: webhook
webhook_id: bearclaw_maintenance_log_v1
allowed_methods:
- POST
- PUT
local_only: false
- platform: event
id: internal_event
event_type: maintenance_log
variables:
payload: >-
{% if trigger.id == 'webhook' and trigger.json is mapping %}
{{ trigger.json }}
{% elif trigger.id == 'internal_event' and trigger.event.data is mapping %}
{{ trigger.event.data }}
{% else %}
{{ dict() }}
{% endif %}
source_item: "{{ payload.item_key | default('', true) | string | trim | lower }}"
item_key: >-
{% set aliases = {
'water_softener_salt': 'water_softener_salt',
'softener_salt': 'water_softener_salt',
'water_softener': 'water_softener_salt',
'softener': 'water_softener_salt',
'water softener salt': 'water_softener_salt',
'salt': 'water_softener_salt',
'softner': 'water_softener_salt'
} %}
{{ aliases.get(source_item, source_item) }}
action: "{{ payload.action | default('add', true) | string | trim | lower }}"
source_unit: "{{ payload.amount_unit | default('lb', true) | string | trim | lower }}"
unit: >-
{% set units = {
'lb': 'lb',
'lbs': 'lb',
'pound': 'lb',
'pounds': 'lb'
} %}
{{ units.get(source_unit, source_unit) }}
amount_value: "{{ payload.amount_value | default(0, true) | float(0) }}"
amount_lb: >-
{% if unit == 'lb' %}
{{ amount_value | round(2) }}
{% else %}
0
{% endif %}
event_id: >-
{% set raw_id = payload.event_id | default('', true) | string | trim %}
{% if raw_id %}
{{ raw_id }}
{% else %}
maint_{{ now().timestamp() | int }}
{% endif %}
occurred_at_raw: "{{ payload.occurred_at | default(now().isoformat(), true) }}"
occurred_at: >-
{% set parsed = as_datetime(occurred_at_raw, default=none) %}
{% if parsed is none %}
{{ now().isoformat() }}
{% else %}
{{ parsed.isoformat() }}
{% endif %}
occurred_local_datetime: >-
{% set dt = as_local(as_datetime(occurred_at, default=now())) %}
{{ dt.strftime('%Y-%m-%d %H:%M:%S') }}
occurred_display: >-
{% set dt = as_datetime(occurred_local_datetime, default=now()) %}
{{ dt.strftime('%Y-%m-%d %I:%M %p') | replace(' 0', ' ') }}
actor: "{{ payload.actor | default('unknown', true) | string | trim }}"
raw_text: "{{ payload.raw_text | default('', true) | string | replace('|', '/') | trim }}"
parse_confidence: "{{ payload.parse_confidence | default('unknown', true) | string | trim }}"
is_duplicate: >-
{% set existing = (states('input_text.water_softener_salt_recent_event_ids')
| default('', true) | string).split('|') %}
{{ event_id in existing }}
is_supported_item: "{{ item_key == 'water_softener_salt' }}"
is_add_action: "{{ action in ['add', 'top_off', 'refill'] }}"
effective_amount_lb: >-
{% if item_key == 'water_softener_salt' and action in ['add', 'top_off', 'refill'] %}
80
{% else %}
{{ amount_lb }}
{% endif %}
note_value: >-
{% set text = raw_text if raw_text else 'Logged by Joanna webhook.' %}
{{ text | truncate(255, true, '') }}
recent_event_line: "{{ occurred_local_datetime }}|{{ effective_amount_lb | round(1) }} lb|{{ note_value }}"
next_recent_events: >-
{% set current = (states('input_text.water_softener_salt_recent_events')
| default('', true) | string).split('||') %}
{% set ns = namespace(items=[recent_event_line]) %}
{% for raw in current %}
{% set value = raw | trim %}
{% if value and value not in ['unknown', 'unavailable', 'none'] and value != recent_event_line and (ns.items | count) < 10 %}
{% set ns.items = ns.items + [value] %}
{% endif %}
{% endfor %}
{{ ns.items | join('||') }}
next_recent_ids: >-
{% set current = (states('input_text.water_softener_salt_recent_event_ids')
| default('', true) | string).split('|') %}
{% set ns = namespace(items=[event_id]) %}
{% for raw in current %}
{% set value = raw | trim %}
{% if value and value not in ['unknown', 'unavailable', 'none'] and value != event_id and (ns.items | count) < 8 %}
{% set ns.items = ns.items + [value] %}
{% endif %}
{% endfor %}
{{ ns.items | join('|') }}
current_total_lb: "{{ states('input_number.water_softener_salt_total_added_lb') | float(0) }}"
next_total_lb: >-
{% if is_add_action %}
{{ (current_total_lb + effective_amount_lb) | round(2) }}
{% else %}
{{ current_total_lb | round(2) }}
{% endif %}
action:
- choose:
- conditions:
- condition: template
value_template: "{{ not is_supported_item }}"
sequence:
- service: script.send_to_logbook
data:
topic: MAINTENANCE
message: >-
Ignored unsupported maintenance item "{{ item_key }}" (event_id={{ event_id }}).
- conditions:
- condition: template
value_template: "{{ effective_amount_lb <= 0 }}"
sequence:
- service: script.send_to_logbook
data:
topic: MAINTENANCE
message: >-
Ignored maintenance payload with invalid amount (event_id={{ event_id }}, raw_amount={{ amount_value }} {{ unit }}).
- conditions:
- condition: template
value_template: "{{ is_duplicate }}"
sequence:
- service: script.send_to_logbook
data:
topic: MAINTENANCE
message: >-
Duplicate maintenance event ignored for softener salt (event_id={{ event_id }}).
default:
- service: input_number.set_value
data:
entity_id: input_number.water_softener_salt_last_amount_lb
value: "{{ effective_amount_lb }}"
- service: input_number.set_value
data:
entity_id: input_number.water_softener_salt_total_added_lb
value: "{{ next_total_lb }}"
- service: counter.increment
data:
entity_id: counter.water_softener_salt_event_count
- service: input_datetime.set_datetime
data:
entity_id: input_datetime.water_softener_salt_last_occurred_at
datetime: "{{ occurred_local_datetime }}"
- service: input_text.set_value
data:
entity_id: input_text.water_softener_salt_last_note
value: "{{ note_value }}"
- service: input_text.set_value
data:
entity_id: input_text.water_softener_salt_recent_event_ids
value: "{{ next_recent_ids | truncate(255, true, '') }}"
- service: input_text.set_value
data:
entity_id: input_text.water_softener_salt_recent_events
value: "{{ next_recent_events | truncate(255, true, '') }}"
- service: script.send_to_logbook
data:
topic: MAINTENANCE
message: >-
Softener salt logged: {{ effective_amount_lb | round(1) }} lb at {{ occurred_display }}.
total={{ next_total_lb | round(1) }} lb, count={{ states('counter.water_softener_salt_event_count') | int(0) + 1 }},
actor={{ actor }}, confidence={{ parse_confidence }}, event_id={{ event_id }},
raw="{{ raw_text | truncate(110, true, '...') }}".