class Base:
    """
    Build a running BACnet/IP device that accepts WhoIs and IAm requests
    Initialization requires some minimial information about the local device.

    :param localIPAddr='127.0.0.1':
    :param localObjName='BAC0':
    :param deviceId=None:
    :param maxAPDULengthAccepted='1024':
    :param maxSegmentsAccepted='1024':
    :param segmentationSupported='segmentedBoth':
    """

    _used_ips = set()

    def __init__(
        self,
        localIPAddr="127.0.0.1",
        localObjName="BAC0",
        deviceId=None,
        firmwareRevision="".join(sys.version.split("|")[:2]),
        maxAPDULengthAccepted="1024",
        maxSegmentsAccepted="1024",
        segmentationSupported="segmentedBoth",
        bbmdAddress=None,
        bbmdTTL=0,
        bdtable=None,
        modelName=CharacterString("BAC0 Scripting Tool"),
        vendorId=842,
        vendorName=CharacterString("SERVISYS inc."),
        description=CharacterString("http://christiantremblay.github.io/BAC0/"),
    ):

        self._log.debug("Configurating app")

        self.timehandler = TimeHandler()

        if not _COMPLETE:
            self._log.debug(
                "To be able to run the web server, you must install pandas, bokeh, flask and flask_bootstrap"
            )
            self._log.debug(
                "Those are not all installed so BAC0 will work in Lite mode only."
            )

        self.response = None
        self._initialized = False
        self._started = False
        self._stopped = False

        if localIPAddr in Base._used_ips:
            raise InitializationError(
                "IP Address provided ({}) already used by BAC0. Check if another software is using port 47808 on this network interface. If so, you can define multiple IP per interface. Or specify another IP using BAC0.lite(ip='IP/mask')".format(
                    localIPAddr
                )
            )

        if validate_ip_address(localIPAddr):
            self.localIPAddr = localIPAddr
        else:
            raise InitializationError(
                "IP Address provided ({}) invalid. Check if another software is using port 47808 on this network interface. If so, you can define multiple IP per interface. Or specify another IP using BAC0.lite(ip='IP/mask')".format(
                    localIPAddr
                )
            )

        self.Boid = (
            int(deviceId) if deviceId else (3056177 + int(random.uniform(0, 1000)))
        )

        self.segmentationSupported = segmentationSupported
        self.maxSegmentsAccepted = maxSegmentsAccepted
        self.localObjName = localObjName
        self.local_objects = LocalObjects(device=self)

        self.maxAPDULengthAccepted = maxAPDULengthAccepted
        self.vendorId = vendorId
        self.vendorName = vendorName
        self.modelName = modelName
        self.description = description

        self.discoveredDevices = None
        self.systemStatus = DeviceStatus(1)

        self.bbmdAddress = bbmdAddress
        self.bbmdTTL = bbmdTTL
        self.bdtable = bdtable

        self.firmwareRevision = firmwareRevision
        self._ric = {}
        self.subscription_contexts = {}
        self.database = None

        try:
            self.startApp()
        except InitializationError as error:
            raise InitializationError(
                "Gros probleme : {}. Address requested : {}".format(error, localIPAddr)
            )

    def startApp(self):
        """
        Define the local device, including services supported.
        Once defined, start the BACnet stack in its own thread.
        """
        self._log.debug("Create Local Device")
        try:
            # make a device object
            self.this_device = LocalDeviceObject(
                objectName=self.localObjName,
                objectIdentifier=self.Boid,
                maxApduLengthAccepted=int(self.maxAPDULengthAccepted),
                segmentationSupported=self.segmentationSupported,
                vendorIdentifier=self.vendorId,
                vendorName=self.vendorName,
                modelName=self.modelName,
                systemStatus=self.systemStatus,
                description=self.description,
                firmwareRevision=self.firmwareRevision,
                applicationSoftwareVersion=infos.__version__,
                protocolVersion=1,
                protocolRevision=0,
                utcOffset=self.timehandler.utcOffset(),
                daylightSavingsStatus=self.timehandler.is_dst(),
            )

            # make an application
            if self.bdtable:
                self.this_application = BAC0BBMDDeviceApplication(
                    self.this_device,
                    self.localIPAddr,
                    bdtable=self.bdtable,
                    iam_req=self._iam_request(),
                    subscription_contexts=self.subscription_contexts,
                )
                app_type = "BBMD Device"
            elif self.bbmdAddress and self.bbmdTTL > 0:

                self.this_application = BAC0ForeignDeviceApplication(
                    self.this_device,
                    self.localIPAddr,
                    bbmdAddress=self.bbmdAddress,
                    bbmdTTL=self.bbmdTTL,
                    iam_req=self._iam_request(),
                    subscription_contexts=self.subscription_contexts,
                )
                app_type = "Foreign Device"
            else:
                self.this_application = BAC0Application(
                    self.this_device,
                    self.localIPAddr,
                    iam_req=self._iam_request(),
                    subscription_contexts=self.subscription_contexts,
                )
                app_type = "Simple BACnet/IP App"
            self._log.debug("Starting")
            self._initialized = True
            try:
                self._startAppThread()
                Base._used_ips.add(self.localIPAddr)
                self._log.info("Registered as {}".format(app_type))
            except OSError as error:
                self._log.warning("Error opening socket: {}".format(error))
                raise InitializationError("Error opening socket: {}".format(error))
            self._log.debug("Running")
        except OSError as error:
            self._log.error("an error has occurred: {}".format(error))
            raise InitializationError("Error starting app: {}".format(error))
        finally:
            self._log.debug("finally")

    def register_foreign_device(self, addr=None, ttl=0):
        self.this_application.bip.register(addr, ttl)

    def unregister_foreign_device(self):
        self.this_application.bip.unregister()

    def disconnect(self):
        """
        Stop the BACnet stack.  Free the IP socket.
        """
        self._log.debug("Stopping All running tasks")
        stopAllTasks()
        self._log.debug("Stopping BACnet stack")
        # Freeing socket
        try:
            self.this_application.mux.directPort.handle_close()
        except:
            self.this_application.mux.broadcastPort.handle_close()

        stopBacnetIPApp()  # Stop Core
        self._stopped = True  # Stop stack thread
        self.t.join()
        self._started = False
        Base._used_ips.discard(self.localIPAddr)
        self._log.info("BACnet stopped")

    def _startAppThread(self):
        """
        Starts the BACnet stack in its own thread so requests can be processed.
        As signal cannot be called in another thread than the main thread
        when calling startBacnetIPApp, we must pass None to both parameters
        """
        self._log.info("Starting app...")
        enable_sleeping(0.0005)
        self.t = Thread(
            target=startBacnetIPApp,
            kwargs={"sigterm": None, "sigusr1": None},
            daemon=True,
        )
        try:
            self.t.start()
            self._started = True
            self._log.info("BAC0 started")
        except OSError:
            stopBacnetIPApp()
            self.t.join()
            raise

    @property
    def discoveredNetworks(self):
        return self.this_application.nse._learnedNetworks or set()

    #    @property
    #    def routing_table(self):
    #        return self.this_application.nse._routing_table or {}

    @property
    def routing_table(self):
        """
        Routing Table will give all the details about routers and how they
        connect BACnet networks together.

        It's a decoded presentation of what bacpypes.router_info_cache contains.

        Returns a dict with the address of routers as key.
        """

        class Router:
            def __init__(self, router_info, index=None, path=None):
                self.source_network = router_info.snet
                self.address = router_info.address
                self.destination_networks = router_info.dnets
                self.index = index
                self.path = path

            def __repr__(self):
                return "Source Network: {} | Address: {} | Destination Networks: {} | Path: {}".format(
                    self.source_network,
                    self.address,
                    self.destination_networks,
                    self.path,
                )

        self._routers = {}

        self._ric = {}
        ric = self.this_application.nsap.router_info_cache

        for networks, routers in ric.routers.items():
            for address, router in routers.items():
                self._routers[str(address)] = Router(router, index=networks)
        for path, router in ric.path_info.items():
            self._routers[str(router.address)].path = path

        return self._routers class ReadProperty:
    """
    Defines BACnet Read functions: readProperty and readPropertyMultiple.
    Data exchange is made via a Queue object
    A timeout of 10 seconds allows detection of invalid device or communciation errors.
    """

    def read(
        self,
        args,
        arr_index=None,
        vendor_id=0,
        bacoid=None,
        timeout=10,
        show_property_name=False,
    ):
        """
        Build a ReadProperty request, wait for the answer and return the value

        :param args: String with <addr> <type> <inst> <prop> [ <indx> ]
        :returns: data read from device (str representing data like 10 or True)

        *Example*::

            import BAC0
            myIPAddr = '192.168.1.10/24'
            bacnet = BAC0.connect(ip = myIPAddr)
            bacnet.read('2:5 analogInput 1 presentValue')

        Requests the controller at (Network 2, address 5) for the presentValue of
        its analog input 1 (AI:1).
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        args_split = args.split()

        self.log_title("Read property", args_split)

        vendor_id = vendor_id
        bacoid = bacoid

        try:
            # build ReadProperty request
            iocb = IOCB(
                self.build_rp_request(
                    args_split, arr_index=arr_index, vendor_id=vendor_id, bacoid=bacoid
                )
            )
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)
            self._log.debug("{:<20} {!r}".format("iocb", iocb))

        except ReadPropertyException as error:
            # construction error
            self._log.exception("exception: {!r}".format(error))

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, ReadPropertyACK):  # expecting an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug("Not an ack. | APDU : {}".format(apdu))
                return

            # find the datatype
            datatype = get_datatype(
                apdu.objectIdentifier[0], apdu.propertyIdentifier, vendor_id=vendor_id
            )
            if not datatype:
                # raise TypeError("unknown datatype")
                value = cast_datatype_from_tag(
                    apdu.propertyValue,
                    apdu.objectIdentifier[0],
                    apdu.propertyIdentifier,
                )
            else:
                # special case for array parts, others are managed by cast_out
                if issubclass(datatype, Array) and (
                    apdu.propertyArrayIndex is not None
                ):
                    if apdu.propertyArrayIndex == 0:
                        value = apdu.propertyValue.cast_out(Unsigned)
                    else:
                        value = apdu.propertyValue.cast_out(datatype.subtype)
                else:
                    value = apdu.propertyValue.cast_out(datatype)

                self._log.debug("{:<20} {:<20}".format("value", "datatype"))
                self._log.debug("{!r:<20} {!r:<20}".format(value, datatype))
            if not show_property_name:
                return value

            try:
                int(apdu.propertyIdentifier)
                objid = apdu.objectIdentifier
                prop_id = "@prop_{}".format(apdu.propertyIdentifier)
                value = list(value.items())[0][1]
            except ValueError:
                prop_id = apdu.propertyIdentifier
            return (value, prop_id)
        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            if reason == "segmentationNotSupported":
                value = self._split_the_read_request(args, arr_index)
                return value
            else:
                if reason == "unknownProperty":
                    if "description" in args:
                        self._log.warning(
                            "The description property is not implemented in the device. Using a default value for internal needs."
                        )
                        return "Property Not Implemented"
                    elif "inactiveText" in args:
                        self._log.warning(
                            "The inactiveText property is not implemented in the device. Using a default value of Off for internal needs."
                        )
                        return "Off"
                    elif "activeText" in args:
                        self._log.warning(
                            "The activeText property is not implemented in the device. Using a default value of On for internal needs."
                        )
                        return "On"
                    else:
                        raise UnknownPropertyError("Unknown property {}".format(args))
                elif reason == "unknownObject":
                    self._log.warning("Unknown object {}".format(args))
                    raise UnknownObjectError("Unknown object {}".format(args))
                elif reason == "bufferOverflow":
                    self._log.warning(
                        "Buffer capacity exceeded in device {}".format(args)
                    )
                    return self._split_the_read_request(args, arr_index)
                else:
                    # Other error... consider NoResponseFromController (65)
                    # even if the real reason is another one
                    raise NoResponseFromController(
                        "APDU Abort Reason : {}".format(reason)
                    )

    def _split_the_read_request(self, args, arr_index):
        """
        When a device doesn't support segmentation, this function
        will split the request according to the length of the
        predicted result which can be known when reading the array_index
        number 0.

        This can be a very long process as some devices count a large
        number of properties without supporting segmentation
        (FieldServers are a good example)
        """
        nmbr_obj = self.read(args, arr_index=0)
        return [self.read(args, arr_index=i) for i in range(1, nmbr_obj + 1)]

    def readMultiple(
        self, args, request_dict=None, vendor_id=0, timeout=10, show_property_name=False
    ):
        """Build a ReadPropertyMultiple request, wait for the answer and return the values

        :param args: String with <addr> ( <type> <inst> ( <prop> [ <indx> ] )... )...
        :returns: data read from device (str representing data like 10 or True)

        *Example*::

            import BAC0
            myIPAddr = '192.168.1.10/24'
            bacnet = BAC0.connect(ip = myIPAddr)
            bacnet.readMultiple('2:5 analogInput 1 presentValue units')

        Requests the controller at (Network 2, address 5) for the (presentValue and units) of
        its analog input 1 (AI:1).
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        if request_dict is not None:
            request = self.build_rpm_request_from_dict(request_dict, vendor_id)
        else:
            args = args.split()
            request = self.build_rpm_request(args, vendor_id=vendor_id)
            self.log_title("Read Multiple", args)

        values = []
        dict_values = {}

        try:
            # build an ReadPropertyMultiple request
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)
            self._log.debug("{:<20} {!r}".format("iocb", iocb))

        except ReadPropertyMultipleException as error:
            # construction error
            self._log.exception("exception: {!r}".format(error))

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, ReadPropertyMultipleACK):  # expecting an ACK
                self._log.debug("{:<20}".format("not an ack"))
                self._log.warning(
                    "Not an Ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

            # loop through the results
            for result in apdu.listOfReadAccessResults:
                # here is the object identifier
                objectIdentifier = result.objectIdentifier

                self.log_subtitle(
                    "{!r} : {!r}".format(objectIdentifier[0], objectIdentifier[1]),
                    width=114,
                )
                self._log.debug(
                    "{:<20} {:<20} {:<30} {:<20}".format(
                        "propertyIdentifier", "propertyArrayIndex", "value", "datatype"
                    )
                )
                self._log.debug("-" * 114)
                dict_values[objectIdentifier] = []
                # now come the property values per object
                for element in result.listOfResults:
                    # get the property and array index
                    propertyIdentifier = element.propertyIdentifier
                    propertyArrayIndex = element.propertyArrayIndex

                    readResult = element.readResult

                    if propertyArrayIndex is not None:
                        _prop_id = "{}@idx:{}".format(
                            propertyIdentifier, propertyArrayIndex
                        )
                    else:
                        _prop_id = propertyIdentifier

                    if readResult.propertyAccessError is not None:
                        self._log.debug(
                            "Property Access Error for {}".format(
                                readResult.propertyAccessError
                            )
                        )
                        values.append(None)
                        dict_values[objectIdentifier].append((_prop_id, None))
                    else:
                        # here is the value
                        propertyValue = readResult.propertyValue

                        # find the datatype
                        datatype = get_datatype(
                            objectIdentifier[0], propertyIdentifier, vendor_id=vendor_id
                        )

                        if not datatype:
                            value = cast_datatype_from_tag(
                                propertyValue, objectIdentifier[0], propertyIdentifier
                            )
                        else:
                            # special case for array parts, others are managed by cast_out
                            if issubclass(datatype, Array) and (
                                propertyArrayIndex is not None
                            ):
                                if propertyArrayIndex == 0:
                                    value = propertyValue.cast_out(Unsigned)
                                else:
                                    value = propertyValue.cast_out(datatype.subtype)
                            elif propertyValue.is_application_class_null():
                                value = None
                            else:
                                value = propertyValue.cast_out(datatype)

                            self._log.debug(
                                "{!r:<20} {!r:<20} {!r:<30} {!r:<20}".format(
                                    propertyIdentifier,
                                    propertyArrayIndex,
                                    value,
                                    datatype,
                                )
                            )
                        if show_property_name:
                            try:
                                int(
                                    propertyIdentifier
                                )  # else it will be a name like maxMaster
                                prop_id = "@prop_{}".format(propertyIdentifier)
                                _obj, _id = apdu.listOfReadAccessResults[
                                    0
                                ].objectIdentifier
                                _key = (str(_obj), vendor_id)
                                if _key in registered_object_types.keys():
                                    _classname = registered_object_types[_key].__name__
                                    for k, v in registered_object_types["BAC0"][
                                        _classname
                                    ].items():
                                        if v["obj_id"] == propertyIdentifier:
                                            prop_id = (k, propertyIdentifier)
                                if isinstance(value, dict):
                                    value = list(value.items())[0][1]

                            except ValueError:
                                prop_id = propertyIdentifier
                            values.append((value, prop_id))
                            dict_values[objectIdentifier].append(
                                (_prop_id, (value, prop_id))
                            )
                        else:
                            values.append(value)
                            dict_values[objectIdentifier].append((_prop_id, value))

            if request_dict is not None:
                return dict_values
            else:
                return values

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            self._log.warning("APDU Abort Reject Reason : {}".format(reason))
            self._log.debug("The Request was : {}".format(args))
            if reason == "unrecognizedService":
                raise UnrecognizedService()
            elif reason == "segmentationNotSupported":
                # value = self._split_the_read_request(args, arr_index)
                # return value
                self.segmentation_supported = False
                raise SegmentationNotSupported()
            elif reason == "unknownObject":
                self._log.warning("Unknown object {}".format(args))
                raise UnknownObjectError("Unknown object {}".format(args))
            elif reason == "unknownProperty":
                self._log.warning("Unknown property {}".format(args))

                values.append("")
                return values
            else:
                self._log.warning("No response from controller {}".format(reason))
                values.append("")
                return values

    def build_rp_request(self, args, arr_index=None, vendor_id=0, bacoid=None):
        addr, obj_type, obj_inst, prop_id = args[:4]
        vendor_id = vendor_id
        bacoid = bacoid

        if obj_type.isdigit():
            obj_type = int(obj_type)
        elif "@obj_" in obj_type:
            obj_type = int(obj_type.split("_")[1])
        elif not get_object_class(obj_type, vendor_id=vendor_id):
            raise ValueError("Unknown object type : {}".format(obj_type))

        obj_inst = int(obj_inst)

        if prop_id.isdigit():
            prop_id = int(prop_id)
        elif "@prop_" in prop_id:
            prop_id = int(prop_id.split("_")[1])

        # datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)

        # build a request
        request = ReadPropertyRequest(
            objectIdentifier=(obj_type, obj_inst),
            propertyIdentifier=prop_id,
            propertyArrayIndex=arr_index,
        )
        request.pduDestination = Address(addr)

        if len(args) == 5:
            request.propertyArrayIndex = int(args[4])
        self._log.debug("{:<20} {!r}".format("REQUEST", request))
        return request

    def build_rpm_request(self, args, vendor_id=0):
        """
        Build request from args
        """
        self._log.debug(args)
        i = 0
        addr = args[i]
        i += 1
        vendor_id = vendor_id

        read_access_spec_list = []
        while i < len(args):
            obj_type = args[i]
            i += 1

            if obj_type.isdigit():
                obj_type = int(obj_type)
            elif "@obj_" in obj_type:
                obj_type = int(obj_type.split("_")[1])
            elif not get_object_class(obj_type, vendor_id=vendor_id):
                raise ValueError("Unknown object type : {}".format(obj_type))

            obj_inst = int(args[i])
            i += 1

            prop_reference_list = []
            while i < len(args):
                prop_id = args[i]
                if "@obj_" in prop_id:
                    break
                if prop_id not in PropertyIdentifier.enumerations:
                    try:
                        if "@prop_" in prop_id:
                            prop_id = int(prop_id.split("_")[1])
                            self._log.debug(
                                "Proprietary property : {} | {} -> Vendor : {}".format(
                                    obj_type, prop_id, vendor_id
                                )
                            )
                        else:
                            break
                    except:
                        break

                elif prop_id not in (
                    "all",
                    "required",
                    "optional",
                    "objectName",
                    "objectType",
                    "objectIdentifier",
                    "polarity",
                ):
                    datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)
                    if not datatype:
                        raise ValueError(
                            "invalid property for object type : {} | {}".format(
                                obj_type, prop_id
                            )
                        )
                i += 1

                # build a property reference
                prop_reference = PropertyReference(propertyIdentifier=prop_id)

                # check for an array index
                if (i < len(args)) and args[i].isdigit():
                    prop_reference.propertyArrayIndex = int(args[i])
                    i += 1

                prop_reference_list.append(prop_reference)

            if not prop_reference_list:
                raise ValueError("provide at least one property")

            # build a read access specification
            read_access_spec = ReadAccessSpecification(
                objectIdentifier=(obj_type, obj_inst),
                listOfPropertyReferences=prop_reference_list,
            )

            read_access_spec_list.append(read_access_spec)

        if not read_access_spec_list:
            raise RuntimeError("at least one read access specification required")

        # build the request
        request = ReadPropertyMultipleRequest(
            listOfReadAccessSpecs=read_access_spec_list
        )
        request.pduDestination = Address(addr)
        return request

    def build_rpm_request_from_dict(self, request_dict, vendor_id):
        """
        Read property multiple allow to read a lot of properties with only one request
        The existing RPM function is made using a string that must be created using bacpypes
        console style and is hard to automate.

        This new version will be an attempt to improve that::

            _rpm = {'address': '11:2',
                'objects': {'analogInput:1': ['presentValue', 'description', 'unit', 'objectList@idx:0'],
                            'analogInput:2': ['presentValue', 'description', 'unit', 'objectList@idx:0'],
                },
                vendor_id: 842
                }

        """
        vendor_id = 842
        addr = request_dict["address"]
        objects = request_dict["objects"]
        if "vendor_id" in request_dict.keys():
            vendor_id = int(request_dict["vendor_id"])

        read_access_spec_list = []

        for obj, list_of_properties in objects.items():
            obj_type, obj_instance = obj.split(":")
            obj_type = validate_object_type(obj_type, vendor_id=vendor_id)
            obj_instance = int(obj_instance)
            property_reference_list = build_property_reference_list(
                obj_type, list_of_properties
            )
            read_acces_spec = build_read_access_spec(
                obj_type, obj_instance, property_reference_list
            )
            read_access_spec_list.append(read_acces_spec)

        if not read_access_spec_list:
            raise RuntimeError("at least one read access specification required")

        # build the request
        request = ReadPropertyMultipleRequest(
            listOfReadAccessSpecs=read_access_spec_list
        )
        request.pduDestination = Address(addr)

        return request

    def build_rrange_request(
        self, args, range_params=None, arr_index=None, vendor_id=0, bacoid=None
    ):
        addr, obj_type, obj_inst, prop_id = args[:4]

        vendor_id = vendor_id
        bacoid = bacoid

        if obj_type.isdigit():
            obj_type = int(obj_type)
        elif not get_object_class(obj_type, vendor_id=vendor_id):
            raise ValueError("Unknown object type {}".format(obj_type))

        obj_inst = int(obj_inst)

        if prop_id.isdigit():
            prop_id = int(prop_id)
        datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)
        if not datatype:
            raise ValueError("invalid property for object type")

        # build a request
        request = ReadRangeRequest(
            objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id
        )
        request.pduDestination = Address(addr)
        if range_params is not None:
            range_type, first, date, time, count = range_params
            if range_type == "p":
                rbp = RangeByPosition(referenceIndex=int(first), count=int(count))
                request.range = Range(byPosition=rbp)
            elif range_type == "s":
                rbs = RangeBySequenceNumber(
                    referenceSequenceNumber=int(first), count=int(count)
                )
                request.range = Range(bySequenceNumber=rbs)
            elif range_type == "t":
                rbt = RangeByTime(
                    referenceTime=DateTime(
                        date=Date(date).value, time=Time(time).value
                    ),
                    count=int(count),
                )
                request.range = Range(byTime=rbt)
            elif range_type == "x":
                # should be missing required parameter
                request.range = Range()
            else:
                raise ValueError("unknown range type: %r" % (range_type,))

        if len(args) == 5:
            request.propertyArrayIndex = int(args[4])
        self._log.debug("{:<20} {!r}".format("REQUEST", request))
        return request

    def readRange(
        self,
        args,
        range_params=None,
        arr_index=None,
        vendor_id=0,
        bacoid=None,
        timeout=10,
    ):
        """
        Build a ReadProperty request, wait for the answer and return the value

        :param args: String with <addr> <type> <inst> <prop> [ <indx> ]
        :returns: data read from device (str representing data like 10 or True)

        *Example*::

            import BAC0
            myIPAddr = '192.168.1.10/24'
            bacnet = BAC0.connect(ip = myIPAddr)
            bacnet.read('2:5 analogInput 1 presentValue')

        Requests the controller at (Network 2, address 5) for the presentValue of
        its analog input 1 (AI:1).
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        args_split = args.split()

        self.log_title("Read range ", args_split)

        vendor_id = vendor_id
        bacoid = bacoid

        try:
            # build ReadProperty request
            request = self.build_rrange_request(
                args_split,
                range_params=range_params,
                arr_index=arr_index,
                vendor_id=vendor_id,
                bacoid=bacoid,
            )
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)
            self._log.debug("{:<20} {!r}".format("iocb", iocb))

        except ReadRangeException as error:
            # construction error
            self._log.exception("exception: {!r}".format(error))

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, ReadRangeACK):  # expecting an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

            # find the datatype
            datatype = get_datatype(
                apdu.objectIdentifier[0], apdu.propertyIdentifier, vendor_id=vendor_id
            )
            if not datatype:
                # raise TypeError("unknown datatype")
                datatype = cast_datatype_from_tag(
                    apdu.propertyValue,
                    apdu.objectIdentifier[0],
                    apdu.propertyIdentifier,
                )

            try:
                value = apdu.itemData.cast_out(datatype)
            except TypeError as error:
                self._log.error(
                    "Problem casting value : {} | Datatype : {} | error : {}".format(
                        apdu.itemData, datatype, error
                    )
                )
                return apdu

            self._log.debug("{:<20} {:<20}".format("value", "datatype"))
            self._log.debug("{!r:<20} {!r:<20}".format(value, datatype))
            return value

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            if reason == "segmentationNotSupported":
                self._log.warning(
                    "Segmentation not supported... will read properties one by one..."
                )
                self._log.debug("The Request was : {}".format(args_split))
                value = self._split_the_read_request(args, arr_index)
                return value
            else:
                if reason == "unknownProperty":
                    if "priorityArray" in args:
                        self._log.debug("Unknown property {}".format(args))
                    else:
                        self._log.warning("Unknown property {}".format(args))
                    if "description" in args:
                        return ""
                    elif "inactiveText" in args:
                        return "Off"
                    elif "activeText" in args:
                        return "On"
                    else:
                        raise UnknownPropertyError("Unknown property {}".format(args))
                elif reason == "unknownObject":
                    self._log.warning("Unknown object {}".format(args))
                    raise UnknownObjectError("Unknown object {}".format(args))
                else:
                    # Other error... consider NoResponseFromController (65)
                    # even if the realy reason is another one
                    raise NoResponseFromController(
                        "APDU Abort Reason : {}".format(reason)
                    )

    def read_priority_array(self, addr, obj, obj_instance):
        pa = self.read("{} {} {} priorityArray".format(addr, obj, obj_instance))
        res = [pa]
        for each in range(1, 17):
            _pa = pa[each]
            for k, v in _pa.__dict__.items():
                if v != None:
                    res.append(v)
        return res class WriteProperty:
    """
    Defines BACnet Write functions: WriteProperty [WritePropertyMultiple not supported]

    """

    def write(self, args, vendor_id=0, timeout=10):
        """Build a WriteProperty request, wait for an answer, and return status [True if ok, False if not].

        :param args: String with <addr> <type> <inst> <prop> <value> [ <indx> ] - [ <priority> ]
        :returns: return status [True if ok, False if not]

        *Example*::

            import BAC0
            bacnet = BAC0.lite()
            bacnet.write('2:5 analogValue 1 presentValue 100 - 8')

        Direct the controller at (Network 2, address 5) to write 100 to the presentValues of
        its analogValue 1 (AV:1) at priority 8
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")
        args = args.split()
        self.log_title("Write property", args)

        try:
            # build a WriteProperty request
            iocb = IOCB(self.build_wp_request(args, vendor_id=vendor_id))
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)
            self._log.debug("{:<20} {!r}".format("iocb", iocb))

        except WritePropertyException as error:
            # construction error
            self._log.exception("exception: {!r}".format(error))

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

    def _parse_wp_args(self, args):
        """
        Utility to parse the string of the request.
        Supports @obj_ and @prop_ syntax for objest type and property id, useful with proprietary objects and properties.
        """
        if not isinstance(args, list):
            args = args.split()
        obj_type, obj_inst, prop_id = args[:3]
        if obj_type.isdigit():
            obj_type = int(obj_type)
        elif "@obj_" in obj_type:
            obj_type = int(obj_type.split("_")[1])
        obj_inst = int(obj_inst)
        value = args[3]
        indx = None
        if len(args) >= 5 and args[4] != "-":
            indx = int(args[4])
        priority = None
        if len(args) >= 6:
            priority = int(args[5])
        if "@prop_" in prop_id:
            prop_id = prop_id.split("_")[1]
        if prop_id.isdigit():
            prop_id = int(prop_id)

        return (obj_type, obj_inst, prop_id, value, priority, indx)

    def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value):
        """
        This will ensure the value can be encoded and is valid in the context
        """
        # get the datatype
        datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)
        # change atomic values into something encodeable, null is a special
        # case
        if value == "null":
            value = Null()
        elif issubclass(datatype, Atomic):
            if (
                datatype is Integer
                # or datatype is not Real
                or datatype is Unsigned
                or datatype is Enumerated
            ):
                value = int(value)
            elif datatype is Real:
                value = float(value)
                # value = datatype(value)
            else:
                # value = float(value)
                value = datatype(value)

            value = datatype(value)
        elif issubclass(datatype, Array) and (indx is not None):
            if indx == 0:
                value = Integer(value)
            elif issubclass(datatype.subtype, Atomic):
                value = datatype.subtype(value)
            elif not isinstance(value, datatype.subtype):
                raise TypeError(
                    "invalid result datatype, expecting {}".format(
                        (datatype.subtype.__name__,)
                    )
                )

        elif not isinstance(value, datatype):
            raise TypeError(
                "invalid result datatype, expecting {}".format((datatype.__name__,))
            )

        self._log.debug("{:<20} {!r} {}".format("Encodeable value", value, type(value)))

        _value = Any()
        try:
            _value.cast_in(value)
        except WritePropertyCastError as error:
            self._log.error("WriteProperty cast error: {!r}".format(error))
            raise

        return _value

    def build_wp_request(self, args, vendor_id=0):
        vendor_id = vendor_id
        addr = args[0]
        args = args[1:]
        obj_type, obj_inst, prop_id, value, priority, indx = self._parse_wp_args(args)

        value = self._validate_value_vs_datatype(
            obj_type, prop_id, indx, vendor_id, value
        )
        # build a request
        request = WritePropertyRequest(
            objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id
        )
        request.pduDestination = Address(addr)

        # save the value
        request.propertyValue = value

        # optional array index
        if indx is not None:
            request.propertyArrayIndex = indx

        # optional priority
        if priority is not None:
            request.priority = priority

        self.log_subtitle("Creating Request")
        self._log.debug(
            "{:<20} {:<20} {:<20} {:<20}".format(
                "indx", "priority", "datatype", "value"
            )
        )
        datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)

        self._log.debug(
            "{!r:<20} {!r:<20} {!r:<20} {!r:<20}".format(
                indx, priority, datatype, value
            )
        )

        self._log.debug("{:<20} {}".format("REQUEST", request))
        return request

    def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10):
        """Build a WritePropertyMultiple request, wait for an answer

        :param addr: destination of request (ex. '2:3' or '192.168.1.2')
        :param args: list of String with <type> <inst> <prop> <value> [ <indx> ] - [ <priority> ]
        :param vendor_id: Mandatory for registered proprietary object and properties
        :param timeout: used by IOCB to discard request if timeout reached
        :returns: return status [True if ok, False if not]

        *Example*::

            import BAC0
            bacnet = BAC0.lite()
            r = ['analogValue 1 presentValue 100','analogValue 2 presentValue 100','analogValue 3 presentValue 100 - 8','@obj_142 1 @prop_1042 True']
            bacnet.writeMultiple(addr='2:5',args=r,vendor_id=842)
            # or
            # bacnet.writeMultiple('2:5',r)

        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        self.log_title("Write property multiple", args)

        try:
            # build a WritePropertyMultiple request
            iocb = IOCB(self.build_wpm_request(args, vendor_id=vendor_id, addr=addr))
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)
            self._log.debug("{:<20} {!r}".format("iocb", iocb))

        except WritePropertyException as error:
            # construction error
            self._log.exception("exception: {!r}".format(error))

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

    def build_wpm_request(self, args, vendor_id=0, addr=None):
        if not addr:
            raise ValueError("Please provide addr")

        self.log_subtitle("Creating Write Multiple Request")
        self._log.debug(
            "{:<20} {:<20} {:<20} {:<20}".format(
                "indx", "priority", "datatype", "value"
            )
        )

        was = []
        for each in args:
            property_values = []
            if isinstance(each, str):
                (
                    obj_type,
                    obj_inst,
                    prop_id,
                    value,
                    priority,
                    indx,
                ) = self._parse_wp_args(each)
            elif isinstance(each, tuple):
                # Supported but not really the best choice as the parser will
                # catch some edge cases for you
                obj_type, obj_inst, prop_id, value, priority, indx = each
            else:
                raise ValueError("Wrong encoding of request")
            value = self._validate_value_vs_datatype(
                obj_type, prop_id, indx, vendor_id, value
            )

            existingObject = next(
                (obj for obj in was if obj.objectIdentifier == (obj_type, obj_inst)),
                None,
            )

            if existingObject == None:
                property_values.append(
                    PropertyValue(
                        propertyIdentifier=prop_id,
                        propertyArrayIndex=indx,
                        value=value,
                        priority=priority,
                    )
                )

                was.append(
                    WriteAccessSpecification(
                        objectIdentifier=(obj_type, obj_inst),
                        listOfProperties=property_values,
                    )
                )
            else:
                existingObject.listOfProperties.append(
                    PropertyValue(
                        propertyIdentifier=prop_id,
                        propertyArrayIndex=indx,
                        value=value,
                        priority=priority,
                    )
                )

            datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)

            self._log.debug(
                "{!r:<20} {!r:<20} {!r:<20} {!r:<20}".format(
                    indx, priority, datatype, value
                )
            )

        # build a request
        request = WritePropertyMultipleRequest(listOfWriteAccessSpecs=was)
        request.pduDestination = Address(addr)

        self._log.debug("{:<20} {}".format("REQUEST", request))
        return request class HostIP:
    """
    Special class to identify host IP informations
    """

    def __init__(self, port=47808):
        ip = self._findIPAddr()
        mask = self._findSubnetMask(ip)
        self._port = port
        self.interface = ipaddress.IPv4Interface("{}/{}".format(ip, mask))

    @property
    def ip_address_subnet(self):
        """
        IP Address/subnet
        """
        return "{}/{}".format(
            self.interface.ip.compressed, self.interface.exploded.split("/")[-1]
        )

    @property
    def ip_address(self):
        """
        IP Address/subnet
        """
        return "{}".format(self.interface.ip.compressed)

    @property
    def address(self):
        """
        IP Address using bacpypes Address format
        """
        port = ""
        if self._port:
            port = ":{}".format(self._port)
        return Address(
            "{}/{}{}".format(
                self.interface.ip.compressed,
                self.interface.exploded.split("/")[-1],
                port,
            )
        )

    @property
    def mask(self):
        """
        Subnet mask
        """
        return self.interface.exploded.split("/")[-1]

    @property
    def port(self):
        """
        IP Port used
        """
        return self._port

    def _findIPAddr(self):
        """
        Retrieve the IP address connected to internet... used as
        a default IP address when defining Script

        :returns: IP Adress as String
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.connect(("google.com", 0))
            addr = s.getsockname()[0]
            s.close()
        except socket.error:
            raise NetworkInterfaceException(
                "Impossible to retrieve IP, please provide one manually"
            )
        return addr

    def _findSubnetMask(self, ip):
        """
        Retrieve the broadcast IP address connected to internet... used as
        a default IP address when defining Script

        :param ip: (str) optionnal IP address. If not provided, default to getIPAddr()

        :returns: broadcast IP Adress as String
        """
        try:
            import netifaces

            interfaces = netifaces.interfaces()
            for nic in interfaces:
                addresses = netifaces.ifaddresses(nic)
                try:
                    for address in addresses[netifaces.AF_INET]:
                        if address["addr"] == ip:
                            return address["netmask"]
                except KeyError:
                    pass

            return "255.255.255.255"
        except ImportError:
            self._log.warning(
                "Netifaces not installed on your system. BAC0 can't detect the subnet.\nPlease provide subnet for now, we'll consider 255.255.255.0 (/24).\nYou can install netifaces using 'pip install netifaces'."
            )
            return "255.255.255.0" class Discover:
    """
    Define BACnet WhoIs and IAm functions.
    """

    def whois(self, *args, global_broadcast=False, destination=None):
        """
        Build a WhoIs request

        :param args: string built as [ <addr>] [ <lolimit> <hilimit> ] **optional**
        :returns: discoveredDevices as a defaultdict(int)

        Example::

            whois(global_broadcast=True) # WhoIs broadcast globally.  Every device will respond with an IAm
            whois('2:5')                 # WhoIs looking for the device at (Network 2, Address 5)
            whois('10 1000')             # WhoIs looking for devices in the ID range (10 - 1000)

        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        if args:
            args = args[0].split()
        msg = args if args else "any"

        self._log.debug("do_whois {!r}".format(msg))

        # build a request
        request = WhoIsRequest()
        if (len(args) == 1) or (len(args) == 3):
            request.pduDestination = Address(args[0])
            del args[0]
        else:
            if global_broadcast:
                request.pduDestination = GlobalBroadcast()
            else:
                request.pduDestination = LocalBroadcast()

        if len(args) == 2:
            try:
                request.deviceInstanceRangeLowLimit = int(args[0])
                request.deviceInstanceRangeHighLimit = int(args[1])
            except ValueError:
                pass
        self._log.debug("{:>12} {}".format("- request:", request))

        if destination:
            request.pduDestination = Address(destination)
        iocb = IOCB(request)  # make an IOCB
        self.this_application._last_i_am_received = []
        # pass to the BACnet stack
        deferred(self.this_application.request_io, iocb)

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

        if iocb.ioError:  # unsuccessful: error/reject/abort
            pass

        time.sleep(3)
        self.discoveredDevices = self.this_application.i_am_counter
        return self.this_application._last_i_am_received

    def _iam_request(self, destination=None):
        """
        Build the IOCB request for a I Am
        """
        try:
            # build a response
            request = IAmRequest()
            request.pduDestination = destination if destination else GlobalBroadcast()
            # fill the response with details about us (from our device object)
            request.iAmDeviceIdentifier = self.this_device.objectIdentifier
            request.maxAPDULengthAccepted = self.this_device.maxApduLengthAccepted
            request.segmentationSupported = self.this_device.segmentationSupported
            request.vendorID = self.this_device.vendorIdentifier
            self._log.debug("{:>12} {}".format("- request:", request))

            return request

        except Exception as error:
            self._log.error("exception: {!r}".format(error))
            raise

    def iam(self, destination=None):
        """
        Build an IAm response.  IAm are sent in response to a WhoIs request that;
        matches our device ID, whose device range includes us, or is a broadcast.
        Content is defined by the script (deviceId, vendor, etc...)

        :returns: bool

        Example::

            iam()
        """

        self._log.debug("do_iam")

        try:
            # build a response
            request = self._iam_request(destination=destination)
            iocb = IOCB(request)  # make an IOCB
            deferred(self.this_application.request_io, iocb)
            iocb.wait()
            return True

        except Exception as error:
            self._log.error("exception: {!r}".format(error))
            return False

    def whois_router_to_network(self, network=None, *, destination=None):
        # build a request
        try:
            request = WhoIsRouterToNetwork()
            if network:
                request.wirtnNetwork = int(network)
            if destination:
                request.pduDestination = Address(destination)
                self._log.debug(
                    "WhoIsRouterToNetwork Destination : {}".format(destination)
                )
            else:
                request.pduDestination = LocalBroadcast()
        except:
            self._log.error("WhoIsRouterToNetwork : invalid arguments")
            return
        iocb = IOCB((self.this_application.nsap.local_adapter, request))  # make an IOCB
        iocb.set_timeout(2)
        deferred(self.this_application.nse.request_io, iocb)
        iocb.wait()

        try:
            self.init_routing_table(str(self.this_application.nse._iartn.pop()))
        except IndexError:
            pass

    def init_routing_table(self, address):
        """
        irt <addr>

        Send an empty Initialize-Routing-Table message to an address, a router
        will return an acknowledgement with its routing table configuration.
        """
        # build a request
        self._log.info("Addr : {}".format(address))
        try:
            request = InitializeRoutingTable()
            request.pduDestination = Address(address)
        except:
            self._log.error("invalid arguments")
            return

        iocb = IOCB((self.this_application.nsap.local_adapter, request))  # make an IOCB
        iocb.set_timeout(2)
        deferred(self.this_application.nse.request_io, iocb)
        iocb.wait()

    def what_is_network_number(self, destination=None):
        """
        winn [ <addr> ]

        Send a What-Is-Network-Number message.  If the address is unspecified
        the message is locally broadcast.
        """
        # build a request

        request = WhatIsNetworkNumber()
        if destination:
            request.pduDestination = Address(destination)
        else:
            request.pduDestination = LocalBroadcast()

        iocb = IOCB((self.this_application.nsap.local_adapter, request))  # make an IOCB
        iocb.set_timeout(2)
        deferred(self.this_application.nse.request_io, iocb)
        iocb.wait()

    def whohas(
        self,
        object_id=None,
        object_name=None,
        instance_range_low_limit=0,
        instance_range_high_limit=4194303,
        destination=None,
        global_broadcast=False,
    ):
        """
        Object ID : analogInput:1
        Object Name : string
        Instance Range Low Limit : 0
        Instance Range High Limit : 4194303
        destination (optional) : If empty, local broadcast will be used.
        global_broadcast : False

        """
        obj_id = ObjectIdentifier(object_id)
        if object_name and not object_id:
            obj_name = CharacterString(object_name)
            obj = WhoHasObject(objectName=obj_name)
        elif object_id and not object_name:
            obj = WhoHasObject(objectIdentifier=obj_id)
        else:
            obj = WhoHasObject(objectIdentifier=obj_id, objectName=obj_name)
        limits = WhoHasLimits(
            deviceInstanceRangeLowLimit=instance_range_low_limit,
            deviceInstanceRangeHighLimit=instance_range_high_limit,
        )
        request = WhoHasRequest(object=obj, limits=limits)
        if destination:
            request.pduDestination = Address(destination)
        else:
            if global_broadcast:
                request.pduDestination = GlobalBroadcast()
            else:
                request.pduDestination = LocalBroadcast()
        iocb = IOCB(request)  # make an IOCB
        iocb.set_timeout(2)
        deferred(self.this_application.request_io, iocb)
        iocb.wait()

        iocb = IOCB(request)  # make an IOCB
        self.this_application._last_i_have_received = []

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

        if iocb.ioError:  # unsuccessful: error/reject/abort
            pass

        time.sleep(3)
        # self.discoveredObjects = self.this_application.i_am_counter
        return self.this_application._last_i_have_received class TimeSync:
    """
    Mixin to support Time Synchronisation from BAC0 to other devices
    """

    def time_sync(self, destination=None, datetime=None, UTC=False):
        """
        Take local time and send it to devices. User can also provide
        a datetime value (constructed following bacpypes.basetypes.Datetime
        format).

        To create a DateTime ::

            from bacpypes.basetypes import DateTime
            from bacpypes.primitivedata import Date, Time

            # Create date and time
            _date = Date('2019-08-05')
            _time = Time('16:45')

            # Create Datetime
            _datetime = DateTime(date=_date.value, time=_time.value)

            # Pass this to the function
            bacnet.time_sync(datetime=_datetime)


        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        if not datetime:
            _datetime = _build_datetime(UTC=UTC)
        elif isinstance(datetime, DateTime):
            _datetime = datetime
        else:
            raise ValueError(
                "Please provide valid DateTime in bacpypes.basetypes.DateTime format"
            )

        # build a request
        if UTC:
            request = UTCTimeSynchronizationRequest(time=_datetime)
        else:
            request = TimeSynchronizationRequest(time=_datetime)

        if destination:
            if destination.lower() == "global":
                request.pduDestination = GlobalBroadcast()
            elif destination.lower() == "local":
                request.pduDestination = LocalBroadcast()
            else:
                try:
                    request.pduDestination = Address(destination)
                except (TypeError, ValueError):
                    self._log.warning(
                        "Destination unrecognized ({}), setting local broadcast".format(
                            destination
                        )
                    )
                    request.pduDestination = LocalBroadcast()
        else:
            request.pduDestination = LocalBroadcast()

        self._log.debug("{:>12} {}".format("- request:", request))

        iocb = IOCB(request)  # make an IOCB

        # pass to the BACnet stack
        deferred(self.this_application.request_io, iocb)

        # Unconfirmed request...so wait until complete
        iocb.wait()  # Wait for BACnet response
        year, month, day, dow = _datetime.date
        year = year + 1900
        hour, minutes, sec, msec = _datetime.time
        d = dt.datetime(year, month, day, hour, minutes, sec, msec)
        self._log.info("Time Sync Request sent to network : {}".format(d.isoformat())) class Reinitialize:
    """
    Mixin to support Reinitialize from BAC0 to other devices
    """

    def reinitialize(self, address=None, password=None, state="coldstart"):
        """
        Will send reinitialize request
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        if not address:
            raise ValueError("Provide address for request")

        # build a request
        request = ReinitializeDeviceRequest()
        request.reinitializedStateOfDevice = ReinitializeDeviceRequestReinitializedStateOfDevice.enumerations[
            state
        ]
        request.pduDestination = Address(address)
        request.password = CharacterString(password)

        self._log.debug("{:>12} {}".format("- request:", request))

        iocb = IOCB(request)  # make an IOCB

        # pass to the BACnet stack
        deferred(self.this_application.request_io, iocb)

        # Unconfirmed request...so wait until complete
        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

        self._log.info("Reinitialize request sent to device : {}".format(address)) class DeviceCommunicationControl:
    """
    Mixin to support DeviceCommunicationControl from BAC0 to other devices
    """

    def dcc(self, address=None, duration=None, password=None, state=None):
        """
        Will send DeviceCommunicationControl request
        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        if not address:
            raise ValueError("Provide address for request")

        if not state:
            raise ValueError("Provide state ('enable', 'disable', 'disableInitiation'")

        # build a request
        request = DeviceCommunicationControlRequest()
        request.enableDisable = DeviceCommunicationControlRequestEnableDisable.enumerations[
            state
        ]
        request.pduDestination = Address(address)
        if duration:
            request.duration = Unsigned16(duration)

        request.password = CharacterString(password)

        self._log.debug("{:>12} {}".format("- request:", request))

        iocb = IOCB(request)  # make an IOCB

        # pass to the BACnet stack
        deferred(self.this_application.request_io, iocb)

        # Unconfirmed request...so wait until complete
        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

        self._log.info(
            "DeviceCommunicationControl request sent to device : {}".format(address)
        ) class CoV:
    """
    Mixin to support COV registration
    """

    def send_cov_subscription(self, request):
        self._log.debug("Request : {}".format(request))
        iocb = IOCB(request)
        self._log.debug("IOCB : {}".format(iocb))

        iocb.add_callback(self.subscription_acknowledged)

        # pass to the BACnet stack
        deferred(self.this_application.request_io, iocb)

    def subscription_acknowledged(self, iocb):
        if iocb.ioResponse:
            self._log.info("Subscription success")

        if iocb.ioError:
            self._log.error("Subscription failed. {}".format(iocb.ioError))

    def cov(self, address, objectID, confirmed=True, lifetime=0, callback=None):
        address = Address(address)
        context = self._build_cov_context(
            address, objectID, confirmed=confirmed, lifetime=lifetime, callback=callback
        )
        request = self._build_cov_request(context)

        self.send_cov_subscription(request)

    def cancel_cov(self, address, objectID, callback=None):
        address = Address(address)
        context = self._build_cov_context(
            address, objectID, confirmed=None, lifetime=None, callback=callback
        )
        request = self._build_cov_request(context)

        self.send_cov_subscription(request)

    def _build_cov_context(
        self, address, objectID, confirmed=True, lifetime=None, callback=None
    ):
        context = SubscriptionContext(
            address=address,
            objectID=objectID,
            confirmed=confirmed,
            lifetime=lifetime,
            callback=callback,
        )
        self.subscription_contexts[context.subscriberProcessIdentifier] = context

        if "context_callback" not in self.subscription_contexts.keys():
            self.subscription_contexts["context_callback"] = self.context_callback
        return context

    def _build_cov_request(self, context):
        request = SubscribeCOVRequest(
            subscriberProcessIdentifier=context.subscriberProcessIdentifier,
            monitoredObjectIdentifier=context.monitoredObjectIdentifier,
        )
        request.pduDestination = context.address

        # optional parameters
        if context.issueConfirmedNotifications is not None:
            request.issueConfirmedNotifications = context.issueConfirmedNotifications
        if context.lifetime is not None:
            request.lifetime = context.lifetime

        return request

    # def context_callback(self, elements, callback=None):
    def context_callback(self, elements):
        self._log.info("Received COV Notification for {}".format(elements))
        # if callback:
        #    callback()
        for device in self.registered_devices:
            if str(device.properties.address) == str(elements["source"]):
                device[elements["object_changed"]].cov_registered = True
                for prop, value in elements["properties"].items():
                    if prop == "presentValue":
                        device[elements["object_changed"]]._trend(value)
                    else:
                        device[elements["object_changed"]].properties.bacnet_properties[
                            prop
                        ] = value
                break class Schedule:
    """
    Everything you need to write to a schedule
    """

    WeeklySchedule = ArrayOf(DailySchedule)
    schedules = {}
    days = [
        "monday",
        "tuesday",
        "wednesday",
        "thursday",
        "friday",
        "saturday",
        "sunday",
    ]

    schedule_example_multistate = {
        "states": {"Occupied": 1, "UnOccupied": 2, "Standby": 3, "Not Set": 4},
        "week": {
            "monday": [("1:00", "Occupied"), ("17:00", "UnOccupied")],
            "tuesday": [("2:00", "Occupied"), ("17:00", "UnOccupied")],
            "wednesday": [("3:00", "Occupied"), ("17:00", "UnOccupied")],
            "thursday": [("4:00", "Occupied"), ("17:00", "UnOccupied")],
            "friday": [("5:00", "Occupied"), ("17:00", "UnOccupied")],
            "saturday": [("6:00", "Occupied"), ("17:00", "UnOccupied")],
            "sunday": [("7:00", "Occupied"), ("17:00", "UnOccupied")],
        },
    }
    schedule_example_binary = {
        "states": {"inactive": 0, "active": 1},
        "week": {
            "monday": [("1:00", "active"), ("16:00", "inactive")],
            "tuesday": [("2:00", "active"), ("16:00", "inactive")],
            "wednesday": [("3:00", "active"), ("16:00", "inactive")],
            "thursday": [("4:00", "active"), ("16:00", "inactive")],
            "friday": [("5:00", "active"), ("16:00", "inactive")],
            "saturday": [("6:00", "active"), ("16:00", "inactive")],
            "sunday": [("7:00", "active"), ("16:00", "inactive")],
        },
    }
    schedule_example_analog = {
        "states": "analog",
        "week": {
            "monday": [("1:00", 22), ("18:00", 19)],
            "tuesday": [("2:00", 22), ("18:00", 19)],
            "wednesday": [("3:00", 22), ("18:00", 19)],
            "thursday": [("4:00", 22), ("18:00", 19)],
            "friday": [("5:00", 22), ("18:00", 19)],
            "saturday": [("6:00", 22), ("18:00", 19)],
            "sunday": [("7:00", 22), ("18:00", 19)],
        },
    }

    def create_weeklySchedule(self, dict_schedule, object_reference=None):
        """
        From a structured dict (see schedule_example), create a WeeklySchedule
        an ArrayOf(DailySchedule)
        """
        ds = dict_schedule
        if object_reference is not None:
            self.schedules[object_reference] = ds

        def _set_value(v):
            try:
                if dict_schedule["states"].lower() == "analog":
                    return Real(v)
            except AttributeError:
                if dict_schedule["states"] == ["inactive", "active"]:
                    return Integer(dict_schedule["states"][v])
                else:
                    return Integer(dict_schedule["states"][v] - 1)

        daily_schedules = []
        for day in Schedule.days:
            list_of_events = dict_schedule["week"][day]
            _daily_schedule = [
                TimeValue(time=event[0], value=_set_value(event[1]))
                for event in list_of_events
            ]
            daily_schedules.append(DailySchedule(daySchedule=_daily_schedule))
        return Schedule.WeeklySchedule(daily_schedules)

    def make_weeklySchedule_request(self, destination, object_instance, weeklySchedule):
        request = WritePropertyRequest(
            objectIdentifier=("schedule", object_instance),
            propertyIdentifier="weeklySchedule",
        )

        address = Address(destination)
        request.pduDestination = address
        request.propertyValue = Any()
        request.propertyValue.cast_in(weeklySchedule)
        request.priority = 15
        return request

    def send_weeklyschedule_request(self, request, timeout=10):
        iocb = IOCB(request)
        iocb.set_timeout(timeout)
        deferred(self.this_application.request_io, iocb)

        iocb.wait()

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return
        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

        self._log.info(
            "Schedule Write request sent to device : {}".format(request.pduDestination)
        )

    def write_weeklySchedule(self, destination, schedule_instance, schedule):
        weeklyschedule = self.create_weeklySchedule(schedule)
        request = self.make_weeklySchedule_request(
            destination=destination,
            object_instance=schedule_instance,
            weeklySchedule=weeklyschedule,
        )
        self.send_weeklyschedule_request(request)

    def read_weeklySchedule(self, address, schedule_instance):
        """
        This function will turn the weeklySchedule received into a
        human readable dict.
        This dict can then be modified and used to write back to controller
        using bacnet.write_weeklySchedule
        """
        try:
            (
                _schedule,
                object_references,
                reliability,
                priority,
                presentValue,
            ) = self.readMultiple(
                "{} schedule {} weeklySchedule listOfObjectPropertyReferences reliability priorityForWriting presentValue".format(
                    address, schedule_instance
                )
            )

        except Exception:
            # Read Multiple not supported... try single prop read
            def _read(prop):
                return self.read(
                    "{} schedule {} {}".format(address, schedule_instance, prop)
                )

            try:
                _schedule = _read("weeklySchedule")
                object_references = _read("listOfObjectPropertyReferences")
                reliability = _read("reliability")
                priority = _read("priorityForWriting")
                presentValue = _read("presentValue")

            except Exception:
                raise ()

        schedule = {}
        _state_text = None
        offset_MV = 0 if len(object_references) == 0 else 1

        try:
            first_obj_id = object_references[0]
            obj_type, obj_instance = first_obj_id.objectIdentifier
            if obj_type == "binaryValue":
                _state_text = ["inactive", "active"]
            elif "multi" in obj_type:
                _state_text = self.read(
                    "{} {} {} stateText".format(address, obj_type, obj_instance)
                )
            elif "analog" in obj_type:
                _state_text = "analog"

            schedule["object_references"] = [
                objectId.objectIdentifier for objectId in object_references
            ]
            schedule["references_names"] = [
                self.read(
                    "{} {} {} objectName".format(
                        address, each.objectIdentifier[0], each.objectIdentifier[1]
                    )
                )
                for each in object_references
            ]
        except Exception as error:
            self._log.error("Error {}".format(error))
            # Not used in device, not linked...
            _state_text = range(255)
            schedule["object_references"] = []
            schedule["references_names"] = []

        schedule["states"] = {}
        if _state_text == ["inactive", "active"]:
            for i, each in enumerate(_state_text):
                schedule["states"][each] = i
            presentValue = "{} ({})".format(
                list(schedule["states"].keys())[int(presentValue.value)],
                presentValue.value,
            )
        elif _state_text != "analog":
            for i, each in enumerate(_state_text):
                schedule["states"][each] = i + offset_MV
            try:
                presentValue = "{} ({})".format(
                    list(schedule["states"].keys())[
                        int(presentValue.value) - offset_MV
                    ],
                    presentValue.value,
                )
            except TypeError:
                presentValue = presentValue.value
        else:
            schedule["states"] = _state_text
            presentValue = "{}".format(presentValue.value)
        schedule["reliability"] = reliability
        schedule["priority"] = priority
        schedule["presentValue"] = presentValue
        schedule["week"] = self.decode_weeklySchedule(_schedule, _state_text, offset_MV)

        return schedule

    def decode_weeklySchedule(self, weeklySchedule, states, offset_MV):
        week = {}
        for i, day in enumerate(Schedule.days):
            week[day] = self.decode_dailySchedule(weeklySchedule[i], states, offset_MV)
        return week

    def decode_dailySchedule(self, dailySchedule, states, offset_MV):
        events = []
        for each in dailySchedule.daySchedule:
            hour, minute, second, hundreth = each.time
            _time = dt_time(hour, minute, second, hundreth).strftime("%H:%M")
            try:
                if states == ["inactive", "active"]:
                    _value = states[int(each.value.value)]
                elif states == "analog":
                    _value = float(each.value.value)
                else:
                    _value = states[int(each.value.value) - offset_MV]
                events.append((_time, _value))
            except IndexError:
                events.append((_time, "??? ({})".format(each.value.value)))
        return events class Calendar:
    """
    Everything you need to write dates and date ranges to a calendar object.
    """

    DateList = ArrayOf(CalendarEntry)
    datelist_example = {
        "dates": [
            {"date": "2021/3/14", "recurring": False},
            {"date": "2021/3/10", "recurring": True},
        ],
        "dateRanges": [
            {"startDate": "2021/3/16", "endDate": "2021/3/21"},
            {"startDate": "2021/3/5", "endDate": "2021/3/7"},
        ],
    }

    def create_calendar(self, dict_calendar):
        """
        From a structured dict (see dateList_example), create a DateList
        an ArrayOf(CalendarEntry)
        """

        entries = []
        if "dates" in dict_calendar.keys():
            for date_entry in dict_calendar["dates"]:
                year, month, day = (int(x) for x in date_entry["date"].split("/"))
                if date_entry["recurring"]:
                    weekday = 255
                else:
                    weekday = dt.date(year, month, day).weekday() + 1
                    if weekday > 7:
                        weekday = 1
                _date = (year - 1900, month, day, weekday)
                entries.append(CalendarEntry(date=_date))

        if "dateRanges" in dict_calendar.keys():
            for date_range_entry in dict_calendar["dateRanges"]:
                year, month, day = (
                    int(x) for x in date_range_entry["startDate"].split("/")
                )
                weekday = dt.date(year, month, day).weekday() + 1
                if weekday > 7:
                    weekday = 1
                start_date = (year - 1900, month, day, weekday)

                year, month, day = (
                    int(x) for x in date_range_entry["endDate"].split("/")
                )
                weekday = dt.date(year, month, day).weekday() + 1
                if weekday > 7:
                    weekday = 1
                end_date = (year - 1900, month, day, weekday)

                date_range = DateRange(startDate=start_date, endDate=end_date)
                entries.append(CalendarEntry(dateRange=date_range))

        dateList = self.DateList(entries)
        return dateList

    def make_calendar_request(self, destination, object_instance, dateList):
        request = WritePropertyRequest(
            objectIdentifier=("calendar", object_instance),
            propertyIdentifier="dateList",
        )

        address = Address(destination)
        request.pduDestination = address
        request.propertyValue = Any()
        request.propertyValue.cast_in(dateList)
        return request

    def send_calendar_request(self, request, timeout=10):
        iocb = IOCB(request)
        iocb.set_timeout(timeout)
        deferred(self.this_application.request_io, iocb)

        iocb.wait()  # Wait for BACnet response

        if iocb.ioResponse:  # successful response
            apdu = iocb.ioResponse

            if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                self._log.warning("Not an ack, see debug for more infos.")
                self._log.debug(
                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
                )
                return

        if iocb.ioError:  # unsuccessful: error/reject/abort
            apdu = iocb.ioError
            reason = find_reason(apdu)
            raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

        self._log.info(
            "Calendar Write request sent to device : {}".format(request.pduDestination)
        )

    def write_calendar_dateList(self, destination, calendar_instance, dates):
        calendar = self.create_calendar(dates)
        request = self.make_calendar_request(
            destination=destination,
            object_instance=calendar_instance,
            dateList=calendar,
        )
        self.send_calendar_request(request)

    def read_calendar_dateList(self, address, calendar_instance):
        """
        This function will read the dateList property of given calendar object and
        pass it to decode_dateList() to convert it into a human readable dict.
        """

        try:
            dateList_object = self.read(
                "{} calendar {} dateList".format(address, calendar_instance)
            )
            dict_calendar = self.decode_dateList(dateList_object)
        except Exception as error:
            self._log.error("exception: {!r}".format(error))
            return {}

        return dict_calendar

    def decode_dateList(self, dateList_object):
        dict_calendar = {"dates": [], "dateRanges": []}
        for entry in dateList_object:
            entry_dict = {}
            if entry.date:
                if entry.date[3] == 255:
                    recurring = True
                else:
                    recurring = False
                entry_dict["date"] = "{}/{}/{}".format(
                    entry.date[0] + 1900, entry.date[1], entry.date[2]
                )
                entry_dict["recurring"] = recurring
                dict_calendar["dates"].append(entry_dict)
            elif entry.dateRange:
                entry_dict["startDate"] = "{}/{}/{}".format(
                    entry.dateRange.startDate[0] + 1900,
                    entry.dateRange.startDate[1],
                    entry.dateRange.startDate[2],
                )
                entry_dict["endDate"] = "{}/{}/{}".format(
                    entry.dateRange.endDate[0] + 1900,
                    entry.dateRange.endDate[1],
                    entry.dateRange.endDate[2],
                )
                dict_calendar["dateRanges"].append(entry_dict)

        return dict_calendar class TextMixin:
    """
    Mixin with functions to deal with text properties.
    Adding features to "network" itself.
    """

    def send_text_write_request(
        self, addr, obj_type, obj_inst, value, prop_id="description"
    ):
        request = self.build_text_write_request(
            addr=addr,
            obj_type=obj_type,
            obj_inst=obj_inst,
            value=value,
            prop_id=prop_id,
        )
        self.write_text_value(request)

    def build_text_write_request(
        self, addr, obj_type, obj_inst, value, prop_id="description"
    ):
        request = WritePropertyRequest(
            objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id
        )
        request.pduDestination = Address(addr)

        _value = Any()
        _value.cast_in(CharacterString(value))
        request.propertyValue = _value

        return request

    def write_text_value(self, request, timeout=10):
        try:
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(self.this_application.request_io, iocb)

            iocb.wait()  # Wait for BACnet response

            if iocb.ioResponse:  # successful response
                apdu = iocb.ioResponse

                if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                    self._log.error("Not an ack, see debug for more infos.")
                    return

            if iocb.ioError:  # unsuccessful: error/reject/abort
                apdu = iocb.ioError
                reason = find_reason(apdu)
                raise NoResponseFromController("APDU Abort Reason : {}".format(reason))

        except WritePropertyException as error:
            # construction error
            self._log.error(("exception: {!r}".format(error))) class Simulation:
    """
    Global informations regarding simulation
    """

    def sim(self, args):
        """
        Simulate I/O points by setting the Out_Of_Service property, then doing a
        WriteProperty to the point's Present_Value.

        :param args: String with <addr> <type> <inst> <prop> <value> [ <indx> ] [ <priority> ]

        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        # with self.this_application._lock: if use lock...won't be able to call read...
        args = args.split()
        addr, obj_type, obj_inst, prop_id, value = args[:5]

        if self.read("{} {} {} outOfService".format(addr, obj_type, obj_inst)):
            self.write(
                "{} {} {} {} {}".format(addr, obj_type, obj_inst, prop_id, value)
            )
        else:
            try:
                self.write(
                    "{} {} {} outOfService True".format(addr, obj_type, obj_inst)
                )
            except NoResponseFromController as e:
                self._log.warning(
                    "Failed to write to OutOfService property ({})".format(e)
                )

            try:
                if self.read("{} {} {} outOfService".format(addr, obj_type, obj_inst)):
                    self.write(
                        "{} {} {} {} {}".format(
                            addr, obj_type, obj_inst, prop_id, value
                        )
                    )
                else:
                    raise OutOfServiceNotSet()
            except NoResponseFromController as e:
                self._log.warning(
                    "Failed to write to OutOfService property ({})".format(e)
                )

    def out_of_service(self, args):
        """
        Set the Out_Of_Service property so the Present_Value of an I/O may be written.

        :param args: String with <addr> <type> <inst> <prop> <value> [ <indx> ] [ <priority> ]

        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        # with self.this_application._lock: if use lock...won't be able to call read...
        args = args.split()
        addr, obj_type, obj_inst = args[:3]
        try:
            self.write("{} {} {} outOfService True".format(addr, obj_type, obj_inst))
        except NoResponseFromController as e:
            self._log.warning("Failed to write to OutOfService property ({})".format(e))

    def release(self, args):
        """
        Set the Out_Of_Service property to False - to release the I/O point back to
        the controller's control.

        :param args: String with <addr> <type> <inst>

        """
        if not self._started:
            raise ApplicationNotStarted("BACnet stack not running - use startApp()")

        args = args.split()
        addr, obj_type, obj_inst = args[:3]
        try:
            self.write("{} {} {} outOfService False".format(addr, obj_type, obj_inst))
        except NoResponseFromController as e:
            self._log.warning("Failed to write to OutOfService property ({})".format(e))

        try:
            if self.read("{} {} {} outOfService".format(addr, obj_type, obj_inst)):
                raise OutOfServiceSet()
            else:
                pass  # Everything is ok"
        except NoResponseFromController as e:
            self._log.warning("Failed to read OutOfService property ({})".format(e))
 class Point:
    """
    Represents a device BACnet point.  Used to NumericPoint, BooleanPoint and EnumPoints.

    Each point implements a history feature. Each time the point is read, its value (with timestamp)
    is added to a history table. Histories capture the changes to point values over time.
    """

    _cache_delta = timedelta(seconds=1)

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

        self._history = namedtuple("_history", ["timestamp", "value"])
        self.properties = PointProperties()

        self._polling_task = namedtuple("_polling_task", ["task", "running"])
        self._polling_task.task = None
        self._polling_task.running = False

        self._match_task = namedtuple("_match_task", ["task", "running"])
        self._match_task.task = None
        self._match_task.running = False

        self._history.timestamp = []
        self._history.value = [presentValue]
        self._history.timestamp.append(datetime.now().astimezone())

        self.properties.history_size = history_size

        self.properties.device = device
        self.properties.name = pointName
        self.properties.type = pointType
        self.properties.address = pointAddress

        self.properties.description = description
        self.properties.units_state = units_state
        self.properties.simulated = (False, 0)
        self.properties.overridden = (False, 0)

        self.cov_registered = False

        self.tags = tags

        self._cache = {"_previous_read": (None, None)}

    @property
    def value(self):
        """
        Retrieve value of the point
        """
        if (
            self._cache["_previous_read"][0]
            and datetime.now().astimezone() - self._cache["_previous_read"][0]
            < Point._cache_delta
        ):
            return self._cache["_previous_read"][1]

        try:
            res = self.properties.device.properties.network.read(
                "{} {} {} presentValue".format(
                    self.properties.device.properties.address,
                    self.properties.type,
                    str(self.properties.address),
                ),
                vendor_id=self.properties.device.properties.vendor_id,
            )
            # self._trend(res)
        except Exception as e:
            raise
        self._cache["_previous_read"] = (datetime.now().astimezone(), res)
        return res

    def read_priority_array(self):
        """
        Retrieve priority array of the point
        """
        if self.properties.priority_array != False:
            try:
                res = self.properties.device.properties.network.read(
                    "{} {} {} priorityArray".format(
                        self.properties.device.properties.address,
                        self.properties.type,
                        str(self.properties.address),
                    ),
                    vendor_id=self.properties.device.properties.vendor_id,
                )
                self.properties.priority_array = res
            except (ValueError, UnknownPropertyError):
                self.properties.priority_array = False
            except Exception as e:
                raise Exception(
                    "Problem reading : {} | {}".format(self.properties.name, e)
                )

    def read_property(self, prop):
        try:
            return self.properties.device.properties.network.read(
                "{} {} {} {}".format(
                    self.properties.device.properties.address,
                    self.properties.type,
                    str(self.properties.address),
                    prop,
                ),
                vendor_id=self.properties.device.properties.vendor_id,
            )
        except Exception as e:
            raise Exception("Problem reading : {} | {}".format(self.properties.name, e))

    def update_bacnet_properties(self):
        """
        Retrieve bacnet properties for this point
        To retrieve something general, forcing vendor id 0
        """
        try:
            res = self.properties.device.properties.network.readMultiple(
                "{} {} {} all".format(
                    self.properties.device.properties.address,
                    self.properties.type,
                    str(self.properties.address),
                ),
                vendor_id=self.properties.device.properties.vendor_id,
                show_property_name=True,
            )
            for each in res:
                if not each:
                    continue
                v, prop = each
                self.properties.bacnet_properties[prop] = v

        except Exception as e:
            raise Exception("Problem reading : {} | {}".format(self.properties.name, e))

    @property
    def bacnet_properties(self):
        if not self.properties.bacnet_properties:
            self.update_bacnet_properties()
        return self.properties.bacnet_properties

    @property
    def is_overridden(self):
        self.read_priority_array()
        if self.properties.priority_array == False:
            return False
        if self.priority(8) or self.priority(1):
            self.properties.overridden = (True, self.value)
            return True
        else:
            return False

    def priority(self, priority=None):
        if self.properties.priority_array == False:
            return None

        self.read_priority_array()
        if not priority:
            return self.properties.priority_array.debug_contents()
        if priority < 1 or priority > 16:
            raise IndexError("Please provide priority to read (1-16)")

        else:
            pa = self.properties.priority_array.value[priority]
            try:
                key, value = zip(*pa.dict_contents().items())
                if key[0] == "null":
                    return None
                else:
                    return (key[0], value[0])
            except ValueError:
                return None

    def _trend(self, res):
        # now = datetime.now(tz=pytz.UTC)
        now = datetime.now().astimezone()
        self._history.timestamp.append(now)
        self._history.value.append(res)
        if self.properties.device.properties.network.database:
            self.properties.device.properties.network.database.write_points_lastvalue_to_db(
                [self]
            )
        if self.properties.history_size is None:
            return
        else:
            if self.properties.history_size < 1:
                self.properties.history_size = 1
            if len(self._history.timestamp) >= self.properties.history_size:
                try:
                    self._history.timestamp = self._history.timestamp[
                        -self.properties.history_size :
                    ]
                    self._history.value = self._history.value[
                        -self.properties.history_size :
                    ]
                    assert len(self._history.timestamp) == len(self._history.value)

                except Exception as e:
                    self._log.exception("Can't append to history")

    @property
    def units(self):
        """
        Should return units
        """
        raise Exception("Must be overridden")

    @property
    def lastValue(self):
        """
        returns: last value read
        """
        if _PANDAS:
            return self.history.dropna().iloc[-1]
        else:
            return self._history.value[-1]

    @property
    def lastTimestamp(self):
        """
        returns: last value read
        """
        if _PANDAS:
            return self.history.dropna().index[-1]
        else:
            return self._history.timestamp[-1]

    @property
    def history(self):
        """
        returns : (pd.Series) containing timestamp and value of all readings
        """
        if not _PANDAS:
            return dict(zip(self._history.timestamp, self._history.value))
        idx = self._history.timestamp.copy()
        his_table = pd.Series(index=idx, data=self._history.value[: len(idx)])
        del idx
        his_table.name = ("{}/{}").format(
            self.properties.device.properties.name, self.properties.name
        )
        his_table.units = self.properties.units_state
        if self.properties.name in self.properties.device.binary_states:
            his_table.states = "binary"
        elif self.properties.name in self.properties.device.multi_states:
            his_table.states = "multistates"
        else:
            his_table.states = "analog"
        his_table.description = self.properties.description

        his_table.datatype = self.properties.type
        return his_table

    def clear_history(self):
        self._history.timestamp = []
        self._history.value = []

    def chart(self, remove=False):
        """
        Add point to the bacnet trending list
        """
        if remove:
            self.properties.device.properties.network.remove_trend(self)
        else:
            self.properties.device.properties.network.add_trend(self)

    def __getitem__(self, key):
        """
        Way to get points... presentValue, status, flags, etc...

        :param key: state
        :returns: list of enum states
        """
        if str(key).lower() in ["unit", "units", "state", "states"]:
            key = "units_state"
        try:
            return getattr(self.properties, key)
        except AttributeError:
            try:
                if "@prop_" in key:
                    key = key.split("prop_")[1]
                return self.read_property(key)
            except Exception:
                raise ValueError("Cannot find property named {}".format(key))

    def write(self, value, *, prop="presentValue", priority=""):
        """
        Write to present value of a point

        :param value: (float) numeric value
        :param prop: (str) property to write. Default = presentValue
        :param priority: (int) priority to which write.

        """
        if prop == "description":
            self.update_description(value)
        else:
            if priority != "":
                if (
                    isinstance(float(priority), float)
                    and float(priority) >= 1
                    and float(priority) <= 16
                ):
                    priority = "- {}".format(priority)
                else:
                    raise ValueError("Priority must be a number between 1 and 16")

            try:
                self.properties.device.properties.network.write(
                    "{} {} {} {} {} {}".format(
                        self.properties.device.properties.address,
                        self.properties.type,
                        self.properties.address,
                        prop,
                        value,
                        priority,
                    ),
                    vendor_id=self.properties.device.properties.vendor_id,
                )
            except NoResponseFromController:
                raise

            # Read after the write so history gets updated.
            self.value

    def default(self, value):
        self.write(value, prop="relinquishDefault")

    def sim(self, value, *, force=False):
        """
        Simulate a value.  Sets the Out_Of_Service property- to disconnect the point from the
        controller's control.  Then writes to the Present_Value.
        The point name is added to the list of simulated points (self.simPoints)

        :param value: (float) value to simulate
        """
        if (
            not self.properties.simulated[0]
            or self.properties.simulated[1] != value
            or force != False
        ):
            self.properties.device.properties.network.sim(
                "{} {} {} presentValue {}".format(
                    self.properties.device.properties.address,
                    self.properties.type,
                    str(self.properties.address),
                    str(value),
                )
            )
            self.properties.simulated = (True, value)

    def out_of_service(self):
        """
        Sets the Out_Of_Service property [to True].
        """
        self.properties.device.properties.network.out_of_service(
            "{} {} {}".format(
                self.properties.device.properties.address,
                self.properties.type,
                str(self.properties.address),
            )
        )
        self.properties.simulated = (True, None)

    def release(self):
        """
        Clears the Out_Of_Service property [to False] - so the controller regains control of the point.
        """
        self.properties.device.properties.network.release(
            "{} {} {}".format(
                self.properties.device.properties.address,
                self.properties.type,
                str(self.properties.address),
            )
        )
        self.properties.simulated = (False, None)

    def ovr(self, value):
        self.write(value, priority=8)
        self.properties.overridden = (True, value)

    def auto(self):
        self.write("null", priority=8)
        self.properties.overridden = (False, 0)

    def release_ovr(self):
        self.write("null", priority=1)
        self.write("null", priority=8)
        self.properties.overridden = (False, None)

    def _setitem(self, value):
        """
        Called by _set, will trigger right function depending on
        point type to write to the value and make tests.
        This is default behaviour of the point  :
        AnalogValue are written to
        AnalogOutput are overridden
        """
        if "characterstring" in self.properties.type:
            self.write(value)

        elif "Value" in self.properties.type:
            if str(value).lower() == "auto":
                raise ValueError(
                    "Value was not simulated or overridden, cannot release to auto"
                )
            # analog value must be written to
            self.write(value)

        elif "Output" in self.properties.type:
            # analog output must be overridden
            if str(value).lower() == "auto":
                self.auto()
            else:
                self.ovr(value)
        else:
            # input are left... must be simulated
            if str(value).lower() == "auto":
                self.release()
            else:
                self.sim(value)

    def _set(self, value):
        """
        Allows the syntax:
            device['point'] = value
        """
        raise NotImplementedError("Must be overridden")

    def poll(self, command="start", *, delay=10):
        """
        Poll a point every x seconds (delay=x sec)
        Stopped by using point.poll('stop') or .poll(0) or .poll(False)
        or by setting a delay = 0
        """
        if (
            str(command).lower() == "stop"
            or command == False
            or command == 0
            or delay == 0
        ):

            if isinstance(self._polling_task.task, Poll):
                self._polling_task.task.stop()
                self._polling_task.task = None
                self._polling_task.running = False

        elif self._polling_task.task is None:
            self._polling_task.task = Poll(self, delay=delay)
            self._polling_task.task.start()
            self._polling_task.running = True

        elif self._polling_task.running:
            self._polling_task.task.stop()
            self._polling_task.running = False
            self._polling_task.task = Poll(self, delay=delay)
            self._polling_task.task.start()
            self._polling_task.running = True

        else:
            raise RuntimeError("Stop polling before redefining it")

    def match(self, point, *, delay=5):
        """
        This allow functions like :
            device['status'].match('command')

        A fan status for example will follow the command...
        """
        if self._match_task.task is None:
            self._match_task.task = Match(command=point, status=self, delay=delay)
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay > 0:
            self._match_task.task.stop()
            self._match_task.running = False
            time.sleep(1)

            self._match_task.task = Match(command=point, status=self, delay=delay)
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay == 0:
            self._match_task.task.stop()
            self._match_task.running = False

        else:
            raise RuntimeError("Stop task before redefining it")

    def match_value(self, value, *, delay=5, use_last_value=False):
        """
        This allow functions like :
            device['point'].match('value')

        A sensor will follow a calculation...
        """
        if self._match_task.task is None:
            self._match_task.task = Match_Value(
                value=value, point=self, delay=delay, use_last_value=use_last_value
            )
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay > 0:
            self._match_task.task.stop()
            self._match_task.running = False
            time.sleep(1)

            self._match_task.task = Match_Value(
                value=value, point=self, delay=delay, use_last_value=use_last_value
            )
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay == 0:
            self._match_task.task.stop()
            self._match_task.running = False

        else:
            raise RuntimeError("Stop task before redefining it")

    def __len__(self):
        """
        Length of a point = # of history records
        """
        return len(self.history)

    def subscribe_cov(self, confirmed=True, lifetime=None, callback=None):
        address = self.properties.device.properties.address
        obj_tuple = (self.properties.type, int(self.properties.address))
        self.properties.device.properties.network.cov(
            address,
            obj_tuple,
            confirmed=confirmed,
            lifetime=lifetime,
            callback=callback,
        )

    def cancel_cov(self, callback=None):
        address = self.properties.device.properties.address
        obj_tuple = (self.properties.type, int(self.properties.address))
        self.properties.device.properties.network.cancel_cov(
            address, obj_tuple, callback=callback
        )

    def update_description(self, value):
        """
        This will write to the BACnet point and modify the description
        of the object
        """
        self.properties.device.properties.network.send_text_write_request(
            addr=self.properties.device.properties.address,
            obj_type=self.properties.type,
            obj_inst=int(self.properties.address),
            value=value,
            prop_id="description",
        )
        self.properties.description = self.read_property("description")

    def tag(self, tag_id, tag_value, lst=None):
        """
        Add tag to point. Those tags can be used to make queries,
        add information, etc.
        They will be included in InfluxDB is used.
        """
        if lst is None:
            self.tag.append((tag_id, tag_value))
        else:
            for each in lst:
                tag_id, tag_value = each
                self.tag.append((tag_id, tag_value)) class RPDeviceConnected(DeviceConnected, ReadProperty):
    """
    [Device state] If device is connected but doesn't support ReadPropertyMultiple

    BAC0 will not poll such points automatically (since it would cause excessive network traffic).
    Instead manual polling must be used as needed via the poll() function.
    """

    def __str__(self):
        return "connected [for ReadProperty]"
 class RPMDeviceConnected(DeviceConnected, ReadPropertyMultiple):
    """
    [Device state] If device is connected and supports ReadPropertyMultiple
    """

    def __str__(self):
        return "connected [for ReadPropertyMultiple]"
 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__() class VirtualPoint(VirtualPointProperties):
    """
    Virtual points could be used to show calculation result on Bokeh
    A function is passed at creation time and this function must return a pandas Serie
    """

    def __init__(
        self,
        name,
        initial_value=None,
        history_fn=None,
        description=None,
        units="No Units",
    ):
        if description is None:
            raise ValueError("Please provide description")
        if not _PANDAS:
            raise ImportError("Pandas required to use VirtualPoints")
        self.name = name
        self.properties = VirtualPointProperties()
        self.properties.name = name
        self.properties.description = description
        self.properties.units_state = units
        self._history_fn = history_fn

        self._history = namedtuple("_history", ["timestamp", "value"])
        self._history.timestamp = []
        self._history.value = []

        self._match_task = namedtuple("_match_task", ["task", "running"])
        self._match_task.task = None
        self._match_task.running = False

        self.fake_pv = None
        if initial_value:
            self._set(initial_value)

    def chart(self, remove=False):
        """
        Add point to the bacnet trending list
        """
        self._log.warning("Use bacnet.add_trend(point) instead")

    def _set(self, value):
        if value == "auto":
            pass
        elif self._history_fn is None:
            self.fake_pv = value
            self._trend(value)
        else:
            self._log.warning(
                "Point is configured as a function of other points. You can't set a new value"
            )

    def _trend(self, res):
        self._history.timestamp.append(datetime.now().astimezone())
        self._history.value.append(res)
        if self.properties.history_size is None:
            return
        else:
            if self.properties.history_size < 1:
                self.properties.history_size = 1
            if len(self._history.timestamp) >= self.properties.history_size:
                try:
                    self._history.timestamp = self._history.timestamp[
                        -self.properties.history_size :
                    ]
                    self._history.value = self._history.value[
                        -self.properties.history_size :
                    ]
                    assert len(self._history.timestamp) == len(self._history.value)
                except Exception as e:
                    self._log.exception("Can't append to history")

    @property
    def value(self):
        """
        Retrieve value of the point
        """
        return self.lastValue

    @property
    def lastValue(self):
        """
        returns: last value read
        """
        if _PANDAS:
            return self.history.dropna().iloc[-1]
        else:
            return self._history.value[-1]

    @property
    def history(self):
        """
        returns : (pd.Series) containing timestamp and value of all readings
        """
        if self._history_fn is not None:
            return self._history_fn()
        else:
            if not _PANDAS:
                return dict(zip(self._history.timestamp, self._history.value))
            idx = self._history.timestamp.copy()
            his_table = pd.Series(index=idx, data=self._history.value[: len(idx)])
            del idx
            his_table.name = ("{}/{}").format(
                self.properties.device.properties.name, self.properties.name
            )
            his_table.units = self.properties.units_state
            his_table.states = "virtual"

            his_table.description = self.properties.description

            his_table.datatype = self.properties.type
            return his_table

    def match_value(self, value, *, delay=5, use_last_value=False):
        """
        This allow functions like :
            device['point'].match('value')

        A sensor will follow a calculation...
        """
        if self._match_task.task is None or not self._match_task.running:
            self._match_task.task = Match_Value(
                value=value, point=self, delay=delay, use_last_value=use_last_value
            )
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay > 0:
            self._match_task.task.stop()
            self._match_task.running = False
            time.sleep(1)

            self._match_task.task = Match_Value(
                value=value, point=self, delay=delay, use_last_value=use_last_value
            )
            self._match_task.task.start()
            self._match_task.running = True

        elif self._match_task.running and delay == 0:
            self._match_task.task.stop()
            self._match_task.running = False

        else:
            raise RuntimeError("Stop task before redefining it")

    def __repr__(self):
        return "{}/{} : {:.2f} {}".format(
            self.properties.device.properties.name,
            self.properties.name,
            float(self.value),
            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 def note_and_log(cls):
    """
    This will be used as a decorator on class to activate
    logging and store messages in the variable cls._notes
    This will allow quick access to events in the web app.
    A note can be added to cls._notes without logging if passing
    the argument log=false to function note()
    Something can be logged without addind a note using function log()
    """
    if hasattr(cls, "DEBUG_LEVEL"):
        if cls.DEBUG_LEVEL == "debug":
            file_level = logging.DEBUG
            console_level = logging.DEBUG
        elif cls.DEBUG_LEVEL == "info":
            file_level = logging.INFO
            console_level = logging.INFO
    else:
        file_level = logging.WARNING
        console_level = logging.INFO
    # Notes object
    cls._notes = namedtuple("_notes", ["timestamp", "notes"])
    cls._notes.timestamp = []
    cls._notes.notes = []

    # Defining log object
    cls.logname = "{} | {}".format(cls.__module__, cls.__name__)
    cls._log = logging.getLogger("BAC0_Root.{}.{}".format(cls.__module__, cls.__name__))

    # Set level to debug so filter is done by handler
    cls._log.setLevel(logging.DEBUG)

    # Console Handler
    ch = logging.StreamHandler(sys.stderr)
    ch.set_name("stderr")
    ch.setLevel(logging.CRITICAL)

    ch2 = logging.StreamHandler(sys.stdout)
    ch2.set_name("stdout")
    ch2.setLevel(console_level)

    formatter = logging.Formatter("{asctime} - {levelname:<8}| {message}", style="{")

    # File Handler
    _PERMISSION_TO_WRITE = True
    logUserPath = expanduser("~")
    logSaveFilePath = join(logUserPath, ".BAC0")

    logFile = join(logSaveFilePath, "BAC0.log")
    try:
        if not os.path.exists(logSaveFilePath):
            os.makedirs(logSaveFilePath)
        fh = FileHandler(logFile)
        fh.set_name("file_handler")
        fh.setLevel(file_level)
        fh.setFormatter(formatter)

    except OSError:
        _PERMISSION_TO_WRITE = False

    ch.setFormatter(formatter)
    ch2.setFormatter(formatter)
    # Add handlers the first time only...
    if not len(cls._log.handlers):
        if _PERMISSION_TO_WRITE:
            cls._log.addHandler(fh)
        cls._log.addHandler(ch)
        cls._log.addHandler(ch2)

    LogList.LOGGERS.append(cls._log)

    def log_title(self, title, args=None, width=35):
        cls._log.debug("")
        cls._log.debug("#" * width)
        cls._log.debug("# {}".format(title))
        cls._log.debug("#" * width)
        if args:
            cls._log.debug("{!r}".format(args))
            cls._log.debug("#" * 35)

    def log_subtitle(self, subtitle, args=None, width=35):
        cls._log.debug("")
        cls._log.debug("=" * width)
        cls._log.debug("{}".format(subtitle))
        cls._log.debug("=" * width)
        if args:
            cls._log.debug("{!r}".format(args))
            cls._log.debug("=" * width)

    def log(self, note, *, level=logging.DEBUG):
        """
        Add a log entry...no note
        """
        if not note:
            raise ValueError("Provide something to log")
        note = "{} | {}".format(cls.logname, note)
        cls._log.log(level, note)

    def note(self, note, *, level=logging.INFO, log=True):
        """
        Add note to the object. By default, the note will also
        be logged
        :param note: (str) The note itself
        :param level: (logging.level)
        :param log: (boolean) Enable or disable logging of note
        """
        if not note:
            raise ValueError("Provide something to log")
        note = "{} | {}".format(cls.logname, note)
        cls._notes.timestamp.append(datetime.now().astimezone())
        cls._notes.notes.append(note)
        if log:
            cls.log(level, note)

    @property
    def notes(self):
        """
        Retrieve notes list as a Pandas Series
        """
        if not _PANDAS:
            return dict(zip(self._notes.timestamp, self._notes.notes))
        return pd.Series(self._notes.notes, index=self._notes.timestamp)

    def clear_notes(self):
        """
        Clear notes object
        """
        cls._notes.timestamp = []
        cls._notes.notes = []

    # Add the functions to the decorated class
    cls.clear_notes = clear_notes
    cls.note = note
    cls.notes = notes
    cls.log = log
    cls.log_title = log_title
    cls.log_subtitle = log_subtitle
    return cls class NoResponseFromController(Exception):
    """
    This exception is used when trying to read or write and there is not answer.
    """

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

    pass class Timeout(Exception):
    pass class NumerousPingFailures(Exception):
    pass class RecurringTask(Task):
    """
    Start a recurring task (a function passed)
    """

    def __init__(self, fnc, delay=60, name="recurring"):
        """
        :param fnc: a function or a tuple (function, args)
        :param delay: (int) Delay between reads executions

        :returns: Nothing
        """
        self.fnc_args = None
        if isinstance(fnc, tuple):
            self.func, self.fnc_args = fnc
        elif hasattr(fnc, "__call__") :
            self.func = fnc
        else:
            raise ValueError("You must pass a function or a tuple (function,args) to this...")
        Task.__init__(self, name=name, delay=delay)
        

    def task(self):
        if self.fnc_args:
            self.func(self.fnc_args)
        else:
            self.func() class Update_local_COV(Task):
    """
    Start a task to validate each points inside local device and send
    cov notifications to subscribers.
    """

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

        A delay cannot be < 10sec
        For delays under 10s, use DeviceFastPoll class.

        :returns: Nothing
        """
        self._device = weakref.ref(device)
        Task.__init__(self, name="{}_{}".format(prefix, name), delay=delay)
        self._counter = 0

    @property
    def device(self):
        return self._device()

    def task(self):
        try:
            for k, cov in self.device.this_application.cov_detections.items():
                objName = k.objectName
                obj = self.device.this_application.get_object_name(objName)
                # get the detection algorithm object
                cov_detection = self.device.this_application.cov_detections.get(
                    obj, None
                )
                if (not cov_detection) or (len(cov_detection.cov_subscriptions) == 0):
                    self._log.debug(
                        "no subscriptions for that object : {}".format(objName)
                    )
                    continue

                # tell it to send out notifications
                self._log.info("Sending COV for {}".format(objName))
                deferred(cov_detection.send_cov_notifications)

        except Exception as error:
            self.device._log.error(
                "Something is wrong with update_local_cov : {}".format(error)
            )
import time
import weakref
from datetime import datetime
from collections import namedtuple
from ..scripts.Base import Base
from ..core.io.Read import ReadProperty
from ..core.io.Write import WriteProperty
from ..core.functions.GetIPAddr import HostIP
from ..core.functions.Discover import Discover
from ..core.functions.TimeSync import TimeSync
from ..core.functions.Reinitialize import Reinitialize
from ..core.functions.DeviceCommunicationControl import DeviceCommunicationControl
from ..core.functions.cov import CoV
from ..core.functions.Schedule import Schedule
from ..core.functions.Calendar import Calendar
from ..core.functions.Text import TextMixin
from ..core.io.Simulate import Simulation
from ..core.devices.Points import Point
from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected
from ..core.devices.Trends import TrendLog
from ..core.devices.Virtuals import VirtualPoint
from ..core.utils.notes import note_and_log
from ..core.io.IOExceptions import (
    NoResponseFromController,
    UnrecognizedService,
    Timeout,
    NumerousPingFailures,
)
from ..tasks.RecurringTask import RecurringTask
from ..tasks.UpdateCOV import Update_local_COV
from ..infos import __version__ as version
    from ..db.influxdb import InfluxDB, ConnectionError
from bacpypes.pdu import Address
        we'll ping again until reconnection, where the device will be bring back online.

        To permanently disconnect a device, an explicit device.disconnect(unregister=True [default value])
        will be needed. This way, the device won't be in the registered_devices list and
        BAC0 won't try to ping it.
        """
        for each in self.registered_devices:
            if isinstance(each, RPDeviceConnected) or isinstance(
                each, RPMDeviceConnected
            ):
                try:
                    self._log.debug(
                        "Ping {}|{}".format(
                            each.properties.name, each.properties.address
                        )
                    )
                    each.ping()
                    if each.properties.ping_failures > 3:
                        raise NumerousPingFailures

                except NumerousPingFailures:
                    self._log.warning(
                        "{}|{} is offline, disconnecting it.".format(
                            each.properties.name, each.properties.address
                        )
                    )
                    each.disconnect(unregister=False)

            else:
                device_id = each.properties.device_id
                addr = each.properties.address
                name = self.read("{} device {} objectName".format(addr, device_id))
                if name == each.properties.name:
                    each.properties.ping_failures = 0
                    self._log.info(
                        "{}|{} is back online, reconnecting.".format(
                            each.properties.name, each.properties.address
                        )
                    )
                    each.connect(network=self)
                    each.poll(delay=each.properties.pollDelay)

    @property
    def registered_devices(self):
        """
        Devices that have been created using BAC0.device(args)
        """
        return list(self._registered_devices.values())

    def unregister_device(self, device):
        """
        Remove from the registered list
        """
        oid = id(device)
        try:
            del self._registered_devices[oid]
        except KeyError:
            pass

    def add_trend(self, point_to_trend):
        """
        Add point to the list of histories that will be handled by Bokeh

        Argument provided must be of type Point or TrendLog
        ex. bacnet.add_trend(controller['point_name'])
        """
        if (
            isinstance(point_to_trend, Point)
            or isinstance(point_to_trend, TrendLog)
            or (isinstance(point_to_trend, VirtualPoint))
        ):
            oid = id(point_to_trend)
            self._points_to_trend[oid] = point_to_trend
        else:
            raise TypeError("Please provide point containing history")

    def remove_trend(self, point_to_remove):
        """
        Remove point from the list of histories that will be handled by Bokeh

        Argument provided must be of type Point or TrendLog
        ex. bacnet.remove_trend(controller['point_name'])
        """
        if (
            isinstance(point_to_remove, Point)
            or isinstance(point_to_remove, TrendLog)
            or isinstance(point_to_remove, VirtualPoint)
        ):
            oid = id(point_to_remove)
        else:
            raise TypeError("Please provide point or trendLog containing history")
        if oid in self._points_to_trend.keys():
            del self._points_to_trend[oid]

    @property
    def devices(self):
        """
        This property will create a good looking table of all the discovered devices
        seen on the network.

        For that, some requests will be sent over the network to look for name,
        manufacturer, etc and in big network, this could be a long process.
        """
        lst = []
        for device in list(self.discoveredDevices):
            try:
                deviceName, vendorName = self.readMultiple(
                    "{} device {} objectName vendorName".format(device[0], device[1])
                )
            except (UnrecognizedService, ValueError):
                self._log.warning(
                    "Unrecognized service for {} | {}".format(device[0], device[1])
                )
                try:
                    deviceName = self.read(
                        "{} device {} objectName".format(device[0], device[1])
                    )
                    vendorName = self.read(
                        "{} device {} vendorName".format(device[0], device[1])
                    )

                except NoResponseFromController: