Repository: ChristianTremblay/BAC0 (language: python)

Issue:
Adjusting spin parameters
Issue #341


problem 'read_weeklySchedule' with incomplete instance
Version 21.12.03

There are 2 problems in __Schedule.py__ when the instance is incomplete:
1. If the instance has no current value (presentValue), an error is thrown (line 264): 
AttributeError: 'NoneType' object has no attribute 'value'
2. If a DailySchedule has no value, an error is thrown (line 287): 
TypeError: float() argument must be a string or a real number, not 'tuple'

I made the following changes (suggestion) which fixed the problems for me:

suggestion for problem 1:
```
262       else:
263            schedule["states"] = _state_text
264            if presentValue is not None:  # added
265                presentValue = "{}".format(presentValue.value)
```

suggestion for problem 2:
```
284            try:
285                _value = None  # added
286                if each.value.value:  # added
287                    if states == ["inactive", "active"]:
```

The output (JSON) with null values:
```
{
    "weeklySchedule": {
        "object_references": [
            [
                "analogValue",
                52
            ]
        ],
        "references_names": [
            "photovoltaic.001.discharge-scheduler-out"
        ],
        "states": "analog",
        "reliability": "noFaultDetected",
        "priority": 16,
        "presentValue": null,
        "week": {
            "monday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "tuesday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "wednesday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "thursday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "friday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "saturday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ],
            "sunday": [
                [
                    "07:00",
                    20.0
                ],
                [
                    "18:00",
                    null
                ]
            ]
        }
    }
}
```



Difference latency in Read Services
Hello,

I'm using BAC0 for logging data in SCADA-networks. High resolution of the measurement data is required for data analysis.  Therefore, it should be possible to read data with one second per cycle. My program is starting a thread in a specified cycle. In this thread, the data points are updated with the Read Services.

---
The pictures below shows the updating process of seven data points in a four seconds cycle. Each point in the diagram is a Read Services (diagram1: ReadProperty; diagram2: ReadPropertyMultiple) and the y-axis is shown the latency between the read-request and returning a value.

###### Latency ReadProperty
<img width="451" alt="ReadProperty" src="https://user-images.githubusercontent.com/72434534/185089187-bc10afcd-f4d1-40b7-8c29-cf8a349b1119.PNG">

###### Latency ReadPropertyMultiple
<img width="443" alt="ReadMultiple" src="https://user-images.githubusercontent.com/72434534/185089474-9ad35153-4380-40c1-9da2-6c9f676ee3b3.PNG">

---
## Problem
I would expect the latencies to be in the 100ms range. But always the first value to be read in the new thread takes up to a second.
The problem seems to be with IOCB.
```
iocb.wait()  # Wait for BACnet response
```
If I analyze the network-packages with Wireshark I can see that the bacnet-device answers the read request with few milliseconds.

> #### So why does it take sometimes more than one second, from request to return the value?

---
To verify that this behavior is due to the library, I tried this:
- different devices and data points
- increase of the scanning cycle
- run read()/ readMultiple() and meusure latency manual on cmd
- check real latency of bacnet-messages with Wireshark

--> But the result is always the same.

---

Thank you!
Difference latency in Read Services
Hello,

I'm using BAC0 for logging data in SCADA-networks. High resolution of the measurement data is required for data analysis.  Therefore, it should be possible to read data with one second per cycle. My program is starting a thread in a specified cycle. In this thread, the data points are updated with the Read Services.

---
The pictures below shows the updating process of seven data points in a four seconds cycle. Each point in the diagram is a Read Services (diagram1: ReadProperty; diagram2: ReadPropertyMultiple) and the y-axis is shown the latency between the read-request and returning a value.

###### Latency ReadProperty
<img width="451" alt="ReadProperty" src="https://user-images.githubusercontent.com/72434534/185089187-bc10afcd-f4d1-40b7-8c29-cf8a349b1119.PNG">

###### Latency ReadPropertyMultiple
<img width="443" alt="ReadMultiple" src="https://user-images.githubusercontent.com/72434534/185089474-9ad35153-4380-40c1-9da2-6c9f676ee3b3.PNG">

---
## Problem
I would expect the latencies to be in the 100ms range. But always the first value to be read in the new thread takes up to a second.
The problem seems to be with IOCB.
```
iocb.wait()  # Wait for BACnet response
```
If I analyze the network-packages with Wireshark I can see that the bacnet-device answers the read request with few milliseconds.

> #### So why does it take sometimes more than one second, from request to return the value?

---
To verify that this behavior is due to the library, I tried this:
- different devices and data points
- increase of the scanning cycle
- run read()/ readMultiple() and meusure latency manual on cmd
- check real latency of bacnet-messages with Wireshark

--> But the result is always the same.

---

Thank you!


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 7bcb8ceb..f3f57e12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,7 @@ tests/manual_test_bokeh.py
 .env
 samples/pascal_stlouis.py
 samples/johnhensen.py
+/samples/Climatic-chamber-.ipynb
+samples/bartinglmqtt.py
+samples/bartling2.py
+samples/ferkelcode.py
diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py
index 504d1ea3..8ca60689 100755
--- a/BAC0/core/devices/Device.py
+++ b/BAC0/core/devices/Device.py
@@ -811,7 +811,11 @@ def ping(self):
                 self.properties.ping_failures += 1
                 return False
         except NoResponseFromController as e:
-            self._log.error("Error in ping : {}".format(e))
+            self._log.error(
+                "{} ({})| Ping failure ({}).".format(
+                    self.properties.name, self.properties.address, e
+                )
+            )
             self.properties.ping_failures += 1
             return False
 
@@ -883,11 +887,10 @@ def connect(self, *, db=None):
                 else:
                     segmentation_supported = True
 
-                if name:
-                    if segmentation_supported:
-                        self.new_state(RPMDeviceConnected)
-                    else:
-                        self.new_state(RPDeviceConnected)
+                if segmentation_supported:
+                    self.new_state(RPMDeviceConnected)
+                else:
+                    self.new_state(RPDeviceConnected)
 
             except SegmentationNotSupported:
                 self.segmentation_supported = False
diff --git a/BAC0/core/devices/Trends.py b/BAC0/core/devices/Trends.py
index 4693e24e..31628e61 100755
--- a/BAC0/core/devices/Trends.py
+++ b/BAC0/core/devices/Trends.py
@@ -118,10 +118,12 @@ def update_properties(self):
             raise Exception("Problem reading trendLog informations: {}".format(error))
 
     def _total_record_count(self):
-        self.properties.total_record_count = self.properties.device.properties.network.read(
-            "{addr} trendLog {oid} totalRecordCount".format(
-                addr=self.properties.device.properties.address,
-                oid=str(self.properties.oid),
+        self.properties.total_record_count = (
+            self.properties.device.properties.network.read(
+                "{addr} trendLog {oid} totalRecordCount".format(
+                    addr=self.properties.device.properties.address,
+                    oid=str(self.properties.oid),
+                )
             )
         )
         return self.properties.total_record_count
diff --git a/BAC0/core/devices/local/object.py b/BAC0/core/devices/local/object.py
index 5f0d6955..b0be58e5 100644
--- a/BAC0/core/devices/local/object.py
+++ b/BAC0/core/devices/local/object.py
@@ -2,7 +2,11 @@
 
 from ...utils.notes import note_and_log
 from ....scripts.Base import Base
-from ...app.ScriptApplication import BAC0Application, BAC0BBMDDeviceApplication, BAC0ForeignDeviceApplication
+from ...app.ScriptApplication import (
+    BAC0Application,
+    BAC0BBMDDeviceApplication,
+    BAC0ForeignDeviceApplication,
+)
 
 from bacpypes.object import (
     AnalogInputObject,
@@ -182,7 +186,11 @@ def clear_objects():
     def add_objects_to_application(self, app):
         if isinstance(app, Base):
             app = app.this_application
-        if not (isinstance(app, BAC0Application) or isinstance(app, BAC0ForeignDeviceApplication) or isinstance(app, BAC0BBMDDeviceApplication)):
+        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 efbed224..56ff1903 100755
--- a/BAC0/core/devices/mixins/read_mixin.py
+++ b/BAC0/core/devices/mixins/read_mixin.py
@@ -5,7 +5,7 @@
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
 """
-read_mixin.py - Add ReadProperty and ReadPropertyMultiple to a device 
+read_mixin.py - Add ReadProperty and ReadPropertyMultiple to a device
 """
 # --- standard Python modules ---
 
@@ -203,6 +203,11 @@ def _discoverPoints(self, custom_object_list=None):
                 obj_cls=EnumPoint, obj_type="multi", objList=objList
             )
         )
+        points.extend(
+            self._process_new_objects(
+                obj_cls=NumericPoint, obj_type="loop", objList=objList
+            )
+        )
         points.extend(
             self._process_new_objects(
                 obj_cls=StringPoint, obj_type="characterstringValue", objList=objList
@@ -259,6 +264,8 @@ def _process_new_objects(
             prop_list = "objectName presentValue inactiveText activeText description"
         elif obj_type == "multi":
             prop_list = "objectName presentValue stateText description"
+        elif obj_type == "loop":
+            prop_list = "objectName presentValue description"
         elif obj_type == "characterstringValue":
             prop_list = "objectName presentValue"
         elif obj_type == "datetimeValue":
@@ -296,7 +303,7 @@ def _find_propid_index(key):
             pointName = point_infos[_find_propid_index("objectName")]
             presentValue = point_infos[_find_propid_index("presentValue")]
             if presentValue != None:
-                if obj_type == "analog":
+                if obj_type == "analog" or obj_type == "loop":
                     presentValue = float(presentValue)
                 elif obj_type == "multi":
                     presentValue = int(presentValue)
@@ -362,6 +369,10 @@ def _process_new_objects(
                 units_state = self.read_single(
                     "{} {} stateText ".format(point_type, point_address)
                 )
+            elif obj_type == "loop":
+                units_state = self.read_single(
+                    "{} {} units ".format(point_type, point_address)
+                )
             elif obj_type == "binary":
                 units_state = (
                     (
@@ -381,7 +392,7 @@ def _process_new_objects(
             presentValue = self.read_single(
                 "{} {} presentValue ".format(point_type, point_address)
             )
-            if obj_type == "analog":
+            if (obj_type == "analog" or obj_type == "loop") and presentValue:
                 presentValue = float(presentValue)
 
             _newpoints.append(
diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py
index 5afaf2ce..f936aa9c 100644
--- a/BAC0/core/functions/DeviceCommunicationControl.py
+++ b/BAC0/core/functions/DeviceCommunicationControl.py
@@ -56,9 +56,9 @@ def dcc(self, address=None, duration=None, password=None, state=None):
 
         # build a request
         request = DeviceCommunicationControlRequest()
-        request.enableDisable = DeviceCommunicationControlRequestEnableDisable.enumerations[
-            state
-        ]
+        request.enableDisable = (
+            DeviceCommunicationControlRequestEnableDisable.enumerations[state]
+        )
         request.pduDestination = Address(address)
         if duration:
             request.duration = Unsigned16(duration)
diff --git a/BAC0/core/functions/Reinitialize.py b/BAC0/core/functions/Reinitialize.py
index f96a2b2b..513f388d 100644
--- a/BAC0/core/functions/Reinitialize.py
+++ b/BAC0/core/functions/Reinitialize.py
@@ -52,9 +52,9 @@ def reinitialize(self, address=None, password=None, state="coldstart"):
 
         # build a request
         request = ReinitializeDeviceRequest()
-        request.reinitializedStateOfDevice = ReinitializeDeviceRequestReinitializedStateOfDevice.enumerations[
-            state
-        ]
+        request.reinitializedStateOfDevice = (
+            ReinitializeDeviceRequestReinitializedStateOfDevice.enumerations[state]
+        )
         request.pduDestination = Address(address)
         request.password = CharacterString(password)
 
diff --git a/BAC0/core/functions/Schedule.py b/BAC0/core/functions/Schedule.py
index de04e5f9..3f1d59ce 100644
--- a/BAC0/core/functions/Schedule.py
+++ b/BAC0/core/functions/Schedule.py
@@ -109,7 +109,7 @@ def _set_value(v):
                 if dict_schedule["states"] == ["inactive", "active"]:
                     return Integer(dict_schedule["states"][v])
                 else:
-                    return Integer(dict_schedule["states"][v] - 1)
+                    return Enumerated(dict_schedule["states"][v] - 1)
 
         daily_schedules = []
         for day in Schedule.days:
@@ -261,7 +261,8 @@ def _read(prop):
                 presentValue = presentValue.value
         else:
             schedule["states"] = _state_text
-            presentValue = "{}".format(presentValue.value)
+            if presentValue is not None:
+                presentValue = "{}".format(presentValue.value)
         schedule["reliability"] = reliability
         schedule["priority"] = priority
         schedule["presentValue"] = presentValue
@@ -281,12 +282,14 @@ def decode_dailySchedule(self, dailySchedule, states, offset_MV):
             hour, minute, second, hundreth = each.time
             _time = dt_time(hour, minute, second, hundreth).strftime("%H:%M")
             try:
-                if states == ["inactive", "active"]:
-                    _value = states[int(each.value.value)]
-                elif states == "analog":
-                    _value = float(each.value.value)
-                else:
-                    _value = states[int(each.value.value) - offset_MV]
+                _value = None
+                if each.value.value is not None:
+                    if states == ["inactive", "active"]:
+                        _value = states[int(each.value.value)]
+                    elif states == "analog":
+                        _value = float(each.value.value)
+                    else:
+                        _value = states[int(each.value.value) - offset_MV]
                 events.append((_time, _value))
             except IndexError:
                 events.append((_time, "??? ({})".format(each.value.value)))
diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py
index 743f4337..8d85a969 100644
--- a/BAC0/core/functions/TimeSync.py
+++ b/BAC0/core/functions/TimeSync.py
@@ -156,7 +156,7 @@ def local_date(self):
 
     def utcOffset(self):
         "Returns UTC offset in minutes"
-        return self.now.astimezone().utcoffset().total_seconds() / 60
+        return round(self.now.astimezone().utcoffset().total_seconds() / 60)
 
     def is_dst(self):
         return self.timezone.dst(self.now) != dt.timedelta(0)
diff --git a/BAC0/core/proprietary_objects/jci.py b/BAC0/core/proprietary_objects/jci.py
index 71545a03..c7fde5d7 100644
--- a/BAC0/core/proprietary_objects/jci.py
+++ b/BAC0/core/proprietary_objects/jci.py
@@ -36,231 +36,231 @@
     "objectType": "device",
     "bacpypes_type": DeviceObject,
     "properties": {
-        "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},
+        "ALARM_STATE": {"obj_id": 1006, "datatype": Enumerated, "mutable": False},
+        "ARCHIVE_DATE": {"obj_id": 849, "datatype": Date, "mutable": False},
+        "ARCHIVE_STATUS": {"obj_id": 1187, "datatype": Unsigned, "mutable": False},
+        "ARCHIVE_TIME": {"obj_id": 850, "datatype": Time, "mutable": False},
+        "CPU_USAGE": {"obj_id": 2583, "datatype": Real, "mutable": False},
+        "ENABLED": {"obj_id": 673, "datatype": Boolean, "mutable": True},
+        "ENABLED": {"obj_id": 673, "datatype": Boolean, "mutable": True},
         "EXECUTION_PRIORITY": {
             "obj_id": 2197,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # 0-3
         "EXTENDED_PROTO_VERSION": {
             "obj_id": 2291,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
-        "FLASH_USAGE": {"obj_id": 2584, "primitive": Real, "mutable": False},
+        "FLASH_USAGE": {"obj_id": 2584, "datatype": Real, "mutable": False},
         "ITEM_REFERENCE": {
             "obj_id": 32527,
-            "primitive": CharacterString,
+            "datatype": CharacterString,
             "mutable": False,
         },
-        "JCI_STATUS": {"obj_id": 847, "primitive": Enumerated, "mutable": False},
-        "MEMORY_USAGE": {"obj_id": 2581, "primitive": Real, "mutable": False},
+        "JCI_STATUS": {"obj_id": 847, "datatype": Enumerated, "mutable": False},
+        "MEMORY_USAGE": {"obj_id": 2581, "datatype": Real, "mutable": False},
         "OBJECT_CATEGORY": {
             "obj_id": 908,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # 0-249
-        "OBJECT_MEMORY_USAGE": {"obj_id": 2582, "primitive": Real, "mutable": False},
-        "STATUS": {"obj_id": 512, "primitive": Enumerated, "mutable": False},
+        "OBJECT_MEMORY_USAGE": {"obj_id": 2582, "datatype": Real, "mutable": False},
+        "STATUS": {"obj_id": 512, "datatype": Enumerated, "mutable": False},
         "BACNET_BROADCAST_RECEIVE_RATE": {
             "obj_id": 745,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "DEFAULT_BASE_UNITS": {
             "obj_id": 2206,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": False,
         },
         "ESTIMATED_FLASH_AVAILABLE": {
             "obj_id": 2395,
-            "primitive": Real,
+            "datatype": Real,
             "mutable": False,
         },
-        "LAST_IDLE_SAMPLE": {"obj_id": 30082, "primitive": Real, "mutable": False},
+        "LAST_IDLE_SAMPLE": {"obj_id": 30082, "datatype": Real, "mutable": False},
         "MAX_MESSAGE_BUFFER": {
             "obj_id": 848,
-            "primitive": Unsigned,
+            "datatype": 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},
+        "USER_NAME": {"obj_id": 2390, "datatype": CharacterString, "mutable": False},
+        "PCODE": {"obj_id": 1320, "datatype": CharacterString, "mutable": False},
+        # "SAB_DEVICE_STATUS_LIST": {"obj_id": 4513, "datatype": Complex, "mutable": False},
         "SAB_DEVICE_STATUS_LIST_CHANGED ": {
             "obj_id": 4514,
-            "primitive": Unsigned,
+            "datatype": 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},
+        "EVENTS_LOST ": {"obj_id": 1479, "datatype": Unsigned, "mutable": False},
+        # "STANDARD_TIME_OFFSET ": {"obj_id": 1017, "datatype": Signed, "mutable": False}, # -900-900
+        # "DAYLIGHT_SAVING_TIME_OFFSET ": {"obj_id": 1093, "datatype": Signed, "mutable": False}, # -900-900
+        # "STANDARD_TIME_START ": {"obj_id": 988, "datatype": Complex, "mutable": True},
+        # "DAYLIGHT_SAVING_TIME_START ": {"obj_id": 1040, "datatype": Complex, "mutable": True},
         "ACCEPT_BACNET_TIME_SYNC": {
             "obj_id": 4970,
-            "primitive": Boolean,
+            "datatype": Boolean,
             "mutable": False,
         },
-        # "LAST_BACNET_TIME_SYNC_RECEIVED ": {"obj_id": 5728, "primitive": Complex, "mutable": False},
+        # "LAST_BACNET_TIME_SYNC_RECEIVED ": {"obj_id": 5728, "datatype": Complex, "mutable": False},
         "SUPERVISORY_DEVICE_ONLINE": {
             "obj_id": 3653,
-            "primitive": Boolean,
+            "datatype": Boolean,
             "mutable": True,
         },
         "SUPERVISORY_OFFLINE_TIMEOUT": {
             "obj_id": 6002,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },  # 1-255
         "NEXT_AVAILABLE_OID": {
             "obj_id": 787,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },  # 30,000,001-41,799,991
         "HAS_UNBOUND_REFERENCES": {
             "obj_id": 767,
-            "primitive": Boolean,
+            "datatype": 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},
+        "SURROGATE_CACHE_CNT": {"obj_id": 571, "datatype": Unsigned, "mutable": False},
+        "SURROGATE_CACHE_MAX": {"obj_id": 639, "datatype": Unsigned, "mutable": False},
+        # "ASSET_VERSIONS": {"obj_id": 4960, "datatype": Complex, "mutable": False},
         "LOAD_BALANCER_LEVEL": {
             "obj_id": 4722,
-            "primitive": Enumerated,
+            "datatype": 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_DB_SIZE": {"obj_id": 733, "datatype": Unsigned, "mutable": False},
+        "TIMER_USED": {"obj_id": 734, "datatype": Unsigned, "mutable": False},
+        "TIMER_PEAK": {"obj_id": 735, "datatype": Unsigned, "mutable": False},
         "TIMER_MESSAGES_ABORTED": {
             "obj_id": 4959,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "BACNET_OID_ALLOCATED": {
             "obj_id": 1291,
-            "primitive": Unsigned,
+            "datatype": 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_OID_USED": {"obj_id": 1292, "datatype": Unsigned, "mutable": False},
+        "SIGN_PRI_DB_SIZE": {"obj_id": 730, "datatype": Unsigned, "mutable": False},
+        "SIGN_PRI_USED": {"obj_id": 731, "datatype": Unsigned, "mutable": False},
+        "SIGN_PRI_PEAK": {"obj_id": 732, "datatype": Unsigned, "mutable": False},
         "BACNET_ENCODE_TYPE": {
             "obj_id": 32578,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # 0-3
         "ROUTING_MODE": {
             "obj_id": 4307,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # 0-2
         "BACNET_INTEGRATED_OBJECTS": {
             "obj_id": 4302,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": False,
         },
-        "BACNET_COMPATIBLE": {"obj_id": 4581, "primitive": Boolean, "mutable": False},
+        "BACNET_COMPATIBLE": {"obj_id": 4581, "datatype": Boolean, "mutable": False},
         "SEND_I_AM_RATE": {
             "obj_id": 579,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },  # 60-86,400
         "DEFAULT_TIME_ZONE": {
             "obj_id": 32583,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # #-1,101
         "TIME_ZONE": {
             "obj_id": 1403,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": True,
         },  # #-1,101
-        "LOCAL_TIME_ZONE": {"obj_id": 1404, "primitive": Enumerated, "mutable": False},
+        "LOCAL_TIME_ZONE": {"obj_id": 1404, "datatype": Enumerated, "mutable": False},
         "FC_MULTICAST_RESPONDER": {
             "obj_id": 3390,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },
         "FC_WAIT_BEFORE_POLLING": {
             "obj_id": 3391,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },
         "SUPERVISOR_MAC_ADDRESS": {
             "obj_id": 3652,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "LIBRARY_PART_ID": {
             "obj_id": 3295,
-            "primitive": CharacterString,
+            "datatype": CharacterString,
             "mutable": False,
         },
         "APPLICATION_CLASS_SET_VERSION": {
             "obj_id": 4128,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "DEVICE_MODEL_CLASS_SET_VERSION": {
             "obj_id": 4129,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "COV_MIN_SEND_TIME": {
             "obj_id": 3929,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },  # 10-255
         "COV_TRANSMITS_PER_MINUTE": {
             "obj_id": 651,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "CONTROL_SEQUENCE_IN_TEST": {
             "obj_id": 3651,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": True,
         },
-        "END_OF_LINE": {"obj_id": 603, "primitive": Boolean, "mutable": False},
-        "DEVICE_ADDRESS": {"obj_id": 876, "primitive": Unsigned, "mutable": False},
+        "END_OF_LINE": {"obj_id": 603, "datatype": Boolean, "mutable": False},
+        "DEVICE_ADDRESS": {"obj_id": 876, "datatype": Unsigned, "mutable": False},
         "FC_BUS_COMMUNICATION_MODE": {
             "obj_id": 4400,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": False,
         },
-        "SYSTEM_TYPE": {"obj_id": 3900, "primitive": Unsigned, "mutable": False},
+        "SYSTEM_TYPE": {"obj_id": 3900, "datatype": Unsigned, "mutable": False},
         "SYSTEM_CONFIGURATION": {
             "obj_id": 3899,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "SABusPerformance": {
             "obj_id": 12157,
-            "primitive": Enumerated,
+            "datatype": Enumerated,
             "mutable": False,
         },
         "SABusTokenLoopTime": {
             "obj_id": 12158,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "SABusCOVRcvPerMinute": {
             "obj_id": 12159,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
         "SABusCOVWritesPerMinute": {
             "obj_id": 12160,
-            "primitive": Unsigned,
+            "datatype": Unsigned,
             "mutable": False,
         },
     },
@@ -274,12 +274,12 @@
     "objectType": "analogValue",
     "bacpypes_type": AnalogValueObject,
     "properties": {
-        "FLOW-SP_EEPROM": {"obj_id": 3113, "primitive": Real, "mutable": True},
-        "Offset": {"obj_id": 956, "primitive": Real, "mutable": True},
-        "Offline": {"obj_id": 913, "primitive": Boolean, "mutable": False},
-        "SABusAddr": {"obj_id": 3645, "primitive": Unsigned, "mutable": False},
-        "PeerToPeer": {"obj_id": 748, "primitive": Atomic, "mutable": False},
-        "P2P_ErrorStatus": {"obj_id": 746, "primitive": Enumerated, "mutable": False},
+        "FLOW-SP_EEPROM": {"obj_id": 3113, "datatype": Real, "mutable": True},
+        "Offset": {"obj_id": 956, "datatype": Real, "mutable": True},
+        "Offline": {"obj_id": 913, "datatype": Boolean, "mutable": False},
+        "SABusAddr": {"obj_id": 3645, "datatype": Unsigned, "mutable": False},
+        "PeerToPeer": {"obj_id": 748, "datatype": Atomic, "mutable": False},
+        "P2P_ErrorStatus": {"obj_id": 746, "datatype": Enumerated, "mutable": False},
     },
 }
 
@@ -289,13 +289,13 @@
     "objectType": "analogInput",
     "bacpypes_type": AnalogInputObject,
     "properties": {
-        "Offset": {"obj_id": 956, "primitive": Real, "mutable": True},
-        "Offline": {"obj_id": 913, "primitive": Boolean, "mutable": False},
-        "SABusAddr": {"obj_id": 3645, "primitive": Unsigned, "mutable": False},
-        "InputRangeLow": {"obj_id": 1293, "primitive": Real, "mutable": True},
-        "InputRangeHigh": {"obj_id": 1294, "primitive": Real, "mutable": True},
-        "OutputRangeLow": {"obj_id": 1295, "primitive": Real, "mutable": True},
-        "OutputRangeHigh": {"obj_id": 1296, "primitive": Real, "mutable": True},
+        "Offset": {"obj_id": 956, "datatype": Real, "mutable": True},
+        "Offline": {"obj_id": 913, "datatype": Boolean, "mutable": False},
+        "SABusAddr": {"obj_id": 3645, "datatype": Unsigned, "mutable": False},
+        "InputRangeLow": {"obj_id": 1293, "datatype": Real, "mutable": True},
+        "InputRangeHigh": {"obj_id": 1294, "datatype": Real, "mutable": True},
+        "OutputRangeLow": {"obj_id": 1295, "datatype": Real, "mutable": True},
+        "OutputRangeHigh": {"obj_id": 1296, "datatype": Real, "mutable": True},
     },
 }
 
@@ -305,14 +305,14 @@
     "objectType": "analogOutput",
     "bacpypes_type": AnalogOutputObject,
     "properties": {
-        "Offline": {"obj_id": 913, "primitive": Boolean, "mutable": False},
-        "SABusAddr": {"obj_id": 3645, "primitive": Unsigned, "mutable": False},
-        "InputRangeLow": {"obj_id": 1293, "primitive": Real, "mutable": True},
-        "InputRangeHigh": {"obj_id": 1294, "primitive": Real, "mutable": True},
-        "OutputRangeLow": {"obj_id": 1295, "primitive": Real, "mutable": True},
-        "OutputRangeHigh": {"obj_id": 1296, "primitive": Real, "mutable": True},
-        "polarity": {"obj_id": "polarity", "primitive": Enumerated, "mutable": True},
-        "stroketime": {"obj_id": 3478, "primitive": Real, "mutable": True},
+        "Offline": {"obj_id": 913, "datatype": Boolean, "mutable": False},
+        "SABusAddr": {"obj_id": 3645, "datatype": Unsigned, "mutable": False},
+        "InputRangeLow": {"obj_id": 1293, "datatype": Real, "mutable": True},
+        "InputRangeHigh": {"obj_id": 1294, "datatype": Real, "mutable": True},
+        "OutputRangeLow": {"obj_id": 1295, "datatype": Real, "mutable": True},
+        "OutputRangeHigh": {"obj_id": 1296, "datatype": Real, "mutable": True},
+        "polarity": {"obj_id": "polarity", "datatype": Enumerated, "mutable": True},
+        "stroketime": {"obj_id": 3478, "datatype": Real, "mutable": True},
     },
 }
 
@@ -349,8 +349,8 @@ def tec_short_point_list(unit_type="2-pipe"):
         ("multiStateValue", 29709),
         ("multiStateValue", 29708),
         ("analogValue", 29505),
-        ("analogValue", 29502),
-        ("analogValue", 29503),
+        # ("analogValue", 29502),
+        # ("analogValue", 29503),
         ("analogValue", 29504),
         ("analogValue", 29506),
         ("analogValue", 29507),
diff --git a/BAC0/core/proprietary_objects/object.py b/BAC0/core/proprietary_objects/object.py
index 833cee40..c66fc022 100644
--- a/BAC0/core/proprietary_objects/object.py
+++ b/BAC0/core/proprietary_objects/object.py
@@ -7,14 +7,22 @@
 
 # Prochaine étape : créer une focntion qui va lire "all" et se redéfinir dynamiquement
 def create_proprietary_object(params):
+
     try:
         _validate_params(params)
     except:
         raise
-    props = [
-        Property(v["obj_id"], v["primitive"], mutable=v["mutable"])
-        for k, v in params["properties"].items()
-    ]
+    # Prevent breaking change for existing code, since issue #311
+    try:
+        props = [
+            Property(v["obj_id"], v["datatype"], mutable=v["mutable"])
+            for k, v in params["properties"].items()
+        ]
+    except KeyError:
+        props = [
+            Property(v["obj_id"], v["primitive"], mutable=v["mutable"])
+            for k, v in params["properties"].items()
+        ]
 
     new_class = type(
         params["name"],
diff --git a/BAC0/db/influxdb.py b/BAC0/db/influxdb.py
index 88b16be9..21e85c22 100644
--- a/BAC0/db/influxdb.py
+++ b/BAC0/db/influxdb.py
@@ -78,13 +78,13 @@ def clean_value(self, object_type, val, units_state):
             _value = val
         elif "multi" in object_type:
             _string_value = "{}".format(val.split(":")[1])
-            _value = "{}".format(val.split(":")[0])
+            _value = int(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])
+            _value = int(val.split(":")[0])
         else:
             _string_value = "{}".format(val)
             _value = val
diff --git a/BAC0/db/sql.py b/BAC0/db/sql.py
index 816c18de..4d19e4a3 100644
--- a/BAC0/db/sql.py
+++ b/BAC0/db/sql.py
@@ -135,15 +135,18 @@ def extract_value_and_string(val):
                         resampling_freq
                     ).mean()
                 else:
-                    #backup[point.properties.name] = point.history.resample(
+                    # backup[point.properties.name] = point.history.resample(
                     #    resampling_freq
-                    #).last()
+                    # ).last()
                     continue
 
             except Exception as error:
                 self._log.error(
-                    "Error in resampling {} | {} (probably not enough points)".format(
-                        point.properties.name, error
+                    "{} ({}) | Error in resampling {} | {} (probably not enough points)".format(
+                        self.properties.device.properties.name,
+                        self.properties.device.properties.address,
+                        point.properties.name,
+                        error,
                     )
                 )
                 if (
@@ -166,7 +169,7 @@ def extract_value_and_string(val):
                         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()]))
diff --git a/BAC0/infos.py b/BAC0/infos.py
index 3e0bbe62..5edfc555 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.12.03"
+__version__ = "22.9.21dev"
 __license__ = "LGPLv3"
diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py
index bfa12f60..05c55961 100644
--- a/BAC0/scripts/Base.py
+++ b/BAC0/scripts/Base.py
@@ -19,39 +19,41 @@
         def startApp()
         def stopApp()
 """
-# --- standard Python modules ---
-from threading import Thread
 import random
 import sys
 
-# --- 3rd party modules ---
+# --- standard Python modules ---
+from threading import Thread
 
+from bacpypes.basetypes import DeviceStatus, ServicesSupported
+from bacpypes.core import enable_sleeping
 from bacpypes.core import run as startBacnetIPApp
 from bacpypes.core import stop as stopBacnetIPApp
-from bacpypes.core import enable_sleeping
 from bacpypes.local.device import LocalDeviceObject
-from bacpypes.basetypes import ServicesSupported, DeviceStatus
 from bacpypes.primitivedata import CharacterString
 
+from .. import infos
+
 # --- this application's modules ---
 from ..core.app.ScriptApplication import (
     BAC0Application,
-    BAC0ForeignDeviceApplication,
     BAC0BBMDDeviceApplication,
+    BAC0ForeignDeviceApplication,
 )
-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 ..core.io.IOExceptions import InitializationError, UnknownObjectError
+from ..core.utils.notes import note_and_log
 from ..tasks.TaskManager import stopAllTasks
 
-from ..core.utils.notes import note_and_log
+# --- 3rd party modules ---
+
 
 try:
-    import pandas
     import bokeh
     import flask
     import flask_bootstrap
+    import pandas
 
     _COMPLETE = True
 except ImportError:
@@ -111,6 +113,7 @@ def __init__(
         vendorId=842,
         vendorName=CharacterString("SERVISYS inc."),
         description=CharacterString("http://christiantremblay.github.io/BAC0/"),
+        spin=None,
     ):
 
         self._log.debug("Configurating app")
@@ -125,6 +128,7 @@ def __init__(
                 "Those are not all installed so BAC0 will work in Lite mode only."
             )
 
+        self._spin = spin
         self.response = None
         self._initialized = False
         self._started = False
@@ -285,9 +289,13 @@ def _startAppThread(self):
         """
         self._log.info("Starting app...")
         enable_sleeping(0.0005)
+        if self._spin:
+            kwargs = {"sigterm": None, "sigusr1": None, "spin": self._spin}
+        else:
+            kwargs = {"sigterm": None, "sigusr1": None}
         self.t = Thread(
             target=startBacnetIPApp,
-            kwargs={"sigterm": None, "sigusr1": None},
+            kwargs=kwargs,
             daemon=True,
         )
         try:
diff --git a/BAC0/scripts/Complete.py b/BAC0/scripts/Complete.py
index bf41e5e2..27337dc6 100644
--- a/BAC0/scripts/Complete.py
+++ b/BAC0/scripts/Complete.py
@@ -165,6 +165,7 @@ def __init__(
         ping=True,
         bokeh_server=True,
         flask_port=8111,
+        spin=None,
         **params
     ):
         Lite.__init__(
@@ -175,6 +176,7 @@ def __init__(
             bbmdAddress=bbmdAddress,
             bbmdTTL=bbmdTTL,
             ping=ping,
+            spin=spin,
             **params
         )
         self.flask_port = flask_port
diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py
index 67fc33ed..aff5130a 100644
--- a/BAC0/tasks/Poll.py
+++ b/BAC0/tasks/Poll.py
@@ -85,14 +85,18 @@ def device(self):
     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"
+                "{} ({}) | Ping failed, skipping polling for now. Resending a ping to speed up things".format(
+                    self.device.properties.name, self.device.properties.address
+                )
             )
             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"
+                    "{} ({}) | Polling failed numerous times in a row... let see what we can do".format(
+                        self.device.properties.name, self.device.properties.address
+                    )
                 )
             self.device.read_multiple(
                 list(self.device.pollable_points_name), points_per_request=25
@@ -109,23 +113,35 @@ def task(self):
             # When creation fail, polling is created and fail the first time...
             # So kill the task
             self.device._log.error(
-                "Something is wrong while creating the polling task."
-                "Error: {} | Type : {}".format(e, type(e))
+                "{} ({}) | Something is wrong while creating the polling task.\nError: {} | Type : {}".format(
+                    self.device.properties.name,
+                    self.device.properties.address,
+                    e,
+                    type(e),
+                )
             )
             # self.stop()
             self.failures += 1
         except ValueError as e:
             self.failures += 1
             self.device._log.error(
-                "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))
+                "{} ({}) | Polling results contains a wrong value. Probably a communication error. Will skip this result and wait for the next cycle.\nError: {} | Type : {}".format(
+                    self.device.properties.name,
+                    self.device.properties.address,
+                    e,
+                    type(e),
+                )
             )
             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))
+                "{} ({}) | Trying to ping device then we'll reset the number of failures and get back with polling\nError: {}| Type : {}".format(
+                    self.device.properties.name,
+                    self.device.properties.address,
+                    e,
+                    type(e),
+                )
             )
             if self.device.ping():
                 self.failures = 0
diff --git a/BAC0/tasks/RecurringTask.py b/BAC0/tasks/RecurringTask.py
index 23d494f6..757368c2 100644
--- a/BAC0/tasks/RecurringTask.py
+++ b/BAC0/tasks/RecurringTask.py
@@ -28,12 +28,13 @@ def __init__(self, fnc, delay=60, name="recurring"):
         self.fnc_args = None
         if isinstance(fnc, tuple):
             self.func, self.fnc_args = fnc
-        elif hasattr(fnc, "__call__") :
+        elif hasattr(fnc, "__call__"):
             self.func = fnc
         else:
-            raise ValueError("You must pass a function or a tuple (function,args) 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):
         if self.fnc_args:
diff --git a/BAC0/tools/__init__.py b/BAC0/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/BAC0/tools/tad_display.py b/BAC0/tools/tad_display.py
index 05379987..8e932670 100644
--- a/BAC0/tools/tad_display.py
+++ b/BAC0/tools/tad_display.py
@@ -4,9 +4,19 @@
 FILE_FOOTER = const.FILE_FOOTER
 TAG = const.TAG
 
+template_example = {
+    "names": [("name", "dev_id")],
+}
 
-def convert(d, device_name=None):
+
+def convert(d, device_name=None, make_template=False):
+    """
+    Template : If true, will not populate name and device id so the file
+    can be reused to create a dictionnary in T3 Studio with multiple
+    devices. This will prevents us from switching dictionnary all the time.
+    """
     list_of_tags = []
+    device_id = None
     OBJECT_TYPE = {
         "analogValue": "AV",
         "analogInput": "AI",
@@ -41,6 +51,11 @@ def convert(d, device_name=None):
         "MO": 8,
         "MV": 10,
     }
+
+    if make_template:
+        device_name = "$DEVICE_NAME"
+        device_id = "$DEVICE_ID"
+
     data = {}
     for each in d.points:
         name = device_name if device_name else d.properties.name
@@ -48,7 +63,7 @@ def convert(d, device_name=None):
         data["group"] = ""
         data["object_type"] = OBJECT_TYPE[each.properties.type]
         data["object_instance"] = each.properties.address
-        data["device_id"] = d.properties.device_id
+        data["device_id"] = device_id if device_id else d.properties.device_id
         data["data_type"] = DATATYPES[data["object_type"]]
         data["object_property"] = 85
         data["write_priority"] = WRITE_PRIORITY[data["object_type"]]
@@ -60,13 +75,36 @@ def convert(d, device_name=None):
         data["array_index"] = -1
         list_of_tags.append(TAG.format(d=data))
 
-        file = FILE_HEADER
+    return (name, list_of_tags)
+
+
+def build_from_templates(list_of_tags, config={}):
+    """
+    Config must be a list of tuple (name, device_id)
+    """
+    _lst_of_tags = []
+    for each in config:
+        name, device_id = each
         for tag in list_of_tags:
-            file += tag
-        file += FILE_FOOTER
-        write_tags_import_file(name, file)
+            if "$DEVICE_NAME" in tag:
+                tag.replace("$DEVICE_NAME", name)
+            if "$DEVICE_ID" in tag:
+                tag.replace("DEVICE_ID", device_id)
+            _lst_of_tags.append(tag)
+    return _lst_of_tags
+
+
+def merge_templates(lst=[]):
+    _lst = []
+    for each in lst:
+        _lst.extend(each)
+    return _lst
 
 
-def write_tags_import_file(name, file):
+def write_tags_import_file(name, list_of_tags):
+    _content = FILE_HEADER
+    for tag in list_of_tags:
+        _content += tag
+    _content += FILE_FOOTER
     with open("{}.xml".format(name), "w") as xml_file:
-        xml_file.write(file)
+        xml_file.write(_content)
diff --git a/README.rst b/README.rst
index a4b90cbd..0c37fb04 100644
--- a/README.rst
+++ b/README.rst
@@ -5,7 +5,7 @@ BAC0 |build-status| |coverage| |docs|
    :alt: Join the chat at https://gitter.im/ChristianTremblay/BAC0
    :target: https://gitter.im/ChristianTremblay/BAC0?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
 
-BAC0 is a Python 3 (3.5 and over) scripting application that uses BACpypes_ to process BACnet messages on a IP network. 
+BAC0 is a Python 3 (3.6 and over) scripting application that uses BACpypes_ to process BACnet™ messages on a IP network. 
 This library brings out simple commands to browse a BACnet network, read properties from BACnet devices or write to them.
 
 Python is a simple language to learn and a very powerful tool for data processing. Coupled to BACnet, it becomes a great 
@@ -13,10 +13,6 @@ tool to test devices an interact with controllers.
 
 BAC0 takes its name from the default IP port used by BACnet/IP communication which is port 47808. In hexadecimal, it's written 0xBAC0.
 
-Web features included
-=====================
-BAC0 includes a local web page that will help the user providing basic informations about the netwok seen by the script and also provide a simple interface to historical trends. Flask is used to render the web page and a Bokeh server is also provided to serve live trends to the user.
-
 Test driven development (TDD) for DDC controls
 ==============================================
 BAC0 is made for building automation system (BAS) programmers. Controllers used in this field are commonly called DDC Controllers (Direct Digital Control).
@@ -46,6 +42,17 @@ This make BAC0 not only a good tool to test your sequence while your in the offi
 But also a really good tool to assist your startup, test and balancing. Using Jupyter Notebook, you'll
 even be able to create nice looking report right from your code.
 
+InfluxDB native support
+========================
+Connect BAC0 histories directly to a InfluxDB_ v2.0 instance. It's then possible to use Grafana_ to explore your data.
+
+Web features included
+=====================
+BAC0 includes a local web page that will help the user providing basic informations about the netwok seen by the script and also provide a simple interface to historical trends. Flask is used to render the web page and a Bokeh server is also provided to serve live trends to the user.
+
+
+.. note::
+   BACnet™ is a trademark of ASHRAE. 
 
 .. |build-status| image:: https://github.com/ChristianTremblay/BAC0/workflows/Build%20&%20Test/badge.svg?branch=master
    :target: https://github.com/ChristianTremblay/BAC0/actions
@@ -62,3 +69,8 @@ even be able to create nice looking report right from your code.
 .. _bacpypes : https://github.com/JoelBender/bacpypes
 
 .. _bokeh : http://www.bokehplots.com
+
+.. _InfluxDB : https://www.influxdata.com
+
+.. _Grafana : https://grafana.com
+
diff --git a/doc/source/proprietary_objects.rst b/doc/source/proprietary_objects.rst
index 3225d23c..3f404b80 100644
--- a/doc/source/proprietary_objects.rst
+++ b/doc/source/proprietary_objects.rst
@@ -59,7 +59,7 @@ do so, a special dictionary need to be declared in this form ::
         "objectType": "type",
         "bacpypes_type": Object,
         "properties": {
-            "NameOfProprietaryProp": {"obj_id": 1110, "primitive": Boolean, "mutable": True},
+            "NameOfProprietaryProp": {"obj_id": 1110, "datatype": Boolean, "mutable": True},
         },
     }
 
@@ -70,7 +70,7 @@ do so, a special dictionary need to be declared in this form ::
     # properties : list of proprietary properties to add 
     #     name of the property (for reference)
     #     obj_id : instance of the property, usually an integer
-    #     primitive : the kind of data for this property. Refer to `bacpypes.primitivedata`
+    #     datatype : the kind of data for this property. Refer to `bacpypes.primitivedata` or `bacpypes.constructeddata`
     #     mutable : true = writable, default to false
 
 
@@ -147,8 +147,8 @@ them all in the same new class (the example presents 2 new properties).
         "objectType": "device",
         "bacpypes_type": DeviceObject,
         "properties": {
-            "SupervisorOnline": {"obj_id": 3653, "primitive": Boolean, "mutable": True},
-            "Model": {"obj_id": 1320, "primitive": CharacterString, "mutable": False},
+            "SupervisorOnline": {"obj_id": 3653, "datatype": Boolean, "mutable": True},
+            "Model": {"obj_id": 1320, "datatype": CharacterString, "mutable": False},
         },
     }
 
diff --git a/setup.py b/setup.py
index 74fda5b3..4462af0f 100644
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,7 @@
 Setup.py
 """
 from setuptools import setup
+
 from BAC0 import infos
 
 requirements = ["bacpypes", "colorama"]
@@ -30,6 +31,7 @@
         "BAC0.tasks",
         "BAC0.web",
         "BAC0.db",
+        "BAC0.tools",
     ],
     include_package_data=True,
     requires=requirements,


Test patch:
diff --git a/tests/test_proprietaryobjects.py b/tests/test_proprietaryobjects.py
new file mode 100644
index 00000000..8979a8b5
--- /dev/null
+++ b/tests/test_proprietaryobjects.py
@@ -0,0 +1,94 @@
+import pytest
+from BAC0.core.proprietary_objects.object import create_proprietary_object
+
+from bacpypes.primitivedata import (
+    Real,
+    Boolean,
+    CharacterString,
+    Enumerated,
+    Unsigned,
+    Atomic,
+    Date,
+    Time,
+    OctetString,
+)
+from bacpypes.object import (
+    Object,
+    DeviceObject,
+    AnalogValueObject,
+    AnalogInputObject,
+    AnalogOutputObject,
+    BinaryValueObject,
+    BinaryInputObject,
+    BinaryOutputObject,
+    Property,
+    register_object_type,
+)
+from bacpypes.constructeddata import (
+    Any,
+    Array,
+    ArrayOf,
+    List,
+    ListOf,
+    Choice,
+    Element,
+    Sequence,
+)
+
+MyDeviceProprietaryProperties = {
+    "name": "MyDeviceProprietaryProperties",
+    "vendor_id": 842,
+    "objectType": "binaryValue",  # Yes the object type in question is of type binaryValue
+    "bacpypes_type": BinaryValueObject,
+    "properties": {
+        "My_Constructed_Data": {"obj_id": 1234, "datatype": Array, "mutable": False},
+    },
+}
+
+legacy = {
+    "name": "MyDeviceProprietaryProperties",
+    "vendor_id": 842,
+    "objectType": "binaryValue",  # Yes the object type in question is of type binaryValue
+    "bacpypes_type": BinaryValueObject,
+    "properties": {
+        "My_Constructed_Data": {"obj_id": 1234, "primitive": Array, "mutable": False},
+    },
+}
+
+wrong = {
+    "name": "MyDeviceProprietaryProperties",
+    "vendor_id": 842,
+    "objectType": "binaryValue",  # Yes the object type in question is of type binaryValue
+    "bacpypes_type": BinaryValueObject,
+    "properties": {
+        "My_Constructed_Data": {"obj_id": 1234, "wrongname": Array, "mutable": False},
+    },
+}
+
+no_vendor_id = {
+    "name": "MyDeviceProprietaryProperties",
+    "vendor_id": 0,
+    "objectType": "binaryValue",  # Yes the object type in question is of type binaryValue
+    "bacpypes_type": BinaryValueObject,
+    "properties": {
+        "My_Constructed_Data": {"obj_id": 1234, "wrongname": Array, "mutable": False},
+    },
+}
+
+
+def test_create_object_using_datatype_keyname():
+    create_proprietary_object(MyDeviceProprietaryProperties)
+
+
+def test_create_object_using_primitive_keyname():
+    create_proprietary_object(legacy)
+
+
+def test_create_object_using_wrong_keyname():
+    with pytest.raises(KeyError):
+        create_proprietary_object(wrong)
+
+
+def test_create_object_no_vendor_id():
+    with pytest.raises(ValueError):
+        create_proprietary_object(no_vendor_id)