Send OctoEverywhere alerts to Google Chat instead of Notify.Events

Replace the Notify.Events delivery with a Google Chat space webhook. The
automation builds a cardsV2 card (printer/event header, print details,
camera snapshot, Open printer button) and falls back to plain text if
Google rejects the card or the request fails.

Adds rest_command.google_chat_octoeverywhere, which needs
google_chat_octoeverywhere_webhook in secrets.yaml on the host. The
notify_event_octoeverywhere_token secret is no longer used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Joshua King
2026-11-04 13:44:51 -05:00
co-authored by Claude Opus 5.5
parent 7831b35af2
commit 54eaa88616
3 changed files with 133 additions and 100 deletions
@@ -0,0 +1,124 @@
### OctoEverywhere -> Google Chat ###
# OctoEverywhere (LAN mode) POSTs fixed JSON to this webhook. Google Chat needs a
# `text` or `cardsV2` body, so this automation builds a card and sends it through
# rest_command.google_chat_octoeverywhere (helpers/rest_command.yaml).
# Requires `google_chat_octoeverywhere_webhook` in secrets.yaml.
#
# Card: header (printer + event), details (file, progress, layer, time, error),
# camera snapshot, and an "Open printer" button to OctoEverywhere Quick View.
# If Google rejects the card or the request fails, a plain-text message is sent.
#
# Event types: https://docs.octoeverywhere.com/webhook-notifications/event-types/
# Forwarded ("important only"): 1-5, 7-12, 16. Skipped: 6 Progress, 13, 14, 15.
- id: '1793812953197'
alias: OctoEverywhere to Google Chat
description: Turns OctoEverywhere webhook JSON into Google Chat cards
mode: queued
max: 10
triggers:
- trigger: webhook
webhook_id: octoeverywhere-7301a739d954078c5a5c5fb3
allowed_methods:
- POST
local_only: true
actions:
# HA only fills trigger.json when the request's Content-Type contains "json".
# Otherwise the body is dropped and p is empty, which the safety net catches.
- variables:
p: "{{ trigger.json | default({}, true) }}"
ev: "{{ p.EventType | default(0) | int(0) }}"
wanted: [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 16]
names:
"1": Print started
"2": Print complete
"3": Print failed
"4": Print paused
"5": Print resumed
"6": Print progress
"7": Gadget possible failure
"8": Gadget paused the print
"9": Printer error
"10": First layer complete
"11": Filament change required
"12": Needs your attention
"13": OctoEverywhere daily notification limit reached
"14": Third layer complete
"15": Bed cool down complete
"16": Test notification
# Safety net: body wasn't readable JSON, so say so instead of failing silently
- if:
- condition: template
value_template: "{{ not p }}"
then:
- action: rest_command.google_chat_octoeverywhere
data:
payload:
text: "⚠️ *OctoEverywhere webhook problem:* Home Assistant got a webhook it could not read as JSON. Check the trace for the \"OctoEverywhere to Google Chat\" automation."
- stop: Body was not JSON
# Drop events we don't want
- condition: template
value_template: "{{ ev in wanted }}"
# Build the message
- variables:
printer: "{{ p.PrinterName | default('Printer', true) | string }}"
event_name: "{{ names[ev | string] | default('Event ' ~ ev, true) }}"
emoji: "{{ '🛑' if ev in [3, 8, 9] else '⚠️' if ev in [4, 7, 11, 12] else '✅' if ev == 2 else '🖨️' }}"
# [label, value] rows, shared by the card and the plain-text fallback
details: >-
{%- set rows = [] -%}
{%- if p.FileName %}{% set rows = rows + [['File', p.FileName | string]] %}{% endif -%}
{%- if p.Progress is defined and ev not in [1, 2] %}{% set rows = rows + [['Progress', (p.Progress | int(0)) ~ '%']] %}{% endif -%}
{%- if (p.TotalLayers | int(0)) > 0 and ev not in [1, 2] %}{% set rows = rows + [['Layer', (p.CurrentLayer | int(0)) ~ ' of ' ~ (p.TotalLayers | int(0))]] %}{% endif -%}
{%- if (p.TimeRemainingSec | int(0)) > 0 and ev not in [2, 3] -%}
{%- set s = p.TimeRemainingSec | int(0) -%}
{%- set rows = rows + [['Time left', (s // 3600) ~ 'h ' ~ ((s % 3600) // 60) ~ 'm']] -%}
{%- endif -%}
{%- if p.DurationSec is defined and ev in [2, 3] -%}
{%- set d = p.DurationSec | int(0) -%}
{%- set rows = rows + [['Ran for', (d // 3600) ~ 'h ' ~ ((d % 3600) // 60) ~ 'm']] -%}
{%- endif -%}
{%- if p.Error %}{% set rows = rows + [['Error', p.Error | string]] %}{% endif -%}
{{ rows }}
# Google Chat Message body: short text (shows in phone notifications) + card
card: >-
{%- set ns = namespace(w=[]) -%}
{%- for r in details -%}
{%- set ns.w = ns.w + [{'decoratedText': {'topLabel': r[0], 'text': r[1] | replace('&', '&amp;') | replace('<', '&lt;') | replace('>', '&gt;'), 'wrapText': true}}] -%}
{%- endfor -%}
{%- set sections = [{'widgets': ns.w if ns.w else [{'decoratedText': {'text': event_name, 'wrapText': true}}]}] -%}
{%- if p.SnapshotUrl %}{% set sections = sections + [{'widgets': [{'image': {'imageUrl': p.SnapshotUrl | string, 'altText': printer ~ ' camera snapshot'}}]}] %}{% endif -%}
{%- if p.QuickViewUrl %}{% set sections = sections + [{'widgets': [{'buttonList': {'buttons': [{'text': 'Open printer', 'onClick': {'openLink': {'url': p.QuickViewUrl | string}}}]}}]}] %}{% endif -%}
{{ {'text': emoji ~ ' *' ~ printer ~ ':* ' ~ event_name,
'cardsV2': [{'cardId': 'octoeverywhere-' ~ ev,
'card': {'header': {'title': printer, 'subtitle': emoji ~ ' ' ~ event_name},
'sections': sections}}]} }}
# Plain-text fallback in Google Chat markup (*bold*, <url|label>)
plain: >-
{%- set ns = namespace(lines=[emoji ~ ' *' ~ printer ~ ':* ' ~ event_name]) -%}
{%- for r in details %}{% set ns.lines = ns.lines + ['*' ~ r[0] ~ ':* ' ~ r[1]] %}{% endfor -%}
{%- if p.SnapshotUrl %}{% set ns.lines = ns.lines + ['<' ~ p.SnapshotUrl ~ '|Snapshot>'] %}{% endif -%}
{%- if p.QuickViewUrl %}{% set ns.lines = ns.lines + ['<' ~ p.QuickViewUrl ~ '|Open printer>'] %}{% endif -%}
{{ ns.lines | join('\n') }}
# Send the card
- action: rest_command.google_chat_octoeverywhere
data:
payload: "{{ card }}"
response_variable: chat_resp
continue_on_error: true
# rest_command doesn't raise on HTTP 4xx/5xx, so check the status. If the card
# was rejected (or the request failed), send plain text so the alert still lands.
- if:
- condition: template
value_template: "{{ chat_resp is not defined or (chat_resp.status | int(0)) != 200 }}"
then:
- delay: 2
- action: rest_command.google_chat_octoeverywhere
data:
payload:
text: "{{ plain }}"
@@ -1,100 +0,0 @@
### OctoEverywhere -> Notify.Events ###
# OctoEverywhere (LAN mode) POSTs fixed JSON to this webhook. Notify.Events can't
# parse that JSON itself, so this automation turns it into a readable alert.
#
# Token: the notify_events platform ignores per-platform api_key and always uses
# the global notify_events token (Cats). Passing `token` inside `data` overrides
# it per message, same pattern as script.notify_events_alerts.
# Requires `notify_event_octoeverywhere_token` in secrets.yaml (the Home Assistant
# source token from the Notify.Events "OctoEverywhere" channel).
#
# Event types: https://docs.octoeverywhere.com/webhook-notifications/event-types/
# Forwarded ("important only"): 1-5, 7-12, 16. Skipped: 6 Progress, 13, 14, 15.
- id: '1793812953197'
alias: OctoEverywhere to Notify.Events
description: Turns OctoEverywhere webhook JSON into readable Notify.Events alerts
mode: queued
max: 10
triggers:
- trigger: webhook
webhook_id: octoeverywhere-7301a739d954078c5a5c5fb3
allowed_methods:
- POST
local_only: true
actions:
# HA only fills trigger.json when the request's Content-Type contains "json".
# Otherwise the body is dropped and p is empty, which the safety net catches.
- variables:
p: "{{ trigger.json | default({}, true) }}"
ev: "{{ p.EventType | default(0) | int(0) }}"
wanted: [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 16]
names:
"1": Print started
"2": Print complete
"3": Print failed
"4": Print paused
"5": Print resumed
"6": Print progress
"7": Gadget possible failure
"8": Gadget paused the print
"9": Printer error
"10": First layer complete
"11": Filament change required
"12": Needs your attention
"13": OctoEverywhere daily notification limit reached
"14": Third layer complete
"15": Bed cool down complete
"16": Test notification
# Safety net: body wasn't readable JSON, so say so instead of failing silently
- if:
- condition: template
value_template: "{{ not p }}"
then:
- action: notify.notify_events_alerts
data:
message: OctoEverywhere sent a webhook that Home Assistant could not read as JSON. Check this automation's trace.
data:
token: !secret notify_event_octoeverywhere_token
title: OctoEverywhere webhook problem
level: warning
- stop: Body was not JSON
# Drop events we don't want
- condition: template
value_template: "{{ ev in wanted }}"
# Build the message
- variables:
printer: "{{ p.PrinterName | default('Printer', true) }}"
event_name: "{{ names[ev | string] | default('Event ' ~ ev, true) }}"
level: "{{ 'error' if ev in [3, 8, 9] else 'warning' if ev in [4, 7, 11, 12] else 'success' if ev == 2 else 'info' }}"
priority: "{{ 'high' if ev in [3, 4, 7, 8, 9, 11, 12] else 'normal' }}"
images: "{{ [{'url': p.SnapshotUrl, 'name': 'snapshot.jpg'}] if p.SnapshotUrl else [] }}"
body: >-
{%- set parts = [] -%}
{%- if p.FileName %}{% set parts = parts + ['<b>File:</b> ' ~ p.FileName] %}{% endif -%}
{%- if p.Progress is defined and ev not in [1, 2] %}{% set parts = parts + ['<b>Progress:</b> ' ~ p.Progress ~ '%'] %}{% endif -%}
{%- if p.TimeRemainingSec is defined and (p.TimeRemainingSec | int(0)) > 0 and ev not in [2, 3] -%}
{%- set s = p.TimeRemainingSec | int(0) -%}
{%- set parts = parts + ['<b>Time left:</b> ' ~ (s // 3600) ~ 'h ' ~ ((s % 3600) // 60) ~ 'm'] -%}
{%- endif -%}
{%- if p.DurationSec is defined and ev in [2, 3] -%}
{%- set d = p.DurationSec | int(0) -%}
{%- set parts = parts + ['<b>Ran for:</b> ' ~ (d // 3600) ~ 'h ' ~ ((d % 3600) // 60) ~ 'm'] -%}
{%- endif -%}
{%- if p.Error %}{% set parts = parts + ['<b>Error:</b> ' ~ p.Error] %}{% endif -%}
{%- if p.QuickViewUrl %}{% set parts = parts + ['<a href="' ~ p.QuickViewUrl ~ '">Open printer</a>'] %}{% endif -%}
{{ parts | join('<br>') }}
# Send it. If a snapshot ever breaks delivery, remove the images line.
- action: notify.notify_events_alerts
data:
message: "{{ body if body else event_name }}"
data:
token: !secret notify_event_octoeverywhere_token
title: "{{ printer }}: {{ event_name }}"
level: "{{ level }}"
priority: "{{ priority }}"
images: "{{ images }}"
+9
View File
@@ -35,3 +35,12 @@ notion_update_print:
"date:Completed:start": { "date": { "start": "{{ completed }}", "time_zone": "America/New_York" } } "date:Completed:start": { "date": { "start": "{{ completed }}", "time_zone": "America/New_York" } }
} }
} }
# Google Chat incoming webhook (space webhook) for OctoEverywhere printer alerts.
# Call with `payload` = a dict shaped like a Chat Message body (text and/or cardsV2).
# Used by automations/05_printers_and_workshop/05c_octoeverywhere_google_chat.yaml.
google_chat_octoeverywhere:
url: !secret google_chat_octoeverywhere_webhook
method: POST
content_type: "application/json; charset=UTF-8"
payload: "{{ payload | to_json }}"