#!/usr/bin/env python3
#
# Copyright (C) 2026 LinuxCNC contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# Home Assistant bridge for the LinuxCNC MTConnect agent (OPTIONAL CONTRIB).
#
# Publishes LinuxCNC machine state as Home Assistant MQTT Discovery, reusing the
# MTConnect agent's auto-generated device model.  This is deliberately NOT part
# of the core mtconnect-agent: Home Assistant's discovery format is HA-specific,
# so it lives here as an opt-in.  The core agent publishes only the vendor-neutral
# MTConnect MQTT binding.
#
# All Home Assistant / broker settings are passed as arguments, so no HA-specific
# key is needed in the machine INI.  Load it from a HAL file alongside the agent,
# wiring the broker details from any INI section you like:
#
#   loadusr -W mtconnect-ha-bridge \
#     --broker=[HA]BROKER --username=[HA]USER --password=[HA]PASSWORD
#
# The machine INI comes from $INI_FILE_NAME (only the device model is read from
# it; nothing HA-specific).

import argparse
import json
import os
import signal
import sys
import time

# Local HA helper (ha.py in this directory) + the installed mtc package.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
_EMC2_HOME = os.environ.get("EMC2_HOME")
if _EMC2_HOME:
    sys.path.insert(0, os.path.join(_EMC2_HOME, "lib", "python"))

import ha as ha_mod                        # noqa: E402
from mtc.agent import AgentState           # noqa: E402


def make_hal_pins():
    """Minimal HAL component so 'loadusr -W' can wait for us; else None."""
    try:
        import hal
    except ImportError:
        return None
    comp = hal.component("mtconnect-ha-bridge")
    comp.newpin("enable", hal.Type.BOOL, hal.Dir.IN)
    comp["enable"] = True
    comp.newpin("connected", hal.Type.BOOL, hal.Dir.OUT)
    comp.ready()
    return comp


def main():
    p = argparse.ArgumentParser(description="Home Assistant bridge for the "
                                            "LinuxCNC MTConnect agent")
    p.add_argument("ini", nargs="?", default=os.environ.get("INI_FILE_NAME"),
                   help="LinuxCNC INI file (default: $INI_FILE_NAME)")
    p.add_argument("--broker", default="localhost")
    p.add_argument("--port", type=int, default=1883)
    p.add_argument("--username")
    p.add_argument("--password")
    p.add_argument("--ha-prefix", default="homeassistant",
                   help="Home Assistant discovery prefix (default homeassistant)")
    p.add_argument("--mqtt-prefix", default="MTConnect",
                   help="namespace for the bridge state/availability topics")
    p.add_argument("--sample-hz", type=float, default=2.0)
    args = p.parse_args()
    if not args.ini:
        p.error("no INI file (set INI_FILE_NAME or pass one)")

    try:
        import paho.mqtt.client as mqtt
    except ModuleNotFoundError:
        print("error: Missing Python module paho.mqtt "
              "(Debian: 'sudo apt install python3-paho-mqtt').")
        return 2

    state = AgentState(args.ini)
    sensors = ha_mod.build_sensors(state.model, state.config)
    uuid = state.config.uuid
    prefix = args.mqtt_prefix.rstrip("/")
    state_topic = "%s/ha/%s/state" % (prefix, uuid)
    avail_topic = "%s/ha/%s/availability" % (prefix, uuid)
    node = ha_mod.node_id(state.config)

    comp = make_hal_pins()
    stop = {"v": False}
    signal.signal(signal.SIGTERM, lambda *a: stop.__setitem__("v", True))
    signal.signal(signal.SIGINT, lambda *a: stop.__setitem__("v", True))

    def on_connect(client, userdata, flags, rc, *a):
        if comp is not None:
            comp["connected"] = (rc == 0)
        if rc == 0:
            for s in sensors:
                topic = "%s/sensor/%s/%s/config" % (args.ha_prefix, node, s["key"])
                payload = ha_mod.discovery_payload(s, state.config,
                                                   state_topic, avail_topic)
                client.publish(topic, json.dumps(payload), retain=True)
            client.publish(avail_topic, "online", retain=True)
            print("info: HA discovery published under %s/sensor/%s/*"
                  % (args.ha_prefix, node))
        else:
            print("error: MQTT connect failed (rc=%s)" % rc)

    def on_disconnect(client, userdata, rc, *a):
        if comp is not None:
            comp["connected"] = False

    try:
        client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1,
                             client_id="linuxcnc-mtconnect-ha")
    except AttributeError:
        client = mqtt.Client(client_id="linuxcnc-mtconnect-ha")
    if args.username:
        client.username_pw_set(args.username, args.password)
    client.will_set(avail_topic, "offline", retain=True)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.connect_async(args.broker, args.port, keepalive=60)
    client.loop_start()
    print("info: mtconnect-ha-bridge -> %s:%d (HA prefix '%s')"
          % (args.broker, args.port, args.ha_prefix))

    try:
        while not stop["v"]:
            if comp is not None and not comp["enable"]:
                time.sleep(0.2)
                continue
            state.poll_once()
            client.publish(state_topic,
                           ha_mod.state_json(state.latest_values(), sensors),
                           retain=True)
            time.sleep(1.0 / max(args.sample_hz, 0.1))
    except KeyboardInterrupt:
        pass
    finally:
        client.publish(avail_topic, "offline", retain=True)
        client.loop_stop()
        client.disconnect()
    return 0


if __name__ == "__main__":
    sys.exit(main())
