Quantcast
Channel: Raspberry Pi Forums
Viewing all articles
Browse latest Browse all 7513

General • Raspberry Pi Pico 2 W Simon XT alarm revival

$
0
0
Screenshot 2025-12-24 at 20.26.13.jpeg
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:

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)
I used a 1CH Optocoupler PC817 1 Channel Isolation Board Voltage Converter Adapter Module 3.6-30V Driver Photoelectric Isolated Module (1CH Optocoupler)

Statistics: Posted by jonathanlee — Thu Dec 25, 2025 4:21 am — Replies 0 — Views 29



Viewing all articles
Browse latest Browse all 7513

Trending Articles