# SPDX-FileCopyrightText: 2019 Dan Cogliano for Adafruit Industries
# SPDX-FileCopyrightText: 2021 kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_mcp9600`
================================================================================

CircuitPython driver for the MCP9600 thermocouple I2C amplifier


* Author(s): Dan Cogliano, Kattni Rembor

Implementation Notes
--------------------

**Hardware:**

* `Adafruit MCP9600 I2C Thermocouple Amplifier:
  <https://www.adafruit.com/product/4101>`_ (Product ID: 4101)

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""

from struct import unpack
from micropython import const
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_struct import UnaryStruct
from adafruit_register.i2c_bits import RWBits, ROBits
from adafruit_register.i2c_bit import RWBit, ROBit

try:
    # Used only for typing
    import typing  # pylint: disable=unused-import
    from busio import I2C
except ImportError:
    pass

__version__ = "1.3.5"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MCP9600.git"

_DEFAULT_ADDRESS = const(0x67)

_REGISTER_HOT_JUNCTION = const(0x00)
_REGISTER_DELTA_TEMP = const(0x01)
_REGISTER_COLD_JUNCTION = const(0x02)
_REGISTER_THERM_CFG = const(0x05)
_REGISTER_VERSION = const(0x20)


class MCP9600:
    """
    Interface to the MCP9600 thermocouple amplifier breakout

    :param ~busio.I2C i2c: The I2C bus the MCP9600 is connected to.
    :param int address: The I2C address of the device. Defaults to :const:`0x67`
    :param str tctype: Thermocouple type. Defaults to :const:`"K"`
    :param int tcfilter: Value for the temperature filter. Can limit spikes in
     temperature readings. Defaults to :const:`0`


    **Quickstart: Importing and using the MCP9600 temperature sensor**

        Here is one way of importing the `MCP9600` class so you can use it with the name ``mcp``.
        First you will need to import the libraries to use the sensor

        .. code-block:: python

            import busio
            import board
            import adafruit_mcp9600

        Once this is done you can define your `busio.I2C` object and define your sensor object

        .. code-block:: python

            i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
            mcp = adafruit_mcp9600.MCP9600(i2c)


        Now you have access to the change in temperature using the
        :attr:`delta_temperature` attribute, the thermocouple or hot junction
        temperature in degrees Celsius using the :attr:`temperature`
        attribute and the ambient or cold-junction temperature in degrees Celsius
        using the :attr:`ambient_temperature` attribute

        .. code-block:: python

            delta_temperature = mcp.delta_temperature
            temperature = mcp.temperature
            ambient_temperature = mcp.ambient_temperature


    """

    # Shutdown mode options
    NORMAL = 0b00
    SHUTDOWN = 0b01
    BURST = 0b10

    # Burst mode sample options
    BURST_SAMPLES_1 = 0b000
    BURST_SAMPLES_2 = 0b001
    BURST_SAMPLES_4 = 0b010
    BURST_SAMPLES_8 = 0b011
    BURST_SAMPLES_16 = 0b100
    BURST_SAMPLES_32 = 0b101
    BURST_SAMPLES_64 = 0b110
    BURST_SAMPLES_128 = 0b111

    # Alert temperature monitor options
    AMBIENT = 1
    THERMOCOUPLE = 0

    # Temperature change type to trigger alert. Rising is heating up. Falling is cooling down.
    RISING = 1
    FALLING = 0

    # Alert output options
    ACTIVE_HIGH = 1
    ACTIVE_LOW = 0

    # Alert mode options
    INTERRUPT = 1  # Interrupt clear option must be set when using this mode!
    COMPARATOR = 0

    # Ambient (cold-junction) temperature sensor resolution options
    AMBIENT_RESOLUTION_0_0625 = 0  # 0.0625 degrees Celsius
    AMBIENT_RESOLUTION_0_25 = 1  # 0.25 degrees Celsius

    # STATUS - 0x4
    burst_complete = RWBit(0x4, 7)
    """Burst complete."""
    temperature_update = RWBit(0x4, 6)
    """Temperature update."""
    input_range = ROBit(0x4, 4)
    """Input range."""
    alert_1 = ROBit(0x4, 0)
    """Alert 1 status."""
    alert_2 = ROBit(0x4, 1)
    """Alert 2 status."""
    alert_3 = ROBit(0x4, 2)
    """Alert 3 status."""
    alert_4 = ROBit(0x4, 3)
    """Alert 4 status."""
    # Device Configuration - 0x6
    ambient_resolution = RWBit(0x6, 7)
    """Ambient (cold-junction) temperature resolution. Options are ``AMBIENT_RESOLUTION_0_0625``
    (0.0625 degrees Celsius) or ``AMBIENT_RESOLUTION_0_25`` (0.25 degrees Celsius)."""
    burst_mode_samples = RWBits(3, 0x6, 2)
    """The number of samples taken during a burst in burst mode. Options are ``BURST_SAMPLES_1``,
    ``BURST_SAMPLES_2``, ``BURST_SAMPLES_4``, ``BURST_SAMPLES_8``, ``BURST_SAMPLES_16``,
    ``BURST_SAMPLES_32``, ``BURST_SAMPLES_64``, ``BURST_SAMPLES_128``."""
    shutdown_mode = RWBits(2, 0x6, 0)
    """Shutdown modes. Options are ``NORMAL``, ``SHUTDOWN``, and ``BURST``."""
    # Alert 1 Configuration - 0x8
    _alert_1_interrupt_clear = RWBit(0x8, 7)
    _alert_1_monitor = RWBit(0x8, 4)
    _alert_1_temp_direction = RWBit(0x8, 3)
    _alert_1_state = RWBit(0x8, 2)
    _alert_1_mode = RWBit(0x8, 1)
    _alert_1_enable = RWBit(0x8, 0)
    # Alert 2 Configuration - 0x9
    _alert_2_interrupt_clear = RWBit(0x9, 7)
    _alert_2_monitor = RWBit(0x9, 4)
    _alert_2_temp_direction = RWBit(0x9, 3)
    _alert_2_state = RWBit(0x9, 2)
    _alert_2_mode = RWBit(0x9, 1)
    _alert_2_enable = RWBit(0x9, 0)
    # Alert 3 Configuration - 0xa
    _alert_3_interrupt_clear = RWBit(0xA, 7)
    _alert_3_monitor = RWBit(0xA, 4)
    _alert_3_temp_direction = RWBit(0xA, 3)
    _alert_3_state = RWBit(0xA, 2)
    _alert_3_mode = RWBit(0xA, 1)
    _alert_3_enable = RWBit(0xA, 0)
    # Alert 4 Configuration - 0xb
    _alert_4_interrupt_clear = RWBit(0xB, 7)
    _alert_4_monitor = RWBit(0xB, 4)
    _alert_4_temp_direction = RWBit(0xB, 3)
    _alert_4_state = RWBit(0xB, 2)
    _alert_4_mode = RWBit(0xB, 1)
    _alert_4_enable = RWBit(0xB, 0)
    # Alert 1 Hysteresis - 0xc
    _alert_1_hysteresis = UnaryStruct(0xC, ">H")
    # Alert 2 Hysteresis - 0xd
    _alert_2_hysteresis = UnaryStruct(0xD, ">H")
    # Alert 3 Hysteresis - 0xe
    _alert_3_hysteresis = UnaryStruct(0xE, ">H")
    # Alert 4 Hysteresis - 0xf
    _alert_4_hysteresis = UnaryStruct(0xF, ">H")
    # Alert 1 Limit - 0x10
    _alert_1_temperature_limit = UnaryStruct(0x10, ">H")
    # Alert 2 Limit - 0x11
    _alert_2_limit = UnaryStruct(0x11, ">H")
    # Alert 3 Limit - 0x12
    _alert_3_limit = UnaryStruct(0x12, ">H")
    # Alert 4 Limit - 0x13
    _alert_4_limit = UnaryStruct(0x13, ">H")
    # Device ID/Revision - 0x20
    _device_id = ROBits(8, 0x20, 8, register_width=2, lsb_first=False)
    _revision_id = ROBits(8, 0x20, 0, register_width=2)

    types = ("K", "J", "T", "N", "S", "E", "B", "R")

    def __init__(
        self,
        i2c: I2C,
        address: int = _DEFAULT_ADDRESS,
        tctype: str = "K",
        tcfilter: int = 0,
    ) -> None:
        self.buf = bytearray(3)
        # Do not probe for the device with a zero-length write.
        # The MCP960x does not like zero-length writes and will usually NAK,
        # unless the write is rapidly repeated.
        self.i2c_device = I2CDevice(i2c, address, probe=False)
        self.type = tctype
        # is this a valid thermocouple type?
        if tctype not in MCP9600.types:
            raise Exception("invalid thermocouple type ({})".format(tctype))
        # filter is from 0 (none) to 7 (max), can limit spikes in
        # temperature readings
        tcfilter = min(7, max(0, tcfilter))
        ttype = MCP9600.types.index(tctype)

        self.buf[0] = _REGISTER_THERM_CFG
        self.buf[1] = tcfilter | (ttype << 4)
        with self.i2c_device as tci2c:
            tci2c.write(self.buf, end=2)
        if self._device_id not in (0x40, 0x41):
            raise RuntimeError("Failed to find MCP9600 or MCP9601 - check wiring!")

    def alert_config(
        self,
        *,
        alert_number: int,
        alert_temp_source: int,
        alert_temp_limit: float,
        alert_hysteresis: float,
        alert_temp_direction: int,
        alert_mode: int,
        alert_state: int
    ) -> None:
        """Configure a specified alert pin. Alert is enabled by default when alert is configured.
        To disable an alert pin, use :meth:`alert_disable`.

        :param int alert_number: The alert pin number. Must be 1-4.
        :param alert_temp_source: The temperature source to monitor for the alert. Options are:
                                  ``THERMOCOUPLE`` (hot-junction) or ``AMBIENT`` (cold-junction).
                                  Temperatures are in Celsius.
        :param float alert_temp_limit: The temperature in degrees Celsius at which the alert should
                                       trigger. For rising temperatures, the alert will trigger when
                                       the temperature rises above this limit. For falling
                                       temperatures, the alert will trigger when the temperature
                                       falls below this limit.
        :param float alert_hysteresis: The alert hysteresis range. Must be 0-255 degrees Celsius.
                                       For rising temperatures, the hysteresis is below alert limit.
                                       For falling temperatures, the hysteresis is above alert
                                       limit. See data-sheet for further information.
        :param alert_temp_direction: The direction the temperature must change to trigger the alert.
                                     Options are ``RISING`` (heating up) or ``FALLING`` (cooling
                                     down).
        :param alert_mode: The alert mode. Options are ``COMPARATOR`` or ``INTERRUPT``. In
                           comparator mode, the pin will follow the alert, so if the temperature
                           drops, for example, the alert pin will go back low. In interrupt mode,
                           by comparison, once the alert goes off, you must manually clear it. If
                           setting mode to ``INTERRUPT``, use :meth:`alert_interrupt_clear`
                           to clear the interrupt flag.
        :param alert_state: Alert pin output state. Options are ``ACTIVE_HIGH`` or ``ACTIVE_LOW``.


        For example, to configure alert 1:

        .. code-block:: python

            import board
            import busio
            import digitalio
            import adafruit_mcp9600

            i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
            mcp = adafruit_mcp9600.MCP9600(i2c)
            alert_1 = digitalio.DigitalInOut(board.D5)
            alert_1.switch_to_input()

            mcp.alert_config(alert_number=1, alert_temp_source=mcp.THERMOCOUPLE,
                             alert_temp_limit=25, alert_hysteresis=0,
                             alert_temp_direction=mcp.RISING, alert_mode=mcp.COMPARATOR,
                             alert_state=mcp.ACTIVE_LOW)

        """
        if alert_number not in (1, 2, 3, 4):
            raise ValueError("Alert pin number must be 1-4.")
        if not 0 <= alert_hysteresis < 256:
            raise ValueError("Hysteresis value must be 0-255.")
        setattr(self, "_alert_%d_monitor" % alert_number, alert_temp_source)
        setattr(
            self,
            "_alert_%d_temperature_limit" % alert_number,
            int(alert_temp_limit / 0.0625),
        )
        setattr(self, "_alert_%d_hysteresis" % alert_number, alert_hysteresis)
        setattr(self, "_alert_%d_temp_direction" % alert_number, alert_temp_direction)
        setattr(self, "_alert_%d_mode" % alert_number, alert_mode)
        setattr(self, "_alert_%d_state" % alert_number, alert_state)
        setattr(self, "_alert_%d_enable" % alert_number, True)

    def alert_disable(self, alert_number: int) -> None:
        """Configuring an alert using :meth:`alert_config` enables the specified alert by default.
        Use :meth:`alert_disable` to disable an alert pin.

        :param int alert_number: The alert pin number. Must be 1-4.

        """
        if alert_number not in (1, 2, 3, 4):
            raise ValueError("Alert pin number must be 1-4.")
        setattr(self, "_alert_%d_enable" % alert_number, False)

    def alert_interrupt_clear(
        self, alert_number: int, interrupt_clear: bool = True
    ) -> None:
        """Turns off the alert flag in the MCP9600, and clears the pin state (not used if the alert
        is in comparator mode). Required when ``alert_mode`` is ``INTERRUPT``.

        :param int alert_number: The alert pin number. Must be 1-4.
        :param bool interrupt_clear: The bit to write the interrupt state flag

        """
        if alert_number not in (1, 2, 3, 4):
            raise ValueError("Alert pin number must be 1-4.")
        setattr(self, "_alert_%d_interrupt_clear" % alert_number, interrupt_clear)

    @property
    def version(self) -> int:
        """MCP9600 chip version"""
        data = self._read_register(_REGISTER_VERSION, 2)
        return unpack(">xH", data)[0]

    @property
    def ambient_temperature(self) -> float:
        """Cold junction/ambient/room temperature in Celsius"""
        data = self._read_register(_REGISTER_COLD_JUNCTION, 2)
        value = unpack(">xH", data)[0] * 0.0625
        if data[1] & 0x80:
            value -= 4096
        return value

    @property
    def temperature(self) -> float:
        """Hot junction temperature in Celsius"""
        data = self._read_register(_REGISTER_HOT_JUNCTION, 2)
        value = unpack(">xH", data)[0] * 0.0625
        if data[1] & 0x80:
            value -= 4096
        return value

    @property
    def delta_temperature(self) -> float:
        """Delta temperature in Celsius"""
        data = self._read_register(_REGISTER_DELTA_TEMP, 2)
        value = unpack(">xH", data)[0] * 0.0625
        if data[1] & 0x80:
            value -= 4096
        return value

    def _read_register(self, reg: int, count: int = 1) -> bytearray:
        self.buf[0] = reg
        with self.i2c_device as i2c:
            i2c.write_then_readinto(self.buf, self.buf, out_end=count, in_start=1)
        return self.buf
