Repository: ChristianTremblay/BAC0 (language: python)

Issue:
Two small changes to be able to talk to a Johnson Controls NCE
Docs for the device here: https://cgproducts.johnsoncontrols.com/met_pdf/1201531.pdf?ref=binfind.com/web

Explanations in the commit messages, needed both to be able to consistently talk to a (probably overloaded) controller device
pip3 install BAC0 fails
hello, please be advised the pip3 install fails but it works if I provide older version of BAC0

```
pip3 install BAC0
Processing /Users/zgoglo716/Library/Caches/pip/wheels/58/8e/71/554a56d3182b3c6c9eca912b48581f46e1fa64d11c15d36e30/BAC0-20.11.21-py3-none-any.whl
Requirement already satisfied: bacpypes in /usr/local/lib/python3.9/site-packages (from BAC0) (0.18.3)
Requirement already satisfied: colorama in /usr/local/lib/python3.9/site-packages (from BAC0) (0.4.4)
Collecting netifaces
  Using cached netifaces-0.10.9.tar.gz (28 kB)
Building wheels for collected packages: netifaces
  Building wheel for netifaces (setup.py) ... error
  ERROR: Command errored out with exit status 1:
   command: /usr/local/opt/python@3.9/bin/python3.9 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"'; __file__='"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' bdist_wheel -d /private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-wheel-j99r7gp4
       cwd: /private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/
  Complete output (16 lines):
  running bdist_wheel
  running build
  running build_ext
  checking for getifaddrs...not found.
  checking for getnameinfo...not found.
  checking for socket IOCTLs...not found.
  checking for optional header files...none found.
  checking whether struct sockaddr has a length field...no.
  checking which sockaddr_xxx structs are defined...none!
  checking for routing socket support...no.
  checking for sysctl(CTL_NET...) support...no.
  checking for netlink support...no.
  building 'netifaces' extension
  clang -Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers -DNETIFACES_VERSION=0.10.9 -I/usr/local/include -I/usr/local/opt/openssl@1.1/include -I/usr/local/opt/sqlite/include -I/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/include/python3.9 -c netifaces.c -o build/temp.macosx-10.14-x86_64-3.9/netifaces.o
  xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
  error: command '/usr/bin/clang' failed with exit code 1
  ----------------------------------------
  ERROR: Failed building wheel for netifaces
  Running setup.py clean for netifaces
Failed to build netifaces
Installing collected packages: netifaces, BAC0
    Running setup.py install for netifaces ... error
    ERROR: Command errored out with exit status 1:
     command: /usr/local/opt/python@3.9/bin/python3.9 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"'; __file__='"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-record-op6qbbia/install-record.txt --single-version-externally-managed --compile --install-headers /usr/local/include/python3.9/netifaces
         cwd: /private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/
    Complete output (16 lines):
    running install
    running build
    running build_ext
    checking for getifaddrs...not found.
    checking for getnameinfo...not found.
    checking for socket IOCTLs...not found.
    checking for optional header files...none found.
    checking whether struct sockaddr has a length field...no.
    checking which sockaddr_xxx structs are defined...none!
    checking for routing socket support...no.
    checking for sysctl(CTL_NET...) support...no.
    checking for netlink support...no.
    building 'netifaces' extension
    clang -Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers -DNETIFACES_VERSION=0.10.9 -I/usr/local/include -I/usr/local/opt/openssl@1.1/include -I/usr/local/opt/sqlite/include -I/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/include/python3.9 -c netifaces.c -o build/temp.macosx-10.14-x86_64-3.9/netifaces.o
    xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
    error: command '/usr/bin/clang' failed with exit code 1
    ----------------------------------------
ERROR: Command errored out with exit status 1: /usr/local/opt/python@3.9/bin/python3.9 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"'; __file__='"'"'/private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-install-6onp053e/netifaces/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /private/var/folders/qm/9jnwq5m542q6qwwdpwm3dh300000gp/T/pip-record-op6qbbia/install-record.txt --single-version-externally-managed --compile --install-headers /usr/local/include/python3.9/netifaces Check the logs for full command output.

```





Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..2d88f002
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,26 @@
+name: Upload Python Package
+
+on:
+  release:
+    types: [created]
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install setuptools wheel twine
+    - name: Build and publish
+      env:
+        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+      run: |
+        python setup.py sdist bdist_wheel
+        twine upload dist/*
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 000ca779..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-language: python
-python:
-- '3.6'
-- '3.7'
-- '3.8'
-
-bundler_args: "--retry 3"
-install:
-- pip install -r requirements.txt
-- pip install coveralls
-- pip install colorama
-- pip install pytest
-- pip install pytest-cov
-- pip install pandas
-- pip install bokeh
-- pip install flask
-- pip install flask_bootstrap
-- pip install colorama
-
-script:
-- coverage run --source BAC0 -m pytest -v
-- coverage report
-after_success: coveralls
-deploy:
-  provider: pypi
-  user: ChristianTremblay
-  password:
-    secure: Wgb3PCFut7RCbajytFP4IchYZ1ghA6UH0qjLtBm5qdCYesQKipo3p4Pm5N2IlyOfen1RGd0igaksQqHj19bpwoMXvvR6Rb/Q/eqON+mUWtN1BDPJFh60bWfyJ9c3ZsrCmkVChIQxOPwmyUTX23LV+1Z4nuRdlktdzIDbBZn847BesVylHP9gQ7CKZb3Fxr69ZeOtMbTwAyfK1BbMcFjXMQQGzl4W0R9CuFH7eDa8Ih/fUsvUupDNDKwl6hyKNWwtzDu/DPP0TDBgQexjntIo+MkByatmXyGCoYuHzIrjIMKHCxHGtciCnD6cNGT9meUkwjiCHITfRKCdCAZmwfCeVJuB/AsjgMj4M/dA6kMklqs4A+DEqbw+JiYxMSP1VZL5Sy864Api+7Rhm7tQNNPzjQywSlCjwKexEmSdbKy3M0A7OkORzFFsWDE5WUG1OjR8iJGnhKINmAjfvPhlZCjf/9Dgsd5EA735WiUmhTjj7Vzu3MTtMOzg95UtdDVekqZHf4Yjk6cjhqJQxKM0GZSomAdTZhpYiFFMXgpiup9hUmRujPvGMOci/vydi3rWEe4QARGdDZb/6spydyCy/9sw1O+D93hzLq09fSm3IWN/awdj0dkbhKbHSfH+Gq/436DgRdG/5GlevL3u8b/QARGu2bAXzNJJIuBcGcesTzLch30=
-  on:
-    branch: master
-    python: '3.8'
-    distributions: sdist bdist_wheel
-    repo: ChristianTremblay/BAC0
diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py
index 321db95c..21296fa3 100755
--- a/BAC0/core/devices/Device.py
+++ b/BAC0/core/devices/Device.py
@@ -136,7 +136,8 @@ def __init__(
         auto_save=False,
         save_resampling="1s",
         clear_history_on_save=False,
-        history_size=None
+        history_size=None,
+        reconnect_on_failure=True,
     ):
 
         self.properties = DeviceProperties()
@@ -155,6 +156,7 @@ def __init__(
         self.properties.save_resampling = save_resampling
         self.properties.clear_history_on_save = clear_history_on_save
         self.properties.default_history_size = history_size
+        self._reconnect_on_failure = reconnect_on_failure
 
         self.segmentation_supported = segmentation_supported
         self.custom_object_list = object_list
@@ -540,8 +542,11 @@ def _buildPointList(self):
             self.segmentation_supported = False
             self.new_state(DeviceDisconnected)
         except IndexError as error:
-            self._log.error("Device creation failed... disconnecting")
-            self.new_state(DeviceDisconnected)
+            if self._reconnect_on_failure:
+                self._log.error("Device creation failed... re-connecting")
+                self.new_state(DeviceDisconnected)
+            else:
+                self._log.error("Device creation failed... disconnecting")
 
     def __getitem__(self, point_name):
         """
@@ -751,6 +756,8 @@ def update_bacnet_properties(self):
                 prop_id_required=True,
             )
             for each in res:
+                if not each:
+                    continue
                 v, prop = each
                 self.properties.bacnet_properties[prop] = v
 
diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py
index b4082e87..b678571a 100644
--- a/BAC0/core/devices/Points.py
+++ b/BAC0/core/devices/Points.py
@@ -202,6 +202,8 @@ def update_bacnet_properties(self):
                 prop_id_required=True,
             )
             for each in res:
+                if not each:
+                    continue
                 v, prop = each
                 self.properties.bacnet_properties[prop] = v
 
@@ -542,7 +544,7 @@ def match(self, point, *, delay=5):
         else:
             raise RuntimeError("Stop task before redefining it")
 
-    def match_value(self, value, *, delay=5):
+    def match_value(self, value, *, delay=5, use_last_value=False):
         """
         This allow functions like :
             device['point'].match('value')
@@ -550,7 +552,9 @@ def match_value(self, value, *, delay=5):
         A sensor will follow a calculation...
         """
         if self._match_task.task is None:
-            self._match_task.task = Match_Value(value=value, point=self, delay=delay)
+            self._match_task.task = Match_Value(
+                value=value, point=self, delay=delay, use_last_value=use_last_value
+            )
             self._match_task.task.start()
             self._match_task.running = True
 
diff --git a/BAC0/core/devices/Virtuals.py b/BAC0/core/devices/Virtuals.py
index dca1b485..091adb9f 100644
--- a/BAC0/core/devices/Virtuals.py
+++ b/BAC0/core/devices/Virtuals.py
@@ -81,10 +81,11 @@ def __init__(self):
         self.description = ""
         self.units_state = ""
         self.type = "virtual"
+        self.history_size = None
         self._df = None
 
     def __repr__(self):
-        return "{} | Descr : {}".format(self.name, self.description)
+        return "VIRTUAL POINT-- {} | Descr : {}".format(self.name, self.description)
 
 
 @note_and_log
@@ -94,7 +95,14 @@ class VirtualPoint(VirtualPointProperties):
     A function is passed at creation time and this function must return a pandas Serie
     """
 
-    def __init__(self, name, fn, description=None, units="No Units"):
+    def __init__(
+        self,
+        name,
+        initial_value=None,
+        history_fn=None,
+        description=None,
+        units="No Units",
+    ):
         if description is None:
             raise ValueError("Please provide description")
         if not _PANDAS:
@@ -104,21 +112,155 @@ def __init__(self, name, fn, description=None, units="No Units"):
         self.properties.name = name
         self.properties.description = description
         self.properties.units_state = units
-        self._history = fn
+        self._history_fn = history_fn
+
+        self._history = namedtuple("_history", ["timestamp", "value"])
+        self._history.timestamp = []
+        self._history.value = []
+
+        self._match_task = namedtuple("_match_task", ["task", "running"])
+        self._match_task.task = None
+        self._match_task.running = False
+
+        self.fake_pv = None
+        if initial_value:
+            self._set(initial_value)
+
+    def chart(self, remove=False):
+        """
+        Add point to the bacnet trending list
+        """
+        self._log.warning("Use bacnet.add_trend(point) instead")
+
+    def _set(self, value):
+        if self._history_fn is None:
+            self.fake_pv = value
+            self._trend(value)
+        else:
+            self._log.warning(
+                "Point is configured as a function of other points. You can't set a new value"
+            )
+
+    def _trend(self, res):
+        self._history.timestamp.append(datetime.now())
+        self._history.value.append(res)
+        if self.properties.history_size is None:
+            return
+        else:
+            if self.properties.history_size < 1:
+                self.properties.history_size = 1
+            if len(self._history.timestamp) >= self.properties.history_size:
+                try:
+                    self._history.timestamp = self._history.timestamp[
+                        -self.properties.history_size :
+                    ]
+                    self._history.value = self._history.value[
+                        -self.properties.history_size :
+                    ]
+                    assert len(self._history.timestamp) == len(self._history.value)
+                except Exception as e:
+                    self._log.exception("Can't append to history")
+
+    @property
+    def value(self):
+        """
+        Retrieve value of the point
+        """
+        return self.lastValue
+
+    @property
+    def lastValue(self):
+        """
+        returns: last value read
+        """
+        if _PANDAS:
+            return self.history.dropna().iloc[-1]
+        else:
+            return self._history.value[-1]
 
     @property
     def history(self):
-        _result = self._history()
-        if not isinstance(_result, pd.Series):
-            raise ValueError("Function of virtual point must return a Series")
+        """
+        returns : (pd.Series) containing timestamp and value of all readings
+        """
+        if self._history_fn is not None:
+            return self._history_fn()
         else:
-            return _result
+            if not _PANDAS:
+                return dict(zip(self._history.timestamp, self._history.value))
+            idx = self._history.timestamp.copy()
+            his_table = pd.Series(index=idx, data=self._history.value[: len(idx)])
+            del idx
+            his_table.name = ("{}/{}").format(
+                self.properties.device.properties.name, self.properties.name
+            )
+            his_table.units = self.properties.units_state
+            his_table.states = "virtual"
 
-    def chart(self, remove=False):
+            his_table.description = self.properties.description
+
+            his_table.datatype = self.properties.type
+            return his_table
+
+    def match_value(self, value, *, delay=5):
         """
-        Add point to the bacnet trending list
+        This allow functions like :
+            device['point'].match('value')
+
+        A sensor will follow a calculation...
         """
-        self._log.warning("Use bacnet.add_trend() instead")
+        if self._match_task.task is None:
+            self._match_task.task = Match_Value(value=value, point=self, delay=delay)
+            self._match_task.task.start()
+            self._match_task.running = True
+
+        elif self._match_task.running and delay > 0:
+            self._match_task.task.stop()
+            self._match_task.running = False
+            time.sleep(1)
+
+            self._match_task.task = Match_Value(value=value, point=self, delay=delay)
+            self._match_task.task.start()
+            self._match_task.running = True
+
+        elif self._match_task.running and delay == 0:
+            self._match_task.task.stop()
+            self._match_task.running = False
+
+        else:
+            raise RuntimeError("Stop task before redefining it")
 
     def __repr__(self):
-        return self.properties.__repr__()
+        return "{}/{} : {:.2f} {}".format(
+            self.properties.device.properties.name,
+            self.properties.name,
+            self.value,
+            self.properties.units_state,
+        )
+
+    def __add__(self, other):
+        return self.value + other
+
+    def __sub__(self, other):
+        return self.value - other
+
+    def __mul__(self, other):
+        return self.value * other
+
+    def __truediv__(self, other):
+        return self.value / other
+
+    def __lt__(self, other):
+        return self.value < other
+
+    def __le__(self, other):
+        return self.value <= other
+
+    def __eq__(self, other):
+        return self.value == other
+
+    def __gt__(self, other):
+        return self.value > other
+
+    def __ge__(self, other):
+        return self.value >= other
diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py
index 22860016..63f9860a 100755
--- a/BAC0/core/devices/mixins/read_mixin.py
+++ b/BAC0/core/devices/mixins/read_mixin.py
@@ -394,7 +394,7 @@ def _find_propid_index(key):
                 tl,
             )
 
-        self._log.debug("RPM Mixin : {} | {} | {}".format(objList, points, trendlogs))
+        self._log.debug("RPM Mixin : %s | %s | %s", objList, points, trendlogs)
         self._log.info("Ready!")
         return (objList, points, trendlogs)
 
@@ -529,11 +529,13 @@ def read_multiple(
         device.read_multiple(['point1', 'point2', 'point3'], points_per_request = 10)
         """
         if isinstance(points_list, list):
-            (requests, _) = self._rpm_request_by_name(points_list)
-            for each in requests:
-                self.read_single(
-                    each, points_per_request=1, discover_request=discover_request
+            (requests, points) = self._rpm_request_by_name(points_list)
+            for (i, req) in enumerate(requests):
+                val = self.read_single(
+                    req, points_per_request=1, discover_request=discover_request
                 )
+                if val is not None and val != "":
+                    points[i]._trend(val)
         else:
             self.read_single(
                 points_list, points_per_request=1, discover_request=discover_request
diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py
index 0190ff5f..d6dc49c8 100644
--- a/BAC0/core/functions/GetIPAddr.py
+++ b/BAC0/core/functions/GetIPAddr.py
@@ -20,9 +20,10 @@
 import ipaddress
 import sys
 import re
-import netifaces
+from ...core.utils.notes import note_and_log
 
 
+@note_and_log
 class HostIP:
     """
     Special class to identify host IP informations
@@ -107,16 +108,25 @@ def _findSubnetMask(self, ip):
         
         :returns: broadcast IP Adress as String
         """
-        interfaces = netifaces.interfaces()
-        for nic in interfaces:
-            addresses = netifaces.ifaddresses(nic)
-            try:
-                for address in addresses[netifaces.AF_INET]:
-                    if address["addr"] == ip:
-                        return address["netmask"]
-            except KeyError:
-                pass
-        return "255.255.255.255"
+        try:
+            import netifaces
+
+            interfaces = netifaces.interfaces()
+            for nic in interfaces:
+                addresses = netifaces.ifaddresses(nic)
+                try:
+                    for address in addresses[netifaces.AF_INET]:
+                        if address["addr"] == ip:
+                            return address["netmask"]
+                except KeyError:
+                    pass
+
+            return "255.255.255.255"
+        except ImportError:
+            self._log.warning(
+                "Netifaces not installed on your system. BAC0 can't detect the subnet.\nPlease provide subnet for now, we'll consider 255.255.255.0 (/24).\nYou can install netifaces using 'pip install netifaces'."
+            )
+            return "255.255.255.0"
 
 
 def validate_ip_address(ip):
diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py
index c7d14b00..24114cfa 100644
--- a/BAC0/core/io/Read.py
+++ b/BAC0/core/io/Read.py
@@ -348,6 +348,8 @@ def readMultiple(
                                     value = propertyValue.cast_out(Unsigned)
                                 else:
                                     value = propertyValue.cast_out(datatype.subtype)
+                            elif propertyValue.is_application_class_null():
+                                value = None
                             else:
                                 value = propertyValue.cast_out(datatype)
 
diff --git a/BAC0/infos.py b/BAC0/infos.py
index 793ec038..126b36ef 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__ = "20.11.21"
+__version__ = "21.01.28"
 __license__ = "LGPLv3"
diff --git a/BAC0/tasks/Match.py b/BAC0/tasks/Match.py
index 32f58a01..fd56a106 100644
--- a/BAC0/tasks/Match.py
+++ b/BAC0/tasks/Match.py
@@ -72,18 +72,25 @@ class Match_Value(Task):
     i.e. Does Fan value = On after 5 seconds. 
     """
 
-    def __init__(self, value=None, point=None, delay=5, name=None):
+    def __init__(
+        self, value=None, point=None, delay=5, name=None, use_last_value=False
+    ):
         self._log.debug("Creating MatchValue task for {} and {}".format(value, point))
         self.value = value
         self.point = point
+        self.use_last_value = use_last_value
         if not name:
             name = "Match_Value on " + point.properties.name
         Task.__init__(self, delay=delay, name=name)
 
     def task(self):
         try:
+            if self.use_last_value:
+                _point = self.point.lastValue
+            else:
+                _point = self.point.value
             value = self.value() if hasattr(self.value, "__call__") else self.value
-            if value != self.point.value:
+            if value != _point:
                 self.point._set(value)
         except Exception:
             self._log.error(
diff --git a/BAC0/tasks/UpdateCOV.py b/BAC0/tasks/UpdateCOV.py
index 7dd2dbde..183f24ee 100644
--- a/BAC0/tasks/UpdateCOV.py
+++ b/BAC0/tasks/UpdateCOV.py
@@ -51,7 +51,7 @@ def task(self):
 
                 # tell it to send out notifications
                 self._log.info("Sending COV for {}".format(objName))
-                deferred(cov_detection.send_cov_notifications())
+                deferred(cov_detection.send_cov_notifications)
 
         except Exception as error:
             self.device._log.error(
diff --git a/README.rst b/README.rst
index 280707de..e2256b91 100644
--- a/README.rst
+++ b/README.rst
@@ -47,8 +47,8 @@ But also a really good tool to assist your startup, test and balancing. Using Ju
 even be able to create nice looking report right from your code.
 
 
-.. |build-status| image:: https://travis-ci.org/ChristianTremblay/BAC0.svg?branch=master
-   :target: https://travis-ci.org/ChristianTremblay/BAC0
+.. |build-status| image:: https://github.com/ChristianTremblay/BAC0/.github/workflows/test/badge.svg?branch=master
+   :target: https://github.com/ChristianTremblay/BAC0/actions
    :alt: Build status
      
 .. |docs| image:: https://readthedocs.org/projects/bac0/badge/?version=latest
diff --git a/contributors.txt b/contributors.txt
index 77153e0a..05be026d 100644
--- a/contributors.txt
+++ b/contributors.txt
@@ -1,6 +1,6 @@
 Contributors:
 
 Christian Tremblay        christian.tremblay@servisys.com
-Ken J. LoSee https://github.com/ChristianTremblay/BAC0/graphs/contributors
+And https://github.com/ChristianTremblay/BAC0/graphs/contributors
 
 Thanks !
diff --git a/doc/README.rst b/doc/README.rst
new file mode 100644
index 00000000..e2256b91
--- /dev/null
+++ b/doc/README.rst
@@ -0,0 +1,64 @@
+BAC0 |build-status| |coverage| |docs|
+=====================================
+
+.. image:: https://badges.gitter.im/ChristianTremblay/BAC0.svg
+   :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. 
+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 
+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).
+
+Typical controllers can be programmed in different ways, depending on the manufacturer selling them (block programming, basic "kinda" scripts, C code, etc...). 
+BAC0, is a unified way, using Python language and BACnet/IP communication, to interact with those controllers once their sequence is built.
+
+BAC0 allows users to simply test an application even if sensors are not connected to the controller. Using the out_of_service
+property, it's easy to write a value to the input so the controller will think an input is conencted. 
+
+It's also possible to do "manual commands" on output (often called overrides). In fact, every variable is exposed and seen by BAC0 and 
+it's possible to interact with them using a simple scripting language or a complete unit test suite (like Pytest).
+
+Without a program like BAC0, you can rely on your DDC programming tool... but it is often slow and
+every test must be done manually. That means also that if you want to repeat the tests, the more complicated they are, the less chance you'll be able to do so.
+
+Now you can write your test and run them as often as you want. We'll show you how it works.
+
+Better start-up with data acquisition
+=====================================
+As you will discover, when you define a controller in BAC0, you will get access to historical data of
+every variables in the controllers. Every points are trended every 10 seconds by default. Which means 
+that you can do data analysis on everything while you're doing your startup. It allows to see performances and
+trouble really fast.
+
+This make BAC0 not only a good tool to test your sequence while your in the office.
+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.
+
+
+.. |build-status| image:: https://github.com/ChristianTremblay/BAC0/.github/workflows/test/badge.svg?branch=master
+   :target: https://github.com/ChristianTremblay/BAC0/actions
+   :alt: Build status
+     
+.. |docs| image:: https://readthedocs.org/projects/bac0/badge/?version=latest
+   :target: http://bac0.readthedocs.org/
+   :alt: Documentation
+   
+.. |coverage| image:: https://coveralls.io/repos/github/ChristianTremblay/BAC0/badge.svg?branch=master
+   :target: https://coveralls.io/github/ChristianTremblay/BAC0?branch=master
+   :alt: Coverage
+
+.. _bacpypes : https://github.com/JoelBender/bacpypes
+
+.. _bokeh : http://www.bokehplots.com
diff --git a/requirements.txt b/requirements.txt
index b9e60d3f..babe9bf5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,2 @@
 bacpypes>=0.18.3
-netifaces
 colorama
diff --git a/setup.py b/setup.py
index dcf527b7..1ff69e95 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
 from setuptools import setup
 from BAC0 import infos
 
-requirements = ["bacpypes", "netifaces", "colorama"]
+requirements = ["bacpypes", "colorama"]
 
 setup(
     name="BAC0",


Test patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/test.yml
similarity index 65%
rename from .github/workflows/python-package.yml
rename to .github/workflows/test.yml
index e2dcab03..3b51173d 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/test.yml
@@ -5,17 +5,16 @@ name: Python package
 
 on:
   push:
-    branches: [ master ]
+    branches: [ master, develop ]
   pull_request:
-    branches: [ master ]
+    branches: [ master, develop ]
 
 jobs:
   build:
-
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8]
+        python-version: [3.6, 3.7, 3.8, 3.9]
 
     steps:
     - uses: actions/checkout@v2
@@ -26,8 +25,19 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install -r requirements.txt
         pip install flake8 pytest
+        pip install coveralls
+        pip install colorama
+        pip install pytest
+        pip install pytest-cov
+        pip install pandas
+        pip install bokeh
+        pip install flask
+        pip install flask_bootstrap
+        pip install colorama
+        pip install netifaces
+        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+        pip install .
     - name: Lint with flake8
       run: |
         # stop the build if there are Python syntax errors or undefined names
@@ -36,4 +46,5 @@ jobs:
         flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
     - name: Test with pytest
       run: |
-        pytest
+        coverage run --source BAC0 -m pytest -v
+        coverage report
diff --git a/tests/test_BAC0_lite.py b/tests/test_BAC0_lite.py
index 637f7088..7770860e 100644
--- a/tests/test_BAC0_lite.py
+++ b/tests/test_BAC0_lite.py
@@ -32,6 +32,7 @@ def test_bac0_lite_ok():
         modelName=fake_model_name,
         vendorId=fake_vendor_id,
         vendorName=fake_vendor_name,
+        port=47811,
     )
 
     assert bacnet.localObjName == fake_bac0_obj
@@ -45,4 +46,4 @@ def test_bac0_lite_ok():
     assert bacnet.vendorName == fake_vendor_name
 
     bacnet.disconnect()
-    time.sleep(1)
+    time.sleep(2)
diff --git a/tests/test_BAC0_web.py b/tests/test_BAC0_web.py
index 28433932..104d5f01 100644
--- a/tests/test_BAC0_web.py
+++ b/tests/test_BAC0_web.py
@@ -32,6 +32,7 @@ def test_bac0_web_ok():
         modelName=fake_model_name,
         vendorId=fake_vendor_id,
         vendorName=fake_vendor_name,
+        port=47812,
     )
 
     assert bacnet.localObjName == fake_bac0_obj
@@ -45,4 +46,4 @@ def test_bac0_web_ok():
     assert bacnet.vendorName == fake_vendor_name
 
     bacnet.disconnect()
-    time.sleep(1)
+    time.sleep(2)