class DeviceNormalPoll(DevicePoll):
    """
    Start a normal polling task to repeatedly read a list of points from a device using
    ReadPropertyMultiple requests.

    Normal polling will limit the polling speed to 10 second minimum

    """

    def __init__(self, device, delay=10, name=""):
        """
        :param device: (BAC0.core.devices.Device.Device) device to poll
        :param delay: (int) Delay between polls in seconds, defaults = 10sec

        :returns: Nothing
        """
        if delay < 10:
            delay = 10
        self._log.info(
            "Device defined for normal polling with a delay of {}sec".format(delay)
        )
        DevicePoll.__init__(
            self, device=device, name=name, delay=delay, prefix="rpm_normal_poll"
        )
 class DeviceFastPoll(DevicePoll):
    """
    Start a fast polling task to repeatedly read a list of points from a device using
    ReadPropertyMultiple requests.
    Delay allowed will be 0 to 10 seconds
    Normal polling will limit the polling speed to 10 second minimum

    Warning : Fast polling must be used with care or network flooding may occur

    """

    def __init__(self, device, delay=1, name=""):
        """
        :param device: (BAC0.core.devices.Device.Device) device to poll
        :param delay: (int) Delay between polls in seconds, defaults = 1sec

        :returns: Nothing
        """
        if delay < 0:
            delay = 0.01
        elif delay > 10:
            delay = 10
        self._log.warning(
            "Device defined for fast polling with a delay of {}sec".format(delay)
        )
        DevicePoll.__init__(
            self, device=device, name=name, delay=delay, prefix="rpm_fast_poll"
        )
 class ReadPropertyMultipleException(ValueError):
    """
    This exception is used when trying to read multiple properties.
    """

    pass class NoResponseFromController(Exception):
    """
    This exception is used when trying to read or write and there is not answer.
    """

    pass class SegmentationNotSupported(Exception):
    pass class BufferOverflow(Exception):
    """
    Buffer capacity of device exceeded.
    """

    pass class NumericPoint(Point):
    """
    Representation of a Numeric value
    """

    def __init__(
        self,
        device=None,
        pointType=None,
        pointAddress=None,
        pointName=None,
        description=None,
        presentValue=None,
        units_state=None,
        history_size=None,
    ):

        Point.__init__(
            self,
            device=device,
            pointType=pointType,
            pointAddress=pointAddress,
            pointName=pointName,
            description=description,
            presentValue=presentValue,
            units_state=units_state,
            history_size=history_size,
        )

    @property
    def value(self):
        res = super().value
        self._trend(res)
        return res

    @property
    def units(self):
        return self.properties.units_state

    def _set(self, value):
        if str(value).lower() == "auto":
            self._setitem(value)
        else:
            try:
                if isinstance(value, Point):
                    value = value.lastValue
                val = float(value)
                if isinstance(val, float):
                    self._setitem(value)
            except Exception as error:
                raise WritePropertyException(
                    "Problem writing to device : {}".format(error)
                )

    def __repr__(self):
        try:
            polling = self.properties.device.properties.pollDelay
            if (polling < 90 and polling > 0) or self.cov_registered:
                val = float(self.lastValue)
            else:
                val = float(self.value)
        except ValueError:
            self._log.error(
                "Cannot convert value {}. Device probably disconnected or the response is inconsistent".format(
                    self.value
                )
            )
            # Probably disconnected
            val = None
        return "{}/{} : {:.2f} {}".format(
            self.properties.device.properties.name,
            self.properties.name,
            val,
            self.properties.units_state,
        )

    def __add__(self, other):
        return self.value + other

    __radd__ = __add__

    def __sub__(self, other):
        return self.value - other

    def __rsub__(self, other):
        return other - self.value

    def __mul__(self, other):
        return self.value * other

    __rmul__ = __mul__

    def __truediv__(self, other):
        return self.value / other

    def __rtruediv__(self, other):
        return other / self.value

    def __lt__(self, other):
        return self.value < other

    def __le__(self, other):
        return self.value <= other

    def __eq__(self, other):
        return self.value == other

    def __gt__(self, other):
        return self.value > other

    def __ge__(self, other):
        return self.value >= other class BooleanPoint(Point):
    """
    Representation of a Boolean value
    """

    def __init__(
        self,
        device=None,
        pointType=None,
        pointAddress=None,
        pointName=None,
        description=None,
        presentValue=None,
        units_state=None,
        history_size=None,
    ):

        Point.__init__(
            self,
            device=device,
            pointType=pointType,
            pointAddress=pointAddress,
            pointName=pointName,
            description=description,
            presentValue=presentValue,
            units_state=units_state,
            history_size=history_size,
        )

    def _trend(self, res):
        res = "1: active" if res == "active" else "0: inactive"
        super()._trend(res)

    @property
    def value(self):
        """
        Read the value from BACnet network
        """
        res = super().value
        self._trend(res)

        if res == "inactive":
            self._key = 0
            self._boolKey = False
        else:
            self._key = 1
            self._boolKey = True
        return res

    @property
    def boolValue(self):
        """
        returns : (boolean) Value
        """
        if ":" in self.lastValue:
            _val = int(self.lastValue.split(":")[0])
        else:
            _val = self.lastValue
        if _val in [1, "active"]:
            self._key = 1
            self._boolKey = True
        else:
            self._key = 0
            self._boolKey = False
        return self._boolKey

    @property
    def units(self):
        """
        Boolean points don't have units
        """
        return None

    def _set(self, value):
        try:
            if value == True:
                self._setitem("active")
            elif value == False:
                self._setitem("inactive")
            elif str(value) in ["inactive", "active"] or str(value).lower() == "auto":
                self._setitem(value)
            else:
                raise ValueError(
                    'Value must be boolean True, False or "active"/"inactive"'
                )
        except (Exception, ValueError) as error:
            raise WritePropertyException("Problem writing to device : {}".format(error))

    def __repr__(self):
        polling = self.properties.device.properties.pollDelay
        if (polling >= 90 or polling <= 0) and not self.cov_registered:
            # Force reading
            self.value
        return "{}/{} : {}".format(
            self.properties.device.properties.name, self.properties.name, self.boolValue
        )

    def __or__(self, other):
        return self.boolValue | other

    def __and__(self, other):
        return self.boolValue & other

    def __xor__(self, other):
        return self.boolValue ^ other

    def __eq__(self, other):
        return self.boolValue == other class EnumPoint(Point):
    """
    Representation of an Enumerated (multiState) value
    """

    def __init__(
        self,
        device=None,
        pointType=None,
        pointAddress=None,
        pointName=None,
        description=None,
        presentValue=None,
        units_state=None,
        history_size=None,
    ):

        Point.__init__(
            self,
            device=device,
            pointType=pointType,
            pointAddress=pointAddress,
            pointName=pointName,
            description=description,
            presentValue=presentValue,
            units_state=units_state,
            history_size=history_size,
        )

    def _trend(self, res):
        res = "{}: {}".format(res, self.get_state(res))
        super()._trend(res)

    @property
    def value(self):
        res = super().value
        # self._log.info("Value : {}".format(res))
        # self._log.info("EnumValue : {}".format(self.get_state(res)))
        self._trend(res)
        return res

    def get_state(self, v):
        try:
            return self.properties.units_state[v - 1]
        except (TypeError, IndexError):
            return "n/a"

    @property
    def enumValue(self):
        """
        returns: (str) Enum state value
        """
        try:
            if ":" in self.lastValue:
                _val = int(self.lastValue.split(":")[0])
                return self.get_state(_val)
        except TypeError:
            # probably first occurence of history... retry
            return self.get_state(self.lastValue)
        except IndexError:
            value = "unknown"
        except ValueError:
            value = "NaN"
        return value

    @property
    def units(self):
        """
        Enums have 'state text' instead of units.
        """
        return None

    def _set(self, value):
        try:
            if isinstance(value, int):
                self._setitem(value)
            elif str(value) in self.properties.units_state:
                self._setitem(self.properties.units_state.index(value) + 1)
            elif str(value).lower() == "auto":
                self._setitem("auto")
            else:
                raise ValueError(
                    "Value must be integer or correct enum state : {}".format(
                        self.properties.units_state
                    )
                )
        except (Exception, ValueError) as error:
            raise WritePropertyException("Problem writing to device : {}".format(error))

    def __repr__(self):
        polling = self.properties.device.properties.pollDelay
        if (polling >= 90 or polling <= 0) and not self.cov_registered:
            # Force reading
            self.value
        return "{}/{} : {}".format(
            self.properties.device.properties.name, self.properties.name, self.enumValue
        )

    def __eq__(self, other):
        return self.value == self.properties.units_state.index(other) + 1 class StringPoint(Point):
    """
    Representation of CharacterString value
    """

    def __init__(
        self,
        device=None,
        pointType=None,
        pointAddress=None,
        pointName=None,
        description=None,
        units_state=None,
        presentValue=None,
        history_size=None,
    ):

        Point.__init__(
            self,
            device=device,
            pointType=pointType,
            pointAddress=pointAddress,
            pointName=pointName,
            description=description,
            presentValue=presentValue,
            history_size=history_size,
        )

    @property
    def units(self):
        """
        Characterstring value do not have units or state text
        """
        return None

    def _trend(self, res):
        super()._trend(res)

    @property
    def value(self):
        res = super().value
        self._trend(res)
        return res

    def _set(self, value):
        try:
            if isinstance(value, str):
                self._setitem(value)
            elif isinstance(value, CharacterString):
                self._setitem(value.value)
            else:
                raise ValueError("Value must be string or CharacterString")
        except (Exception, ValueError) as error:
            raise WritePropertyException("Problem writing to device : {}".format(error))

    def __repr__(self):
        try:
            polling = self.properties.device.properties.pollDelay
            if (polling < 90 and polling > 0) or self.cov_registered:
                val = str(self.lastValue)
            else:
                val = str(self.value)
        except ValueError:
            self._log.error("Cannot convert value. Device probably disconnected")
            # Probably disconnected
            val = None
        return "{}/{} : {}".format(
            self.properties.device.properties.name, self.properties.name, val
        )

    def __eq__(self, other):
        return self.value == other.value class OfflinePoint(Point):
    """
    When offline (DB state), points needs to behave in a particular way
    (we can't read on bacnet...)
    """

    def __init__(self, device, name):
        self.properties = PointProperties()
        self.properties.device = device
        dev_name = self.properties.device.properties.db_name
        try:
            props = self.properties.device.read_point_prop(dev_name, name)
        except RemovedPointException:
            raise

        self.properties.name = props["name"]
        self.properties.type = props["type"]
        self.properties.address = props["address"]

        self.properties.description = props["description"]
        self.properties.units_state = props["units_state"]
        self.properties.simulated = "Offline"
        self.properties.overridden = "Offline"

        if "analog" in self.properties.type:
            self.new_state(NumericPointOffline)
        elif "multi" in self.properties.type:
            self.new_state(EnumPointOffline)
        elif "binary" in self.properties.type:
            self.new_state(BooleanPointOffline)
        elif "string" in self.properties.type:
            self.new_state(StringPointOffline)
        else:
            raise TypeError("Unknown point type")

    def new_state(self, newstate):
        self.__class__ = newstate class DateTimePoint(Point):
    """
    Representation of DatetimeValue value
    """

    def __init__(
        self,
        device=None,
        pointType=None,
        pointAddress=None,
        pointName=None,
        description=None,
        units_state=None,
        presentValue=None,
        history_size=None,
    ):

        Point.__init__(
            self,
            device=device,
            pointType=pointType,
            pointAddress=pointAddress,
            pointName=pointName,
            description=description,
            presentValue=presentValue,
            history_size=history_size,
        )

    @property
    def units(self):
        """
        Characterstring value do not have units or state text
        """
        return None

    def _trend(self, res):
        # super()._trend(res)
        return

    @property
    def value(self):
        res = super().value
        # self._trend(res)
        year, month, day, dayofweek = res.date
        hour, minutes, seconds, ms = res.time
        res = datetime(year + 1900, month, day, hour, minutes, seconds)
        return res

    def _set(self, value):
        # try:
        #    if isinstance(value, str):
        #        self._setitem(value)
        #    elif isinstance(value, CharacterString):
        #        self._setitem(value.value)
        #    else:
        #        raise ValueError("Value must be string or CharacterString")
        # except (Exception, ValueError) as error:
        #    raise WritePropertyException("Problem writing to device : {}".format(error))`
        raise NotImplementedError("Writing to Datetime is not supported yet")

    def __repr__(self):
        try:
            # polling = self.properties.device.properties.pollDelay
            # if (polling < 90 and polling > 0) or self.cov_registered:
            #    val = str(self.lastValue)
            # else:
            val = self.value
        except ValueError:
            self._log.error("Cannot convert value. Device probably disconnected")
            # Probably disconnected
            val = None
        return "{}/{} : {}".format(
            self.properties.device.properties.name, self.properties.name, val
        )

    def __eq__(self, other):
        return self.value == other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __le__(self, other):
        return self.value <= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __lt__(self, other):
        return self.value < other.value class TrendLog(TrendLogProperties):
    """
    BAC0 simplification of TrendLog Object
    """

    def __init__(
        self, OID, device=None, read_log_on_creation=True, multiple_request=None
    ):
        self.properties = TrendLogProperties()
        self.properties.device = device
        self.properties.oid = OID
        self.update_properties()

        if read_log_on_creation:
            self.read_log_buffer()
        self._last_index = 0

    def update_properties(self):
        try:
            (
                self.properties.object_name,
                self.properties.description,
                self.properties.record_count,
                self.properties.buffer_size,
                self.properties.total_record_count,
                self.properties.log_device_object_property,
                self.properties.statusFlags,
                self.properties.log_interval,
            ) = self.properties.device.properties.network.readMultiple(
                "{addr} trendLog {oid} objectName description recordCount bufferSize totalRecordCount logDeviceObjectProperty statusFlags logInterval".format(
                    addr=self.properties.device.properties.address,
                    oid=str(self.properties.oid),
                )
            )
        except Exception as error:
            raise Exception("Problem reading trendLog informations: {}".format(error))

    def _total_record_count(self):
        self.properties.total_record_count = self.properties.device.properties.network.read(
            "{addr} trendLog {oid} totalRecordCount".format(
                addr=self.properties.device.properties.address,
                oid=str(self.properties.oid),
            )
        )
        return self.properties.total_record_count

    def read_log_buffer(self):
        RECORDS = 10
        log_buffer = set()
        _actual_index = self._total_record_count()
        start = max(_actual_index - self.properties.record_count, self._last_index) + 1
        _count = _actual_index - start
        steps = int(_count / 10) + int(_count % 10)

        self._log.debug("Reading log : {} {} {}".format(start, _count, steps))

        _from = start
        for each in range(steps):
            range_params = ("s", _from, Date("1979-01-01"), Time("00:00"), RECORDS)
            _chunk = self.properties.device.properties.network.readRange(
                "{} trendLog {} logBuffer".format(
                    self.properties.device.properties.address, str(self.properties.oid)
                ),
                range_params=range_params,
            )
            _from += len(_chunk)
            for chunk in _chunk:
                log_buffer.add(chunk)

        self._last_index = _from
        self.create_dataframe(log_buffer)

    def create_dataframe(self, log_buffer):
        for each in log_buffer:
            year, month, day, dow = each.timestamp.date
            year = year + 1900
            hours, minutes, seconds, ms = each.timestamp.time
            self.properties._history_components["index"].append(
                pd.to_datetime(
                    "{}-{}-{} {}:{}:{}.{}".format(
                        year, month, day, hours, minutes, seconds, ms
                    ),
                    format="%Y-%m-%d %H:%M:%S.%f",
                )
            )
            self.properties._history_components["logdatum"].append(
                each.logDatum.dict_contents()
            )
            self.properties._history_components["status"].append(each.statusFlags)

        if _PANDAS:
            df = pd.DataFrame(
                {
                    "index": self.properties._history_components["index"],
                    "logdatum": self.properties._history_components["logdatum"],
                    "status": self.properties._history_components["status"],
                }
            )
            df = df.set_index("index")
            df["choice"] = df["logdatum"].apply(lambda x: list(x.keys())[0])
            df[self.properties.object_name] = df["logdatum"].apply(
                lambda x: list(x.values())[0]
            )

            self.properties._df = df
        else:
            # self.properties._history_components = (self.index, self.logdatum, self.status)
            self._log.warning(
                "Pandas not installed. Treating histories as simple list."
            )

    @property
    def history(self):
        self.read_log_buffer()
        if not _PANDAS:
            return dict(
                zip(
                    self.properties._history_components["index"],
                    self.properties._history_components["logdatum"],
                )
            )
        (
            objectType,
            objectAddress,
        ) = self.properties.log_device_object_property.objectIdentifier
        try:
            logged_point = self.properties.device.find_point(objectType, objectAddress)
        except ValueError:
            logged_point = None
        serie = self.properties._df[self.properties.object_name].copy()
        serie.units = logged_point.properties.units_state if logged_point else "n/a"
        serie.name = ("{}/{}").format(
            self.properties.device.properties.name, self.properties.object_name
        )
        if not logged_point:
            serie.states = "unknown"
        else:
            if logged_point.properties.name in self.properties.device.binary_states:
                serie.states = "binary"
            elif logged_point.properties.name in self.properties.device.multi_states:
                serie.states = "multistates"
            else:
                serie.states = "analog"
        serie.description = self.properties.description
        serie.datatype = objectType
        return serie.sort_index()

    def chart(self, remove=False):
        """
        Add point to the bacnet trending list
        """
        if not _PANDAS:
            self._log.error(
                "Pandas must be installed to use live chart feature. See documentation how how to run BAC0 in complete mode"
            )
        else:
            if remove:
                self.properties.device.properties.network.remove_trend(self)
            else:
                self.properties.device.properties.network.add_trend(self)

    def __repr__(self):
        return self.properties.__repr__()
from ....tasks.Poll import DeviceNormalPoll, DeviceFastPoll
from ...io.IOExceptions import (
    ReadPropertyMultipleException,
    NoResponseFromController,
    SegmentationNotSupported,
    BufferOverflow,
)
from ..Points import (
    NumericPoint,
    BooleanPoint,
    EnumPoint,
    StringPoint,
    OfflinePoint,
    DateTimePoint,
)
from ..Trends import TrendLog

                raise TrendLogCreationException
            (
                ldop_type,
                ldop_addr,
            ) = tl.properties.log_device_object_property.objectIdentifier
            ldop_prop = tl.properties.log_device_object_property.propertyIdentifier
            trendlogs["{}_{}_{}".format(ldop_type, ldop_addr, ldop_prop)] = (
                tl.properties.object_name,
                tl,
            )
        except TrendLogCreationException:
            device._log.error("Problem creating {}".format(each))
            continue
    return trendlogs


# def create_schedules(objList, device):
#    schedules = {}
#    for each in retrieve_type(objList, "schedule"):
#        point_address = str(each[1])
#        try:
#            tl = Schedule(point_address, device, read_log_on_creation=False)
#            if tl.properties.log_device_object_property is None:
#                raise TrendLogCreationException
#            (
#                ldop_type,
#                ldop_addr,
#            ) = tl.properties.log_device_object_property.objectIdentifier
#            ldop_prop = tl.properties.log_device_object_property.propertyIdentifier
#            trendlogs["{}_{}_{}".format(ldop_type, ldop_addr, ldop_prop)] = (
#                tl.properties.object_name,
#                tl,
#            )
#        except TrendLogCreationException:
#            device._log.error("Problem creating {}".format(each))
#            continue
#    return schedules


class ReadUtilsMixin:
    """
    Handle ReadPropertyMultiple for a device
    """

    def _rpm_request_by_name(self, point_list):
        """
        :param point_list: a list of point
        :returns: (tuple) read request for each points, points
        """
        points = []
        requests = []
        for each in point_list:
            str_list = []
            point = self._findPoint(each, force_read=False)
            points.append(point)

            str_list.append(
                " {} {} presentValue".format(
                    point.properties.type, point.properties.address
                )
            )
            rpm_param = "".join(str_list)
            requests.append(rpm_param)

        return (requests, points)


class DiscoveryUtilsMixin:
    """
    Those functions are used in the process of discovering points in a device
    """

    def read_objects_list(self, custom_object_list=None):
        if custom_object_list:
            objList = custom_object_list
        else:
            try:
                objList = self.properties.network.read(
                    "{} device {} objectList".format(
                        self.properties.address, self.properties.device_id
                    ),
                    vendor_id=self.properties.vendor_id,
                )

            except NoResponseFromController:
                self._log.error(
                    "No object list available. Please provide a custom list using the object_list parameter"
                )
                objList = []

            except (SegmentationNotSupported, BufferOverflow):
                objList = []
                number_of_objects = self.properties.network.read(
                    "{} device {} objectList".format(
                        self.properties.address, self.properties.device_id
                    ),
                    arr_index=0,
                    vendor_id=self.properties.vendor_id,
                )

                for i in range(1, number_of_objects + 1):
                    objList.append(
                        self.properties.network.read(
                            "{} device {} objectList".format(
                                self.properties.address, self.properties.device_id
                            ),
                            arr_index=i,
                            vendor_id=self.properties.vendor_id,
                        )
                    )
        return objList

    def _discoverPoints(self, custom_object_list=None):
        objList = self.read_objects_list(custom_object_list=custom_object_list)

        points = []
        trendlogs = {}

        points.extend(
            self._process_new_objects(

                obj_cls=NumericPoint, obj_type="analog", objList=objList
