Repository: ChristianTremblay/BAC0 (language: python)

Issue:
Exception on calling .disconnect() twice
```
In [11]: bacnet = BAC0.connect()
In [12]: bacnet.disconnect()

In [13]: bacnet.disconnect()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-13-4c4805f0a1d6> in <module>
----> 1 bacnet.disconnect()

~/Env/data_handler/lib/python3.5/site-packages/BAC0/scripts/Lite.py in disconnect(self)
    414         for each in self.registered_devices:
    415             each.disconnect()
--> 416         super().disconnect()
    417
    418     def __repr__(self):

~/Env/data_handler/lib/python3.5/site-packages/BAC0/scripts/Base.py in disconnect(self)
    269         self.t.join()
    270         self._started = False
--> 271         Base._used_ips.remove(self.localIPAddr)
    272         self._log.info("BACnet stopped")
    273

KeyError: <Address 192.168.7.110>
```

probably want to replace `remove` with `discard` 
How to write calendar entries to dateList?
Hey! I would like to add entries to a calendar's property `dateList`. Now I have a list with multiple `bacpypes.basetypes.CalendarEntry`. How can I write it to  `dateList`? I tried to figure out how you did similar things in the `Schedule.py` but I don't really get how this whole request building and sending processes work, especially when I'm sending a list or an array. Can you help me? Is there an easy way for me to write dates to a calendar?
Update Device.py
This solves the reconnecting issue after a device is being disconnected. The issue occurs where DeviceProperties does not save "default_history_size" when connecting from the database.
The issue can be replicated for the original file by executing the following command after the device is being connected: 
Device.properties (the array has the attribute "default_history_size")
Device.disconnect()
Device.properties (the array does NOT have the attribute "default_history_size")
Device.connect(network=bacnet) - fails because not attribute call default_history_size. Also missing other important properties.


Discussion / hints:

I think I got it to work for me. Is this a clean way to do it?

```
from bacpypes.core import deferred
from bacpypes.pdu import Address
from BAC0.core.io.Read import find_reason
from BAC0.core.io.IOExceptions import NoResponseFromController, WritePropertyException
from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
from bacpypes.iocb import IOCB
from bacpypes.constructeddata import ArrayOf, Any
from bacpypes.basetypes import CalendarEntry
import BAC0


class Calendar:
    DateList = ArrayOf(CalendarEntry)

    def build_write_request(self, deviceIp, calendarInstance, dateList):
        request = WritePropertyRequest(
            objectIdentifier=("calendar", calendarInstance),
            propertyIdentifier="dateList"
        )
        request.pduDestination = Address(deviceIp)

        _value = Any()
        _value.cast_in(dateList)
        request.propertyValue = _value

        return request

    def write_value(self, bacnet, request, vendor_id=0, timeout=10):
        try:
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(bacnet.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
                    print("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
            print(("exception: {!r}".format(error)))

    def write_calendar(self, localIp, deviceIp, calendarInstance, dateList):
        bacnet = BAC0.connect(ip=localIp)
        request = self.build_write_request(deviceIp=deviceIp, calendarInstance=calendarInstance, dateList=dateList)
        self.write_value(bacnet, request)


if __name__ == '__main__':
    dt1 = (121, 2, 19, 255)
    dt2 = (121, 2, 20, 255)
    listEntries = [CalendarEntry(date=dt1), CalendarEntry(date=dt2)]
    dateList = Calendar.DateList(listEntries)

    Cal = Calendar()
    Cal.write_calendar(localIp="192.168.178.2", deviceIp="192.168.178.3", calendarInstance=10104, dateList=dateList)

```
Now that the basic mechanism works, I'd like to think about the way BAC0 should represent the calendar, preferably with something like a dict, like schedules. So it's easy to read or write.

I'm curious also to see if we could use the `calendar` python package(https://github.com/python/cpython/blob/3.9/Lib/calendar.py)

Would be cool to have access to a calendar view...
I didn't look into `calendar.py` yet but I thought about a way to represent the calendar object's datelist as a dict. I also wrote the `write_calendar_dateList()` and `read_calendar_dateList()` functions that could be implemented in BAC0. Currently supported are (non-)recurring dates and date ranges but  not yet the weeknday option of the calendar entry. What do you think?
```python
from bacpypes.core import deferred
from bacpypes.pdu import Address
from BAC0.core.io.Read import find_reason
from BAC0.core.io.IOExceptions import NoResponseFromController, WritePropertyException
from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
from bacpypes.iocb import IOCB
from bacpypes.constructeddata import ArrayOf, Any
from bacpypes.basetypes import CalendarEntry, DateRange

import BAC0
import datetime


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 build_write_request(self, deviceIp, calendar_instance, dateList):
        request = WritePropertyRequest(
            objectIdentifier=("calendar", calendar_instance),
            propertyIdentifier="dateList"
        )
        request.pduDestination = Address(deviceIp)

        _value = Any()
        _value.cast_in(dateList)
        request.propertyValue = _value

        return request

    def write_value(self, bacnet, request, vendor_id=0, timeout=10):
        try:
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(bacnet.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
                    print("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
            print(("exception: {!r}".format(error)))

    def write_calendar_dateList(self, bacnet, deviceIp, calendar_instance, dateList_dict):
        """
        This function will create a dateList from the given dict and write it to
        the given calendar object.
        """

        try:
            dateList = self.encode_dateList(dateList_dict)
            request = self.build_write_request(deviceIp=deviceIp, calendar_instance=calendar_instance, dateList=dateList)
            self.write_value(bacnet, request)
        except Exception as error:
            print(("exception: {!r}".format(error)))
        

    def read_calendar_dateList(self, bacnet, deviceIp, 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 = bacnet.read("{} calendar {} dateList".format(deviceIp, calendar_instance))
            dateList_dict = self.decode_dateList(dateList_object)
        except Exception as error:
            print(("exception: {!r}".format(error)))
            return {}

        return dateList_dict

    def decode_dateList(self, dateList_object):
        dateList_dict = {"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
                dateList_dict["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])
                dateList_dict["dateRanges"].append(entry_dict)

        return dateList_dict

    def encode_dateList(self, dateList_dict):
        """
        From a structured dict (see dateList_example), create a DateList
        an ArrayOf(CalendarEntry)
        """

        entries = []
        if "dates" in dateList_dict.keys():
            for date_entry in dateList_dict["dates"]:
                year, month, day = (int(x) for x in date_entry["date"].split('/'))
                if date_entry["recurring"]:
                    weekday = 255
                else:
                    weekday = datetime.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 dateList_dict.keys():
            for date_range_entry in dateList_dict["dateRanges"]:
                year, month, day = (int(x) for x in date_range_entry["startDate"].split('/'))
                weekday = datetime.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 = datetime.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
```



Fix patch:
diff --git a/.env~ b/.env~
new file mode 100644
index 00000000..435657eb
--- /dev/null
+++ b/.env~
@@ -0,0 +1,10 @@
+# InfluxDB Params Example .env file
+INFLUXDB_V2_URL=http://192.168.1.10:8086
+INFLUXDB_V2_ORG=my-org
+INFLUXDB_V2_TOKEN=123456789abcdefg
+# INFLUXDB_V2_TIMEOUT= 
+# INFLUXDB_V2_VERIFY_SSL= 
+# INFLUXDB_V2_SSL_CA_CERT= 
+# INFLUXDB_V2_CONNECTION_POOL_MAXSIZE= 
+# INFLUXDB_V2_AUTH_BASIC=
+# INFLUXDB_V2_PROFILERS=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 80b88f49..7bcb8ceb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,6 @@ obj300.bin
 wily-report.html
 make_doc.bat
 tests/manual_test_bokeh.py
+.env
+samples/pascal_stlouis.py
+samples/johnhensen.py
diff --git a/BAC0/__init__.py b/BAC0/__init__.py
index c05efba9..c38bb604 100644
--- a/BAC0/__init__.py
+++ b/BAC0/__init__.py
@@ -36,6 +36,17 @@
     except ImportError:
         _COMPLETE = False
 
+    try:
+        #
+        import os
+
+        if os.path.isfile("{}/.env".format(os.getcwd())):
+            from dotenv import load_dotenv
+
+            load_dotenv(os.path.join(os.getcwd(), ".env"))
+    except ImportError:
+        print("You need to pip install python-dotenv to use your .env file")
+
     from .scripts.Lite import Lite as lite
 
     if _COMPLETE:
diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py
index 641cc0b1..425d3f3e 100644
--- a/BAC0/core/app/ScriptApplication.py
+++ b/BAC0/core/app/ScriptApplication.py
@@ -52,7 +52,7 @@
 
 class common_mixin:
     """
-    They take message coming from the network that are not generated from 
+    They take message coming from the network that are not generated from
     a request we made.
     """
 
@@ -415,7 +415,7 @@ def __init__(
         # on the UDP multiplexer
         self.bip = BIPBBMD(self.localAddress)
         self.annexj = AnnexJCodec()
-        self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True)
+        self.mux = UDPMultiplexer(self.localAddress, noBroadcast=False)
 
         # bind the bottom layers
         # bind(self.bip, self.annexj, self.mux.annexJ)
diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py
index 807a2d4a..504d1ea3 100755
--- a/BAC0/core/devices/Device.py
+++ b/BAC0/core/devices/Device.py
@@ -15,9 +15,6 @@
 
 import os.path
 
-# --- 3rd party modules ---
-import sqlite3
-
 try:
     import pandas as pd
 
@@ -49,7 +46,7 @@
 )
 
 # from ...bokeh.BokehRenderer import BokehPlot
-from ...sql.sql import SQLMixin
+from ...db.sql import SQLMixin
 from ...tasks.DoOnce import DoOnce
 from .mixins.read_mixin import ReadPropertyMultiple, ReadProperty
 from .Virtuals import VirtualPoint
@@ -77,7 +74,13 @@ def __init__(self):
         self.db_name = None
         self.segmentation_supported = True
         self.history_size = None
+        self.save_resampling = "1s"
+        self.clear_history_on_save = None
         self.bacnet_properties = {}
+        self.auto_save = None
+        self.fast_polling = False
+        self.vendor_id = 0
+        self.ping_failures = 0
 
     def __repr__(self):
         return "{}".format(self.asdict)
@@ -156,7 +159,7 @@ def __init__(
         self.properties.auto_save = auto_save
         self.properties.save_resampling = save_resampling
         self.properties.clear_history_on_save = clear_history_on_save
-        self.properties.default_history_size = history_size
+        self.properties.history_size = history_size
         self._reconnect_on_failure = reconnect_on_failure
 
         self.segmentation_supported = segmentation_supported
@@ -531,12 +534,14 @@ def _buildPointList(self):
             )
         )
         try:
-            self.properties.objects_list, self.points, self._list_of_trendlogs = self._discoverPoints(
-                self.custom_object_list
-            )
+            (
+                self.properties.objects_list,
+                self.points,
+                self._list_of_trendlogs,
+            ) = self._discoverPoints(self.custom_object_list)
             if self.properties.pollDelay > 0:
                 self.poll(delay=self.properties.pollDelay)
-            self.update_history_size(size=self.properties.default_history_size)
+            self.update_history_size(size=self.properties.history_size)
             # self.clear_histories()
         except NoResponseFromController as error:
             self._log.error("Cannot retrieve object list, disconnecting...")
@@ -726,7 +731,9 @@ def read_property(self, prop):
             request = "{} {} {} {}".format(
                 self.properties.address, _obj, _instance, _prop
             )
-            val = self.properties.network.read(request, vendor_id=0)
+            val = self.properties.network.read(
+                request, vendor_id=self.properties.vendor_id
+            )
         except KeyError as error:
             raise Exception("Unknown property : {}".format(error))
         return val
@@ -757,15 +764,15 @@ def write_property(self, prop, value, priority=None):
     def update_bacnet_properties(self):
         """
         Retrieve bacnet properties for this device
-        To retrieve something general, forcing vendor id 0
+
         """
         try:
             res = self.properties.network.readMultiple(
                 "{} device {} all".format(
                     self.properties.address, str(self.properties.device_id)
                 ),
-                vendor_id=0,
-                prop_id_required=True,
+                vendor_id=self.properties.vendor_id,
+                show_property_name=True,
             )
             for each in res:
                 if not each:
@@ -795,6 +802,19 @@ def update_description(self, value):
         )
         self.properties.description = self.read_property("description")
 
+    def ping(self):
+        try:
+            if self.read_property("objectName") == self.properties.name:
+                self.properties.ping_failures = 0
+                return True
+            else:
+                self.properties.ping_failures += 1
+                return False
+        except NoResponseFromController as e:
+            self._log.error("Error in ping : {}".format(e))
+            self.properties.ping_failures += 1
+            return False
+
     def __repr__(self):
         return "{} / Connected".format(self.properties.name)
 
@@ -1061,6 +1081,10 @@ def initialize_device_from_db(self):
         self.properties.serving_chart = {}
         self.properties.charts = []
         self.properties.multistates = self._props["multistates"]
+        self.properties.auto_save = self._props["auto_save"]
+        self.properties.save_resampling = self._props["save_resampling"]
+        self.properties.clear_history_on_save = self._props["clear_history_on_save"]
+        self.properties.default_history_size = self._props["history_size"]
         self._log.info("Device restored from db")
         self._log.info(
             'You can reconnect to network using : "device.connect(network=bacnet)"'
diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py
index 1497b629..9227d612 100644
--- a/BAC0/core/devices/Points.py
+++ b/BAC0/core/devices/Points.py
@@ -9,7 +9,8 @@
 """
 
 # --- standard Python modules ---
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
+import pytz
 from collections import namedtuple
 import time
 
@@ -101,6 +102,7 @@ def __init__(
         presentValue=None,
         units_state=None,
         history_size=None,
+        tags=[],
     ):
 
         self._history = namedtuple("_history", ["timestamp", "value"])
@@ -116,7 +118,7 @@ def __init__(
 
         self._history.timestamp = []
         self._history.value = [presentValue]
-        self._history.timestamp.append(datetime.now())
+        self._history.timestamp.append(datetime.now().astimezone())
 
         self.properties.history_size = history_size
 
@@ -132,6 +134,8 @@ def __init__(
 
         self.cov_registered = False
 
+        self.tags = tags
+
         self._cache = {"_previous_read": (None, None)}
 
     @property
@@ -141,7 +145,8 @@ def value(self):
         """
         if (
             self._cache["_previous_read"][0]
-            and datetime.now() - self._cache["_previous_read"][0] < Point._cache_delta
+            and datetime.now().astimezone() - self._cache["_previous_read"][0]
+            < Point._cache_delta
         ):
             return self._cache["_previous_read"][1]
 
@@ -157,7 +162,7 @@ def value(self):
             # self._trend(res)
         except Exception as e:
             raise
-        self._cache["_previous_read"] = (datetime.now(), res)
+        self._cache["_previous_read"] = (datetime.now().astimezone(), res)
         return res
 
     def read_priority_array(self):
@@ -191,7 +196,7 @@ def read_property(self, prop):
                     str(self.properties.address),
                     prop,
                 ),
-                vendor_id=0,
+                vendor_id=self.properties.device.properties.vendor_id,
             )
         except Exception as e:
             raise Exception("Problem reading : {} | {}".format(self.properties.name, e))
@@ -208,8 +213,8 @@ def update_bacnet_properties(self):
                     self.properties.type,
                     str(self.properties.address),
                 ),
-                vendor_id=0,
-                prop_id_required=True,
+                vendor_id=self.properties.device.properties.vendor_id,
+                show_property_name=True,
             )
             for each in res:
                 if not each:
@@ -259,8 +264,14 @@ def priority(self, priority=None):
                 return None
 
     def _trend(self, res):
-        self._history.timestamp.append(datetime.now())
+        # 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:
@@ -275,6 +286,7 @@ def _trend(self, res):
                         -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")
 
@@ -295,6 +307,16 @@ def lastValue(self):
         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):
         """
@@ -614,6 +636,10 @@ def cancel_cov(self, callback=None):
         )
 
     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,
@@ -623,6 +649,19 @@ def update_description(self, value):
         )
         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))
+
 
 # ------------------------------------------------------------------------------
 
@@ -690,7 +729,7 @@ def __repr__(self):
                 val = float(self.value)
         except ValueError:
             self._log.error(
-                "Cannot convert value {}. Device probably disconnected".format(
+                "Cannot convert value {}. Device probably disconnected or the response is inconsistent".format(
                     self.value
                 )
             )
@@ -901,7 +940,7 @@ def value(self):
     def get_state(self, v):
         try:
             return self.properties.units_state[v - 1]
-        except TypeError:
+        except (TypeError, IndexError):
             return "n/a"
 
     @property
@@ -1033,6 +1072,97 @@ def __eq__(self, other):
         return self.value == other.value
 
 
+class DateTimePoint(Point):
+    """
+    Representation of DatetimeValue value
+    """
+
+    def __init__(
+        self,
+        device=None,
+        pointType=None,
+        pointAddress=None,
+        pointName=None,
+        description=None,
+        units_state=None,
+        presentValue=None,
+        history_size=None,
+    ):
+
+        Point.__init__(
+            self,
+            device=device,
+            pointType=pointType,
+            pointAddress=pointAddress,
+            pointName=pointName,
+            description=description,
+            presentValue=presentValue,
+            history_size=history_size,
+        )
+
+    @property
+    def units(self):
+        """
+        Characterstring value do not have units or state text
+        """
+        return None
+
+    def _trend(self, res):
+        # super()._trend(res)
+        return
+
+    @property
+    def value(self):
+        res = super().value
+        # self._trend(res)
+        year, month, day, dayofweek = res.date
+        hour, minutes, seconds, ms = res.time
+        res = datetime(year + 1900, month, day, hour, minutes, seconds)
+        return res
+
+    def _set(self, value):
+        # try:
+        #    if isinstance(value, str):
+        #        self._setitem(value)
+        #    elif isinstance(value, CharacterString):
+        #        self._setitem(value.value)
+        #    else:
+        #        raise ValueError("Value must be string or CharacterString")
+        # except (Exception, ValueError) as error:
+        #    raise WritePropertyException("Problem writing to device : {}".format(error))`
+        raise NotImplementedError("Writing to Datetime is not supported yet")
+
+    def __repr__(self):
+        try:
+            # polling = self.properties.device.properties.pollDelay
+            # if (polling < 90 and polling > 0) or self.cov_registered:
+            #    val = str(self.lastValue)
+            # else:
+            val = self.value
+        except ValueError:
+            self._log.error("Cannot convert value. Device probably disconnected")
+            # Probably disconnected
+            val = None
+        return "{}/{} : {}".format(
+            self.properties.device.properties.name, self.properties.name, val
+        )
+
+    def __eq__(self, other):
+        return self.value == other.value
+
+    def __ge__(self, other):
+        return self.value >= other.value
+
+    def __le__(self, other):
+        return self.value <= other.value
+
+    def __gt__(self, other):
+        return self.value > other.value
+
+    def __lt__(self, other):
+        return self.value < other.value
+
+
 # ------------------------------------------------------------------------------
 
 
diff --git a/BAC0/core/devices/Trends.py b/BAC0/core/devices/Trends.py
index 3997251b..4693e24e 100755
--- a/BAC0/core/devices/Trends.py
+++ b/BAC0/core/devices/Trends.py
@@ -99,7 +99,16 @@ def __init__(
 
     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(
+            (
+                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),
@@ -192,9 +201,10 @@ def history(self):
                     self.properties._history_components["logdatum"],
                 )
             )
-        objectType, objectAddress = (
-            self.properties.log_device_object_property.objectIdentifier
-        )
+        (
+            objectType,
+            objectAddress,
+        ) = self.properties.log_device_object_property.objectIdentifier
         try:
             logged_point = self.properties.device.find_point(objectType, objectAddress)
         except ValueError:
diff --git a/BAC0/core/devices/Virtuals.py b/BAC0/core/devices/Virtuals.py
index 9715e815..5c59dc15 100644
--- a/BAC0/core/devices/Virtuals.py
+++ b/BAC0/core/devices/Virtuals.py
@@ -144,7 +144,7 @@ def _set(self, value):
             )
 
     def _trend(self, res):
-        self._history.timestamp.append(datetime.now())
+        self._history.timestamp.append(datetime.now().astimezone())
         self._history.value.append(res)
         if self.properties.history_size is None:
             return
diff --git a/BAC0/core/devices/create_objects.py b/BAC0/core/devices/create_objects.py
index 7010b878..a2e13f5d 100644
--- a/BAC0/core/devices/create_objects.py
+++ b/BAC0/core/devices/create_objects.py
@@ -27,7 +27,7 @@
 
 
 def _make_mutable(obj, identifier="presentValue", mutable=True):
-    """ 
+    """
     This function is not the way to go as it changes the class
     property...As bacpypes issue #224, it will need a lot of work
     """
diff --git a/BAC0/core/devices/local/models.py b/BAC0/core/devices/local/models.py
index e0506a3f..fe06c9df 100644
--- a/BAC0/core/devices/local/models.py
+++ b/BAC0/core/devices/local/models.py
@@ -19,6 +19,7 @@
     Boolean,
     EventState,
     Date,
+    Time,
     DateTime,
     Unsigned,
 )
@@ -88,7 +89,7 @@ def analog(**kwargs):
 def analog_input(**kwargs):
     kwargs["name"] = set_default_if_not_provided("name", "AI", **kwargs)
     kwargs["objectType"] = AnalogInputObject
-    kwargs["instance"] = 0
+    kwargs["instance"] = set_default_if_not_provided("instance", 0, **kwargs)
     # kwargs['is_commandable'] = True # Futur
 
     return analog(**kwargs)
@@ -97,7 +98,7 @@ def analog_input(**kwargs):
 def analog_output(**kwargs):
     kwargs["name"] = set_default_if_not_provided("name", "AO", **kwargs)
     kwargs["objectType"] = AnalogOutputObject
-    kwargs["instance"] = 0
+    kwargs["instance"] = set_default_if_not_provided("instance", 0, **kwargs)
     kwargs["is_commandable"] = True
 
     return analog(**kwargs)
@@ -106,7 +107,7 @@ def analog_output(**kwargs):
 def analog_value(**kwargs):
     kwargs["name"] = set_default_if_not_provided("name", "AV", **kwargs)
     kwargs["objectType"] = AnalogValueObject
-    kwargs["instance"] = 0
+    kwargs["instance"] = set_default_if_not_provided("instance", 0, **kwargs)
     kwargs["is_commandable"] = True
     return analog(**kwargs)
 
@@ -256,7 +257,7 @@ def datetime_value(**kwargs):
         "objectType": DateTimeValueObject,
         "instance": 0,
         "description": "No description",
-        "presentValue": DateTime(),
+        "presentValue": DateTime(date=Date().now().value, time=Time().now().value),
         "properties": {},
         "is_commandable": False,
         "relinquish_default": "inactive",
diff --git a/BAC0/core/devices/local/object.py b/BAC0/core/devices/local/object.py
index 643be15a..5f0d6955 100644
--- a/BAC0/core/devices/local/object.py
+++ b/BAC0/core/devices/local/object.py
@@ -2,7 +2,7 @@
 
 from ...utils.notes import note_and_log
 from ....scripts.Base import Base
-from ...app.ScriptApplication import BAC0Application
+from ...app.ScriptApplication import BAC0Application, BAC0BBMDDeviceApplication, BAC0ForeignDeviceApplication
 
 from bacpypes.object import (
     AnalogInputObject,
@@ -34,18 +34,18 @@ class ObjectFactory(object):
     """
     This is an exploration of a method to create local objects in BAC0 instance.
 
-    First you need to know what bacpypes class of object you want to create ex. AnalogValueObject 
+    First you need to know what bacpypes class of object you want to create ex. AnalogValueObject
     This class must be imported from bacpypes ex. from bacpypes.object import AnalogValueObject
 
     You must also define supplemental properties for the object in a dict.
-    ex. : 
+    ex. :
         properties = {"outOfService" : False,
                 "relinquishDefault" : 0,
                 "units": "degreesCelsius",
                 "highLimit": 98}
 
     Then you can use the factory to create your object
-    ex. : 
+    ex. :
 
         av0 = ObjectFactory(AnalogValueObject, 1, 'av0', )
 
@@ -182,7 +182,7 @@ def clear_objects():
     def add_objects_to_application(self, app):
         if isinstance(app, Base):
             app = app.this_application
-        if not isinstance(app, BAC0Application):
+        if not (isinstance(app, BAC0Application) or isinstance(app, BAC0ForeignDeviceApplication) or isinstance(app, BAC0BBMDDeviceApplication)):
             raise TypeError("Provide BAC0Application object or BAC0 Base instance")
         for k, v in self.objects.items():
             try:
diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py
index 03e582a2..efbed224 100755
--- a/BAC0/core/devices/mixins/read_mixin.py
+++ b/BAC0/core/devices/mixins/read_mixin.py
@@ -19,9 +19,18 @@
     SegmentationNotSupported,
     BufferOverflow,
 )
-from ..Points import NumericPoint, BooleanPoint, EnumPoint, StringPoint, OfflinePoint
+from ..Points import (
+    NumericPoint,
+    BooleanPoint,
+    EnumPoint,
+    StringPoint,
+    OfflinePoint,
+    DateTimePoint,
+)
 from ..Trends import TrendLog
 
+# from ...functions.Schedule import Schedule
+
 # ------------------------------------------------------------------------------
 
 # Requests processing
@@ -40,7 +49,7 @@ def to_float_if_possible(val):
 
 def batch_requests(request, points_per_request):
     """
-    Generator for creating 'request batches'.  Each batch contains a maximum of "points_per_request" 
+    Generator for creating 'request batches'.  Each batch contains a maximum of "points_per_request"
     points to read.
     :params: request a list of point_name as a list
     :params: (int) points_per_request
@@ -62,9 +71,10 @@ def create_trendlogs(objList, device):
             tl = TrendLog(point_address, device, read_log_on_creation=False)
             if tl.properties.log_device_object_property is None:
                 raise TrendLogCreationException
-            ldop_type, ldop_addr = (
-                tl.properties.log_device_object_property.objectIdentifier
-            )
+            (
+                ldop_type,
+                ldop_addr,
+            ) = tl.properties.log_device_object_property.objectIdentifier
             ldop_prop = tl.properties.log_device_object_property.propertyIdentifier
             trendlogs["{}_{}_{}".format(ldop_type, ldop_addr, ldop_prop)] = (
                 tl.properties.object_name,
@@ -76,6 +86,29 @@ def create_trendlogs(objList, device):
     return trendlogs
 
 
+# def create_schedules(objList, device):
+#    schedules = {}
+#    for each in retrieve_type(objList, "schedule"):
+#        point_address = str(each[1])
+#        try:
+#            tl = Schedule(point_address, device, read_log_on_creation=False)
+#            if tl.properties.log_device_object_property is None:
+#                raise TrendLogCreationException
+#            (
+#                ldop_type,
+#                ldop_addr,
+#            ) = tl.properties.log_device_object_property.objectIdentifier
+#            ldop_prop = tl.properties.log_device_object_property.propertyIdentifier
+#            trendlogs["{}_{}_{}".format(ldop_type, ldop_addr, ldop_prop)] = (
+#                tl.properties.object_name,
+#                tl,
+#            )
+#        except TrendLogCreationException:
+#            device._log.error("Problem creating {}".format(each))
+#            continue
+#    return schedules
+
+
 class ReadUtilsMixin:
     """
     Handle ReadPropertyMultiple for a device
@@ -175,7 +208,11 @@ def _discoverPoints(self, custom_object_list=None):
                 obj_cls=StringPoint, obj_type="characterstringValue", objList=objList
             )
         )
-
+        points.extend(
+            self._process_new_objects(
+                obj_cls=DateTimePoint, obj_type="datetimeValue", objList=objList
+            )
+        )
         # TrendLogs
         trendlogs = create_trendlogs(objList, self)
 
@@ -224,6 +261,8 @@ def _process_new_objects(
             prop_list = "objectName presentValue stateText description"
         elif obj_type == "characterstringValue":
             prop_list = "objectName presentValue"
+        elif obj_type == "datetimeValue":
+            prop_list = "objectName presentValue"
         else:
             raise ValueError("Unsupported objectType")
 
@@ -256,10 +295,11 @@ def _find_propid_index(key):
 
             pointName = point_infos[_find_propid_index("objectName")]
             presentValue = point_infos[_find_propid_index("presentValue")]
-            if obj_type == "analog":
-                presentValue = float(presentValue)
-            elif obj_type == "multi":
-                presentValue = int(presentValue)
+            if presentValue != None:
+                if obj_type == "analog":
+                    presentValue = float(presentValue)
+                elif obj_type == "multi":
+                    presentValue = int(presentValue)
             try:
                 point_description = point_infos[_find_propid_index("description")]
             except KeyError:
diff --git a/BAC0/core/functions/Calendar.py b/BAC0/core/functions/Calendar.py
new file mode 100644
index 00000000..be124d34
--- /dev/null
+++ b/BAC0/core/functions/Calendar.py
@@ -0,0 +1,194 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
+# Licensed under LGPLv3, see file LICENSE in this source tree.
+#
+"""
+ScheduleWrite.py - creation of ReinitializeDeviceRequest
+
+"""
+from ..io.Read import find_reason
+from ..io.IOExceptions import NoResponseFromController
+from ...core.utils.notes import note_and_log
+
+# --- standard Python modules ---
+import datetime as dt
+
+# --- 3rd party modules ---
+from bacpypes.pdu import Address, GlobalBroadcast
+from bacpypes.primitivedata import Integer, Date, Time, CharacterString
+from bacpypes.basetypes import DateTime
+from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
+from bacpypes.iocb import IOCB
+from bacpypes.core import deferred
+from bacpypes.basetypes import (
+    DateTime,
+    DailySchedule,
+    TimeValue,
+    Time,
+    CalendarEntry,
+    DateRange,
+)
+from bacpypes.constructeddata import ArrayOf, Any
+from BAC0.core.io.IOExceptions import NoResponseFromController, WritePropertyException
+
+from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real, Enumerated
+
+from datetime import time as dt_time
+from datetime import datetime as dt
+
+
+@note_and_log
+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
diff --git a/BAC0/core/functions/Discover.py b/BAC0/core/functions/Discover.py
index e2053104..d1ef4c75 100644
--- a/BAC0/core/functions/Discover.py
+++ b/BAC0/core/functions/Discover.py
@@ -124,11 +124,11 @@ def request(self, arg):
 
     def indication(self, adapter, npdu):
         if isinstance(npdu, IAmRouterToNetwork):
-            if isinstance(self._request, WhoIsRouterToNetwork):
-                address, netlist = str(npdu.pduSource), npdu.iartnNetworkList
-                self._log.info("{} router to {}".format(address, netlist))
-                self._iartn.append(address)
-                self._routing_table[address] = netlist
+            # if isinstance(self._request, WhoIsRouterToNetwork):
+            address, netlist = str(npdu.pduSource), npdu.iartnNetworkList
+            self._log.info("{} router to {}".format(address, netlist))
+            self._iartn.append(address)
+            self._routing_table[address] = netlist
             for each in npdu.iartnNetworkList:
                 self._learnedNetworks.add(int(each))
 
diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py
index d6dc49c8..84a11819 100644
--- a/BAC0/core/functions/GetIPAddr.py
+++ b/BAC0/core/functions/GetIPAddr.py
@@ -105,7 +105,7 @@ def _findSubnetMask(self, ip):
         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:
diff --git a/BAC0/core/functions/Schedule.py b/BAC0/core/functions/Schedule.py
index ee017788..de04e5f9 100644
--- a/BAC0/core/functions/Schedule.py
+++ b/BAC0/core/functions/Schedule.py
@@ -97,7 +97,6 @@ def create_weeklySchedule(self, dict_schedule, object_reference=None):
         From a structured dict (see schedule_example), create a WeeklySchedule
         an ArrayOf(DailySchedule)
         """
-        object_reference = object_reference
         ds = dict_schedule
         if object_reference is not None:
             self.schedules[object_reference] = ds
@@ -135,9 +134,9 @@ def make_weeklySchedule_request(self, destination, object_instance, weeklySchedu
         request.priority = 15
         return request
 
-    def send_weeklyschedule_request(self, request):
+    def send_weeklyschedule_request(self, request, timeout=10):
         iocb = IOCB(request)
-        iocb.set_timeout(10)
+        iocb.set_timeout(timeout)
         deferred(self.this_application.request_io, iocb)
 
         iocb.wait()
@@ -171,20 +170,26 @@ def write_weeklySchedule(self, destination, schedule_instance, schedule):
 
     def read_weeklySchedule(self, address, schedule_instance):
         """
-        This function will turn the weeklySchedule received into a 
-        human readable dict. 
+        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,
+                object_references,
+                reliability,
+                priority,
+                presentValue,
+            ) = self.readMultiple(
                 "{} schedule {} weeklySchedule listOfObjectPropertyReferences reliability priorityForWriting presentValue".format(
                     address, schedule_instance
                 )
             )
 
         except Exception:
-            # Rwad Multiple not supported... try single prop read
+            # Read Multiple not supported... try single prop read
             def _read(prop):
                 return self.read(
                     "{} schedule {} {}".format(address, schedule_instance, prop)
diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py
index 9c5c68d2..743f4337 100644
--- a/BAC0/core/functions/TimeSync.py
+++ b/BAC0/core/functions/TimeSync.py
@@ -28,6 +28,7 @@
 from bacpypes.apdu import TimeSynchronizationRequest, UTCTimeSynchronizationRequest
 from bacpypes.iocb import IOCB
 from bacpypes.core import deferred
+import pytz
 
 
 def _build_datetime(UTC=False):
@@ -128,3 +129,37 @@ def time_sync(self, destination=None, datetime=None, UTC=False):
         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 TimeHandler(object):
+    """
+    This class will deal with Time / Timezone related features
+    To deal with DateTime Value correctly we need to be aware
+    of timezone.
+    """
+
+    def __init__(self, tz="America/Montreal"):
+        self.set_timezone(tz)
+
+    def set_timezone(self, tz):
+        self.timezone = pytz.timezone(tz)
+
+    @property
+    def now(self):
+        return dt.datetime.now()
+
+    def local_time(self):
+        return self.now.time()
+
+    def local_date(self):
+        return self.now.date()
+
+    def utcOffset(self):
+        "Returns UTC offset in minutes"
+        return self.now.astimezone().utcoffset().total_seconds() / 60
+
+    def is_dst(self):
+        return self.timezone.dst(self.now) != dt.timedelta(0)
+
+    def __repr__(self):
+        return "{}".format(self.__dict__)
diff --git a/BAC0/core/io/IOExceptions.py b/BAC0/core/io/IOExceptions.py
index be33773f..f78cb77a 100644
--- a/BAC0/core/io/IOExceptions.py
+++ b/BAC0/core/io/IOExceptions.py
@@ -104,7 +104,7 @@ class OutOfServiceSet(Exception):
 
 class NetworkInterfaceException(Exception):
     """
-    This exception covers different network related exc eption (like finding IP 
+    This exception covers different network related exc eption (like finding IP
     or subnet mask...)
     """
 
@@ -145,7 +145,7 @@ class Timeout(Exception):
 
 class RemovedPointException(Exception):
     """
-    When defining a device from DB it may not be identical to the 
+    When defining a device from DB it may not be identical to the
     actual device.
     """
 
@@ -167,3 +167,7 @@ class DeviceNotConnected(Exception):
 
 class WrongParameter(Exception):
     pass
+
+
+class NumerousPingFailures(Exception):
+    pass
diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py
index 2298fa0a..667a4eef 100644
--- a/BAC0/core/io/Read.py
+++ b/BAC0/core/io/Read.py
@@ -67,6 +67,7 @@ def readMultiple()
     UnknownObjectError,
     BufferOverflow,
 )
+from bacpypes.object import registered_object_types
 
 from ..utils.notes import note_and_log
 
@@ -88,7 +89,7 @@ def read(
         vendor_id=0,
         bacoid=None,
         timeout=10,
-        prop_id_required=False,
+        show_property_name=False,
     ):
         """
         Build a ReadProperty request, wait for the answer and return the value
@@ -167,11 +168,12 @@ def read(
 
                 self._log.debug("{:<20} {:<20}".format("value", "datatype"))
                 self._log.debug("{!r:<20} {!r:<20}".format(value, datatype))
-            if not prop_id_required:
+            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:
@@ -232,9 +234,9 @@ def _split_the_read_request(self, args, arr_index):
         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, prop_id_required=False
+        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
+        """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)
@@ -360,11 +362,26 @@ def readMultiple(
                                     datatype,
                                 )
                             )
-                        if prop_id_required:
+                        if show_property_name:
                             try:
-                                int(propertyIdentifier)
+                                int(
+                                    propertyIdentifier
+                                )  # else it will be a name like maxMaster
                                 prop_id = "@prop_{}".format(propertyIdentifier)
-                                value = list(value.items())[0][1]
+                                _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))
@@ -535,7 +552,7 @@ def build_rpm_request(self, args, vendor_id=0):
     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 
+        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::
diff --git a/BAC0/core/io/Simulate.py b/BAC0/core/io/Simulate.py
index 47f201bb..0f8039aa 100644
--- a/BAC0/core/io/Simulate.py
+++ b/BAC0/core/io/Simulate.py
@@ -29,7 +29,7 @@ class Simulation:
 
     def sim(self, args):
         """
-        Simulate I/O points by setting the Out_Of_Service property, then doing a 
+        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> ]
@@ -90,7 +90,7 @@ def out_of_service(self, args):
 
     def release(self, args):
         """
-        Set the Out_Of_Service property to False - to release the I/O point back to 
+        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>
diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py
index 89a72cfe..bea281db 100644
--- a/BAC0/core/io/Write.py
+++ b/BAC0/core/io/Write.py
@@ -64,7 +64,7 @@ class WriteProperty:
     """
 
     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].
+        """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]
@@ -237,7 +237,7 @@ def build_wp_request(self, args, vendor_id=0):
         return request
 
     def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10):
-        """ Build a WritePropertyMultiple request, wait for an answer
+        """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> ]
@@ -251,9 +251,9 @@ def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10):
             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 
+            # or
             # bacnet.writeMultiple('2:5',r)
-            
+
         """
         if not self._started:
             raise ApplicationNotStarted("BACnet stack not running - use startApp()")
@@ -304,9 +304,14 @@ def build_wpm_request(self, args, vendor_id=0, addr=None):
         for each in args:
             property_values = []
             if isinstance(each, str):
-                obj_type, obj_inst, prop_id, value, priority, indx = self._parse_wp_args(
-                    each
-                )
+                (
+                    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
diff --git a/BAC0/core/proprietary_objects/jci.py b/BAC0/core/proprietary_objects/jci.py
index 66e93780..71545a03 100644
--- a/BAC0/core/proprietary_objects/jci.py
+++ b/BAC0/core/proprietary_objects/jci.py
@@ -10,6 +10,9 @@
     Enumerated,
     Unsigned,
     Atomic,
+    Date,
+    Time,
+    #    Signed,
 )
 from bacpypes.object import (
     Object,
@@ -24,6 +27,7 @@
 
 #
 #   Proprietary Objects and their attributes
+#   https://cgproducts.johnsoncontrols.com/MET_PDF/12013102.pdf
 #
 
 JCIDeviceObject = {
@@ -32,15 +36,213 @@
     "objectType": "device",
     "bacpypes_type": DeviceObject,
     "properties": {
-        "SupervisorOnline": {"obj_id": 3653, "primitive": Boolean, "mutable": True},
-        "Model": {"obj_id": 1320, "primitive": CharacterString, "mutable": True},
-        "ModelType": {"obj_id": 32527, "primitive": CharacterString, "mutable": True},
-        "State": {"obj_id": 2390, "primitive": CharacterString, "mutable": False},
-        "MemoryUsage": {"obj_id": 2581, "primitive": Real, "mutable": False},
-        "ObjectMemoryUsage": {"obj_id": 2582, "primitive": Real, "mutable": False},
-        "CPU": {"obj_id": 2583, "primitive": Real, "mutable": False},
-        "FlashUsage": {"obj_id": 2584, "primitive": Real, "mutable": False},
-        "JCISystemStatus": {"obj_id": 847, "primitive": Enumerated, "mutable": False},
+        "ALARM_STATE": {"obj_id": 1006, "primitive": Enumerated, "mutable": False},
+        "ARCHIVE_DATE": {"obj_id": 849, "primitive": Date, "mutable": False},
+        "ARCHIVE_STATUS": {"obj_id": 1187, "primitive": Unsigned, "mutable": False},
+        "ARCHIVE_TIME": {"obj_id": 850, "primitive": Time, "mutable": False},
+        "CPU_USAGE": {"obj_id": 2583, "primitive": Real, "mutable": False},
+        "ENABLED": {"obj_id": 673, "primitive": Boolean, "mutable": True},
+        "ENABLED": {"obj_id": 673, "primitive": Boolean, "mutable": True},
+        "EXECUTION_PRIORITY": {
+            "obj_id": 2197,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # 0-3
+        "EXTENDED_PROTO_VERSION": {
+            "obj_id": 2291,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "FLASH_USAGE": {"obj_id": 2584, "primitive": Real, "mutable": False},
+        "ITEM_REFERENCE": {
+            "obj_id": 32527,
+            "primitive": CharacterString,
+            "mutable": False,
+        },
+        "JCI_STATUS": {"obj_id": 847, "primitive": Enumerated, "mutable": False},
+        "MEMORY_USAGE": {"obj_id": 2581, "primitive": Real, "mutable": False},
+        "OBJECT_CATEGORY": {
+            "obj_id": 908,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # 0-249
+        "OBJECT_MEMORY_USAGE": {"obj_id": 2582, "primitive": Real, "mutable": False},
+        "STATUS": {"obj_id": 512, "primitive": Enumerated, "mutable": False},
+        "BACNET_BROADCAST_RECEIVE_RATE": {
+            "obj_id": 745,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "DEFAULT_BASE_UNITS": {
+            "obj_id": 2206,
+            "primitive": Enumerated,
+            "mutable": False,
+        },
+        "ESTIMATED_FLASH_AVAILABLE": {
+            "obj_id": 2395,
+            "primitive": Real,
+            "mutable": False,
+        },
+        "LAST_IDLE_SAMPLE": {"obj_id": 30082, "primitive": Real, "mutable": False},
+        "MAX_MESSAGE_BUFFER": {
+            "obj_id": 848,
+            "primitive": Unsigned,
+            "mutable": True,
+        },  # 98-65535
+        "USER_NAME": {"obj_id": 2390, "primitive": CharacterString, "mutable": False},
+        "PCODE": {"obj_id": 1320, "primitive": CharacterString, "mutable": False},
+        # "SAB_DEVICE_STATUS_LIST": {"obj_id": 4513, "primitive": Complex, "mutable": False},
+        "SAB_DEVICE_STATUS_LIST_CHANGED ": {
+            "obj_id": 4514,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "EVENTS_LOST ": {"obj_id": 1479, "primitive": Unsigned, "mutable": False},
+        # "STANDARD_TIME_OFFSET ": {"obj_id": 1017, "primitive": Signed, "mutable": False}, # -900-900
+        # "DAYLIGHT_SAVING_TIME_OFFSET ": {"obj_id": 1093, "primitive": Signed, "mutable": False}, # -900-900
+        # "STANDARD_TIME_START ": {"obj_id": 988, "primitive": Complex, "mutable": True},
+        # "DAYLIGHT_SAVING_TIME_START ": {"obj_id": 1040, "primitive": Complex, "mutable": True},
+        "ACCEPT_BACNET_TIME_SYNC": {
+            "obj_id": 4970,
+            "primitive": Boolean,
+            "mutable": False,
+        },
+        # "LAST_BACNET_TIME_SYNC_RECEIVED ": {"obj_id": 5728, "primitive": Complex, "mutable": False},
+        "SUPERVISORY_DEVICE_ONLINE": {
+            "obj_id": 3653,
+            "primitive": Boolean,
+            "mutable": True,
+        },
+        "SUPERVISORY_OFFLINE_TIMEOUT": {
+            "obj_id": 6002,
+            "primitive": Unsigned,
+            "mutable": True,
+        },  # 1-255
+        "NEXT_AVAILABLE_OID": {
+            "obj_id": 787,
+            "primitive": Unsigned,
+            "mutable": True,
+        },  # 30,000,001-41,799,991
+        "HAS_UNBOUND_REFERENCES": {
+            "obj_id": 767,
+            "primitive": Boolean,
+            "mutable": False,
+        },
+        "SURROGATE_CACHE_CNT": {"obj_id": 571, "primitive": Unsigned, "mutable": False},
+        "SURROGATE_CACHE_MAX": {"obj_id": 639, "primitive": Unsigned, "mutable": False},
+        # "ASSET_VERSIONS": {"obj_id": 4960, "primitive": Complex, "mutable": False},
+        "LOAD_BALANCER_LEVEL": {
+            "obj_id": 4722,
+            "primitive": Enumerated,
+            "mutable": False,
+        },
+        "TIMER_DB_SIZE": {"obj_id": 733, "primitive": Unsigned, "mutable": False},
+        "TIMER_USED": {"obj_id": 734, "primitive": Unsigned, "mutable": False},
+        "TIMER_PEAK": {"obj_id": 735, "primitive": Unsigned, "mutable": False},
+        "TIMER_MESSAGES_ABORTED": {
+            "obj_id": 4959,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "BACNET_OID_ALLOCATED": {
+            "obj_id": 1291,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "BACNET_OID_USED": {"obj_id": 1292, "primitive": Unsigned, "mutable": False},
+        "SIGN_PRI_DB_SIZE": {"obj_id": 730, "primitive": Unsigned, "mutable": False},
+        "SIGN_PRI_USED": {"obj_id": 731, "primitive": Unsigned, "mutable": False},
+        "SIGN_PRI_PEAK": {"obj_id": 732, "primitive": Unsigned, "mutable": False},
+        "BACNET_ENCODE_TYPE": {
+            "obj_id": 32578,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # 0-3
+        "ROUTING_MODE": {
+            "obj_id": 4307,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # 0-2
+        "BACNET_INTEGRATED_OBJECTS": {
+            "obj_id": 4302,
+            "primitive": Enumerated,
+            "mutable": False,
+        },
+        "BACNET_COMPATIBLE": {"obj_id": 4581, "primitive": Boolean, "mutable": False},
+        "SEND_I_AM_RATE": {
+            "obj_id": 579,
+            "primitive": Unsigned,
+            "mutable": True,
+        },  # 60-86,400
+        "DEFAULT_TIME_ZONE": {
+            "obj_id": 32583,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # #-1,101
+        "TIME_ZONE": {
+            "obj_id": 1403,
+            "primitive": Enumerated,
+            "mutable": True,
+        },  # #-1,101
+        "LOCAL_TIME_ZONE": {"obj_id": 1404, "primitive": Enumerated, "mutable": False},
+        "FC_MULTICAST_RESPONDER": {
+            "obj_id": 3390,
+            "primitive": Unsigned,
+            "mutable": True,
+        },
+        "FC_WAIT_BEFORE_POLLING": {
+            "obj_id": 3391,
+            "primitive": Unsigned,
+            "mutable": True,
+        },
+        "SUPERVISOR_MAC_ADDRESS": {
+            "obj_id": 3652,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "LIBRARY_PART_ID": {
+            "obj_id": 3295,
+            "primitive": CharacterString,
+            "mutable": False,
+        },
+        "APPLICATION_CLASS_SET_VERSION": {
+            "obj_id": 4128,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "DEVICE_MODEL_CLASS_SET_VERSION": {
+            "obj_id": 4129,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "COV_MIN_SEND_TIME": {
+            "obj_id": 3929,
+            "primitive": Unsigned,
+            "mutable": True,
+        },  # 10-255
+        "COV_TRANSMITS_PER_MINUTE": {
+            "obj_id": 651,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
+        "CONTROL_SEQUENCE_IN_TEST": {
+            "obj_id": 3651,
+            "primitive": Unsigned,
+            "mutable": True,
+        },
+        "END_OF_LINE": {"obj_id": 603, "primitive": Boolean, "mutable": False},
+        "DEVICE_ADDRESS": {"obj_id": 876, "primitive": Unsigned, "mutable": False},
+        "FC_BUS_COMMUNICATION_MODE": {
+            "obj_id": 4400,
+            "primitive": Enumerated,
+            "mutable": False,
+        },
+        "SYSTEM_TYPE": {"obj_id": 3900, "primitive": Unsigned, "mutable": False},
+        "SYSTEM_CONFIGURATION": {
+            "obj_id": 3899,
+            "primitive": Unsigned,
+            "mutable": False,
+        },
         "SABusPerformance": {
             "obj_id": 12157,
             "primitive": Enumerated,
@@ -61,10 +263,6 @@
             "primitive": Unsigned,
             "mutable": False,
         },
-        "CPU Idle": {"obj_id": 30082, "primitive": Real, "mutable": False},
-        "alarm": {"obj_id": 673, "primitive": Boolean, "mutable": False},
-        "end_of_line": {"obj_id": 603, "primitive": Boolean, "mutable": False},
-        "objectStatus": {"obj_id": 512, "primitive": Enumerated, "mutable": False},
     },
 }
 # EOL ?
@@ -119,8 +317,14 @@
 }
 
 
-def tec_short_point_list():
-    return [
+def tec_short_point_list(unit_type="2-pipe"):
+    """
+    unit_type can be :
+        - 4-pipe
+        - 2-pipe
+        - VAV
+    """
+    _lst = [
         ("binaryInput", 30827),
         ("binaryInput", 30828),
         ("binaryOutput", 86908),
@@ -167,8 +371,11 @@ def tec_short_point_list():
         ("analogValue", 29726),
         ("analogValue", 29727),
         ("analogOutput", 86905),
-        ("analogOutput", 86914),
-        ("analogOutput", 86915),
         ("multiStateValue", 6),
         ("trendLog", 101010),
     ]
+    if unit_type == "4-pipe":
+        _lst.append(("analogOutput", 86914))
+        _lst.append(("analogOutput", 86915))
+
+    return _lst
diff --git a/BAC0/core/proprietary_objects/object.py b/BAC0/core/proprietary_objects/object.py
index 1fb9bb80..833cee40 100644
--- a/BAC0/core/proprietary_objects/object.py
+++ b/BAC0/core/proprietary_objects/object.py
@@ -1,4 +1,9 @@
-from bacpypes.object import Object, Property, register_object_type
+from bacpypes.object import (
+    Object,
+    Property,
+    register_object_type,
+    registered_object_types,
+)
 
 # Prochaine étape : créer une focntion qui va lire "all" et se redéfinir dynamiquement
 def create_proprietary_object(params):
@@ -17,6 +22,10 @@ def create_proprietary_object(params):
         {"objectType": params["objectType"], "properties": props},
     )
     register_object_type(new_class, vendor_id=params["vendor_id"])
+    if "BAC0" not in registered_object_types.keys():
+        registered_object_types["BAC0"] = {}
+
+    registered_object_types["BAC0"][params["name"]] = params["properties"]
 
 
 def _validate_params(params):
diff --git a/BAC0/core/utils/notes.py b/BAC0/core/utils/notes.py
index f5818bc7..6f53ae9b 100644
--- a/BAC0/core/utils/notes.py
+++ b/BAC0/core/utils/notes.py
@@ -69,16 +69,16 @@ def update_log_level(
         BAC0.log_level(file='warning', stdout='info', stderr='critical')
         # Debug in file and console... this is a bad idea as the console will be filled
         BAC0.log_level(file='debug', stdout='debug', stderr='critical')
-           
+
         # Preferably, debug in the file, keep console limited to info
         BAC0.log_level('debug')
         # OR
-        BAC0.log_level(file='debug', stdout='info', stderr='critical')   
-        
+        BAC0.log_level(file='debug', stdout='info', stderr='critical')
+
 
     Giving only one parameter will set file and console to the same level.
     I tend to keep stderr CRITICAL
-     
+
     """
     update_log_file_lvl = False
     update_stderr_lvl = False
@@ -204,17 +204,17 @@ def note_and_log(cls):
     logSaveFilePath = join(logUserPath, ".BAC0")
 
     logFile = join(logSaveFilePath, "BAC0.log")
-    if not os.path.exists(logSaveFilePath):
-        try:
+    try:
+        if not os.path.exists(logSaveFilePath):
             os.makedirs(logSaveFilePath)
-        except:
-            _PERMISSION_TO_WRITE = False
-    if _PERMISSION_TO_WRITE:
         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...
@@ -264,7 +264,7 @@ def note(self, note, *, level=logging.INFO, log=True):
         if not note:
             raise ValueError("Provide something to log")
         note = "{} | {}".format(cls.logname, note)
-        cls._notes.timestamp.append(datetime.now())
+        cls._notes.timestamp.append(datetime.now().astimezone())
         cls._notes.notes.append(note)
         if log:
             cls.log(level, note)
diff --git a/BAC0/sql/__init__.py b/BAC0/db/__init__.py
similarity index 100%
rename from BAC0/sql/__init__.py
rename to BAC0/db/__init__.py
diff --git a/BAC0/db/influxdb.py b/BAC0/db/influxdb.py
new file mode 100644
index 00000000..88b16be9
--- /dev/null
+++ b/BAC0/db/influxdb.py
@@ -0,0 +1,225 @@
+try:
+    from influxdb_client import InfluxDBClient, Point, WriteOptions
+
+except ImportError:
+    raise ImportError("Install influxdb to use this feature")
+
+import pytz
+from datetime import datetime
+
+
+class InfluxDB:
+    """
+    Connection to InfluxDB to write to DB or to read from it
+    """
+
+    url = None
+    port = 8086
+    token = None
+    org = None
+    bucket = None
+    tags_file = None
+    username = None
+    password = None
+    client = None
+
+    def __init__(self, params):
+        # params should be a dict with name=InfluxDB and bucket=valid_bucket_to_use.
+        # optional params : url, port, token and org may be provided
+        # if they are not provided, BAC0 will try to get them from environment
+        # variables that shoudl have been provided in a .env file, which is loaded
+        # when BAC0 is imported.
+        for k, v in params.items():
+            setattr(self, k, v)
+        if self.bucket is None:
+            raise ValueError("Missing bucket name, please provide one in db_params")
+        self.connect_to_db()
+
+        self.write_api = self.client.write_api(
+            write_options=WriteOptions(
+                batch_size=getattr(self, "batch_size", 25),
+                flush_interval=getattr(self, "flush_interval", 10_000),
+                jitter_interval=getattr(self, "jitter_interval", 2_000),
+                retry_interval=getattr(self, "retry_interval", 5_000),
+                max_retries=getattr(self, "max_retries", 5),
+                max_retry_delay=getattr(self, "max_retry_delay", 30_000),
+                exponential_base=getattr(self, "exponential_base", 2),
+            )
+        )
+        self.query_api = self.client.query_api()
+
+    def connect_to_db(self):
+        if self.url is None:
+            # Will try environment variables
+            self.client = InfluxDBClient.from_env_properties()
+        else:
+            _url = "{}:{}".format(self.url, self.port)
+            if self.token:
+                self.client = InfluxDBClient(url=_url, token=self.token)
+            else:
+                self.client = InfluxDBClient(
+                    url=_url,
+                    token="{}:{}".format(self.username, self.password),
+                    bucket=self.bucket,
+                    org="-",
+                )
+        try:
+            self.health
+        except:
+            raise ConnectionError("Error connecting to InfluxDB")
+
+    @property
+    def health(self):
+        return self.client.health()
+
+    def clean_value(self, object_type, val, units_state):
+        if "analog" in object_type:
+            _string_value = "{:.3f} {}".format(val, units_state)
+            _value = val
+        elif "multi" in object_type:
+            _string_value = "{}".format(val.split(":")[1])
+            _value = "{}".format(val.split(":")[0])
+        elif "binary" in object_type:
+            try:
+                _string_value = "{}".format(units_state[int(val.split(":"[0]))])
+            except:
+                _string_value = "{}".format(val.split(":")[1])
+            _value = "{}".format(val.split(":")[0])
+        else:
+            _string_value = "{}".format(val)
+            _value = val
+        return (_value, _string_value)
+
+    def write_points_lastvalue_to_db(self, list_of_points):
+        _points = []
+
+        for point in list_of_points:
+            _object_name = point.properties.name
+            _devicename = point.properties.device.properties.name
+            _device_id = point.properties.device.properties.device_id
+            _units_state = "{}".format(point.properties.units_state)
+            _description = point.properties.description
+            _object = "{}:{}".format(point.properties.type, point.properties.address)
+            _value, _string_value = self.clean_value(
+                point.properties.type, point.lastValue, point.properties.units_state
+            )
+            _name = "{}/{}".format(_devicename, _object_name)
+            _id = "Device_{}/{}".format(_device_id, _object)
+            _point = (
+                Point(_id)
+                .tag("object_name", _object_name)
+                .tag("name", _name)
+                .tag("description", _description)
+                .tag("units_state", _units_state)
+                .tag("object", _object)
+                .tag("device", _devicename)
+                .tag("device_id", _device_id)
+                .field("value", _value)
+                .field("string_value", _string_value)
+                .time(point.lastTimestamp.astimezone(pytz.UTC))
+            )
+            for each in point.tags:
+                _tag_id, _tag_value = each
+                _point.tag(_tag_id, _tag_value)
+            _points.append(_point)
+        self.write_api.write(self.bucket, self.org, _points)
+
+    def write_all_to_db(self, device):
+        # This is probably not useful... keeping that here in case
+        try:
+            import pandas as pd
+        except ImportError:
+            raise ImportError("You need pandas and numpy to write all histories to db")
+        _value = None
+
+        for each in device:
+            _object = "{}:{}".format(each.properties.type, each.properties.address)
+            _object_name = each.properties.name
+            _description = each.properties.description
+            _units_state = each.properties.units_state
+            _devicename = each.properties.device.properties.name
+            _device_id = each.properties.device.properties.device_id
+            _name = "{}/{}".format(_devicename, _object_name)
+            _data = {"value": each.history}
+            _df = pd.DataFrame(_data)
+            _df.index = _df.index.tz_convert(pytz.utc)
+            _df["value"] = _df.apply(
+                lambda x: self.clean_value(
+                    each.properties.type, x["value"], each.properties.units_state
+                )[0],
+                axis=1,
+            )
+            _df["string_value"] = _df.apply(
+                lambda x: self.clean_value(
+                    each.properties.type, x["value"], each.properties.units_state
+                )[1],
+                axis=1,
+            )
+            _df["object_name"] = _object_name
+            _df["description"] = _description
+            _df["units_state"] = "{}".format(_units_state)
+            _df["object"] = _object
+            _df["device"] = _devicename
+            _df["device_id"] = _device_id
+            try:
+                # print(_name)
+                self.write_api.write(
+                    self.bucket,
+                    org=self.org,
+                    record=_df,
+                    data_frame_measurement_name=_name,
+                    data_frame_tag_columns=[
+                        "object_name",
+                        "description",
+                        "units_state",
+                        "object",
+                        "device",
+                        "device_id",
+                    ],
+                )
+            except Exception as error:
+                # print('Oups', _name, error)
+                continue
+
+    def read_last_value_from_db(self, id=None):
+        # example id : Device_5004/analogInput:1
+        # maybe use device name and object name ?
+        # This must be easy
+
+        """
+        from(bucket: {}")
+        |> range(start: -100y)
+        |> filter(fn: (r) => r["description"] == "DA-T")
+        |> filter(fn: (r) => r["_field"] == "value")
+        |> last()
+        |> yield(name: "last")
+        """.format(
+            self.bucket
+        )
+        pass
+
+    #    def example(self, device_name, object_name):
+    #        p = {"_bucket": self.bucket,
+    #             "_start": datetime.timedelta(hours=-1),
+    #             "_location": "Prague",
+    #             "_desc": True,
+    #             "_floatParam": 25.1,
+    #             "_every": datetime.timedelta(minutes=5)
+    #            }
+    #
+    #        tables = self.query_api.query('''
+    #            from(bucket:_bucket) |> range(start: _start)
+    #                |> filter(fn: (r) => r["_measurement"] == "my_measurement")
+    #                |> filter(fn: (r) => r["_field"] == "temperature")
+    #                |> filter(fn: (r) => r["location"] == _location and r["_value"] > _floatParam)
+    #                |> aggregateWindow(every: _every, fn: mean, createEmpty: true)
+    #                |> sort(columns: ["_time"], desc: _desc)
+    #        ''', params=p)
+    #        return tables
+
+    def read_flux(self, request, params):
+        pass
+
+
+class ConnectionError(Exception):
+    pass
diff --git a/BAC0/sql/sql.py b/BAC0/db/sql.py
similarity index 71%
rename from BAC0/sql/sql.py
rename to BAC0/db/sql.py
index 92954ea4..816c18de 100644
--- a/BAC0/sql/sql.py
+++ b/BAC0/db/sql.py
@@ -19,6 +19,7 @@
 try:
     import pandas as pd
     from pandas.io import sql
+    from pandas.core.base import DataError
 
     try:
         from pandas import Timestamp
@@ -28,7 +29,7 @@
 except ImportError:
     _PANDAS = False
 
-from ..core.io.IOExceptions import RemovedPointException
+from ..core.io.IOExceptions import RemovedPointException, NoResponseFromController
 
 # --- this application's modules ---
 
@@ -37,9 +38,9 @@
 
 class SQLMixin(object):
     """
-    Use SQL to persist a device's contents.  By saving the device contents to an SQL 
-    database, you can work with the device's data while offline, or while the device 
-    is not available. 
+    Use SQL to persist a device's contents.  By saving the device contents to an SQL
+    database, you can work with the device's data while offline, or while the device
+    is not available.
     """
 
     def _read_from_sql(self, request, db_name):
@@ -81,15 +82,18 @@ def backup_histories_df(self, resampling="1s"):
         Please note that this can be disabled using resampling=False
 
         In the process of building the dataframe, analog values are
-        resampled using the mean() function. So we have intermediate 
+        resampled using the mean() function. So we have intermediate
         results between to records.
 
         For binary values, we'll use .last() so we won't get a 0.5 value
-        which means nothing in this context. 
+        which means nothing in this context.
 
         If saving a DB that already exists, previous resampling will survive
         the merge of old data and new data.
         """
+        if not _PANDAS:
+            self._log.error("Pandas is required to create dataframe.")
+            return
         backup = {}
         if isinstance(resampling, str):
             resampling_needed = True
@@ -97,12 +101,32 @@ def backup_histories_df(self, resampling="1s"):
         elif resampling in [0, False]:
             resampling_needed = False
 
+        def extract_value_and_string(val):
+            if isinstance(val, str):
+                if ":" in val:
+                    _v, _s = val.split(":")
+                    return (int(_v), _s)
+                elif val == "active":
+                    val = 1
+                elif val == "inactive":
+                    val = 0
+            return (int(val), "unknown")
+
         # print(resampling, resampling_freq, resampling_needed)
         for point in self.points:
+            _name = point.properties.name
             try:
-                if resampling_needed and "binary" in point.properties.type:
-                    backup[point.properties.name] = (
-                        point.history.replace(["inactive", "active"], [0, 1])
+                if (
+                    "binary" in point.properties.type
+                    or "multi" in point.properties.type
+                ):
+                    backup["{}_str".format(_name)] = (
+                        point.history.apply(lambda x: extract_value_and_string(x)[1])
+                        .resample(resampling_freq)
+                        .last()
+                    )
+                    backup[_name] = (
+                        point.history.apply(lambda x: extract_value_and_string(x)[0])
                         .resample(resampling_freq)
                         .last()
                     )
@@ -111,26 +135,39 @@ def backup_histories_df(self, resampling="1s"):
                         resampling_freq
                     ).mean()
                 else:
-                    backup[point.properties.name] = point.history.resample(
-                        resampling_freq
-                    ).last()
+                    #backup[point.properties.name] = point.history.resample(
+                    #    resampling_freq
+                    #).last()
+                    continue
 
             except Exception as error:
                 self._log.error(
                     "Error in resampling {} | {} (probably not enough points)".format(
-                        point, error
+                        point.properties.name, error
                     )
                 )
-                if "binary" in point.properties.type:
-                    backup[point.properties.name] = point.history.replace(
-                        ["inactive", "active"], [0, 1]
+                if (
+                    "binary" in point.properties.type
+                    or "multi" in point.properties.type
+                ):
+                    backup["{}.str".format(_name)] = (
+                        point.history.apply(lambda x: extract_value_and_string(x)[1])
+                        .resample(resampling_freq)
+                        .last()
                     )
+                    backup["{}.val".format(_name)] = (
+                        point.history.apply(lambda x: extract_value_and_string(x)[0])
+                        .resample(resampling_freq)
+                        .last()
+                    )
+                    backup[_name] = point.history.resample(resampling_freq).last()
                 elif "analog" in point.properties.type:
                     backup[point.properties.name] = point.history.resample(
                         resampling_freq
                     ).mean()
                 else:
-                    backup[point.properties.name] = point.history
+                    #backup[point.properties.name] = point.history
+                    continue
 
         df = pd.DataFrame(dict([(k, pd.Series(v)) for k, v in backup.items()]))
         if resampling_needed:
@@ -145,11 +182,15 @@ def backup_histories_df(self, resampling="1s"):
 
     def save(self, filename=None, resampling=None):
         """
-        Save the point histories to sqlite3 database.  
+        Save the point histories to sqlite3 database.
         Save the device object properties to a pickle file so the device can be reloaded.
 
         Resampling : valid Pandas resampling frequency. If 0 or False, dataframe will not be resampled on save.
         """
+        if not _PANDAS:
+            self._log.error("Pandas is required to save to SQLite.")
+            return
+
         if filename:
             if ".db" in filename:
                 filename = filename.split(".")[0]
@@ -161,6 +202,14 @@ def save(self, filename=None, resampling=None):
             resampling = self.properties.save_resampling
 
         # Does file exist? If so, append data
+
+        def _df_to_backup():
+            try:
+                return self.backup_histories_df(resampling=resampling)
+            except (DataError, NoResponseFromController):
+                self._log.error("Impossible to save right now, error in data")
+                return None
+
         if os.path.isfile("{}.db".format(self.properties.db_name)):
             his = self._read_from_sql(
                 'select * from "{}"'.format("history"), self.properties.db_name
@@ -168,14 +217,16 @@ def save(self, filename=None, resampling=None):
             his.index = his["index"].apply(Timestamp)
             try:
                 last = his.index[-1]
-                df_to_backup = self.backup_histories_df(resampling=resampling)[last:]
-            except IndexError:
-                df_to_backup = self.backup_histories_df(resampling=resampling)
+                df_to_backup = _df_to_backup()[last:]
+            except (IndexError, TypeError):
+                df_to_backup = _df_to_backup()
 
         else:
             self._log.debug("Creating a new backup database")
-            df_to_backup = self.backup_histories_df(resampling=resampling)
+            df_to_backup = _df_to_backup()
 
+        if df_to_backup is None:
+            return
         # DataFrames that will be saved to SQL
         with contextlib.closing(
             sqlite3.connect("{}.db".format(self.properties.db_name))
diff --git a/BAC0/infos.py b/BAC0/infos.py
index a343715b..3e0bbe62 100644
--- a/BAC0/infos.py
+++ b/BAC0/infos.py
@@ -12,5 +12,5 @@
 __email__ = "christian.tremblay@servisys.com"
 __url__ = "https://github.com/ChristianTremblay/BAC0"
 __download_url__ = "https://github.com/ChristianTremblay/BAC0/archive/master.zip"
-__version__ = "21.02.25"
+__version__ = "21.12.03"
 __license__ = "LGPLv3"
diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py
index 857cbbdc..bfa12f60 100644
--- a/BAC0/scripts/Base.py
+++ b/BAC0/scripts/Base.py
@@ -42,6 +42,7 @@ def stopApp()
 from .. import infos
 from ..core.io.IOExceptions import InitializationError, UnknownObjectError
 from ..core.functions.GetIPAddr import validate_ip_address
+from ..core.functions.TimeSync import TimeHandler
 from ..tasks.TaskManager import stopAllTasks
 
 from ..core.utils.notes import note_and_log
@@ -114,6 +115,8 @@ def __init__(
 
         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"
@@ -168,6 +171,7 @@ def __init__(
         self.firmwareRevision = firmwareRevision
         self._ric = {}
         self.subscription_contexts = {}
+        self.database = None
 
         try:
             self.startApp()
@@ -198,6 +202,8 @@ def startApp(self):
                 applicationSoftwareVersion=infos.__version__,
                 protocolVersion=1,
                 protocolRevision=0,
+                utcOffset=self.timehandler.utcOffset(),
+                daylightSavingsStatus=self.timehandler.is_dst(),
             )
 
             # make an application
@@ -268,7 +274,7 @@ def disconnect(self):
         self._stopped = True  # Stop stack thread
         self.t.join()
         self._started = False
-        Base._used_ips.remove(self.localIPAddr)
+        Base._used_ips.discard(self.localIPAddr)
         self._log.info("BACnet stopped")
 
     def _startAppThread(self):
diff --git a/BAC0/scripts/Complete.py b/BAC0/scripts/Complete.py
index 95569872..bf41e5e2 100644
--- a/BAC0/scripts/Complete.py
+++ b/BAC0/scripts/Complete.py
@@ -132,7 +132,7 @@ def network_stats(self):
         statistics["ip_devices"] = sorted(ip_devices)
         statistics["bacoids"] = sorted(bacoids)
         statistics["mstp_map"] = mstp_map
-        statistics["timestamp"] = str(datetime.now())
+        statistics["timestamp"] = str(datetime.now().astimezone())
         statistics["number_of_devices"] = self.number_of_devices
         statistics["number_of_registered_devices"] = len(self.registered_devices)
         statistics["print_mstpnetworks"] = self.print_list(mstpnetworks)
diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py
index 6c3b7516..26594757 100755
--- a/BAC0/scripts/Lite.py
+++ b/BAC0/scripts/Lite.py
@@ -40,6 +40,7 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty)
 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
@@ -51,12 +52,21 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty)
     NoResponseFromController,
     UnrecognizedService,
     Timeout,
+    NumerousPingFailures,
 )
+
 from ..tasks.RecurringTask import RecurringTask
 from ..tasks.UpdateCOV import Update_local_COV
 
 from ..infos import __version__ as version
 
+try:
+    from ..db.influxdb import InfluxDB, ConnectionError
+
+    INFLUXDB = True
+except ImportError:
+    INFLUXDB = False
+
 from bacpypes.pdu import Address
 
 # ------------------------------------------------------------------------------
@@ -74,6 +84,7 @@ class Lite(
     DeviceCommunicationControl,
     CoV,
     Schedule,
+    Calendar,
     TextMixin,
 ):
     """
@@ -96,6 +107,8 @@ def __init__(
         bbmdTTL=0,
         bdtable=None,
         ping=True,
+        ping_delay=300,
+        db_params=None,
         **params
     ):
         self._log.info(
@@ -112,7 +125,7 @@ def __init__(
         # Ping task will deal with all registered device and disconnect them if they do not respond.
 
         self._ping_task = RecurringTask(
-            self.ping_registered_devices, delay=10, name="Ping Task"
+            self.ping_registered_devices, delay=ping_delay, name="Ping Task"
         )
         if ping:
             self._ping_task.start()
@@ -162,10 +175,28 @@ def __init__(
         self._update_local_cov_task.running = True
         self._log.info("Update Local COV Task started")
 
+        # Activate InfluxDB if params are available
+        if db_params and INFLUXDB:
+            try:
+                self.database = (
+                    InfluxDB(db_params)
+                    if db_params["name"].lower() == "influxdb"
+                    else None
+                )
+                self._log.info(
+                    "Connection made to InfluxDB bucket : {}".format(
+                        self.database.bucket
+                    )
+                )
+            except ConnectionError:
+                self._log.error(
+                    "Unable to connect to InfluxDB. Please validate parameters"
+                )
+
     @property
     def known_network_numbers(self):
         """
-        This function will read the table of known network numbers gathered by the 
+        This function will read the table of known network numbers gathered by the
         NetworkServiceElement. It will also look into the discoveredDevices property
         and add any network number that would not be in the NSE table.
         """
@@ -184,10 +215,10 @@ def discover(
         """
         Discover is meant to be the function used to explore the network when we
         connect.
-        It will trigger whois request using the different options provided with 
+        It will trigger whois request using the different options provided with
         parameters.
 
-        By default, a local broadcast will be used. This is required as in big 
+        By default, a local broadcast will be used. This is required as in big
         BACnet network, global broadcast can lead to network flood and loss of data.
 
         If not parameters are given, BAC0 will try to :
@@ -231,7 +262,7 @@ def discover(
                     if network < 65535:
                         _networks.append(network)
             elif networks == "known":
-                _networks = self.known_network_numbers
+                _networks = self.known_network_numbers.copy()
             else:
                 if networks < 65535:
                     _networks.append(networks)
@@ -272,48 +303,50 @@ def register_device(self, device):
 
     def ping_registered_devices(self):
         """
-        Registered device on a network (self) are kept in a list (registered_devices). 
+        Registered device on a network (self) are kept in a list (registered_devices).
         This function will allow pinging thoses device regularly to monitor them. In case
-        of disconnected devices, we will disconnect the device (which will save it). Then 
+        of disconnected devices, we will disconnect the device (which will save it). Then
         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 
+        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
             ):
-                device_connected = True
-            else:
-                device_connected = False
+                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)
 
-            respond_to_ping = True
-            try:
-                self._log.debug(
-                    "Ping {}|{}".format(each.properties.name, each.properties.address)
-                )
+            else:
                 device_id = each.properties.device_id
                 addr = each.properties.address
-                self.read("{} device {} objectName".format(addr, device_id))
-            except NoResponseFromController:
-                respond_to_ping = False
-
-            if not respond_to_ping and device_connected:
-                self._log.warning(
-                    "{}|{} is offline, disconnecting it.".format(
-                        each.properties.name, 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.disconnect(unregister=False)
-            elif not device_connected and respond_to_ping:
-                self._log.info(
-                    "{}|{} is back online, reconnecting.".format(
-                        each.properties.name, each.properties.address
-                    )
-                )
-                each.connect(network=self)
+                    each.connect(network=self)
+                    each.poll(delay=each.properties.pollDelay)
 
     @property
     def registered_devices(self):
@@ -371,9 +404,9 @@ def remove_trend(self, point_to_remove):
     def devices(self):
         """
         This property will create a good looking table of all the discovered devices
-        seen on the network. 
+        seen on the network.
 
-        For that, some requests will be sent over the network to look for name, 
+        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 = []
diff --git a/BAC0/tasks/DoOnce.py b/BAC0/tasks/DoOnce.py
index 1b747ec5..0eafef8d 100644
--- a/BAC0/tasks/DoOnce.py
+++ b/BAC0/tasks/DoOnce.py
@@ -24,7 +24,7 @@ def __init__(self, fnc):
         """
         :param point: (BAC0.core.device.Points.Point) name of the point to read
         :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
+
         A delay cannot be < 5sec (there are risks of overloading the device)
 
         :returns: Nothing
diff --git a/BAC0/tasks/Match.py b/BAC0/tasks/Match.py
index fd56a106..83cf2a08 100644
--- a/BAC0/tasks/Match.py
+++ b/BAC0/tasks/Match.py
@@ -66,10 +66,10 @@ class Match_Value(Task):
     Verify a point's Present_Value equals the given value after a delay of X seconds.
     Thus giving the BACnet controller (and connected equipment) time to respond to the
     command.
-    
+
         Match_Value(On, <AI:1>, 5)
-        
-    i.e. Does Fan value = On after 5 seconds. 
+
+    i.e. Does Fan value = On after 5 seconds.
     """
 
     def __init__(
diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py
index 4ae30a47..67fc33ed 100644
--- a/BAC0/tasks/Poll.py
+++ b/BAC0/tasks/Poll.py
@@ -19,6 +19,8 @@
 from ..core.utils.notes import note_and_log
 
 # ------------------------------------------------------------------------------
+class MultiplePollingFailures(Exception):
+    pass
 
 
 @note_and_log
@@ -33,10 +35,10 @@ def __init__(self, point, *, delay=10):
         """
         :param point: (BAC0.core.device.Points.Point) name of the point to read
         :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
+
         A delay cannot be < 1sec
         This task is meant for single points, so BAC0 will allow short delays.
-        This way, a fast polling is available for some points in a device that 
+        This way, a fast polling is available for some points in a device that
         would not support segmentation.
 
         :returns: Nothing
@@ -56,7 +58,7 @@ def task(self):
 @note_and_log
 class DevicePoll(Task):
     """
-    Start a polling task to repeatedly read a list of points from a device using 
+    Start a polling task to repeatedly read a list of points from a device using
     ReadPropertyMultiple requests.
     """
 
@@ -64,12 +66,14 @@ def __init__(self, device, delay=10, name="", prefix="basic_poll"):
         """
         :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 
+
+        A delay cannot be < 10sec
         For delays under 10s, use DeviceFastPoll class.
 
         :returns: Nothing
         """
+        self.failures = 0
+        self.MAX_FAILURES = 3
         self._device = weakref.ref(device)
         Task.__init__(self, name="{}_{}".format(prefix, name), delay=delay)
         self._counter = 0
@@ -79,7 +83,17 @@ def device(self):
         return self._device()
 
     def task(self):
+        if self.device.properties.ping_failures > 0:
+            self.device._log.warning(
+                "Ping failed, skipping polling for now. Resending a ping to speed up things"
+            )
+            self.device.ping()
+            return
         try:
+            if self.failures >= self.MAX_FAILURES:
+                raise MultiplePollingFailures(
+                    "Polling failed numerous times in a row... let see what we can do"
+                )
             self.device.read_multiple(
                 list(self.device.pollable_points_name), points_per_request=25
             )
@@ -89,25 +103,40 @@ def task(self):
                 if self.device.properties.clear_history_on_save:
                     self.device.clear_histories()
                 self._counter = 0
-        except AttributeError:
+            self.failures = 0
+        except AttributeError as e:
             # This error can be seen when defining a controller on a busy network...
             # When creation fail, polling is created and fail the first time...
             # So kill the task
-            self.stop()
+            self.device._log.error(
+                "Something is wrong while creating the polling task."
+                "Error: {} | Type : {}".format(e, type(e))
+            )
+            # self.stop()
+            self.failures += 1
         except ValueError as e:
+            self.failures += 1
             self.device._log.error(
-                "Something is wrong with polling...stopping. Try setting off "
-                "segmentation. Error: {}".format(e)
+                "Polling results contains a wrong value. Probably a communication error. Will skip this result and wait for the next cycle.\n"
+                "Error: {} | Type : {}".format(e, type(e))
             )
-            self.stop()
+            pass
+
+        except MultiplePollingFailures as e:
+            self.device._log.warning(
+                "Trying to ping device then we'll reset the number of failures and get back with polling"
+                "Error: {}| Type : {}".format(e, type(e))
+            )
+            if self.device.ping():
+                self.failures = 0
 
 
 @note_and_log
 class DeviceNormalPoll(DevicePoll):
     """
-    Start a normal polling task to repeatedly read a list of points from a device using 
+    Start a normal polling task to repeatedly read a list of points from a device using
     ReadPropertyMultiple requests.
-    
+
     Normal polling will limit the polling speed to 10 second minimum
 
     """
@@ -116,7 +145,7 @@ def __init__(self, device, delay=10, name=""):
         """
         :param device: (BAC0.core.devices.Device.Device) device to poll
         :param delay: (int) Delay between polls in seconds, defaults = 10sec
-        
+
         :returns: Nothing
         """
         if delay < 10:
@@ -132,7 +161,7 @@ def __init__(self, device, delay=10, name=""):
 @note_and_log
 class DeviceFastPoll(DevicePoll):
     """
-    Start a fast polling task to repeatedly read a list of points from a device using 
+    Start a fast polling task to repeatedly read a list of points from a device using
     ReadPropertyMultiple requests.
     Delay allowed will be 0 to 10 seconds
     Normal polling will limit the polling speed to 10 second minimum
@@ -145,7 +174,7 @@ def __init__(self, device, delay=1, name=""):
         """
         :param device: (BAC0.core.devices.Device.Device) device to poll
         :param delay: (int) Delay between polls in seconds, defaults = 1sec
-        
+
         :returns: Nothing
         """
         if delay < 0:
diff --git a/BAC0/tasks/RecurringTask.py b/BAC0/tasks/RecurringTask.py
index 6861ed1e..23d494f6 100644
--- a/BAC0/tasks/RecurringTask.py
+++ b/BAC0/tasks/RecurringTask.py
@@ -20,18 +20,23 @@ class RecurringTask(Task):
 
     def __init__(self, fnc, delay=60, name="recurring"):
         """
-        :param point: (BAC0.core.device.Points.Point) name of the point to read
-        :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
-        A delay cannot be < 5sec (there are risks of overloading the device)
+        :param fnc: a function or a tuple (function, args)
+        :param delay: (int) Delay between reads executions
 
         :returns: Nothing
         """
-        if hasattr(fnc, "__call__"):
+        self.fnc_args = None
+        if isinstance(fnc, tuple):
+            self.func, self.fnc_args = fnc
+        elif hasattr(fnc, "__call__") :
             self.func = fnc
-            Task.__init__(self, name=name, delay=delay)
         else:
-            raise ValueError("You must pass a function to this...")
+            raise ValueError("You must pass a function or a tuple (function,args) to this...")
+        Task.__init__(self, name=name, delay=delay)
+        
 
     def task(self):
-        self.func()
+        if self.fnc_args:
+            self.func(self.fnc_args)
+        else:
+            self.func()
diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py
index 63ca8411..d42cd834 100644
--- a/BAC0/tasks/TaskManager.py
+++ b/BAC0/tasks/TaskManager.py
@@ -66,10 +66,15 @@ def process(cls):
                 )
                 cls.tasks.remove(task.id)
             except Exception as error:
-                cls._log.error(
-                    "Super Mega Giga big error {}. Removing task.".format(error)
-                )
-                cls.tasks.remove(task.id)
+                if task.name == "Ping Task":
+                    cls._log.warning(
+                        "Ping failed with error {} ({}).".format(error, task)
+                    )
+                else:
+                    cls._log.error(
+                        "Super Mega Giga big error {}. Removing task.".format(error)
+                    )
+                    cls.tasks.remove(task.id)
             else:
                 if not cls.manager.is_alive() and cls.enable:
                     cls._log.error(
diff --git a/BAC0/tasks/UpdateCOV.py b/BAC0/tasks/UpdateCOV.py
index 183f24ee..68cca44f 100644
--- a/BAC0/tasks/UpdateCOV.py
+++ b/BAC0/tasks/UpdateCOV.py
@@ -12,7 +12,7 @@
 @note_and_log
 class Update_local_COV(Task):
     """
-    Start a task to validate each points inside local device and send 
+    Start a task to validate each points inside local device and send
     cov notifications to subscribers.
     """
 
@@ -20,8 +20,8 @@ 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 
+
+        A delay cannot be < 10sec
         For delays under 10s, use DeviceFastPoll class.
 
         :returns: Nothing
diff --git a/BAC0/tasks/deprecated_DoOnce.py b/BAC0/tasks/deprecated_DoOnce.py
deleted file mode 100644
index 074144cd..00000000
--- a/BAC0/tasks/deprecated_DoOnce.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
-# Licensed under LGPLv3, see file LICENSE in this source tree.
-#
-"""
-DoOnce.py - execute a task once
-"""
-
-from .TaskManager import OneShotTask
-from ..core.utils.notes import note_and_log
-
-
-@note_and_log
-class DoOnce(OneShotTask):
-    """
-    Start a polling task which is in fact a recurring read of the point.
-    ex.
-        device['point_name'].poll(delay=60)
-    """
-
-    def __init__(self, fnc):
-        """
-        :param point: (BAC0.core.device.Points.Point) name of the point to read
-        :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
-        A delay cannot be < 5sec (there are risks of overloading the device)
-
-        :returns: Nothing
-        """
-        if hasattr(fnc, "__call__"):
-            self.func = fnc
-            OneShotTask.__init__(self)
-        else:
-            raise ValueError("You must pass a function to this...")
-
-    def task(self):
-        self.func()
diff --git a/BAC0/tasks/deprecated_Match.py b/BAC0/tasks/deprecated_Match.py
deleted file mode 100644
index f7d2494d..00000000
--- a/BAC0/tasks/deprecated_Match.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
-# Licensed under LGPLv3, see file LICENSE in this source tree.
-#
-"""
-Match.py - verify a point's status matches its commanded value.
-
-Example:
-    Is a fan commanded to 'On' actually 'running'?
-"""
-
-# --- standard Python modules ---
-# --- 3rd party modules ---
-
-# --- this application's modules ---
-from .TaskManager import Task
-from ..core.utils.notes import note_and_log
-
-# ------------------------------------------------------------------------------
-
-
-@note_and_log
-class Match(Task):
-    """
-    Match two properties of a BACnet Object (i.e. a point status with its command).
-    """
-
-    def __init__(self, status=None, command=None, delay=5, name=None):
-        self._log.debug(
-            "Creating Match task for {} and {}. Delay : {}".format(
-                command, status, delay
-            )
-        )
-        if not name:
-            name = "Match on " + status.properties.name
-        self.command = command
-        self.status = status
-        Task.__init__(self, delay=delay, daemon=True, name=name)
-
-    def task(self):
-        try:
-            if self.status.history[-1] != self.command.history[-1]:
-                self.status._setitem(self.command.history[-1])
-        except Exception:
-            self._log.error(
-                "Something wrong matching {} and {}... try again next time...".format(
-                    self.command, self.status
-                )
-            )
-
-    def stop(self):
-        self.status._setitem("auto")
-        self.exitFlag = True
-
-
-@note_and_log
-class Match_Value(Task):
-    """
-    Verify a point's Present_Value equals the given value after a delay of X seconds.
-    Thus giving the BACnet controller (and connected equipment) time to respond to the
-    command.
-    
-        Match_Value(On, <AI:1>, 5)
-        
-    i.e. Does Fan value = On after 5 seconds. 
-    """
-
-    def __init__(self, value=None, point=None, delay=5, name=None):
-        self._log.debug("Creating MatchValue task for {} and {}".format(value, point))
-        self.value = value
-        self.point = point
-        if not name:
-            name = "Match_Value on " + point.properties.name
-        Task.__init__(self, delay=delay, daemon=True, name=name)
-
-    def task(self):
-        try:
-            value = self.value() if hasattr(self.value, "__call__") else self.value
-            if value != self.point.value:
-                self.point._set(value)
-        except Exception:
-            self._log.error(
-                "Something is wrong matching {} and {}... try again next time".format(
-                    self.value, self.point
-                )
-            )
-
-    def stop(self):
-        self.point._set("auto")
-        self.exitFlag = True
diff --git a/BAC0/tasks/deprecated_Poll.py b/BAC0/tasks/deprecated_Poll.py
deleted file mode 100644
index 07773f62..00000000
--- a/BAC0/tasks/deprecated_Poll.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
-# Licensed under LGPLv3, see file LICENSE in this source tree.
-#
-"""
-Poll.py - create a Polling task to repeatedly read a point.
-"""
-
-# --- standard Python modules ---
-import weakref
-
-# --- 3rd party modules ---
-from bacpypes.core import deferred
-
-# --- this application's modules ---
-from .TaskManager import Task
-from ..core.utils.notes import note_and_log
-
-# ------------------------------------------------------------------------------
-
-
-@note_and_log
-class SimplePoll(Task):
-    """
-    Start a polling task to repeatedly read a point's Present_Value.
-    ex.
-        device['point_name'].poll(delay=60)
-    """
-
-    def __init__(self, point, *, delay=10):
-        """
-        :param point: (BAC0.core.device.Points.Point) name of the point to read
-        :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
-        A delay cannot be < 1sec
-        This task is meant for single points, so BAC0 will allow short delays.
-        This way, a fast polling is available for some points in a device that 
-        would not support segmentation.
-
-        :returns: Nothing
-        """
-        if delay < 1:
-            delay = 1
-        if point.properties:
-            self._point = point
-            Task.__init__(self, name="rp_poll", delay=delay)
-        else:
-            raise ValueError("Provide a point object")
-
-    def task(self):
-        self._point.value
-
-
-@note_and_log
-class DevicePoll(Task):
-    """
-    Start a polling task to repeatedly read a list of points from a device using 
-    ReadPropertyMultiple requests.
-    """
-
-    def __init__(self, device, delay=10, name="", prefix="basic_poll"):
-        """
-        :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, daemon=True)
-        self._counter = 0
-
-    @property
-    def device(self):
-        return self._device()
-
-    def task(self):
-        try:
-            self.device.read_multiple(
-                list(self.device.points_name), points_per_request=25
-            )
-            self._counter += 1
-            if self._counter == self.device.properties.auto_save:
-                self.device.save()
-                if self.device.properties.clear_history_on_save:
-                    self.device.clear_histories()
-                self._counter = 0
-        except AttributeError:
-            # This error can be seen when defining a controller on a busy network...
-            # When creation fail, polling is created and fail the first time...
-            # So kill the task
-            self.stop()
-        except ValueError:
-            self.device._log.error(
-                "Something is wrong with polling...stopping. Try setting off segmentation"
-            )
-            self.stop()
-
-
-@note_and_log
-class DeviceNormalPoll(DevicePoll):
-    """
-    Start a normal polling task to repeatedly read a list of points from a device using 
-    ReadPropertyMultiple requests.
-    
-    Normal polling will limit the polling speed to 10 second minimum
-
-    """
-
-    def __init__(self, device, delay=10, name=""):
-        """
-        :param device: (BAC0.core.devices.Device.Device) device to poll
-        :param delay: (int) Delay between polls in seconds, defaults = 10sec
-        
-        :returns: Nothing
-        """
-        if delay < 10:
-            delay = 10
-        self._log.info(
-            "Device defined for normal polling with a delay of {}sec".format(delay)
-        )
-        DevicePoll.__init__(
-            self, device=device, name=name, delay=delay, prefix="rpm_normal_poll"
-        )
-
-
-@note_and_log
-class DeviceFastPoll(DevicePoll):
-    """
-    Start a fast polling task to repeatedly read a list of points from a device using 
-    ReadPropertyMultiple requests.
-    Delay allowed will be 0 to 10 seconds
-    Normal polling will limit the polling speed to 10 second minimum
-
-    Warning : Fast polling must be used with care or network flooding may occur
-
-    """
-
-    def __init__(self, device, delay=1, name=""):
-        """
-        :param device: (BAC0.core.devices.Device.Device) device to poll
-        :param delay: (int) Delay between polls in seconds, defaults = 1sec
-        
-        :returns: Nothing
-        """
-        if delay < 0:
-            delay = 0.01
-        elif delay > 10:
-            delay = 10
-        self._log.warning(
-            "Device defined for fast polling with a delay of {}sec".format(delay)
-        )
-        DevicePoll.__init__(
-            self, device=device, name=name, delay=delay, prefix="rpm_fast_poll"
-        )
diff --git a/BAC0/tasks/deprecated_RecurringTask.py b/BAC0/tasks/deprecated_RecurringTask.py
deleted file mode 100644
index 6861ed1e..00000000
--- a/BAC0/tasks/deprecated_RecurringTask.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
-# Licensed under LGPLv3, see file LICENSE in this source tree.
-#
-"""
-RecurringTask.py - execute a recurring task 
-"""
-
-from .TaskManager import Task
-from ..core.utils.notes import note_and_log
-
-
-@note_and_log
-class RecurringTask(Task):
-    """
-    Start a recurring task (a function passed)
-    """
-
-    def __init__(self, fnc, delay=60, name="recurring"):
-        """
-        :param point: (BAC0.core.device.Points.Point) name of the point to read
-        :param delay: (int) Delay between reads in seconds, defaults = 10sec
-        
-        A delay cannot be < 5sec (there are risks of overloading the device)
-
-        :returns: Nothing
-        """
-        if hasattr(fnc, "__call__"):
-            self.func = fnc
-            Task.__init__(self, name=name, delay=delay)
-        else:
-            raise ValueError("You must pass a function to this...")
-
-    def task(self):
-        self.func()
diff --git a/BAC0/tasks/deprecated_TaskManager.py b/BAC0/tasks/deprecated_TaskManager.py
deleted file mode 100644
index e722e524..00000000
--- a/BAC0/tasks/deprecated_TaskManager.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
-# Licensed under LGPLv3, see file LICENSE in this source tree.
-#
-"""
-TaskManager.py - creation of threads used for repetitive tasks.  
-
-A key building block for point simulation.
-"""
-# --- standard Python modules ---
-from threading import Thread, Lock
-import time
-
-# --- 3rd party modules ---
-# --- this application's modules ---
-from ..core.utils.notes import note_and_log
-
-# ------------------------------------------------------------------------------
-
-
-class Manager:
-    taskList = []
-    threadLock = Lock()
-
-
-def stopAllTasks():
-    for each in Manager.taskList:
-        each.exitFlag = True
-    while True:
-        _alive = [each.is_alive() for each in Manager.taskList]
-        if not any(_alive):
-            clean_tasklist()
-            break
-    return True
-
-
-def clean_tasklist():
-    for each in Manager.taskList:
-        if not each.is_alive():
-            Manager.taskList.remove(each)
-
-
-@note_and_log
-class Task(Thread):
-    def __init__(self, delay=5, daemon=True, name="recurring"):
-        Thread.__init__(self, name=name, daemon=daemon)
-        self.is_running = False
-        self.exitFlag = False
-        self.lock = Manager.threadLock
-        self.delay = delay
-        if self.name not in Manager.taskList:
-            Manager.taskList.append(self)
-
-    def run(self):
-        self.process()
-
-    def process(self):
-        self.is_running = True
-        while not self.exitFlag:
-            self.lock.acquire()
-            self.task()
-            self.lock.release()
-            # This replace a single time.sleep
-            # the goal is to speed up the stop
-            # of the thread by providing an easy way out
-            for _ in range(self.delay * 2):
-                if self.exitFlag:
-                    break
-                time.sleep(0.5)
-            clean_tasklist()
-
-    def task(self):
-        raise RuntimeError("task must be overridden")
-
-    def stop(self):
-        self.is_running = False
-        self.exitFlag = True
-        clean_tasklist()
-
-    def beforeStop(self):
-        """
-        Action done when closing thread
-        """
-        if self in Manager.taskList:
-            Manager.taskList.remove(self)
-
-
-@note_and_log
-class OneShotTask(Thread):
-    def __init__(self, daemon=True, name="Oneshot"):
-        Thread.__init__(self, name=name, daemon=daemon)
-        self.lock = Manager.threadLock
-        if self.name not in Manager.taskList:
-            Manager.taskList.append(self)
-
-    def run(self):
-        self.process()
-
-    def process(self):
-        self.task()
-
-    def task(self):
-        raise RuntimeError("task must be overridden")
-
-    def stop(self):
-        pass
-
-    def beforeStop(self):
-        """
-        Action done when closing thread
-        """
-        if self in Manager.taskList:
-            Manager.taskList.remove(self)
diff --git a/BAC0/tools/const.py b/BAC0/tools/const.py
new file mode 100644
index 00000000..e149a14d
--- /dev/null
+++ b/BAC0/tools/const.py
@@ -0,0 +1,122 @@
+FILE_HEADER = """<?xml version="1.0"?>
+<tags>
+    <tag>
+        <name>n/a</name>
+        <group>n/a</group>
+        <resourceLocator>
+            <protocolName>n/a</protocolName>
+            <object_type>n/a</object_type>
+            <device_id>n/a</device_id>
+            <data_type>n/a</data_type>
+            <arraysize>n/a</arraysize>
+            <conversion>n/a</conversion>
+            <object_instance>n/a</object_instance>
+            <object_property>n/a</object_property>
+            <array_index>n/a</array_index>
+            <write_priority>n/a</write_priority>
+            <cov>n/a</cov>
+        </resourceLocator>
+        <encoding>n/a</encoding>
+        <refreshTime>n/a</refreshTime>
+        <accessMode>n/a</accessMode>
+        <active>n/a</active>
+        <TAGLOCATOR>n/a</TAGLOCATOR>
+        <comment>n/a</comment>
+        <simulator>
+            <DataSimulator>n/a</DataSimulator>
+            <Amplitude>n/a</Amplitude>
+            <Simulator_offset>n/a</Simulator_offset>
+            <Period>n/a</Period>
+        </simulator>
+        <scaling>
+            <enableScaling>n/a</enableScaling>
+            <scalingType>n/a</scalingType>
+            <enableLimits>n/a</enableLimits>
+            <factors>
+                <s1>n/a</s1>
+                <s2>n/a</s2>
+                <s3>n/a</s3>
+                <tagS1>n/a</tagS1>
+                <tagS2>n/a</tagS2>
+                <tagS3>n/a</tagS3>
+            </factors>
+            <limits>
+                <eumin>n/a</eumin>
+                <eumax>n/a</eumax>
+                <elmin>n/a</elmin>
+                <elmax>n/a</elmax>
+            </limits>
+        </scaling>
+        <decimalDigits>
+            <ddTag>n/a</ddTag>
+            <ddDigits>n/a</ddDigits>
+        </decimalDigits>
+        <castType>n/a</castType>
+        <default>n/a</default>
+        <min>n/a</min>
+        <max>n/a</max>
+        <statesText>n/a</statesText>
+    </tag>
+"""
+
+TAG = """    <tag>
+        <name>{d[name]}</name>
+        <group>{d[group]}</group>
+        <resourceLocator>
+            <protocolName>BACN</protocolName>
+            <object_type>{d[object_type]}</object_type>
+            <device_id>{d[device_id]}</device_id>
+            <data_type>{d[data_type]}</data_type>
+            <arraysize></arraysize>
+            <conversion></conversion>
+            <object_instance>{d[object_instance]}</object_instance>
+            <object_property>{d[object_property]}</object_property>
+            <array_index>{d[array_index]}</array_index>
+            <write_priority>{d[write_priority]}</write_priority>
+            <cov>{d[cov]}</cov>
+        </resourceLocator>
+        <encoding></encoding>
+        <refreshTime>{d[refresh_time]}</refreshTime>
+        <accessMode>{d[access_mode]}</accessMode>
+        <active>{d[active]}</active>
+        <TAGLOCATOR></TAGLOCATOR>
+        <comment>{d[comment]}</comment>
+        <simulator>
+            <DataSimulator>Variables</DataSimulator>
+            <Amplitude></Amplitude>
+            <Simulator_offset></Simulator_offset>
+            <Period></Period>
+        </simulator>
+        <scaling>
+            <enableScaling>false</enableScaling>
+            <scalingType>byFormula</scalingType>
+            <enableLimits>false</enableLimits>
+            <factors>
+                <s1>1</s1>
+                <s2>1</s2>
+                <s3>0</s3>
+                <tagS1></tagS1>
+                <tagS2></tagS2>
+                <tagS3></tagS3>
+            </factors>
+            <limits>
+                <eumin>0</eumin>
+                <eumax>100</eumax>
+                <elmin></elmin>
+                <elmax></elmax>
+            </limits>
+        </scaling>
+        <decimalDigits>
+            <ddTag></ddTag>
+            <ddDigits></ddDigits>
+        </decimalDigits>
+        <castType></castType>
+        <default></default>
+        <min>-3.40282e+38</min>
+        <max>3.40282e+38</max>
+        <statesText></statesText>
+    </tag>
+"""
+
+FILE_FOOTER = """</tags>
+"""
diff --git a/BAC0/tools/tad_display.py b/BAC0/tools/tad_display.py
new file mode 100644
index 00000000..05379987
--- /dev/null
+++ b/BAC0/tools/tad_display.py
@@ -0,0 +1,72 @@
+from . import const
+
+FILE_HEADER = const.FILE_HEADER
+FILE_FOOTER = const.FILE_FOOTER
+TAG = const.TAG
+
+
+def convert(d, device_name=None):
+    list_of_tags = []
+    OBJECT_TYPE = {
+        "analogValue": "AV",
+        "analogInput": "AI",
+        "analogOutput": "AO",
+        "binaryInput": "BI",
+        "binaryValue": "BV",
+        "binaryOutput": "BO",
+        "multiStateInput": "MI",
+        "multiStateValue": "MV",
+        "multiStateOutput": "MO",
+    }
+    DATATYPES = {
+        "AI": "float",
+        "AO": "float",
+        "AV": "float",
+        "BI": "boolean",
+        "BO": "boolean",
+        "BV": "boolean",
+        "MI": "unsignedInt",
+        "MO": "unsignedInt",
+        "MV": "unsignedInt",
+    }
+
+    WRITE_PRIORITY = {
+        "AI": 0,
+        "AO": 8,
+        "AV": 10,
+        "BI": 0,
+        "BO": 8,
+        "BV": 10,
+        "MI": 0,
+        "MO": 8,
+        "MV": 10,
+    }
+    data = {}
+    for each in d.points:
+        name = device_name if device_name else d.properties.name
+        data["name"] = "{}/{}".format(name, each.properties.name)
+        data["group"] = ""
+        data["object_type"] = OBJECT_TYPE[each.properties.type]
+        data["object_instance"] = each.properties.address
+        data["device_id"] = d.properties.device_id
+        data["data_type"] = DATATYPES[data["object_type"]]
+        data["object_property"] = 85
+        data["write_priority"] = WRITE_PRIORITY[data["object_type"]]
+        data["cov"] = "false"
+        data["refresh_time"] = 1000
+        data["access_mode"] = "READ"
+        data["active"] = "false"
+        data["comment"] = each.properties.description
+        data["array_index"] = -1
+        list_of_tags.append(TAG.format(d=data))
+
+        file = FILE_HEADER
+        for tag in list_of_tags:
+            file += tag
+        file += FILE_FOOTER
+        write_tags_import_file(name, file)
+
+
+def write_tags_import_file(name, file):
+    with open("{}.xml".format(name), "w") as xml_file:
+        xml_file.write(file)
diff --git a/BAC0/web/BokehRenderer.py b/BAC0/web/BokehRenderer.py
index ee2df7ea..0030e5be 100644
--- a/BAC0/web/BokehRenderer.py
+++ b/BAC0/web/BokehRenderer.py
@@ -695,7 +695,7 @@ def generate_selection(self):
 
 
 class DevicesTableHandler(Handler):
-    """ 
+    """
     This handler will poll the network and show devices.
 
     """
@@ -726,7 +726,7 @@ def modify_document(self, doc):
 
 
 class NetworkPieChartHandler(Handler):
-    """ 
+    """
     This handler will poll the network and show devices.
 
     """
@@ -757,7 +757,7 @@ def modify_document(self, doc):
 
 
 class NotesTableHandler(Handler):
-    """ 
+    """
     This handler will poll the network and show devices.
 
     """
diff --git a/Jupyter/.ipynb_checkpoints/BAC0-checkpoint.ipynb b/Jupyter/.ipynb_checkpoints/BAC0-checkpoint.ipynb
deleted file mode 100644
index 04aa50d1..00000000
--- a/Jupyter/.ipynb_checkpoints/BAC0-checkpoint.ipynb
+++ /dev/null
@@ -1,589 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# BAC0\n",
-    "\n",
-    "BAC0 is a script module based on bacpypes (https://github.com/JoelBender/bacpypes.git) to automate task using Bacnet/IP protocole. It's mainly targetted as a tool to test sequences on DDC controlelrs using bacnet.\n",
-    "\n",
-    "Using the simplciity of python, the power of pandas, it's easy to test and keep a trace of the history of each points.\n",
-    "\n",
-    "## How to use\n",
-    "\n",
-    "First you must install the 2 modules\n",
-    "pip install bacpypes\n",
-    "- git clone https://github.com/ChristianTremblay/BAC0.git (python setup.py install)\n",
-    "\n",
-    "Note : All tests have been done in Windows 7 using Anaconda3 (http://continuum.io/downloads)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Import module"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": false
-   },
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "/home/pi/miniconda3/lib/python3.4/importlib/_bootstrap.py:321: FutureWarning: The pandas.lib module is deprecated and will be removed in a future version. These are private functions and can be accessed from pandas._libs.lib instead\n",
-      "  return f(*args, **kwds)\n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "xlwings not installed. If using Windows or OSX, install to get more features.\n",
-      "Using ip : 192.168.111.13\n",
-      "Click here to open Live Trending Web Page\n",
-      "http://localhost:5006/?bokeh-session-id=WMT6jGoglqAs2Sw2fhmwJhuZF6MsObrLDakK7d7N5kbo\n"
-     ]
-    }
-   ],
-   "source": [
-    "import BAC0\n",
-    "import pandas as pd\n",
-    "%matplotlib inline\n",
-    "# Define network and connect to it\n",
-    "bacnet = BAC0.connect()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'0.99.100'"
-      ]
-     },
-     "execution_count": 2,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "BAC0.version"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Found FX14 0005... building points list\n",
-      "Ready!\n",
-      "Polling started, every values read each 10 seconds\n"
-     ]
-    }
-   ],
-   "source": [
-    "# Define the controller using : controller = BAC0.device(address, device_id, network)\n",
-    "fx = BAC0.device('2:5',5,bacnet)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "[nvoAI3 : 3.74 degreesCelsius,\n",
-       " nvoAI4 : 45.65 percent,\n",
-       " nvoAI6 : 345.20 kilopascals,\n",
-       " nvoAI5 : 1.85 percent,\n",
-       " nvoAI1 : 44.87 degreesCelsius,\n",
-       " nvoAI2 : 22.76 degreesCelsius,\n",
-       " nvoDO6 : 13.85 percent,\n",
-       " nvoEffTempEtage : 22.87 degreesCelsius,\n",
-       " nvoEffTempSS : 20.77 degreesCelsius,\n",
-       " nvoTempRdC : 21.83 degreesCelsius,\n",
-       " nciPCTExtMinVolet : -12.00 degreesCelsius,\n",
-       " nciPIDBLTRetPC : 20.00 degreesCelsius,\n",
-       " nvoDemChauff : 98.66 percent,\n",
-       " nvoDemRefroid : 0.00 percent,\n",
-       " nvoEffPCSS : 22.00 degreesCelsius,\n",
-       " nvoPCCTAli : 44.68 degreesCelsius,\n",
-       " nvoEffPCEtage : 22.20 degreesCelsius,\n",
-       " nciPIDTPSousSolPB : 5.00 degreesCelsius,\n",
-       " nciPIDTPSousSolTI : 800.00 seconds,\n",
-       " nciPIDTPEtageTI : 800.00 seconds,\n",
-       " nciPIDTPRdCTI : 800.00 seconds,\n",
-       " nciPIDTPEtagePB : 5.00 degreesCelsius,\n",
-       " nciPIDTAliPB : 10.00 degreesCelsius,\n",
-       " nciPIDTAliTI : 90.00 seconds,\n",
-       " nciPCTAliMax : 45.00 degreesCelsius,\n",
-       " nciPCTAliMin : 13.00 degreesCelsius,\n",
-       " nciPCTAliMed : 22.00 degreesCelsius,\n",
-       " nciPIDHumPB : 30.00 percent,\n",
-       " nciPIDHumTI : 120.00 seconds,\n",
-       " nciPIDDeshumTI : 120.00 seconds,\n",
-       " nvoPIDDeshumOut : 0.00 percent,\n",
-       " nciPIDDeshumPB : 30.00 percent,\n",
-       " nvoPIDHumOut : 0.00 percent,\n",
-       " nciDemRefroidHV : 95.00 percent,\n",
-       " ncikWTotal : 15.00 noUnits,\n",
-       " nciDeshumNivHV : 50.00 percent,\n",
-       " nciPIDHumPC : 30.00 percent,\n",
-       " nvoEffectkW : 0.66 noUnits,\n",
-       " nvoPressionEauPSI : 50.07 noUnits,\n",
-       " nvoPIDTPSousSolOut : 50.00 percent,\n",
-       " nvoPIDTPRdCOut : 0.69 percent,\n",
-       " nvoPIDTPEtageSortie : 50.00 percent,\n",
-       " nciDemChauffHV : 90.00 percent,\n",
-       " nciDelaiMinOnHV : 120.00 seconds,\n",
-       " nciDiffAlrmPmp : 0.00 kilopascals,\n",
-       " nciPCBassePression : 0.00 kilopascals,\n",
-       " nciDelaiAlrmPompe : 120.00 seconds,\n",
-       " nviPCEtage : 22.20 degreesCelsius,\n",
-       " nviTempEtage : 22.87 degreesCelsius,\n",
-       " nviPCSousSol : 22.00 degreesCelsius,\n",
-       " nviTempSousSol : 20.78 degreesCelsius,\n",
-       " nciPIDBLTRetPB : 5.00 degreesCelsius,\n",
-       " nvoAO1 : 100.00 percent,\n",
-       " nciPIDDeshumPC : 60.00 percent,\n",
-       " nciPIDBLTRetTI : 60.00 seconds,\n",
-       " nvoAO2 : 0.00 percent,\n",
-       " nciDureeOccupSousSol : 180.00 minutes,\n",
-       " nciDelaiMinModeUrgen : 60.00 minutes,\n",
-       " nciPIDTPRdCPB : 5.00 degreesCelsius,\n",
-       " nciPCTPRdCAbsent : 20.00 degreesCelsius,\n",
-       " nciPCTRetMinUrgence : 15.00 degreesCelsius,\n",
-       " nciPCTExtPrmChauffTP : 16.00 degreesCelsius,\n",
-       " nciPCTExtPrmClim : 13.00 degreesCelsius,\n",
-       " nciPCTExtPrmSCR : 10.00 degreesCelsius,\n",
-       " nciPCTExtMinChauffTP : -12.00 degreesCelsius,\n",
-       " nciResetPompe : OFF,\n",
-       " nciOvrdDO9.State : AUTO,\n",
-       " nciOvrdDO8.State : AUTO,\n",
-       " nciOvrdDO6.State : AUTO,\n",
-       " nciOvrdDO7.State : AUTO,\n",
-       " nvoEtapeAct : Chauffage,\n",
-       " SCH_nvoHoraire.Curre : Occupied,\n",
-       " nviOccupRequestSousS : ERR,\n",
-       " nviOccupRequestEtage : ERR,\n",
-       " nvoProbSondeSS : OK,\n",
-       " nvoProbSondeEtage : Ok,\n",
-       " nciOvrdDO5.State : AUTO,\n",
-       " nvoAlarmPompe : OFF,\n",
-       " nvoAlrmGravePompe : OFF,\n",
-       " nvoTempOccup : OFF,\n",
-       " nvoEffOccupancy : Occupe,\n",
-       " nciModeOperation : AUTO,\n",
-       " nciSelectConsigne : Auto,\n",
-       " nvoConsActuelle : RezDeChaussee,\n",
-       " nciOvrdDO2.State : AUTO,\n",
-       " nciOvrdDO3.State : AUTO,\n",
-       " nciOvrdDO4.State : AUTO,\n",
-       " nciOvrdDO1.State : AUTO,\n",
-       " nciModeDeshum : Aucune,\n",
-       " nciOvrdAO1.State : AUTO,\n",
-       " nciOvrdAO2.State : AUTO,\n",
-       " nvoDI7 : False,\n",
-       " nvoDI8 : False,\n",
-       " nvoDI10 : False,\n",
-       " nvoDI9 : False,\n",
-       " nvoDI6 : False,\n",
-       " nvoDI4 : False,\n",
-       " nvoDI3 : False,\n",
-       " nvoDI5 : False,\n",
-       " nvoDI1 : True,\n",
-       " nvoDI2 : False,\n",
-       " nvoDI11 : False,\n",
-       " nvoDI12 : True,\n",
-       " nvoDO8 : True,\n",
-       " nvoDO7 : False,\n",
-       " nvoDO3 : True,\n",
-       " nvoDO9 : False,\n",
-       " nvoDO2 : False,\n",
-       " nvoDO1 : False,\n",
-       " nvoDO5 : False,\n",
-       " nvoDO4 : True,\n",
-       " nciHumidif : False]"
-      ]
-     },
-     "execution_count": 4,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "# Access points list\n",
-    "fx.points"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>nvoAI1</th>\n",
-       "      <th>nvoAI2</th>\n",
-       "      <th>nvoDI1</th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:21.102146</th>\n",
-       "      <td>44.950001</td>\n",
-       "      <td>NaN</td>\n",
-       "      <td>NaN</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:21.117771</th>\n",
-       "      <td>44.950001</td>\n",
-       "      <td>22.799999</td>\n",
-       "      <td>NaN</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:31.172206</th>\n",
-       "      <td>44.950001</td>\n",
-       "      <td>22.799999</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:32.605017</th>\n",
-       "      <td>44.869999</td>\n",
-       "      <td>22.760000</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:36.223253</th>\n",
-       "      <td>44.869999</td>\n",
-       "      <td>22.760000</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:48.120203</th>\n",
-       "      <td>44.740002</td>\n",
-       "      <td>22.799999</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:51.763943</th>\n",
-       "      <td>44.740002</td>\n",
-       "      <td>22.799999</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:53.399209</th>\n",
-       "      <td>44.740002</td>\n",
-       "      <td>22.799999</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:53.539834</th>\n",
-       "      <td>44.740002</td>\n",
-       "      <td>22.760000</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>2015-11-15 08:40:53.696084</th>\n",
-       "      <td>44.740002</td>\n",
-       "      <td>22.760000</td>\n",
-       "      <td>active</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "                               nvoAI1     nvoAI2  nvoDI1\n",
-       "2015-11-15 08:40:21.102146  44.950001        NaN     NaN\n",
-       "2015-11-15 08:40:21.117771  44.950001  22.799999     NaN\n",
-       "2015-11-15 08:40:31.172206  44.950001  22.799999  active\n",
-       "2015-11-15 08:40:32.605017  44.869999  22.760000  active\n",
-       "2015-11-15 08:40:36.223253  44.869999  22.760000  active\n",
-       "2015-11-15 08:40:48.120203  44.740002  22.799999  active\n",
-       "2015-11-15 08:40:51.763943  44.740002  22.799999  active\n",
-       "2015-11-15 08:40:53.399209  44.740002  22.799999  active\n",
-       "2015-11-15 08:40:53.539834  44.740002  22.760000  active\n",
-       "2015-11-15 08:40:53.696084  44.740002  22.760000  active"
-      ]
-     },
-     "execution_count": 6,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "# Create a table with multiple points\n",
-    "\n",
-    "lst = ['nvoAI1', 'nvoAI2', 'nvoDI1']\n",
-    "\n",
-    "fx.df(lst).fillna(method='ffill')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 7,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "<matplotlib.axes._subplots.AxesSubplot at 0x85f66a0>"
-      ]
-     },
-     "execution_count": 7,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWwAAAEBCAYAAABc9Z89AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAEX5JREFUeJzt3XuM3WWdx/H3l46mdCm2RaZTuVgxEqgXirv4R4NyACks\naWk10tK6iMCmbORqUlcgMTsuCSwmRfEPJBuJdMBWYBW2tVlHAkyAILK4vSBUuok0Umyn4BZEoaXQ\n7/4xZ4aZYWbOXM6080zfr+SkZ57L7/ecgXzml+f3nN8TmYkkaew75EAPQJI0OAa2JBXCwJakQhjY\nklQIA1uSCmFgS1IhDGypKiJ+GxGfG6C+LSIu3Z9jkrozsDXmRcTWiNgTEUf0Kl8fEfsi4thhHPPO\niLihe1lmfiIzH63WN0fEXb26ZfUlHRAGtkqQwO+BJZ0FEfFJ4FAMUB1EDGyV4m7gK91+vghoAQIg\nIk6JiB0REZ0NIuKLEbGh94EiYhmwFPjniHg9Iv6zWr41Is6MiHOA64DF1fr1fQ0oIi6JiOci4v8i\n4hfDudKXhsLAVimeBA6PiBMiYgKwmI4QByAz/xv4E3B2tz4XAit7Hygz/x34MXBzZk7OzAWdVR3V\n+QvgRuAn1fqTex8jIhbQEepfAD4IPAasHvnHlPpnYKskd9FxlX0W8BzwUq/6FuAfACJiGjAXWDXA\n8aJG3UD1/wTclJnPZ+Y+4CZgdkQcM+AnkEag4UAPQBqkpCOwHwM+QrfpkG5+DDwbEZOARcCjmdk+\nSuP5MHBrRKzoVX4U8OIonVMHOa+wVYzM/AMdNx//HvhZH/Xb6Jg6+SIdV9q9V3n0aF7rdDXq/wAs\ny8yp3V5/k5lP1ugnDZuBrdJcCpyRmW/2U98CfBP4BH2EejftwHED1O8AZna/idnL7cD1ETELICI+\nEBHnDzhyaYQMbBUlM3+fmf/TvahXk58BxwL3Z+buAQ51BzArInZFRF/Bfl/13z9FxNN9jOMB4Gbg\nJxHxGvAMPW94SnUXg9nAoHpX/mlgW2bOj4hm4B+Bl6tNrqveWZcOuIj4X+CyzHz4QI9FqqfB3nS8\nmo678pOrPydwS2beMiqjkoYpIr5Ix9I8w1rjTs0pkYg4GjgX+CHv3pWvteRJ2u8iog24Dbj8AA9F\nGhWDmcP+LvANYF+3sgSujIiNEXFHREwZldFJQ5CZlcxsyswHD/RYpNEwYGBHxDxgZ2aup+cV9Q/o\nWAs7G9gO9F6LKkmqswFvOkbEjXR8vfdtYCJwOPDTzPxKtzYzgbWZ+ck++vtgHkkahsx8z7TzoFaJ\nAETEacDy6iqRGZm5vVr+deCUzFzaR5/80peSTHq8OgbT92us1I2VcdRzjO/979P3q951+/NcjtEx\njocxzpkTfQb2UL6aHry75vU7EXFS9ecXgMv663Tfff3V6EAY639USvjD5xj7rxsr4yj999ifQV9h\nD0dE5GgeX5LGo4i+r7D9pqMkFcLAlqRC+HhVSUPS//OwNBxDmTY2sCUNmfem6mOof/ycEpGkQhjY\nklQIA1uSCmFgS1IhDGxJGoRKpcK0adN46623epR/9atf5Vvf+lbXz8uWLeOEE05gwoQJrFy5sq5j\nMLAlqYatW7fy1FNP0djYyJo1a3rURUSP1R6zZ8/mtttu49Of/nTdl0Aa2JLGjZkzZ7JixQpOOukk\npkyZwgUXXMCePXs48cQTWbduXVe7t99+myOPPJINGzYAsGbNGj7+8Y8zdepUTj/9dH73u9/1OG5L\nSwuf//znufDCC/u8au6+zPFrX/saZ5xxBhMnTqz75zOwJY0bEcF9991Ha2srL7zwAps2beLOO+9k\n6dKlrF69uqtda2srjY2NzJ49my1btrB06VK+//3v88orr3Duuecyf/589u7d29W+paWFxYsXs2jR\nIlpbW9m5c+eB+HgGtqT66+9xokN5DddVV11FU1MTU6dOZf78+WzYsIGlS5eyZs0adu/eDcCqVatY\nsmQJAPfccw/z5s3jzDPPZMKECSxfvpw333yTJ554AoDHH3+cl156ifPOO4+PfexjzJo1i1WrVo34\ndzQcBrakuhvq41eH8ojRWpqamrreH3roofz1r3/lox/9KCeeeCJr1qzhjTfeYO3atSxd2vEI/+3b\nt3Psscd29YkIjjnmGP74xz8CsHLlSubOncvkyR17kJ9//vl1v5k4WH41XdK41jm/vGTJElavXs07\n77zDrFmzOO644wD40Ic+xDPPPNOj/YsvvshRRx3Fm2++yb333su+ffuYMWMGAHv27OHVV19l06ZN\nfOpTn9qvn8UrbEkHhQsuuIDW1lZuv/12vvzlL3eVL1q0iHXr1vHwww+zd+9eVqxYwcSJE5kzZw4P\nPPAADQ0NbN68mY0bN7Jx40Y2b97MZz/7WVpaWoD3Pldl79697N69m3379vHWW2+xe/fuuj17xcCW\n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\nAAAAAElFTkSuQmCC\n",
-      "text/plain": [
-       "<matplotlib.figure.Figure at 0x52d27f0>"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "# Create a trend with multiple points using the DataFrame\n",
-    "\n",
-    "lst = ['nvoAI1', 'nvoAI2']\n",
-    "\n",
-    "\n",
-    "fx.df(lst).resample('1min').plot(title='My title')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "<matplotlib.axes._subplots.AxesSubplot at 0x8d59978>"
-      ]
-     },
-     "execution_count": 8,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "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\nAAAAAElFTkSuQmCC\n",
-      "text/plain": [
-       "<matplotlib.figure.Figure at 0x8d34b00>"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "fx.chart(lst, title='My title')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "2015-11-15 08:40:31.226769    Controller initialized\n",
-       "2015-11-15 08:41:19.349162      New note to remember\n",
-       "dtype: object"
-      ]
-     },
-     "execution_count": 9,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "fx.notes = 'New note to remember'\n",
-    "fx.notes"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 10,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "FX14 0005"
-      ]
-     },
-     "execution_count": 10,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "fx"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Finding devices on a network"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.whois() # Note that this function is called automatically in ReadWriteScript\n",
-    "\n",
-    "# The result takes a few milliseconds to be processed... it can be used in the same cell."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Using the bacnet object to read  or write objects from devices"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Accessing the list of object of a device can be done using the objectList property\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "# Let's print the first 10 objects...\n",
-    "bacnet.read('2:5 device 5 objectList')[:10]"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.readMultiple('2:5 analogInput 1 objectName description presentValue units')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "import time\n",
-    "obj = '2:5 analogValue 41 presentValue'\n",
-    "old_value = bacnet.read(obj)\n",
-    "print('Old Value is : %s' % old_value)\n",
-    "new_value = 120\n",
-    "\n",
-    "bacnet.write('%s %s' % (obj,new_value))\n",
-    "print('New value is %s' % bacnet.read(obj))"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Stopping the app"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.disconnect()"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.4.3"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 0
-}
diff --git a/Jupyter/BAC0.ipynb b/Jupyter/BAC0.ipynb
deleted file mode 100644
index 3e48995a..00000000
--- a/Jupyter/BAC0.ipynb
+++ /dev/null
@@ -1,285 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# BAC0\n",
-    "\n",
-    "BAC0 is a script module based on bacpypes (https://github.com/JoelBender/bacpypes.git) to automate task using Bacnet/IP protocole. It's mainly targetted as a tool to test sequences on DDC controlelrs using bacnet.\n",
-    "\n",
-    "Using the simplciity of python, the power of pandas, it's easy to test and keep a trace of the history of each points.\n",
-    "\n",
-    "## How to use\n",
-    "\n",
-    "First you must install the 2 modules\n",
-    "pip install bacpypes\n",
-    "- git clone https://github.com/ChristianTremblay/BAC0.git (python setup.py install)\n",
-    "\n",
-    "Note : All tests have been done in Windows 7 using Anaconda3 (http://continuum.io/downloads)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Import module"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": false
-   },
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "/home/pi/miniconda3/lib/python3.4/importlib/_bootstrap.py:321: FutureWarning: The pandas.lib module is deprecated and will be removed in a future version. These are private functions and can be accessed from pandas._libs.lib instead\n",
-      "  return f(*args, **kwds)\n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "xlwings not installed. If using Windows or OSX, install to get more features.\n",
-      "Using ip : 192.168.111.13\n",
-      "Click here to open Live Trending Web Page\n",
-      "http://localhost:5006/?bokeh-session-id=0v2rWfXDL7ipg0yn3TfR8nJX35jfOners4zsE2pIaENq\n",
-      "Failed running bokeh serve\n",
-      "Bokeh server already running\n"
-     ]
-    }
-   ],
-   "source": [
-    "import BAC0\n",
-    "import pandas as pd\n",
-    "%matplotlib inline\n",
-    "# Define network and connect to it\n",
-    "bacnet = BAC0.connect()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "BAC0.version"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [],
-   "source": [
-    "# Define the controller using : controller = BAC0.device(address, device_id, network)\n",
-    "fx = BAC0.device('2:5',5,bacnet)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [],
-   "source": [
-    "# Access points list\n",
-    "fx.points"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "# Create a table with multiple points\n",
-    "\n",
-    "lst = ['nvoAI1', 'nvoAI2', 'nvoDI1']\n",
-    "\n",
-    "fx.df(lst).fillna(method='ffill')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [],
-   "source": [
-    "# Create a trend with multiple points using the DataFrame\n",
-    "\n",
-    "lst = ['nvoAI1', 'nvoAI2']\n",
-    "\n",
-    "\n",
-    "fx.df(lst).resample('1min').plot(title='My title')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "fx.chart(lst, title='My title')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false,
-    "scrolled": true
-   },
-   "outputs": [],
-   "source": [
-    "fx.notes = 'New note to remember'\n",
-    "fx.notes"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "fx"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Finding devices on a network"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.whois() # Note that this function is called automatically in ReadWriteScript\n",
-    "\n",
-    "# The result takes a few milliseconds to be processed... it can be used in the same cell."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Using the bacnet object to read  or write objects from devices"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Accessing the list of object of a device can be done using the objectList property\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "# Let's print the first 10 objects...\n",
-    "bacnet.read('2:5 device 5 objectList')[:10]"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.readMultiple('2:5 analogInput 1 objectName description presentValue units')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "import time\n",
-    "obj = '2:5 analogValue 41 presentValue'\n",
-    "old_value = bacnet.read(obj)\n",
-    "print('Old Value is : %s' % old_value)\n",
-    "new_value = 120\n",
-    "\n",
-    "bacnet.write('%s %s' % (obj,new_value))\n",
-    "print('New value is %s' % bacnet.read(obj))"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Stopping the app"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
-   "outputs": [],
-   "source": [
-    "bacnet.disconnect()"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.4.3"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 0
-}
\ No newline at end of file
diff --git a/doc/source/connect.rst b/doc/source/connect.rst
index 1ad1f328..fa63b772 100644
--- a/doc/source/connect.rst
+++ b/doc/source/connect.rst
@@ -53,6 +53,11 @@ On a device without all the module sufficient to run the "complete" mode, using
 this syntax will also run BAC0 in "Lite" mode::
 
     bacnet = BAC0.connect()
+
+> Device ID 
+> 
+> It's possible to define the device ID you want in your BAC0 instance by
+> using the `deviceId` argument
     
 Complete
 ............
diff --git a/doc/source/database.rst b/doc/source/database.rst
new file mode 100644
index 00000000..e338cd14
--- /dev/null
+++ b/doc/source/database.rst
@@ -0,0 +1,175 @@
+Database
+================
+By default, all data is saved on a SQLite instance where BAC0 run. 
+In some circumstances, it could be required to send data to a more powerful database.
+For that reason, support for [InfluxDB](https://docs.influxdata.com/influxdb/v2.0/) have been added to BAC0.
+I'm trying to make that flexible to allow other databases to be use eventually, using the same db_params 
+argument when creating the network object.
+
+This is still a work in progress.
+
+SQL
+------------
+Technically, BAC0 sends everything to SQLite locally. It would be possible to make some configuration changes 
+to connect to a SQL database as SQLite share mostly the same commands (This is not actually implemented). 
+Even if another databse is configured, the local SQLite file will be used.
+
+
+InfluxDB
+--------------------
+Work is done using InfluxDB v2.0 OSS. 
+My setup is a RaspberryPi 4 running Ubuntu Server 64-bit
+
+InfluxDB is installed on the RPi using default options.
+BAC0 will point to a Bucket (ex. named BAC0) using a token created 
+in the InfluxDB web interface (ex. http://ip_of_rpi:8086)
+
+To create the dashbpard, I use [Grafana](https://grafana.com/oss/)
+which is also installed on the same RaspberryPi. (ex. http://ip_of_rpi:3000)
+
+.. note:: 
+    The python client used works also for InfluxDB v1.8+. Connecting to this version
+    is supported and you must pass a username and a password in db_params
+
+Connection 
+............
+For BAC0 to connect to the inlfuxDB server, it needs to know where to send the data.
+This information can be given by using a dict ::
+
+    _params = {"name": "InfluxDB",
+               "url" : "http://ip_of_rpi",
+               "port" : 8086,
+               "token" : "token_created in influxDB web interface",
+               "org" : "the organization you created",
+               "bucket" : "BAC0"
+               # (V1.8) "user" : " ",
+               # (v1.8) "password" : "",
+               }
+
+Then you pass this information when you instanciate `bacnet`
+
+    bacnet = BAC0.lite(db_params=_params)
+
+The information can also be provided as environment variables. In that
+case, you must still provide name and bucket ::
+
+    _params = {"name": "InfluxDB",
+               "bucket" : "BAC0"
+               }
+
+To use environment variables, BAC0 will count on python-dotenv to 
+load a .env file in the folder when BAC0 is used.
+
+The .env file must contain ::
+
+    # InfluxDB Params Example .env file
+    INFLUXDB_V2_URL=http://192.168.1.10:8086
+    INFLUXDB_V2_ORG=my-org
+    INFLUXDB_V2_TOKEN=123456789abcdefg
+    # INFLUXDB_V2_TIMEOUT= 
+    # INFLUXDB_V2_VERIFY_SSL= 
+    # INFLUXDB_V2_SSL_CA_CERT= 
+    # INFLUXDB_V2_CONNECTION_POOL_MAXSIZE= 
+    # INFLUXDB_V2_AUTH_BASIC=
+    # INFLUXDB_V2_PROFILERS=
+
+.. note:: 
+    The name parameters in db_params would be use if any other implementation is made for another product.
+    For now, only InfluxDB is valid. 
+
+Write Options configuration
+............................
+Other options can be provided in the db_parmas dict to fine tune the configuration of the write_api.
+
+    * batch_size (default = 25)
+    * flush_interval (default =10 000)
+    * jitter_interval (default = 2 000)
+    * retry_interval (default = 5 000)
+    * max_retries (default = 5)
+    * max_retry_delay (default = 30 000)
+    * exponential_base (default = 2)
+
+Please refer to InfluxDB documentation for all the details regarding those parameters.
+
+ex. ::
+
+        _params = {"name": "InfluxDB",
+                "bucket" : "BAC0",               
+                "batch_size" : 25,
+                "flush_interval" : 10000,
+                "jitter_interval" : 2000,
+                "retry_interval" : 5000,
+                "max_retries" : 5,
+                "max_retry_delay" : 30000,
+                "exponential_base" : 2,
+               }
+
+Timestamp
+..............
+Now all timestamps in BAC0 will be timezone aware. As long as you are using 
+in-memory data, the actual timezone will be used. I didn't want to mess with 
+the timestamp for day to day work requiring only quick histories and minor tests.
+But all timestamps that will be sent to InfluxDB will be converted to UTC. 
+This is a requirement and makes things work well with Grafana.
+
+API
+.............
+BAC0 will use the Python package named influxdb-client, which must be pip installed.
+
+    pip install 'influxdb-client'
+
+Refer to [documentation](https://github.com/influxdata/influxdb-client-python) for details.
+
+In my actual tests, I haven't work with ciso8601, RxPy neither. 
+
+The API will accumulate write requests and write them in batch that are configurable. The actual 
+implementation use 25 as the batch parameters. This is subject to change.
+
+Write all
+.............
+I have included a function that write all histories to InfluxDB. This function takes
+all the Pandas Series and turn them into a DataFrame which is then sent to InfluxDB.
+
+I'm not sure if it's really useful as the polling takes care of sending the data 
+constantly. 
+
+Write to the database
+........................
+Each call to `_trend` (which add a record in memory) will call a write request to the API if the
+database is defined.
+
+ID of the record
+.................
+The ID of the record will be ::
+
+    Device_{device_id}/{object} 
+
+For example ::
+
+    Device_5004/analogInput:1
+
+This choice was made to make sure all records ID were unique as using name could lead to errors. As name, 
+device name, etc are provided as tags, I suggest using them in the Flux requests. 
+
+Tags and fields
+..................
+InfluxDB allows the usage of tags and multiple fields for values. This allows making requests 
+based on tags when creating dashboard. I chose to add some information in the form of tags 
+when writing to the database : 
+
+ * object_name
+ * description
+ * units_state (units of measure or state text for multiState and Binary)
+ * object instance (ex. analogInput:1)
+ * device_name (the name of the controller)
+ * device_id (the device instance)
+
+value
+...........
+
+Two value fields are included. A value field and a string_value field.
+This way, when working with binary or multistate, it's possible to use
+aggregation functions using the numerical value (standard value), but it is
+also possible to make database request on the string_value field and get 
+a more readable result (ex. Occupied instead of 0)
+
diff --git a/doc/source/index.rst b/doc/source/index.rst
index f6450265..5a0de041 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -26,6 +26,7 @@ Table of contents
    schedules
    cov
    savedevice
+   database
    local_objects
    webinterface
    jupyter
diff --git a/requirements.txt b/requirements.txt
index babe9bf5..03844075 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 bacpypes>=0.18.3
 colorama
+pytz
diff --git a/samples/excel_from_discovered_devices.py b/samples/excel_from_discovered_devices.py
new file mode 100644
index 00000000..dd35a898
--- /dev/null
+++ b/samples/excel_from_discovered_devices.py
@@ -0,0 +1,63 @@
+import BAC0
+import time
+import pandas as pd
+from BAC0.core.proprietary_objects.jci import tec_short_point_list
+
+"""
+This sample creates an Excel file containing one sheet per controller found on the network
+Each sheet contyains all the points known by BAC0. Some proprietary point could not display here.
+"""
+
+EXCEL_FILE_NAME = "all_controllers_and_points.xlsx"
+
+
+def create_data(discovered_devices, network):
+    devices = {}
+    dataframes = {}
+    for each in discovered_devices:
+        name, vendor, address, device_id = each
+
+        # try excep eventually as we may have some issues with werid devices
+        if "TEC3000" in name:
+            custom_obj_list = tec_short_point_list()
+        else:
+            custom_obj_list = None
+        devices[name] = BAC0.device(
+            address, device_id, network, poll=0, object_list=custom_obj_list
+        )
+
+        # While we are here, make a dataframe with device
+        dataframes[name] = make_dataframe(devices[name])
+    return (devices, dataframes)
+
+
+def make_dataframe(dev):
+    lst = {}
+    for each in dev.points:
+        lst[each.properties.name] = {
+            "value": each.lastValue,
+            "units or states": each.properties.units_state,
+            "description": each.properties.description,
+            "object": "{}:{}".format(each.properties.type, each.properties.address),
+        }
+    df = pd.DataFrame.from_dict(lst, orient="index")
+    return df
+
+
+def make_excel(dfs):
+    with pd.ExcelWriter(EXCEL_FILE_NAME) as writer:
+        for k, v in dfs.items():
+            v.to_excel(writer, sheet_name=k)
+
+
+def main():
+    bacnet = BAC0.lite()
+    bacnet.discover()
+    print("Creating DATA")
+    devices, dataframes = create_data(bacnet.devices, network=bacnet)
+    print("Creating and Excel File")
+    make_excel(dataframes)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.py b/setup.py
index 1ff69e95..74fda5b3 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@
         "BAC0.scripts",
         "BAC0.tasks",
         "BAC0.web",
-        "BAC0.sql",
+        "BAC0.db",
     ],
     include_package_data=True,
     requires=requirements,


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 464e56d9..b431f7dd 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -35,6 +35,7 @@ jobs:
         pip install flask_bootstrap
         pip install colorama
         pip install netifaces
+        pip install python-dotenv
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
         pip install .
     - name: Lint with flake8
@@ -68,4 +69,4 @@ jobs:
 #      env:
 #        GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
 #        COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
-#        COVERALLS_PARALLEL: true
\ No newline at end of file
+#        COVERALLS_PARALLEL: true
diff --git a/tests/test_SaveToSQL.py b/tests/test_SaveToSQL.py
index 4cc88c62..2bb9e038 100644
--- a/tests/test_SaveToSQL.py
+++ b/tests/test_SaveToSQL.py
@@ -5,6 +5,7 @@
 Test Bacnet communication with another device
 """
 import os.path
+import BAC0
 
 
 def test_SaveToSQL(network_and_devices):
@@ -17,3 +18,20 @@ def test_SaveToSQL(network_and_devices):
     assert os.path.isfile("{}.db".format(test_device.properties.db_name))
     assert os.path.isfile("{}.db".format("obj30"))
     # assert os.path.isfile("{}.db".format("obj300"))
+
+
+def test_disconnection_of_device(network_and_devices):
+    test_device = network_and_devices.test_device
+    test_device_30 = network_and_devices.test_device_30
+    test_network = network_and_devices.bacnet
+    # test_device_300 = network_and_devices.test_device_300
+    test_device.disconnect()
+    test_device_30.disconnect()
+    # test_device_300.save(filename="obj300")
+    assert isinstance(test_device, BAC0.core.devices.Device.DeviceFromDB)
+    assert isinstance(test_device_30, BAC0.core.devices.Device.DeviceFromDB)
+    # assert os.path.isfile("{}.db".format("obj300"))
+    test_device.connect(network=test_network)
+    test_device_30.connect(network=test_network)
+    assert isinstance(test_device, BAC0.core.devices.Device.RPMDeviceConnected)
+    assert isinstance(test_device_30, BAC0.core.devices.Device.RPMDeviceConnected)
diff --git a/tests/test_Write.py b/tests/test_Write.py
index e4bcff9b..66b7f9b7 100644
--- a/tests/test_Write.py
+++ b/tests/test_Write.py
@@ -16,7 +16,7 @@ def test_WriteAV(network_and_devices):
     test_device = network_and_devices.test_device
     old_value = test_device["AV"].value
     test_device["AV"] = 11.2
-    time.sleep(1.5) # or cache will play a trick on you
+    time.sleep(1.5)  # or cache will play a trick on you
     new_value = test_device["AV"].value
     assert (new_value - 11.2) < 0.01