diff --git a/automations/05_printers_and_workshop/05c_octoeverywhere_google_chat.yaml b/automations/05_printers_and_workshop/05c_octoeverywhere_google_chat.yaml new file mode 100644 index 0000000..c1a23fe --- /dev/null +++ b/automations/05_printers_and_workshop/05c_octoeverywhere_google_chat.yaml @@ -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('&', '&') | replace('<', '<') | replace('>', '>'), '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*, ) + 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 }}" diff --git a/automations/05_printers_and_workshop/05c_octoeverywhere_notify_events.yaml b/automations/05_printers_and_workshop/05c_octoeverywhere_notify_events.yaml deleted file mode 100644 index e241fd1..0000000 --- a/automations/05_printers_and_workshop/05c_octoeverywhere_notify_events.yaml +++ /dev/null @@ -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 + ['File: ' ~ p.FileName] %}{% endif -%} - {%- if p.Progress is defined and ev not in [1, 2] %}{% set parts = parts + ['Progress: ' ~ 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 + ['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 parts = parts + ['Ran for: ' ~ (d // 3600) ~ 'h ' ~ ((d % 3600) // 60) ~ 'm'] -%} - {%- endif -%} - {%- if p.Error %}{% set parts = parts + ['Error: ' ~ p.Error] %}{% endif -%} - {%- if p.QuickViewUrl %}{% set parts = parts + ['Open printer'] %}{% endif -%} - {{ parts | join('
') }} - - # 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 }}" diff --git a/helpers/rest_command.yaml b/helpers/rest_command.yaml index bb136cd..712945f 100644 --- a/helpers/rest_command.yaml +++ b/helpers/rest_command.yaml @@ -35,3 +35,12 @@ notion_update_print: "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 }}"