#!/usr/bin/env python3 """ Configure Home Assistant (14.4.4.20:8123) Notification Forwarding & Infrastructure Automations: 1. Updates gotify-discord-bridge on 140.44.4.71 with Home Assistant logbook.log watcher. 2. Creates 'script.snarfnet_broadcast' in Home Assistant for unified Gotify + Discord + Mobile App alerts. 3. Creates 'Snarfnet UPS Power & Exhaust Protocol' and 'Snarfnet Server Room Thermal Guard' automations. 4. Enhances the 4 Bambu H2D 3D Printer automations to also broadcast to Gotify & Discord #home-assistant. 5. Triggers a live end-to-end verification notification with real-time Server Room & UPS telemetry. """ import json import sys import time import urllib.request import paramiko sys.stdout.reconfigure(encoding="utf-8") HA_URL = "http://14.4.4.20:8123" HA_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkZWNlMWI5ZjJhZDQ0MTRmODU4NGYzN2I1YjgwZDQxMiIsImlhdCI6MTc5MzQ1MjQ2MywiZXhwIjoyMTA4ODEyNDYzfQ.JznpOqsCSnHz6aTVa8K9pQCz1816DC8SrmS1xn1Sm2k" def ha_req(path, method="GET", data=None): req = urllib.request.Request( f"{HA_URL}{path}", data=json.dumps(data).encode("utf-8") if data is not None else None, headers={ "Authorization": f"Bearer {HA_TOKEN}", "Content-Type": "application/json", }, method=method, ) with urllib.request.urlopen(req, timeout=12) as r: body = r.read().decode("utf-8") return json.loads(body) if body else {} def main(): # 1. Update discord_bridge.py on 140.44.4.71 and restart bridge container with open(r"c:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\scripts\gotify_discord_bridge.py", "r", encoding="utf-8") as f: bridge_code = f.read() ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect("140.44.4.71", username="snarf", password="Katzir476!", timeout=10) sftp = ssh.open_sftp() with sftp.file("/tmp/discord_bridge.py", "w") as rf: rf.write(bridge_code) sftp.close() cmd = r"""echo 'Katzir476!' | sudo -S bash -c ' rm -f /var/lib/docker/volumes/gotify_gotify_data/_data/ha_last_logbook_when.txt /mnt/swarm_shared/legacy_volumes/gotify/ha_last_logbook_when.txt cp /tmp/discord_bridge.py /var/lib/docker/volumes/gotify_gotify_data/_data/discord_bridge.py cp /tmp/discord_bridge.py /mnt/swarm_shared/legacy_volumes/gotify/discord_bridge.py chmod 755 /var/lib/docker/volumes/gotify_gotify_data/_data/discord_bridge.py /mnt/swarm_shared/legacy_volumes/gotify/discord_bridge.py docker service scale gotify_gotify-discord-bridge=0 docker rm -f $(docker ps -aq -f name=gotify_gotify-discord-bridge) 2>/dev/null || true docker service scale gotify_gotify-discord-bridge=1 '""" stdin, stdout, stderr = ssh.exec_command(cmd) stdout.read() print("Updated and restarted gotify_gotify-discord-bridge on 140.44.4.71.") # 2. Provision script.snarfnet_broadcast in Home Assistant broadcast_script = { "alias": "Snarfnet Broadcast Notification", "description": "Broadcasts alerts to Gotify, Discord #home-assistant webhook, and Pixel Companion Apps", "mode": "parallel", "fields": { "title": {"description": "Notification title", "example": "Smart Home Alert"}, "message": {"description": "Notification body", "example": "Event details"}, "notification_id": {"description": "Unique notification key", "example": "snarfnet_alert"}, }, "sequence": [ { "action": "logbook.log", "data": { "name": "{{ title | default('Home Assistant Alert') }}", "message": "{{ message }}", "entity_id": "script.snarfnet_broadcast", }, }, { "action": "persistent_notification.create", "data": { "title": "{{ title | default('Home Assistant Alert') }}", "message": "{{ message }}", "notification_id": "{{ notification_id | default('snarfnet_' ~ (now().timestamp() | int)) }}", }, }, { "action": "notify.mobile_app_pixel_10_pro_fold", "data": { "title": "{{ title | default('Home Assistant Alert') }}", "message": "{{ message }}", }, }, { "action": "notify.mobile_app_pixel_9_pro_fold", "data": { "title": "{{ title | default('Home Assistant Alert') }}", "message": "{{ message }}", }, }, ], } ha_req("/api/config/script/config/snarfnet_broadcast", method="POST", data=broadcast_script) print("Configured script.snarfnet_broadcast in Home Assistant.") # 3. Provision UPS Power State & Exhaust Fan Automation ups_automation = { "id": "1793453000001", "alias": "Snarfnet UPS Power & Exhaust Protocol", "description": "Broadcasts UPS state transitions to Gotify/Discord/Mobile and engages Server Closet exhaust fan on battery", "triggers": [ { "trigger": "state", "entity_id": ["sensor.server_closet_status_data"], } ], "conditions": [ { "condition": "template", "value_template": "{{ trigger.from_state.state != trigger.to_state.state and trigger.to_state.state not in ['unknown', 'unavailable'] }}", } ], "actions": [ { "action": "script.snarfnet_broadcast", "data": { "title": "⚡ UPS Status Changed: {{ states('sensor.server_closet_status') }} ({{ states('sensor.server_closet_status_data') }})", "message": "Server Closet UPS transitioned from {{ trigger.from_state.state }} to {{ trigger.to_state.state }}. Battery: {{ states('sensor.server_closet_battery_charge') }}% | Load: {{ states('sensor.server_closet_load') }}% | Input: {{ states('sensor.server_closet_input_voltage') }}V | Closet Temp: {{ states('sensor.server_room_temperature_temperature') }}°F.", "notification_id": "snarfnet_ups_state", }, }, { "choose": [ { "conditions": [ { "condition": "template", "value_template": "{{ 'OB' in states('sensor.server_closet_status_data') or 'LB' in states('sensor.server_closet_status_data') }}", } ], "sequence": [ { "action": "fan.set_percentage", "target": {"entity_id": "fan.server_closet_switch"}, "data": {"percentage": 100}, } ], } ] }, ], "mode": "queued", } ha_req("/api/config/automation/config/1793453000001", method="POST", data=ups_automation) print("Configured automation 'Snarfnet UPS Power & Exhaust Protocol' (1793453000001).") # 4. Provision Server Room Thermal Guard Automation thermal_automation = { "id": "1793453000002", "alias": "Snarfnet Server Room Thermal Guard", "description": "Automatically engages Server Closet exhaust fan and broadcasts alert if Server Room exceeds 82°F", "triggers": [ { "trigger": "numeric_state", "entity_id": ["sensor.server_room_temperature_temperature"], "above": 82, "for": {"hours": 0, "minutes": 2, "seconds": 0}, } ], "conditions": [], "actions": [ { "action": "fan.set_percentage", "target": {"entity_id": "fan.server_closet_switch"}, "data": {"percentage": 100}, }, { "action": "script.snarfnet_broadcast", "data": { "title": "🌡️ High Server Room Temperature Warning", "message": "Server Room temperature has reached {{ states('sensor.server_room_temperature_temperature') }}°F (Humidity: {{ states('sensor.server_room_temperature_humidity') }}%). Server Closet exhaust fan engaged at 100%.", "notification_id": "snarfnet_thermal_warning", }, }, ], "mode": "single", } ha_req("/api/config/automation/config/1793453000002", method="POST", data=thermal_automation) print("Configured automation 'Snarfnet Server Room Thermal Guard' (1793453000002).") # 5. Enhance the 4 existing 3D Printer automations to call script.snarfnet_broadcast printer_autos = [ ("1788367985433", "🖨️ 3D Print Started", "A 3D print has started on Bambu H2D.", "h2d_print_start"), ("1788368709567", "✅ 3D Print Completed", "A 3D print has finished on Bambu H2D.", "h2d_print_complete"), ("1788370272217", "⚠️ 3D Print Stalled", "A 3D print has stalled/paused on Bambu H2D!", "h2d_print_stalled"), ("1788370321997", "🚨 3D Print Failed", "A 3D print has failed on Bambu H2D!", "h2d_print_failed"), ] for aid, title, msg, nid in printer_autos: cfg = ha_req(f"/api/config/automation/config/{aid}") # Filter out old persistent_notification/snarfnet_broadcast actions and append clean script.snarfnet_broadcast actions = [a for a in cfg.get("actions", []) if a.get("action") not in ("persistent_notification.create", "script.snarfnet_broadcast")] actions.append( { "action": "script.snarfnet_broadcast", "metadata": {}, "data": { "title": title, "message": msg, "notification_id": nid, }, } ) cfg["actions"] = actions ha_req(f"/api/config/automation/config/{aid}", method="POST", data=cfg) print(f"Enhanced 3D Printer automation [{aid}] '{cfg.get('alias')}' with script.snarfnet_broadcast.") ha_req("/api/services/script/reload", method="POST", data={}) ha_req("/api/services/automation/reload", method="POST", data={}) # Wait 5s for restarted bridge container to finish [INIT-HA] time.sleep(5) # 6. Trigger live test via script.snarfnet_broadcast ha_req( "/api/services/script/snarfnet_broadcast", method="POST", data={ "title": "Home Assistant <-> Gotify <-> Discord Linked", "message": "Live Telemetry: Server Room {{ states('sensor.server_room_temperature_temperature') }}°F ({{ states('sensor.server_room_temperature_humidity') }}% RH) | UPS: {{ states('sensor.server_closet_status') }} ({{ states('sensor.server_closet_battery_charge') }}% Battery, {{ states('sensor.server_closet_load') }}% Load, {{ states('sensor.server_closet_input_voltage') }}V).", "notification_id": "snarfnet_ha_bridge_verified", }, ) print("Triggered live script.snarfnet_broadcast test in Home Assistant.") time.sleep(5) stdin, stdout, stderr = ssh.exec_command("echo 'Katzir476!' | sudo -S docker logs $(docker ps -q -f name=gotify_gotify-discord-bridge) --tail 25 2>&1") print("Bridge logs:\n", stdout.read().decode("utf-8", errors="ignore")) ssh.close() if __name__ == "__main__": main()