# coding: utf-8
# python 2 only

# Copyright (c) 2026 TormachTips.com. All rights reserved.
# Licensed under the TormachTips Personal Use License.
# Permission is granted only for private personal use and private personal modification.
# No sharing, publication, distribution, resale, sublicensing, screenshots, code excerpts,
# benchmarks, or videos are permitted without prior written permission.
# Requests:         tormach.1100m@gmail.com
# Information page: https://tormachtips.com/plugins.htm

###########################################################
##                                                       ##
##           TOOL BREAKAGE NOTIFIER v0.96                ##
##           https://www.tormachtips.com                 ##
##                                                       ##
###########################################################

# 0.96 - adds disabled / enabled flag - 4/12/2026

"""
DESCRIPTION:
    This PathPilot plugin monitors for important machine errors,
    such as tool breakage or ETS mismatches, by intercepting messages
    written to the internal error handler. When a watched phrase is detected,
    an email alert is automatically sent to a configured recipient.

    It’s commonly used with G37 — an ETS command that verifies
    tool length against expected values. Deviations trigger a soft alarm,
    which this plugin can detect and notify you about.

INSTALLATION:
    1. Copy both of the following files into your PathPilot system:
          - breakage_plugin.py (this script)
          - breakage_config.ini (configuration file)

    2. Place both files into:
          /home/operator/gcode/python/

    3. Ensure this required support file is also present in that folder:
          - ui_hooks.py

    4. Edit the config (.ini) file. The fields are pretty straightforward.

    Notes:
    - `\\n` in the body becomes a newline.
    - `{msg}` is replaced with the raw PathPilot error message.
    - `{match}` is replaced with the string from [strings] that triggered the alert.
    - You may leave unused `string#` entries blank — they’ll be ignored.

NOTES:
    - The plugin hot-loads the INI on every event trigger.
    - No restart of PathPilot is needed to apply config changes.
    - Email sending is done in the background and does not interfere with machine performance.
"""

import threading
import time
import types
import os
import smtplib
import ConfigParser
from email.mime.text import MIMEText
from ui_hooks import plugin
import singletons
import constants

CURRENT_VER      = "0.95"
SCRIPT_NAME      = "Tool Breakage Notifier"
DESCRIPTION      = "Emails the user on tool breakage events (checks via ETS)."
ENABLED          = 1
DEV_MACHINE      = 0
DEV_MACHINE_FLAG = "/home/operator/gcode/python/dev_machine.txt"

DEFAULT_CONFIG_TEXT = """[settings]
enabled = 1

[strings]
string1 = exceeds tolerance
string2 = exceeds breakage tolerance
string3 = exceeds pullout tolerance
string4 = machine has been estopped
string5 = insert
string6 = 
string7 = 
string8 = 
string9 = 
string10 = 

[email]
sender = your_email@example.com
recipient = recipient@example.com
subject = TOOL BREAK OR OTHER ALERT DETECTED
body = G37 has detected a tool break.\\nMatched pattern: {match}\\n\\nFull message:\\n{msg}
smtp_server = smtp.gmail.com
smtp_port = 465
smtp_username = your_email@example.com
smtp_password = app_password
"""

class UserPlugin(plugin):
    def __init__(self):
        plugin.__init__(self, "Tool Break Detector")
        dev_machine_found = os.path.exists(DEV_MACHINE_FLAG)
        if dev_machine_found:
            plugin_enabled = DEV_MACHINE
        else:
            plugin_enabled = ENABLED
        if plugin_enabled:
            self.config_file = os.path.expanduser('~/gcode/python/tool_breakage_notifier_plugin.ini')
            self.ensure_default_config()
            t = threading.Thread(target=self.setup_hook)
            t.daemon = True
            t.start()
            self.ShowMsg("Tool breakage watcher loaded (via error_handler.write)", constants.ALARM_LEVEL_QUIET)
            return
        else:
            if dev_machine_found:
                self.ShowMsg("Dev machine found. Plugin loaded, but disabled by DEV_MACHINE.", constants.ALARM_LEVEL_QUIET)
            else:
                self.ShowMsg("Plugin loaded, but disabled.", constants.ALARM_LEVEL_QUIET)
                self.ShowMsg("To enable, open script, find ENABLED = 0 and change to ENABLED = 1", constants.ALARM_LEVEL_QUIET)
            return

    def ensure_default_config(self):
        try:
            if os.path.exists(self.config_file):
                return

            config_dir = os.path.dirname(self.config_file)
            if config_dir and not os.path.isdir(config_dir):
                os.makedirs(config_dir)

            handle = open(self.config_file, "wb")
            try:
                handle.write(DEFAULT_CONFIG_TEXT)
            finally:
                handle.close()

            self.ShowMsg("Created default config: " + self.config_file, constants.ALARM_LEVEL_MEDIUM)
        except Exception as e:
            self.ShowMsg("Failed to create default config: " + str(e), constants.ALARM_LEVEL_LOW)

    def setup_hook(self):
        while True:
            try:
                if hasattr(singletons, 'g_Machine') and singletons.g_Machine:
                    self.ui = singletons.g_Machine
                    err_handler = getattr(self.ui, 'error_handler', None)
                    if not err_handler:
                        raise Exception("No error_handler found")
                    original_func = err_handler.write
                    def make_wrapper(plugin_self, orig_func):
                        def wrapped(this, msg, level=constants.ALARM_LEVEL_QUIET, **kwargs):
                            if isinstance(msg, basestring):
                                msg_lower = msg.lower()
                                phrases = plugin_self._load_watch_phrases()
                                for phrase in phrases:
                                    if phrase in msg_lower:
                                        plugin_self.on_tool_break(msg, matched_phrase=phrase)
                                        break
                            return orig_func(msg, level, **kwargs)
                        return wrapped
                    err_handler.write = types.MethodType(make_wrapper(self, original_func), err_handler)
                    self.ShowMsg("Hook on error_handler.write applied", constants.ALARM_LEVEL_QUIET)
                    return
            except Exception as e:
                self.ShowMsg("Hook error: " + str(e), constants.ALARM_LEVEL_LOW)
            time.sleep(0.5)

    def _load_watch_phrases(self):
        phrases = []
        try:
            if not os.path.exists(self.config_file):
                return phrases
            config = ConfigParser.ConfigParser()
            config.read(self.config_file)
            if config.has_section('strings'):
                for key, val in config.items('strings'):
                    val = val.strip()
                    if val:
                        phrases.append(val.lower())
        except Exception as e:
            self.ShowMsg("Failed to load phrases: " + str(e), constants.ALARM_LEVEL_LOW)
        return phrases   
    
    def send_tool_break_email(self, msg, matched_phrase=None):
        try:
            if not os.path.exists(self.config_file):
                raise IOError("Config file not found")
            config = ConfigParser.ConfigParser()
            config.read(self.config_file)
            sender = config.get('email', 'sender')
            recipient = config.get('email', 'recipient')
            subject = config.get('email', 'subject') or "Tool Break Detected"         
            body_template = config.get('email', 'body') or "Tool break detected.\n\nDetails:\n{msg}"
            body = body_template.replace('\\n', '\n').format(msg=msg, match=matched_phrase)                       
            smtp_server = config.get('email', 'smtp_server')
            smtp_port = config.getint('email', 'smtp_port')
            smtp_username = config.get('email', 'smtp_username')
            smtp_password = config.get('email', 'smtp_password')
            mime_msg = MIMEText(body)
            mime_msg['From'] = sender
            mime_msg['To'] = recipient
            mime_msg['Subject'] = subject
            smtp = smtplib.SMTP_SSL(smtp_server, smtp_port)
            smtp.login(smtp_username, smtp_password)
            smtp.sendmail(sender, [recipient], mime_msg.as_string())
            smtp.quit()
            self.ShowMsg("Tool break alert email sent", constants.ALARM_LEVEL_MEDIUM)
        except Exception as e:
            self.ShowMsg("Tool break email failed: " + str(e), constants.ALARM_LEVEL_LOW)

    def is_enabled(self):
        try:
            if not os.path.exists(self.config_file):
                return False
            config = ConfigParser.ConfigParser()
            config.read(self.config_file)
            return config.getint('settings', 'enabled') == 1
        except:
            return False

    def on_tool_break(self, msg, matched_phrase=None):
        if not self.is_enabled():
            return
        self.ShowMsg("TOOL BREAK DETECTED", constants.ALARM_LEVEL_HIGH)
        print "[Tool Break Detected] %s" % msg
        t = threading.Thread(target=self.send_tool_break_email, args=(msg, matched_phrase))
        t.daemon = True
        t.start()

DESCRIPTION_LONG = """This plugin 
    monitors the status window for important or custom messages like tool 
    breakage notifications or ETS mismatches. </font></p>
    <p><font face="Verdana" size="2">When a matching phrase is found, an alert 
    is emailed to the user. </font></p>
    <p><font face="Verdana" size="2">For ETS use, for example. You have G37 
    P0.005 programmed. This checks if the currently loaded tool is within 0.005&quot; 
    of tolerance when the ETS is tripped. </font></p>
    <p><font face="Verdana" size="2">If it's too long (wrong tool or drawbar 
    failure) or too short (wrong tool or broken tool), it will alert PathPilot 
    and thus this script and will email you.</font></p>
    <p><font face="Verdana" size="2">To set this up, you'll need a gmail account 
    with an App Password. </font></p>
    <p><font face="Verdana" size="2">This script uses the same logic and 
    instructions from the&nbsp;M Code email sender. </font></p>
    <p><font face="Verdana" size="2">Follow those instructions and enter your 
    settings in the included INI file."""