"""Definitions and constants for pypck."""

from __future__ import annotations

import math
import re
from enum import Enum, auto
from typing import Any, no_type_check

LCN_ENCODING = "utf-8"
PATTERN_SPLIT_PORT_PIN = re.compile(r"(?P<port>[a-zA-Z]+)(?P<pin>\d+)")


def split_port_pin(portpin: str) -> tuple[str, int]:
    """Split the port and the pin from the given input string.

    :param    str    portpin:    Input string
    """
    res = PATTERN_SPLIT_PORT_PIN.findall(portpin)
    return res[0][0], int(res[0][1])


class OutputPort(Enum):
    """Output port of LCN module."""

    OUTPUT1 = 0
    OUTPUT2 = 1
    OUTPUT3 = 2
    OUTPUT4 = 3

    OUTPUTUP = 0
    OUTPUTDOWN = 1


class RelayPort(Enum):
    """Relay port of LCN module."""

    RELAY1 = 0
    RELAY2 = 1
    RELAY3 = 2
    RELAY4 = 3
    RELAY5 = 4
    RELAY6 = 5
    RELAY7 = 6
    RELAY8 = 7

    MOTORONOFF1 = 0
    MOTORUPDOWN1 = 1
    MOTORONOFF2 = 2
    MOTORUPDOWN2 = 3
    MOTORONOFF3 = 4
    MOTORUPDOWN3 = 5
    MOTORONOFF4 = 6
    MOTORUPDOWN4 = 7


class MotorPort(Enum):
    """Motor ports of LCN module."""

    MOTOR1 = 0
    MOTOR2 = 1
    MOTOR3 = 2
    MOTOR4 = 3
    OUTPUTS = 4


class LedPort(Enum):
    """LED port of LCN module."""

    LED1 = 0
    LED2 = 1
    LED3 = 2
    LED4 = 3
    LED5 = 4
    LED6 = 5
    LED7 = 6
    LED8 = 7
    LED9 = 8
    LED10 = 9
    LED11 = 10
    LED12 = 11


class LogicOpPort(Enum):
    """Logic Operation port of LCN module."""

    LOGICOP1 = 0
    LOGICOP2 = 1
    LOGICOP3 = 2
    LOGICOP4 = 3


class BinSensorPort(Enum):
    """Binary sensor port of LCN module."""

    BINSENSOR1 = 0
    BINSENSOR2 = 1
    BINSENSOR3 = 2
    BINSENSOR4 = 3
    BINSENSOR5 = 4
    BINSENSOR6 = 5
    BINSENSOR7 = 6
    BINSENSOR8 = 7


class Key(Enum):
    """Keys of LCN module."""

    A1 = 0
    A2 = 1
    A3 = 2
    A4 = 3
    A5 = 4
    A6 = 5
    A7 = 6
    A8 = 7

    B1 = 8
    B2 = 9
    B3 = 10
    B4 = 11
    B5 = 12
    B6 = 13
    B7 = 14
    B8 = 15

    C1 = 16
    C2 = 17
    C3 = 18
    C4 = 19
    C5 = 20
    C6 = 21
    C7 = 22
    C8 = 23

    D1 = 24
    D2 = 25
    D3 = 26
    D4 = 27
    D5 = 28
    D6 = 29
    D7 = 30
    D8 = 31


class KeyAction(Enum):
    """Action types for LCN keys."""

    HIT = "hit"
    MAKE = "make"
    BREAK = "break"


class BatteryStatus(Enum):
    """Battery status."""

    WEAK = "weak"
    FULL = "full"


class OutputPortDimMode(Enum):
    """LCN dimming mode.

    If solely modules with firmware 170206 or newer are present, LCN-PRO
    automatically programs STEPS200.
    Otherwise the default is STEPS50.
    Since LCN-PCHK doesn't know the current mode, it must explicitly be set.
    """

    STEPS50 = 0  # 0..50 dimming steps (all LCN module generations)
    STEPS200 = 1  # 0..200 dimming steps (since 170206)


class OutputPortStatusMode(Enum):
    """Tells LCN-PCHK how to format output-port status-messages.

    PERCENT: allows to show the status in half-percent steps (e.g. "10.5").
    NATIVE: is completely backward compatible and there are no restrictions
    concerning the LCN module generations. It requires LCN-PCHK 2.3 or higher
    though.
    """

    PERCENT = "P"  # Default (compatible with all versions of LCN-PCHK)
    NATIVE = "N"  # 0..200 steps (since LCN-PCHK 2.3)


def time_to_ramp_value(time_msec: int) -> int:
    """Convert the given time into an LCN ramp value.

    :param    int    time_msec:    The time in milliseconds.

    :returns: The (LCN-internal) ramp value (0..250).
    :rtype: int
    """
    if time_msec < 250:
        ret = 0
    elif time_msec < 500:
        ret = 1
    elif time_msec < 660:
        ret = 2
    elif time_msec < 1000:
        ret = 3
    elif time_msec < 1400:
        ret = 4
    elif time_msec < 2000:
        ret = 5
    elif time_msec < 3000:
        ret = 6
    elif time_msec < 4000:
        ret = 7
    elif time_msec < 5000:
        ret = 8
    elif time_msec < 6000:
        ret = 9
    else:
        ramp = (time_msec / 1000 - 6) / 2 + 10
        ramp = min(ramp, 250)
        ret = int(ramp)
    return ret


def ramp_value_to_time(ramp_value: int) -> int:
    """Convert the given LCN ramp value into a time.

    :param    int    ramp_value:    The LCN ramp value (0..250).

    :returns: The ramp time in milliseconds.
    :rtype: int
    """
    if not 0 <= ramp_value <= 250:
        raise ValueError("Ramp value has to be in range 0..250")

    if ramp_value < 10:
        times = [0, 250, 500, 660, 1000, 1400, 2000, 3000, 4000, 5000]
        ramp_time = times[ramp_value]
    else:
        ramp_time = int(((ramp_value - 10) * 2 + 6) * 1000)
    return ramp_time


def time_to_native_value(time_msec: int) -> int:
    """Convert time to native LCN time value.

    Scales the given time value in milliseconds to a byte value (0..255).
    Used for RelayTimer.

    :param    int    time_msec:    Duration of timer in milliseconds

    :returns: The duration in native LCN units
    :rtype: int
    """
    if not 0 <= time_msec <= 240960:
        raise ValueError("Time has to be in range 0..240960ms")
    time_scaled = time_msec / (1000 * 0.03 * 32.0) + 1.0

    pre_decimal = int(time_scaled).bit_length() - 1
    decimal = time_scaled / (1 << pre_decimal) - 1

    value = pre_decimal + decimal
    return int(32 * value)


def native_value_to_time(value: int) -> int:
    """Convert native LCN value to time.

    Scales the given byte value (0..255) to a time value in milliseconds.

    :param    int    value:    Duration of timer in native LCN units

    :returns: The duration in milliseconds
    :rtype: int
    """
    if not 0 <= value <= 255:
        raise ValueError("Value has to be in range 0..255")
    pre_decimal = value // 32
    decimal = value / 32 - pre_decimal

    time_scaled = (1 << pre_decimal) * (decimal + 1)

    time_msec = (time_scaled - 1) * 1000 * 0.03 * 32
    return int(time_msec)


def motor_position_time_to_native_value(time_msec: int) -> int:
    """Convert time to native LCN time value.

    Scales the given time value in milliseconds to a two-byte value.

    :param    int    time_msec:    Duration of timer in milliseconds (1001..65535000)

    :returns: The duration in native LCN units
    :rtype: int
    """
    if not 1001 <= time_msec <= 65535000:
        raise ValueError("Time has to be in range 1001..65535000ms")
    value = 0xFFFF * 1000 / time_msec
    return int(value)


def native_value_to_motor_position_time(value: int) -> int:
    """Convert native LCN value to time.

    Scales the given two-byte value (1..65535) to a time value in milliseconds.

    :param    int    value:    Duration of timer in native LCN units

    :returns: The duration in milliseconds
    :rtype: int
    """
    if not 1 <= value <= 0xFFFF:
        raise ValueError("Value has to be in range 1..65535")

    time_msec = 0xFFFF * 1000 / value
    return int(time_msec)


class Var(Enum):
    """LCN variable types."""

    UNKNOWN = -1  # Used if the real type is not known (yet)
    VAR1ORTVAR = 0
    TVAR = 0
    VAR1 = 0
    VAR2ORR1VAR = 1
    R1VAR = 1
    VAR2 = 1
    VAR3ORR2VAR = 2
    R2VAR = 2
    VAR3 = 2
    VAR4 = 3
    VAR5 = 4
    VAR6 = 5
    VAR7 = 6
    VAR8 = 7
    VAR9 = 8
    VAR10 = 9
    VAR11 = 10
    VAR12 = 11  # Since 170206
    R1VARSETPOINT = auto()
    R2VARSETPOINT = auto()  # Set-points for regulators
    THRS1 = auto()
    THRS2 = auto()
    THRS3 = auto()
    THRS4 = auto()
    THRS5 = auto()  # Register 1 (THRS5 only before 170206)
    THRS2_1 = auto()
    THRS2_2 = auto()
    THRS2_3 = auto()
    THRS2_4 = auto()  # Register 2 (since 2012)
    THRS3_1 = auto()
    THRS3_2 = auto()
    THRS3_3 = auto()
    THRS3_4 = auto()  # Register 3 (since 2012)
    THRS4_1 = auto()
    THRS4_2 = auto()
    THRS4_3 = auto()
    THRS4_4 = auto()  # Register 4 (since 2012)
    S0INPUT1 = auto()
    S0INPUT2 = auto()
    S0INPUT3 = auto()
    S0INPUT4 = auto()  # LCN-BU4LJVarValue

    @classmethod
    def variables(cls) -> list[Var]:
        """Return a list of all variable types."""
        return [
            cls.VAR1ORTVAR,
            cls.VAR2ORR1VAR,
            cls.VAR3ORR2VAR,
            cls.VAR4,
            cls.VAR5,
            cls.VAR6,
            cls.VAR7,
            cls.VAR8,
            cls.VAR9,
            cls.VAR10,
            cls.VAR11,
            cls.VAR12,
        ]

    @classmethod
    def variables_new(cls) -> list[Var]:
        """Return a list of all new variable types (firmware >=0x170206)."""
        return cls.variables()

    @classmethod
    def variables_old(cls) -> list[Var]:
        """Return a list of all variable types (firmware <0x170206)."""
        return cls.variables()[:3]

    @classmethod
    def set_points(cls) -> list[Var]:
        """Return a list of all set-point variable types."""
        return [cls.R1VARSETPOINT, cls.R2VARSETPOINT]

    @classmethod
    def thresholds(cls) -> list[list[Var]]:
        """Return a list of all threshold variable types."""
        return [
            [cls.THRS1, cls.THRS2, cls.THRS3, cls.THRS4, cls.THRS5],
            [cls.THRS2_1, cls.THRS2_2, cls.THRS2_3, cls.THRS2_4],
            [cls.THRS3_1, cls.THRS3_2, cls.THRS3_3, cls.THRS3_4],
            [cls.THRS4_1, cls.THRS4_2, cls.THRS4_3, cls.THRS4_4],
        ]

    @classmethod
    def thresholds_new(cls) -> list[list[Var]]:
        """Return a list of all threshold variable types (firmware >=0x170206)."""
        return [cls.thresholds()[0][:4], *cls.thresholds()[1:]]

    @classmethod
    def thresholds_old(cls) -> list[list[Var]]:
        """Return a list of all old threshold variable types (firmware <0x170206)."""
        return [cls.thresholds()[0]]

    @classmethod
    def s0s(cls) -> list[Var]:
        """Return a list of all S0-input variable types."""
        return [cls.S0INPUT1, cls.S0INPUT2, cls.S0INPUT3, cls.S0INPUT4]

    @staticmethod
    def var_id_to_var(var_id: int) -> Var:
        """Translate a given id into a variable type.

        :param    int    varId:    The variable id (0..11)

        :returns: The translated variable enum.
        :rtype: Var
        """
        if (var_id < 0) or (var_id >= len(Var.variables())):
            raise ValueError("Bad var_id.")
        return Var.variables()[var_id]

    @staticmethod
    def set_point_id_to_var(set_point_id: int) -> Var:
        """Translate a given id into a LCN set-point variable type.

        :param     int    set_point_id:    Set-point id 0..1

        :return: The translated var
        :rtype:  Var
        """
        if (set_point_id < 0) or (set_point_id >= len(Var.set_points())):
            raise ValueError("Bad set_point_id.")
        return Var.set_points()[set_point_id]

    @staticmethod
    def thrs_id_to_var(register_id: int, thrs_id: int) -> Var:
        """Translate given ids into a LCN threshold variable type.

        :param    int    register_id:    Register id 0..3
        :param    int    thrs_id:        Threshold id 0..4 for register 0,
                                                      0..3 for registers 1..3

        :return: The translated var
        :rtype:    Var
        """
        if (
            (register_id < 0)
            or (register_id >= len(Var.thresholds()))
            or (thrs_id < 0)
            or (thrs_id >= (5 if (register_id == 0) else 4))
        ):
            raise ValueError("Bad register_id and/or thrs_id.")
        return Var.thresholds()[register_id][thrs_id]

    @staticmethod
    def s0_id_to_var(s0_id: int) -> Var:
        """Translate a given id into a LCN S0-input variable type.

        :param     int    s0_id:     S0 id 0..3

        :return:    The translated var
        :rtype:     Var
        """
        if (s0_id < 0) or (s0_id >= len(Var.s0s())):
            raise ValueError("Bad s0_id.")
        return Var.s0s()[s0_id]

    @staticmethod
    def to_var_id(var: Var) -> int:
        """Translate a given variable type into a variable id.

        :param     Var    var:    The variable type to translate

        :return:     Variable id 0..11 or -1 if wrong type
        :rtype:    int
        """
        if var == Var.VAR1ORTVAR:
            var_id = 0
        elif var == Var.VAR2ORR1VAR:
            var_id = 1
        elif var == Var.VAR3ORR2VAR:
            var_id = 2
        elif var == Var.VAR4:
            var_id = 3
        elif var == Var.VAR5:
            var_id = 4
        elif var == Var.VAR6:
            var_id = 5
        elif var == Var.VAR7:
            var_id = 6
        elif var == Var.VAR8:
            var_id = 7
        elif var == Var.VAR9:
            var_id = 8
        elif var == Var.VAR10:
            var_id = 9
        elif var == Var.VAR11:
            var_id = 10
        elif var == Var.VAR12:
            var_id = 11
        else:
            var_id = -1
        return var_id

    @staticmethod
    def to_set_point_id(var: Var) -> int:
        """Translate a given variable type into a set-point id.

        :param     Var    var:     The variable type to translate

        :return:    Variable id 0..1 or -1 if wrong type
        :rtype:    int
        """
        if var == Var.R1VARSETPOINT:
            set_point_id = 0
        elif var == Var.R2VARSETPOINT:
            set_point_id = 1
        else:
            set_point_id = -1
        return set_point_id

    @staticmethod
    def to_thrs_register_id(var: Var) -> int:
        """Translate a given variable type into a threshold register id.

        :param    Var    var:    The variable type to translate

        :return:    Register id 0..3 or -1 if wrong type
        :rtype:    int
        """
        if var in [Var.THRS1, Var.THRS2, Var.THRS3, Var.THRS4, Var.THRS5]:
            thrs_register_id = 0
        elif var in [Var.THRS2_1, Var.THRS2_2, Var.THRS2_3, Var.THRS2_4]:
            thrs_register_id = 1
        elif var in [Var.THRS3_1, Var.THRS3_2, Var.THRS3_3, Var.THRS3_4]:
            thrs_register_id = 2
        elif var in [Var.THRS4_1, Var.THRS4_2, Var.THRS4_3, Var.THRS4_4]:
            thrs_register_id = 3
        else:
            thrs_register_id = -1
        return thrs_register_id

    @staticmethod
    def to_thrs_id(var: Var) -> int:
        """Translate a given variable type into a threshold id.

        :param    Var    var:    The variable type to translate

        :return:    Threshold id 0..4 or -1 if wrong type
        :rtype:    int
        """
        if var in [Var.THRS1, Var.THRS2_1, Var.THRS3_1, Var.THRS4_1]:
            thrs_id = 0
        elif var in [Var.THRS2, Var.THRS2_2, Var.THRS3_2, Var.THRS4_2]:
            thrs_id = 1
        elif var in [Var.THRS3, Var.THRS2_3, Var.THRS3_3, Var.THRS4_3]:
            thrs_id = 2
        elif var in [Var.THRS4, Var.THRS2_4, Var.THRS3_4, Var.THRS4_4]:
            thrs_id = 3
        elif var == Var.THRS5:
            thrs_id = 4
        else:
            thrs_id = -1
        return thrs_id

    @staticmethod
    def to_s0_id(var: Var) -> int:
        """Translate a given variable type into an S0-input id.

        :param    Var    var:    The variable type to translate

        :return:    S0 id 0..3 or -1 if wrong type
        :rtype:    int
        """
        if var == Var.S0INPUT1:
            s0_id = 0
        elif var == Var.S0INPUT2:
            s0_id = 1
        elif var == Var.S0INPUT3:
            s0_id = 2
        elif var == Var.S0INPUT4:
            s0_id = 3
        else:
            s0_id = -1
        return s0_id

    @staticmethod
    def is_lockable_regulator_source(var: Var) -> bool:
        """Check if the the given variable type is lockable.

        :param    Var    var:    The variable type to check

        :return:    True if lockable, otherwise False
        :rtype:    bool
        """
        return var in [Var.R1VARSETPOINT, Var.R2VARSETPOINT]

    @staticmethod
    def use_lcn_special_values(var: Var) -> bool:
        """Check if the given variable type uses special values.

        Examples for special values: 'No value yet', 'sensor defective' etc.

        :param    Var    var:    The variable type to check

        :return:    True if special values are in use, otherwise False
        :rtype:    bool
        """
        return var not in [Var.S0INPUT1, Var.S0INPUT2, Var.S0INPUT3, Var.S0INPUT4]

    @staticmethod
    def has_type_in_response(var: Var, software_serial: int) -> bool:
        """Module-generation check.

        Check if the given variable type would receive a typed response if
        its status was requested.

        :param    Var    var:    The variable type to check
        :param    int    swAge:  The target LCN-modules firmware version

        :return:    True if a response would contain the variable's type,
                    otherwise False
        :rtype:    bool
        """
        if software_serial < 0x170206:
            if var in [
                Var.VAR1ORTVAR,
                Var.VAR2ORR1VAR,
                Var.VAR3ORR2VAR,
                Var.R1VARSETPOINT,
                Var.R2VARSETPOINT,
            ]:
                return False
        return True

    @staticmethod
    def is_event_based(var: Var, software_serial: int) -> bool:
        """Module-generation check.

        Check if the given variable type automatically sends status-updates
        on value-change. It must be polled otherwise.

        :param    Var    var:    The variable type to check
        :param    int    swAge:  The target LCN-module's firmware version

        :return:    True if the LCN module supports automatic status-messages
                    for this var, otherwise False
        :rtype:    bool
        """
        if (Var.to_set_point_id(var) != -1) or (Var.to_s0_id(var) != -1):
            return True
        return software_serial >= 0x170206

    @staticmethod
    def should_poll_status_after_command(var: Var, is2013: bool) -> bool:
        """Module-generation check.

        Check if the target LCN module would automatically send status-updates
        if the given variable type was changed by command.

        :param    Var    var:    The variable type to check
        :param    bool   is2013: The target module's-generation

        :return:    True if a poll is required to get the new status-value,
                    otherwise False
        :rtype:    bool
        """
        # Regulator set-points will send status-messages on every change
        # (all firmware versions)
        if Var.to_set_point_id(var) != -1:
            return False

        # Thresholds since 170206 will send status-messages on every change
        if is2013 and (Var.to_thrs_register_id(var) != -1):
            return False

        # Others:
        # - Variables before 170206 will never send any status-messages
        # - Variables since 170206 only send status-messages on "big" changes
        # - Thresholds before 170206 will never send any status-messages
        # - S0-inputs only send status-messages on "big" changes
        # (all "big changes" cases force us to poll the status to get faster
        # updates)
        return True

    @staticmethod
    def should_poll_status_after_regulator_lock(
        software_serial: int, lock_state: int
    ) -> bool:
        """Module-generation check.

        Check if the target LCN module would automatically send status-updates
        if the given regulator's lock-state was changed by command.

        :param    int       swAge: The target LCN-module's firmware version
        :param    int   lockState: The lock-state sent via command

        :return:    True if a poll is required to get the new status-value,
                    otherwise False
        :rtype:    bool
        """
        # LCN modules before 170206 will send an automatic status-message for
        # "lock", but not for "unlock"
        return (not lock_state) and (software_serial < 0x170206)


class VarUnit(Enum):
    """Measurement units used with LCN variables."""

    NATIVE = ""  # LCN internal representation (0 = -100C for absolute values)
    CELSIUS = "\u00b0C"
    KELVIN = "\u00b0K"
    FAHRENHEIT = "\u00b0F"
    LUX_T = "Lux_T"
    LUX_I = "Lux_I"
    METERPERSECOND = "m/s"  # Used for LCN-WIH wind speed
    PERCENT = "%"  # Used for humidity
    PPM = "ppm"  # Used by CO2 sensor
    VOLT = "V"
    AMPERE = "A"
    DEGREE = "\u00b0"  # Used for angles,

    @staticmethod
    def parse(unit: str) -> VarUnit:
        """Parse the given unit string and return VarUnit.

        :param    str    unit:    The input unit
        """
        unit = unit.upper()
        if unit in ["", "NATIVE", "LCN"]:
            var_unit = VarUnit.NATIVE
        elif unit in ["CELSIUS", "\u00b0CELSIUS", "\u00b0C"]:
            var_unit = VarUnit.CELSIUS
        elif unit in ["KELVIN", "\u00b0KELVIN", "\u00b0K", "K"]:
            var_unit = VarUnit.KELVIN
        elif unit in ["FAHRENHEIT", "\u00b0FAHRENHEIT", "\u00b0F"]:
            var_unit = VarUnit.FAHRENHEIT
        elif unit in ["LUX_T", "LX_T"]:
            var_unit = VarUnit.LUX_T
        elif unit in ["LUX", "LUX_I", "LX"]:
            var_unit = VarUnit.LUX_I
        elif unit in ["M/S", "METERPERSECOND"]:
            var_unit = VarUnit.METERPERSECOND
        elif unit in ["%", "PERCENT"]:
            var_unit = VarUnit.PERCENT
        elif unit == "PPM":
            var_unit = VarUnit.PPM
        elif unit in ["VOLT", "V"]:
            var_unit = VarUnit.VOLT
        elif unit in ["AMPERE", "AMP", "A"]:
            var_unit = VarUnit.AMPERE
        elif unit in ["DEGREE", "\u00b0"]:
            var_unit = VarUnit.DEGREE
        else:
            raise ValueError("Bad input unit.")
        return var_unit


class VarValue:
    """A value of an LCN variable.

    It internally stores the native LCN value and allows to convert from/into
    other units. Some conversions allow to specify whether the source value is
    absolute or relative. Relative values are used to create varvalues that
    can be added/subtracted from other (absolute) varvalues.

    :param    int    native_value:    The native value
    """

    def __init__(self, native_value: int) -> None:
        """Construct with native LCN value."""
        self.native_value = native_value

    def __eq__(self, other: object) -> bool:
        """Return if instance equals the given object."""
        if isinstance(other, VarValue):
            return self.native_value == other.native_value
        return False

    def __hash__(self) -> int:
        """Calculate the instance hash value."""
        return self.native_value.__hash__()

    def is_locked_regulator(self) -> bool:
        """Return if regulator is locked."""
        return self.is_locked()

    def is_locked_threshold(self) -> bool:
        """Return if threshold is locked."""
        return self.is_locked()

    def is_locked(self) -> bool:
        """Return if variable is locked."""
        return (self.native_value & 0x8000) != 0

    @staticmethod
    def from_var_unit(value: float, unit: VarUnit, is_abs: bool) -> VarValue:
        """Create a variable value from any input.

        :param    float    value:   The input value
        :param    VarUnit  unit:    The input value's unit
        :param    bool     is_abs:  True for absolute values (relative values
                                    are used to add/subtract from other
                                    VarValues), otherwise False

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        if unit == VarUnit.NATIVE:
            var_value = VarValue.from_native(int(value))
        elif unit == VarUnit.CELSIUS:
            var_value = VarValue.from_celsius(value, is_abs)
        elif unit == VarUnit.KELVIN:
            var_value = VarValue.from_kelvin(value, is_abs)
        elif unit == VarUnit.FAHRENHEIT:
            var_value = VarValue.from_fahrenheit(value, is_abs)
        elif unit == VarUnit.LUX_T:
            var_value = VarValue.from_lux_t(value)
        elif unit == VarUnit.LUX_I:
            var_value = VarValue.from_lux_i(value)
        elif unit == VarUnit.METERPERSECOND:
            var_value = VarValue.from_meters_per_second(value)
        elif unit == VarUnit.PERCENT:
            var_value = VarValue.from_percent(value)
        elif unit == VarUnit.PPM:
            var_value = VarValue.from_ppm(value)
        elif unit == VarUnit.VOLT:
            var_value = VarValue.from_volt(value)
        elif unit == VarUnit.AMPERE:
            var_value = VarValue.from_ampere(value)
        elif unit == VarUnit.DEGREE:
            var_value = VarValue.from_degree(value, is_abs)
        else:
            raise ValueError("Wrong unit.")
        return var_value

    @staticmethod
    def from_native(value: int) -> VarValue:
        """Create a variable value from native input.

        :param    int    value:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(value)

    @staticmethod
    def from_celsius(value: float, is_abs: bool = True) -> VarValue:
        """Create a variable value from Celsius input.

        :param    float    value:    The input value
        :param    bool     is_abs:   True for absolute values (relative values
                                     are used to add/subtract from other
                                     VarValues), otherwise False

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        number = int(round(value * 10))
        return VarValue(number + 1000 if is_abs else number)

    @staticmethod
    def from_kelvin(value: float, is_abs: bool = True) -> VarValue:
        """Create a variable value from Kelvin input.

        :param    float    value:    The input value
        :param    bool     is_abs:   True for absolute values (relative values
                                     are used to add/subtract from other
                                     VarValues), otherwise False

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        if is_abs:
            value -= 273.15

        number = int(round(value * 10))
        return VarValue(number + 1000 if is_abs else number)

    @staticmethod
    def from_fahrenheit(value: float, is_abs: bool = True) -> VarValue:
        """Create a variable value from Fahrenheit input.

        :param    float    value:    The input value
        :param    bool     is_abs:   True for absolute values (relative values
                                     are used to add/subtract from other
                                     VarValues), otherwise False

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        if is_abs:
            value -= 32

        number = int(round(value / 0.18))
        return VarValue(number + 1000 if is_abs else number)

    @staticmethod
    def from_lux_t(lux: float) -> VarValue:
        """Create a variable value from lx input.

        Target must be connected to T-port.

        :param    float    l:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(math.log(lux) - 1.689646994) / 0.010380664))

    @staticmethod
    def from_lux_i(lux: float) -> VarValue:
        """Create a variable value from lx input.

        Target must be connected to I-port.

        :param    float    l:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(math.log(lux) * 100)))

    @staticmethod
    def from_percent(value: float) -> VarValue:
        """Create a variable value from % input.

        :param    float    value:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(value)))

    @staticmethod
    def from_ppm(value: float) -> VarValue:
        """Create a variable value from ppm input.

        Used for CO2 sensors.

        :param    float    value:   The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(value)))

    @staticmethod
    def from_meters_per_second(value: float) -> VarValue:
        """Create a variable value from m/s input.

        Used for LCN-WIH wind speed.

        :param    float    value:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(value * 10)))

    @staticmethod
    def from_volt(value: float) -> VarValue:
        """Create a variable value from V input.

        :param    float    value:    The input value

        :return:    The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(value * 400)))

    @staticmethod
    def from_ampere(value: float) -> VarValue:
        """Create a variable value from A input.

        :param    float    value:    The input value

        :return: The variable value (never null)
        :rtype:    VarValue
        """
        return VarValue(int(round(value * 100000)))

    @staticmethod
    def from_degree(value: float, is_abs: bool = True) -> VarValue:
        """Create a variable value from degree (angle) input.

        :param    float    value:        The input value
        :param    bool     is_abs:   True for absolute values (relative values
                                     are used to add/subtract from other
                                     VarValues), otherwise False

        :return:    The variable value (never null)
        :rtype:     VarValue
        """
        number = int(round(value * 10))
        return VarValue(number + 1000 if is_abs else number)

    def to_var_unit(
        self,
        unit: VarUnit,
        is_lockable_regulator_source: bool = False,
    ) -> int | float:
        """Convert the given unit to a VarValue.

        :param    VarUnit    unit:    The variable unit
        :param    bool       is_lockable_regulator_source:  Is lockable source

        :return:    The variable value
        :rtype:     Union[int,float]
        """
        var_value = VarValue(
            self.native_value & 0x7FFF
            if is_lockable_regulator_source
            else self.native_value
        )

        if unit == VarUnit.NATIVE:
            return var_value.to_native()
        if unit == VarUnit.CELSIUS:
            return var_value.to_celsius()
        if unit == VarUnit.KELVIN:
            return var_value.to_kelvin()
        if unit == VarUnit.FAHRENHEIT:
            return var_value.to_fahrenheit()
        if unit == VarUnit.LUX_T:
            return var_value.to_lux_t()
        if unit == VarUnit.LUX_I:
            return var_value.to_lux_i()
        if unit == VarUnit.METERPERSECOND:
            return var_value.to_meters_per_second()
        if unit == VarUnit.PERCENT:
            return var_value.to_percent()
        if unit == VarUnit.PPM:
            return var_value.to_ppm()
        if unit == VarUnit.VOLT:
            return var_value.to_volt()
        if unit == VarUnit.AMPERE:
            return var_value.to_ampere()
        if unit == VarUnit.DEGREE:
            return var_value.to_degree()
        raise ValueError("Wrong unit.")

    def to_native(self) -> int:
        """Convert to native value.

        :return:    The converted value
        :rtype:    int
        """
        return self.native_value

    def to_celsius(self) -> float:
        """Convert to Celsius value.

        :return:    The converted value
        :rtype:    float
        """
        return (self.native_value - 1000) / 10.0

    def to_kelvin(self) -> float:
        """Convert to Kelvin value.

        :return:    The converted value
        :rtype:    float
        """
        return (self.native_value - 1000) / 10.0 + 273.15

    def to_fahrenheit(self) -> float:
        """Convert to Fahrenheit value.

        :return:    The converted value
        :rtype:    float
        """
        return (self.native_value - 1000) * 0.18 + 32.0

    def to_lux_t(self) -> float:
        """Convert to lx value.

        Source must be connected to T-port.

        :return:    The converted value
        :rtype:    float
        """
        return math.exp(0.010380664 * self.native_value + 1.689646994)

    def to_lux_i(self) -> float:
        """Convert to lx value.

        Source must be connected to I-port.

        :return:    The converted value
        :rtype:    float
        """
        return math.exp(self.native_value / 100)

    def to_percent(self) -> int:
        """Convert to % value.

        :return:    The converted value
        :rtype:    int
        """
        return self.native_value

    def to_ppm(self) -> int:
        """Convert to ppm value.

        :return:    The converted value
        :rtype:    int
        """
        return self.native_value

    def to_meters_per_second(self) -> float:
        """Convert to m/s value.

        :return:    The converted value
        :rtype:    float
        """
        return self.native_value / 10.0

    def to_volt(self) -> float:
        """Convert to V value.

        :return:    The converted value
        :rtype:    float
        """
        return self.native_value / 400.0

    def to_ampere(self) -> float:
        """Convert to A value.

        :return:    The converted value
        :rtype:    float
        """
        return self.native_value / 100000.0

    def to_degree(self) -> float:
        """Convert to degree value.

        :return:    The converted value
        :rtype:    float
        """
        return (self.native_value - 1000) / 10.0

    def to_var_unit_string(
        self,
        unit: VarUnit,
        is_lockable_regulator_source: bool = False,
        use_lcn_special_values: bool = False,
    ) -> str:
        """Convert the given unit into a string representation.

        :param    VarUnit    unit:    The input unit
        :param    bool       is_lockable_regulator_source:  Is lockable source
        :param    bool       use_lcn_special_values:  Use LCN special values

        :return:    The string representation of input unit.
        :rtype:     str
        """
        if use_lcn_special_values and (self.native_value == 0xFFFF):  # No value
            ret = "---"
        elif use_lcn_special_values and (
            (self.native_value & 0xFF00) == 0x8100
        ):  # Undefined
            ret = "---"
        elif use_lcn_special_values and (
            (self.native_value & 0xFF00) == 0x7F00
        ):  # Defective
            ret = "!!!"
        else:
            var = VarValue(
                (self.native_value & 0x7FF)
                if is_lockable_regulator_source
                else self.native_value
            )
            if unit == VarUnit.NATIVE:
                ret = f"{var.to_native():.0f}"
            elif unit == VarUnit.CELSIUS:
                ret = f"{var.to_celsius():.01f}"
            elif unit == VarUnit.KELVIN:
                ret = f"{var.to_kelvin():.01f}"
            elif unit == VarUnit.FAHRENHEIT:
                ret = f"{var.to_fahrenheit():.01f}"
            elif unit == VarUnit.LUX_T:
                if var.to_native() > 1152:  # Max. value the HW can do
                    ret = "---"
                else:
                    ret = f"{var.to_lux_t():.0f}"
            elif unit == VarUnit.LUX_I:
                if var.to_native() > 1152:  # Max. value the HW can do
                    ret = "---"
                else:
                    ret = f"{var.to_lux_i():.0f}"
            elif unit == VarUnit.METERPERSECOND:
                ret = f"{var.to_meters_per_second():.0f}"
            elif unit == VarUnit.PERCENT:
                ret = f"{var.to_percent():.0f}"
            elif unit == VarUnit.PPM:
                ret = f"{var.to_ppm():.0f}"
            elif unit == VarUnit.VOLT:
                ret = f"{var.to_volt():.0f}"
            elif unit == VarUnit.AMPERE:
                ret = f"{var.to_ampere():.0f}"
            elif unit == VarUnit.DEGREE:
                ret = f"{var.to_degree():.0f}"
            else:
                raise ValueError("Wrong unit.")

        # handle locked regulators
        if is_lockable_regulator_source and self.is_locked():
            ret = f"({ret:s})"

        return ret


class LedStatus(Enum):
    """Possible states for LCN LEDs."""

    OFF = "A"
    ON = "E"
    BLINK = "B"
    FLICKER = "F"


class LogicOpStatus(Enum):
    """Possible states for LCN logic-operations."""

    NONE = "N"
    SOME = "T"  # Note: Actually not correct since AND won't be OR also
    ALL = "V"


class TimeUnit(Enum):
    """Time units used for several LCN commands."""

    SECONDS = "S"
    MINUTES = "M"
    HOURS = "H"
    DAYS = "D"

    @staticmethod
    def parse(unit: str) -> TimeUnit:
        """Parse the given time_unit into a time unit.

        It supports several alternative terms.

        :param    str    time_unit:    The text to parse

        :return:    TimeUnit enum
        :rtype:    TimeUnit
        """
        unit = unit.upper()
        if unit in ["SECONDS", "SECOND", "SEC", "S"]:
            time_unit = TimeUnit.SECONDS
        elif unit in ["MINUTES", "MINUTE", "MIN", "M"]:
            time_unit = TimeUnit.MINUTES
        elif unit in ["HOURS", "HOUR", "H"]:
            time_unit = TimeUnit.HOURS
        elif unit in ["DAYS", "DAY", "D"]:
            time_unit = TimeUnit.DAYS
        else:
            raise ValueError("Bad time unit input.")
        return time_unit


class RelayStateModifier(Enum):
    """Relay-state modifiers used in LCN commands."""

    ON = "1"
    OFF = "0"
    TOGGLE = "U"
    NOCHANGE = "-"


class MotorStateModifier(Enum):
    """Motor-state modifiers used in LCN commands.

    LCN module has to be configured for motors connected to relays.
    """

    UP = "U"
    DOWN = "D"
    STOP = "S"
    TOGGLEONOFF = "T"  # toggle on/off
    TOGGLEDIR = "R"  # toggle direction
    CYCLE = "C"  # up, stop, down, stop, ...
    NOCHANGE = "-"


class MotorReverseTime(Enum):
    """Motor reverse time user in LCN commands.

    For modules with FW<190C the release time has to be specified.
    """

    RT70 = "RT70"  # 70ms
    RT600 = "RT600"  # 600ms
    RT1200 = "RT1200"  # 1200ms


class MotorPositioningMode(Enum):
    """Motor positioning mode used in LCN commands."""

    NONE = "NONE"
    BS4 = "BS4"
    MODULE = "MODULE"
    # EMULATED = "EMULATED"


class RelVarRef(Enum):
    """Value-reference for relative LCN variable commands."""

    CURRENT = auto()
    PROG = auto()  # Programmed value (LCN-PRO). Set-points and thresholds.


class SendKeyCommand(Enum):
    """Command types used when sending LCN keys."""

    HIT = "K"
    MAKE = "L"
    BREAK = "O"
    DONTSEND = "-"


class KeyLockStateModifier(Enum):
    """Key-lock modifiers used in LCN commands."""

    ON = "1"
    OFF = "0"
    TOGGLE = "U"
    NOCHANGE = "-"


class ThresholdLockStateModifier(Enum):
    """Threshold-lock modifiers used in LCN commands."""

    ON = "1"
    OFF = "0"
    NOCHANGE = "-"


class BeepSound(Enum):
    """Beep sounds supported by LCN modules."""

    NORMAL = "N"
    SPECIAL = "S"


HARDWARE_DESCRIPTIONS = {
    -1: "UnknownModuleType",
    1: "LCN-SW1.0",
    2: "LCN-SW1.1",
    3: "LCN-UP1.0",
    4: "LCN-UP2",
    5: "LCN-SW2",
    6: "LCN-UP-Profi1-Plus",
    7: "LCN-DI12",
    8: "LCN-HU",
    9: "LCN-SH",
    10: "LCN-UP2",
    11: "LCN-UPP",
    12: "LCN-SK",
    14: "LCN-LD",
    15: "LCN-SH-Plus",
    17: "LCN-UPS",
    18: "LCN_UPS24V",
    19: "LCN-GTM",
    20: "LCN-SHS",
    21: "LCN-ESD",
    22: "LCN-EB2",
    23: "LCN-MRS",
    24: "LCN-EB11",
    25: "LCN-UMR",
    26: "LCN-UPU",
    27: "LCN-UMR24V",
    28: "LCN-SHD",
    29: "LCN-SHU",
    30: "LCN-SR6",
    31: "LCN-UMF",
    32: "LCN-WBH",
}


class HardwareType(Enum):
    """Hardware types as returned by serial number request."""

    UNKNOWN = -1
    SW1_0 = 1
    SW1_1 = 2
    UP1_0 = 3
    UP2 = 4
    SW2 = 5
    UP_PROFI1_PLUS = 6
    DI12 = 7
    HU = 8
    SH = 9
    UPP = 11
    SK = 12
    LD = 14
    SH_PLUS = 15
    UPS = 17
    UPS24V = 18
    GTM = 19
    SHS = 20
    ESD = 21
    EB2 = 22
    MRS = 23
    EB11 = 24
    UMR = 25
    UPU = 26
    UMR24V = 27
    SHD = 28
    SHU = 29
    SR6 = 30
    UMF = 31
    WBH = 32

    @property
    def identifier(self) -> Any:
        """Get the LCN hardware identifier."""
        return self.value

    @property
    def description(self) -> str:
        """Get the LCN hardware name."""
        return HARDWARE_DESCRIPTIONS[self.value]


@no_type_check
def hw_type_new(cls, value):
    """Replace Hardwaretype.__new__."""
    if value == 10:
        value = 4
    return super(HardwareType, cls).__new__(cls, value)


setattr(HardwareType, "__new__", hw_type_new)


class AccessControlPeriphery(Enum):
    """Action types for LCN keys."""

    TRANSMITTER = "transmitter"
    TRANSPONDER = "transponder"
    FINGERPRINT = "fingerprint"
    CODELOCK = "codelock"


class AcknowledgeErrorCode(Enum):
    """Acknowledge error codes."""

    UNKNOWN = -1
    OK = 0
    UNKNOWN_COMMAND = 5
    WRONG_PARAMETER_COUNT = 6
    INVALID_PARAMETER_VALUE = 7
    CURRENTLY_NOT_ALLOWED = 8
    NOT_ALLOWED_BY_PROGRAMMING = 9
    INAPPROPRIATE_MODULE = 10
    MISSING_PERIPHERY = 11
    PROGRAMMING_MODE_REQUIRED = 12
    FUSE_DEFECT = 14

    @classmethod
    def _missing_(cls, value: Any) -> AcknowledgeErrorCode:
        """Handle missing values."""
        return cls.UNKNOWN


class LcnEvent(Enum):
    """LCN events."""

    CONNECTION_ESTABLISHED = "connection-established"
    CONNECTION_LOST = "connection-lost"
    CONNECTION_REFUSED = "connection-refused"
    CONNECTION_TIMEOUT = "connection-timeout"
    PING_TIMEOUT = "ping-timeout"
    TIMEOUT_ERROR = "timeout-error"
    LICENSE_ERROR = "license-error"
    AUTHENTICATION_ERROR = "authentication-error"
    BUS_CONNECTED = "bus-connected"
    BUS_DISCONNECTED = "bus-disconnected"
    BUS_CONNECTION_STATUS_CHANGED = "bus-connection-status-changed"


default_connection_settings: dict[str, Any] = {
    "NUM_TRIES": 3,  # Total number of request to sent before going into
    # failed-state.
    "SK_NUM_TRIES": 3,  # Total number of segment coupler scan tries
    "DIM_MODE": OutputPortDimMode.STEPS50,
    "ACKNOWLEDGE": True,  # modules request an acknowledge command
    "DEFAULT_TIMEOUT": 3.5,  # Default timeout for send command retries
    "MAX_STATUS_EVENTBASED_VALUEAGE": 600,  # Poll interval for
    # status values that
    # automatically send
    # their values on change
    "MAX_STATUS_POLLED_VALUEAGE": 30,  # Poll interval for status
    # values that do not send
    # their values on change
    # (always polled)
    "STATUS_REQUEST_DELAY_AFTER_COMMAND": 2,  # Status request delay
    # after a command has
    # been send which
    # potentially changed
    # that status
    "MAX_RESPONSE_AGE": 60,  # Age in seconds after which stored responses are purged
    "BUS_IDLE_TIME": 0.05,  # Time to wait for message traffic before sending
    "PING_SEND_DELAY": 600,  # The default timeout for pings sent to PCHK
    "PING_RECV_TIMEOUT": 10,  # The default timeout for pings expected from PCHK
    "PING_MODULE_TIMEOUT": 60,  # The delay before sending a ping to a module
}
