I just restored an older alarm system and added email alerts. I used the external siren output as a trigger into an optocoupler, which safely bridges the voltage to a Raspberry Pi Pico. When the alarm activates, the Pico detects the signal and sends an email alert. Pretty cool. If you find any thing in the code you recommend fixing let me know. Merry Christmas, this took a lot of testing to get right, AI helped me make this and I hope you enjoy it without all the headaches I had getting it to work right.
Also use a secrets.py with it for your passwords and email
Code:
I used a 1CH Optocoupler PC817 1 Channel Isolation Board Voltage Converter Adapter Module 3.6-30V Driver Photoelectric Isolated Module (1CH Optocoupler)
Also use a secrets.py with it for your passwords and email
Code:
Code:
import timeimport boardimport digitalioimport wifiimport socketpoolimport sslimport binasciiimport structimport rtcimport microcontrollerimport adafruit_ntpfrom secrets import secrets# ---- CONFIG ----DEV_MODE = FalseWIFI_SSID = secrets["WIFI_SSID"]WIFI_PASS = secrets["WIFI_PASS"]SMTP_SERVER = "smtp.gmail.com"SMTP_PORT = 465EMAIL_FROM = secrets["EMAIL_FROM"]EMAIL_TO = secrets["EMAIL_TO"]APP_PASSWORD = secrets["APP_PASSWORD"]SIREN_PIN = board.GP15LED_PIN = board.LEDNTP_SERVER = "192.168.1.1"NTP_PORT = 123NTP_SYNC_INTERVAL = 3600RETRY_INTERVAL = 30NTP_BOOT_TIMEOUT = 30 # secondsDEBOUNCE_INTERVAL = 0.5WATCHDOG_TIMEOUT = 180ALARM_THROTTLE = 120EMAIL_QUEUE_MAX = 20WIFI_FAIL_THRESHOLD = 3ALARM_LOCATION = "Address Here"# ---- GPIO ----siren = digitalio.DigitalInOut(SIREN_PIN)siren.direction = digitalio.Direction.INPUTsiren.pull = digitalio.Pull.UPled = digitalio.DigitalInOut(LED_PIN)led.direction = digitalio.Direction.OUTPUT# ---- RTC ----rtc_obj = rtc.RTC()rtc_obj.datetime = (2025, 1, 1, 0, 0, 0, 0, 0, 1) # yearday=1# ---- TIME HELPERS ----def now_ts(): """Return timestamp (seconds since epoch) from RTC in local time.""" dt = rtc_obj.datetime # Map RTC tuple to struct_time for mktime t = time.struct_time(( dt.tm_year, dt.tm_mon, dt.tm_mday, dt.tm_hour, dt.tm_min, dt.tm_sec, dt.tm_wday, dt.tm_yday, -1 # tm_isdst unknown )) return int(time.mktime(t))def format_time(ts=None): """Format time directly from RTC.""" dt = rtc_obj.datetime return ( f"{dt.tm_year:04}-{dt.tm_mon:02}-{dt.tm_mday:02} " f"{dt.tm_hour:02}:{dt.tm_min:02}:{dt.tm_sec:02}" )# ---- LED ----def led_blink(times=3, interval=0.2): for _ in range(times): led.value = True time.sleep(interval) led.value = False time.sleep(interval)def heartbeat_led(): led.value = not led.value# ---- NVM QUEUE ----last_event_ts = 0def add_event(ts, event_type): global last_event_ts if ts is not None and last_event_ts and ts - last_event_ts < 1: return False last_event_ts = ts nvm = microcontroller.nvm for i in range(0, len(nvm) - 8, 9): if nvm[i] == 0: nvm[i:i+4] = ts.to_bytes(4, "big") if ts else b"\x00"*4 nvm[i+4] = event_type nvm[i+5:i+9] = b"\x00"*4 return True oldest_ts = None oldest_idx = 0 for i in range(0, len(nvm) - 8, 9): t = int.from_bytes(nvm[i:i+4], "big") if oldest_ts is None or t < oldest_ts: oldest_ts = t oldest_idx = i nvm[oldest_idx:oldest_idx+4] = ts.to_bytes(4,"big") if ts else b"\x00"*4 nvm[oldest_idx+4] = event_type nvm[oldest_idx+5:oldest_idx+9] = b"\x00"*4 print(f"{format_time()} - NVM full, overwriting oldest event") return Truedef pop_events(): nvm = microcontroller.nvm events = [] for i in range(0, len(nvm) - 8, 9): if nvm[i] != 0: ts = int.from_bytes(nvm[i:i+4], "big") etype = nvm[i+4] events.append((ts, etype)) nvm[i:i+9] = b"\x00"*9 return events# ---- NTP ----def ntp_sync(): """Sync RTC from NTP server and convert to local time.""" try: pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=-8) # PST offset (use -7 for PDT) t = ntp.datetime # struct_time # FIXED: Match the order that now_ts() expects rtc_obj.datetime = ( t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, t.tm_wday, t.tm_yday, -1 # Match struct_time order ) print(f"{format_time()} - Time synced via NTP") return True except Exception as e: print(f"{format_time()} - NTP failed: {e}") return False# ---- WIFI + EMAIL + ALARM + WATCHDOG ----wifi_fail_count = 0wifi_offline_logged = Falsewifi_connected = Falselast_wifi_attempt = 0wifi_offline_start = Nonedef wifi_connect(): global wifi_connected, last_wifi_attempt, wifi_fail_count, wifi_offline_logged, wifi_offline_start now = now_ts() or int(time.time()) if wifi_connected or now - last_wifi_attempt < RETRY_INTERVAL: return last_wifi_attempt = now try: wifi.radio.connect(WIFI_SSID, WIFI_PASS) wifi_connected = True print(f"{format_time()} - Wi-Fi OK: {wifi.radio.ipv4_address}") if wifi_fail_count >= WIFI_FAIL_THRESHOLD and wifi_offline_logged: add_event(now, 5) # Find and update the WiFi offline email for item in email_queue: if item.is_wifi_offline: offline_duration = now - wifi_offline_start minutes = offline_duration // 60 item.subject = "📡 Wi-Fi Restored at _____ ✅" # Create new body function with captured values ts_start = wifi_offline_start ts_end = now dur = minutes item.body_func = lambda: ( f"Wi-Fi was offline from {format_time(ts_start)} to {format_time(ts_end)}\n" f"Total offline duration: {dur} minutes" ) item.is_wifi_offline = False break wifi_offline_logged = False wifi_offline_start = None wifi_fail_count = 0 except Exception as e: wifi_connected = False wifi_fail_count += 1 print(f"{format_time()} - Wi-Fi retry later: {e}") if wifi_fail_count >= WIFI_FAIL_THRESHOLD and not wifi_offline_logged: wifi_offline_start = now add_event(now, 6) queue_email("⚠️ 📡 Wi-Fi Offline Detected at ____", lambda ts_event=now: f"Wi-Fi went offline at {format_time(ts_event)}", is_wifi_offline=True) wifi_offline_logged = Trueclass EmailItem: def __init__(self, subject, body_func): self.subject = subject self.body_func = body_func self.last_attempt = 0 self.retry_interval = 30 self.attempts = 0 self.is_wifi_offline = False # Flag for WiFi offline emailsemail_queue = []def queue_email(subject, body_func, is_wifi_offline=False): if len(email_queue) >= EMAIL_QUEUE_MAX: email_queue.pop(0) item = EmailItem(subject, body_func) item.is_wifi_offline = is_wifi_offline email_queue.append(item)def process_email_queue(): now = now_ts() or int(time.time()) if not email_queue: return if not wifi.radio.connected: wifi_connect() return for item in list(email_queue): interval = min(item.retry_interval * (2 ** item.attempts), 1800) # cap at 30 min if now - item.last_attempt < interval: continue try: item.last_attempt = now item.attempts += 1 pool = socketpool.SocketPool(wifi.radio) ctx = ssl.create_default_context() ssl_insecure = False try: with open("/lib/certs.pem") as f: ca = f.read() ctx.load_verify_locations(cadata=ca) except Exception as e: print(f"{format_time()} - SSL cert load failed, sending insecure: {e}") ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE ssl_insecure = True s = ctx.wrap_socket(pool.socket(), server_hostname=SMTP_SERVER) s.connect((SMTP_SERVER, SMTP_PORT)) def recv_response(): """Read and return SMTP response code""" try: # CircuitPython SSLSocket uses recv_into with bytearray buf = bytearray(1024) nbytes = s.recv_into(buf) resp = buf[:nbytes].decode() code = resp[:3] if len(resp) >= 3 else '000' return code, resp except Exception as e: print(f"{format_time()} - SMTP recv error: {e}") return '000', str(e) def cmd(c, expected_codes=['250', '354']): """Send command and validate response""" s.send(c+b"\r\n") time.sleep(0.2) code, resp = recv_response() if code not in expected_codes and code != '000': raise Exception(f"SMTP error: {code} - {resp}") return code, resp # Initial connection recv_response() # Read banner cmd(b"EHLO pico") cmd(b"AUTH LOGIN", ['334']) cmd(binascii.b2a_base64(EMAIL_FROM.encode()).strip(), ['334']) cmd(binascii.b2a_base64(APP_PASSWORD.encode()).strip(), ['235']) cmd(f"MAIL FROM:<{EMAIL_FROM}>".encode()) for r in EMAIL_TO: cmd(f"RCPT TO:<{r}>".encode()) cmd(b"DATA", ['354']) body = item.body_func(now) if ssl_insecure: body += "\n\n⚠️ WARNING: This email was sent without SSL certificate verification due to missing or invalid certificate file." msg = f"From: {EMAIL_FROM}\r\nTo: {', '.join(EMAIL_TO)}\r\nSubject: {item.subject}\r\n\r\n{body}\r\n.\r\n" s.send(msg.encode()) code, resp = recv_response() if code != '250': raise Exception(f"Message not accepted: {code} - {resp}") cmd(b"QUIT", ['221']) s.close() print(f"{format_time()} - Email sent: {item.subject}") email_queue.remove(item) break except Exception as e: print(f"{format_time()} - Email failed, will retry later: {e}") try: s.close() except: pass breakalert_sent = Falselast_alarm_ts = Nonedef handle_alarm_trigger(): global alert_sent, last_alarm_ts ts = now_ts() or int(time.time()) if last_alarm_ts and ts - last_alarm_ts < ALARM_THROTTLE: return led.value = True add_event(ts, 1) queue_email( "Security Alert – 🔔 Alarm Triggered at _____", lambda ts_event=ts: f"The Alarm system at {ALARM_LOCATION} has been triggered.\nTime of Event: {format_time(ts_event)}" ) alert_sent = True last_alarm_ts = tsdef handle_alarm_clear(): global alert_sent ts = now_ts() or int(time.time()) led.value = False add_event(ts, 2) queue_email( "Security Alert Cleared ✅ at ______", lambda ts_event=ts: f"The Alarm system at {ALARM_LOCATION} has been cleared.\nTime of Event: {format_time(ts_event)}" ) alert_sent = False# ---- BOOT PROCESS ----events = pop_events()previous_alarms = [ts for ts, etype in events if etype == 1]last_alarm_ts = max(previous_alarms) if previous_alarms else Noneboot_start = time.time()while time.time() - boot_start < NTP_BOOT_TIMEOUT: wifi_connect() if wifi.radio.connected and ntp_sync(): break led_blink(1,0.2) time.sleep(1)boot_ts = now_ts() or int(time.time())boot_wifi_status = "Connected ✅" if wifi.radio.connected else "Offline - will retry"queue_email( "Security Alert – 🔔 Alarm System Online at _____", lambda ts_event=boot_ts, wifi_status=boot_wifi_status, ts_alarm=last_alarm_ts: ( f"The Simon XT monitoring device has booted successfully.\n" f"System time: {format_time(ts_event)}\n" f"Location: {ALARM_LOCATION}\n" f"Wi-Fi Status: {wifi_status}\n\n" f"Previous alarm: {format_time(ts_alarm) if ts_alarm else 'No previous alarms detected'}" ))siren_state = siren.valueif not siren_state: add_event(boot_ts, 1) queue_email( "Security Alert – 🔔 Alarm Triggered at _____ (Boot)", lambda ts_event=boot_ts: f"The Alarm system at {ALARM_LOCATION} is already triggered on boot.\nTime of Event: {format_time(ts_event)}" ) add_event(boot_ts, 3) queue_email( "Security Alert – ⚡ Power Lost during Alarm at _____ (Boot)", lambda ts_event=boot_ts: f"Power loss detected while alarm was active at boot.\nTime of Event: {format_time(ts_event)}" ) last_alarm_ts = boot_tsled.value = True# ---- MAIN LOOP ----last_change = time.monotonic()last_ntp = time.monotonic()last_heartbeat = time.monotonic()watchdog_enabled = Falselast_watchdog_ping = 0while True: try: now = time.monotonic() or int(time.time()) if not watchdog_enabled: last_watchdog_ping = now watchdog_enabled = True # heartbeat if now - last_heartbeat >= 1: heartbeat_led() last_heartbeat = now # watchdog if watchdog_enabled and now - last_watchdog_ping > WATCHDOG_TIMEOUT: add_event(now, 4) print(f"{format_time()} - Watchdog triggered reset") time.sleep(0.5) microcontroller.reset() # periodic NTP if now - last_ntp > NTP_SYNC_INTERVAL: if ntp_sync(): last_ntp = now wifi_connect() process_email_queue() # alarm detection try: current_siren = siren.value except Exception as e: print(f"{format_time()} - GPIO read error: {e}") time.sleep(0.1) continue if current_siren != siren_state and now - last_change > DEBOUNCE_INTERVAL: siren_state = current_siren last_change = now if not siren_state: handle_alarm_trigger() else: handle_alarm_clear() if watchdog_enabled: last_watchdog_ping = now time.sleep(0.1) except Exception as e: print(f"{format_time()} - Loop error: {e}") time.sleep(0.1)Statistics: Posted by jonathanlee — Thu Dec 25, 2025 4:21 am — Replies 0 — Views 29