Repository: ChristianTremblay/BAC0 (language: python)

Issue:
Add mypy type annotations to BAC0
BAC0 doesn't currently have many type annotations in place, I'd like to take a quick first pass at generating and adding them so that it's easier to work with the codebase.

I've previously had good luck with https://github.com/Instagram/MonkeyType and then manually editing its results where necessary. 

I'd also like to add `mypy` to the github test workflows, so we can prevent regressions once the annotations are in place.

This is a larger change so wanted to start a discussion before writing a whole mess of code - what do you think?


Discussion / hints:
I'm not against...  there will be some challenges but why not...
Ok cool. Definitely won't be 100% coverage and I won't add any coverage-based checks to the workflows. I'm hoping to get a chance to work on this in the next couple weeks... 
This issue had no activity for a long period of time. If this issue is still required, please update the status or else, it will be closed. Please note that an issue can be reopened if required.
Finally got started on this last week! Made some good progress but still need to figure out how best to collect types for the TaskManager threads (since monkey type only runs in the parent process) 

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..cd6d1983
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,8 @@
+[flake8]
+ignore = W291,W503,E265,E266,E302,E722
+max-line-length = 127
+exclude =
+  .git,
+  __pycache__,
+  .mypy_cache,
+  .pytest_cache,
diff --git a/BAC0/__init__.py b/BAC0/__init__.py
index c38bb604..947a4196 100644
--- a/BAC0/__init__.py
+++ b/BAC0/__init__.py
@@ -54,7 +54,7 @@
 
         connect = gui
     else:
-        connect = lite
+        connect = lite  # type: ignore[assignment, misc]
         web = lambda: print(
             "All features not available to run BAC0.web(). Some modules are missing (flask, flask-bootstrap, bokeh, pandas). See docs for details. To start BAC0, use BAC0.lite()"
         )
diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py
index 425d3f3e..0a670bf4 100644
--- a/BAC0/core/app/ScriptApplication.py
+++ b/BAC0/core/app/ScriptApplication.py
@@ -5,26 +5,27 @@
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
 """
-SimpleApplication 
+SimpleApplication
 =================
 
-A basic BACnet application (bacpypes BIPSimpleApplication) for interacting with 
-the bacpypes BACnet stack.  It enables the base-level BACnet functionality 
+A basic BACnet application (bacpypes BIPSimpleApplication) for interacting with
+the bacpypes BACnet stack.  It enables the base-level BACnet functionality
 (a.k.a. device discovery) - meaning it can send & receive WhoIs & IAm messages.
 
-Additional functionality is enabled by inheriting this application, and then 
+Additional functionality is enabled by inheriting this application, and then
 extending it with more functions. [See BAC0.scripts for more examples of this.]
 
 """
 # --- standard Python modules ---
 from collections import defaultdict
+import typing as t
 
 # --- 3rd party modules ---
 from bacpypes.app import ApplicationIOController
 from bacpypes.pdu import Address
 from bacpypes.service.object import ReadWritePropertyMultipleServices
 from bacpypes.service.cov import ChangeOfValueServices
-from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement
+from bacpypes.netservice import NetworkServiceAccessPoint
 from bacpypes.bvllservice import (
     BIPSimple,
     BIPForeign,
@@ -32,10 +33,10 @@
     AnnexJCodec,
     UDPMultiplexer,
 )
-from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU, RejectPDU, AbortPDU
+from bacpypes.apdu import IAmRequest, SimpleAckPDU
 
 from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint
-from bacpypes.comm import ApplicationServiceElement, bind, Client
+from bacpypes.comm import bind, Client
 from bacpypes.iocb import IOCB
 from bacpypes.core import deferred
 
@@ -46,6 +47,8 @@
 # --- this application's modules ---
 from ..utils.notes import note_and_log
 from ..functions.Discover import NetworkServiceElementWithRequests
+from bacpypes.local.device import LocalDeviceObject
+from typing import Any, Dict, Optional
 
 # ------------------------------------------------------------------------------
 
@@ -168,15 +171,15 @@ class BAC0Application(
 
     def __init__(
         self,
-        localDevice,
-        localAddress,
+        localDevice: LocalDeviceObject,
+        localAddress: Address,
         bbmdAddress=None,
-        bbmdTTL=0,
+        bbmdTTL: int = 0,
         deviceInfoCache=None,
         aseID=None,
-        iam_req=None,
-        subscription_contexts=None,
-    ):
+        iam_req: Optional[IAmRequest] = None,
+        subscription_contexts: Optional[Dict[Any, Any]] = None,
+    ) -> None:
 
         ApplicationIOController.__init__(
             self, localDevice, deviceInfoCache, aseID=aseID
@@ -222,7 +225,7 @@ def __init__(
         # bind the BIP stack to the network, no network number
         self.nsap.bind(self.bip, address=self.localAddress)
 
-        self.i_am_counter = defaultdict(int)
+        self.i_am_counter: t.Dict[t.Tuple[str, int], int] = defaultdict(int)
         self.i_have_counter = defaultdict(int)
         self.who_is_counter = defaultdict(int)
 
diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py
index 8ca60689..93b1befb 100755
--- a/BAC0/core/devices/Device.py
+++ b/BAC0/core/devices/Device.py
@@ -10,8 +10,6 @@
 """
 # --- standard Python modules ---
 from collections import namedtuple
-from datetime import datetime
-import weakref
 
 import os.path
 
@@ -21,10 +19,9 @@
     _PANDAS = True
 except ImportError:
     _PANDAS = False
-import logging
 
 try:
-    from xlwings import Workbook, Sheet, Range, Chart
+    from xlwings import Workbook, Sheet, Range, Chart  # noqa E401
 
     _XLWINGS = True
 except ImportError:
@@ -176,7 +173,9 @@ def __init__(
         self._polling_task.task = None
         self._polling_task.running = False
 
+        self._find_overrides_progress = 0.0
         self._find_overrides_running = False
+        self._release_overrides_progress = 0.0
         self._release_overrides_running = False
 
         self.note("Controller initialized")
@@ -250,7 +249,7 @@ def simulated_points(self):
         :rtype: BAC0.core.devices.Points.Point
         """
         for each in self.points:
-            if each.properties.simulated:
+            if each.properties.simulated[0]:
                 yield each
 
     def _buildPointList(self):
@@ -383,7 +382,7 @@ def find_overrides(self, force=False):
             )
             return
         lst = []
-        self._find_overrides_progress = 0
+        self._find_overrides_progress = 0.0
         self._find_overrides_running = True
         total = len(self.points)
 
@@ -400,11 +399,11 @@ def _find_overrides():
             )
             self.properties.points_overridden = lst
             self._find_overrides_running = False
-            self._find_overrides_progress = 1
+            self._find_overrides_progress = 1.0
 
         self.do(_find_overrides)
 
-    def find_overrides_progress(self):
+    def find_overrides_progress(self) -> float:
         return self._find_overrides_progress
 
     def release_all_overrides(self, force=False):
@@ -416,7 +415,7 @@ def release_all_overrides(self, force=False):
             )
             return
         self._release_overrides_running = True
-        self._release_overrides_progress = 0
+        self._release_overrides_progress = 0.0
 
         def _release_all_overrides():
             self.find_overrides()
@@ -539,15 +538,15 @@ def _buildPointList(self):
                 self.points,
                 self._list_of_trendlogs,
             ) = self._discoverPoints(self.custom_object_list)
-            if self.properties.pollDelay > 0:
+            if self.properties.pollDelay is not None and self.properties.pollDelay > 0:
                 self.poll(delay=self.properties.pollDelay)
             self.update_history_size(size=self.properties.history_size)
             # self.clear_histories()
-        except NoResponseFromController as error:
+        except NoResponseFromController:
             self._log.error("Cannot retrieve object list, disconnecting...")
             self.segmentation_supported = False
             self.new_state(DeviceDisconnected)
-        except IndexError as error:
+        except IndexError:
             if self._reconnect_on_failure:
                 self._log.error("Device creation failed... re-connecting")
                 self.new_state(DeviceDisconnected)
diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py
index 9227d612..047eaf89 100644
--- a/BAC0/core/devices/Points.py
+++ b/BAC0/core/devices/Points.py
@@ -9,25 +9,16 @@
 """
 
 # --- standard Python modules ---
-from datetime import datetime, timedelta, timezone
-import pytz
+from datetime import datetime, timedelta
 from collections import namedtuple
 import time
-
-from bacpypes.primitivedata import (
-    CharacterString,
-    Null,
-    Atomic,
-    Integer,
-    Unsigned,
-    Real,
-    Enumerated,
-)
+import typing as t
 
 # --- 3rd party modules ---
+from bacpypes.primitivedata import CharacterString
 try:
     import pandas as pd
-    from pandas.io import sql
+    from pandas.io import sql  # noqa E401
 
     try:
         from pandas import Timestamp
@@ -60,12 +51,12 @@ class PointProperties(object):
     def __init__(self):
         self.device = None
         self.name = None
-        self.type = None
-        self.address = None
+        self.type = ''
+        self.address = -1
         self.description = None
         self.units_state = None
-        self.simulated = (False, None)
-        self.overridden = (False, None)
+        self.simulated: t.Tuple[bool, t.Optional[int]] = (False, None)
+        self.overridden: t.Tuple[bool, t.Optional[int]] = (False, None)
         self.priority_array = None
         self.history_size = None
         self.bacnet_properties = {}
@@ -136,7 +127,7 @@ def __init__(
 
         self.tags = tags
 
-        self._cache = {"_previous_read": (None, None)}
+        self._cache: t.Dict[str, t.Tuple[t.Optional[datetime], t.Any]] = {"_previous_read": (None, None)}
 
     @property
     def value(self):
@@ -160,7 +151,7 @@ def value(self):
                 vendor_id=self.properties.device.properties.vendor_id,
             )
             # self._trend(res)
-        except Exception as e:
+        except Exception:
             raise
         self._cache["_previous_read"] = (datetime.now().astimezone(), res)
         return res
@@ -263,8 +254,7 @@ def priority(self, priority=None):
             except ValueError:
                 return None
 
-    def _trend(self, res):
-        # now = datetime.now(tz=pytz.UTC)
+    def _trend(self, res: float) -> None:
         now = datetime.now().astimezone()
         self._history.timestamp.append(now)
         self._history.value.append(res)
@@ -280,14 +270,14 @@ def _trend(self, res):
             if len(self._history.timestamp) >= self.properties.history_size:
                 try:
                     self._history.timestamp = self._history.timestamp[
-                        -self.properties.history_size :
+                        -self.properties.history_size :  # noqa E203
                     ]
                     self._history.value = self._history.value[
-                        -self.properties.history_size :
+                        -self.properties.history_size :  # noqa E203
                     ]
                     assert len(self._history.timestamp) == len(self._history.value)
 
-                except Exception as e:
+                except Exception:
                     self._log.exception("Can't append to history")
 
     @property
@@ -318,7 +308,7 @@ def lastTimestamp(self):
             return self._history.timestamp[-1]
 
     @property
-    def history(self):
+    def history(self) -> t.Dict[datetime, t.Union[int, float, str]]:
         """
         returns : (pd.Series) containing timestamp and value of all readings
         """
@@ -428,7 +418,7 @@ def sim(self, value, *, force=False):
         if (
             not self.properties.simulated[0]
             or self.properties.simulated[1] != value
-            or force != False
+            or force is not False
         ):
             self.properties.device.properties.network.sim(
                 "{} {} {} presentValue {}".format(
@@ -518,7 +508,7 @@ def _set(self, value):
         """
         raise NotImplementedError("Must be overridden")
 
-    def poll(self, command="start", *, delay=10):
+    def poll(self, command="start", *, delay: int = 10) -> None:
         """
         Poll a point every x seconds (delay=x sec)
         Stopped by using point.poll('stop') or .poll(0) or .poll(False)
@@ -526,7 +516,7 @@ def poll(self, command="start", *, delay=10):
         """
         if (
             str(command).lower() == "stop"
-            or command == False
+            or command is False
             or command == 0
             or delay == 0
         ):
@@ -734,7 +724,12 @@ def __repr__(self):
                 )
             )
             # Probably disconnected
-            val = None
+            return "{}/{} : (n/a) {}".format(
+                self.properties.device.properties.name,
+                self.properties.name,
+                self.properties.units_state,
+            )
+
         return "{}/{} : {:.2f} {}".format(
             self.properties.device.properties.name,
             self.properties.name,
@@ -939,7 +934,8 @@ def value(self):
 
     def get_state(self, v):
         try:
-            return self.properties.units_state[v - 1]
+            # errors caught below
+            return self.properties.units_state[v - 1]  # type: ignore[index]
         except (TypeError, IndexError):
             return "n/a"
 
@@ -972,7 +968,7 @@ def _set(self, value):
         try:
             if isinstance(value, int):
                 self._setitem(value)
-            elif str(value) in self.properties.units_state:
+            elif str(value) in self.properties.units_state:  # type: ignore[operator]
                 self._setitem(self.properties.units_state.index(value) + 1)
             elif str(value).lower() == "auto":
                 self._setitem("auto")
@@ -1187,8 +1183,8 @@ def __init__(self, device, name):
 
         self.properties.description = props["description"]
         self.properties.units_state = props["units_state"]
-        self.properties.simulated = "Offline"
-        self.properties.overridden = "Offline"
+        self.properties.simulated = (True, None)
+        self.properties.overridden = (False, None)
 
         if "analog" in self.properties.type:
             self.new_state(NumericPointOffline)
@@ -1311,7 +1307,7 @@ def enumValue(self):
         returns: (str) Enum state value
         """
         try:
-            value = self.properties.units_state[int(self.lastValue) - 1]
+            value = self.properties.units_state[int(self.lastValue) - 1]  # type: ignore[index]
         except IndexError:
             value = "unknown"
         except ValueError:
diff --git a/BAC0/core/devices/Trends.py b/BAC0/core/devices/Trends.py
index 31628e61..d8164a07 100755
--- a/BAC0/core/devices/Trends.py
+++ b/BAC0/core/devices/Trends.py
@@ -4,37 +4,18 @@
 # Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
-"""
-Points.py - Definition of points so operations on Read results are more convenient.
-"""
 
 # --- standard Python modules ---
-from datetime import datetime
-from collections import namedtuple
-import time
-from itertools import islice
-
-
 # --- 3rd party modules ---
 try:
     import pandas as pd
-    from pandas.io import sql
-
-    try:
-        from pandas import Timestamp
-    except ImportError:
-        from pandas.lib import Timestamp
     _PANDAS = True
 except ImportError:
     _PANDAS = False
 
-from bacpypes.object import TrendLogObject
 from bacpypes.primitivedata import Date, Time
 
 # --- this application's modules ---
-from ...tasks.Poll import SimplePoll as Poll
-from ...tasks.Match import Match, Match_Value
-from ..io.IOExceptions import NoResponseFromController, UnknownPropertyError
 from ..utils.notes import note_and_log
 
 
@@ -56,7 +37,6 @@ def __init__(self):
         self.record_count = 0
         self.total_record_count = 0
         self.log_interval = 0
-        self.description = None
         self.statusFlags = None
         self.status_flags = {
             "in_alarm": False,
@@ -196,7 +176,7 @@ def create_dataframe(self, log_buffer):
     @property
     def history(self):
         self.read_log_buffer()
-        if not _PANDAS:
+        if not _PANDAS or self.properties._df is None:
             return dict(
                 zip(
                     self.properties._history_components["index"],
diff --git a/BAC0/core/devices/local/models.py b/BAC0/core/devices/local/models.py
index fe06c9df..782eca25 100644
--- a/BAC0/core/devices/local/models.py
+++ b/BAC0/core/devices/local/models.py
@@ -13,8 +13,6 @@
     MultiStateValueObject,
 )
 from bacpypes.basetypes import (
-    EngineeringUnits,
-    BinaryPV,
     Polarity,
     Boolean,
     EventState,
@@ -24,7 +22,7 @@
     Unsigned,
 )
 from bacpypes.constructeddata import ArrayOf
-from bacpypes.primitivedata import CharacterString, Real
+from bacpypes.primitivedata import CharacterString
 from .object import ObjectFactory
 
 """
@@ -51,8 +49,9 @@ def _create(definition, **kwargs):
     for k, v in kwargs.items():
         if k == "properties":
             for _k, _v in v.items():
-                _definition[k][_k] = _v
-        _definition[k] = v
+                _definition["properties"][_k] = _v  # type: ignore[index]
+        else:
+            _definition[k] = v
     return ObjectFactory.from_dict(_definition)
 
 
diff --git a/BAC0/core/devices/local/object.py b/BAC0/core/devices/local/object.py
index b0be58e5..c1ca36cc 100644
--- a/BAC0/core/devices/local/object.py
+++ b/BAC0/core/devices/local/object.py
@@ -7,30 +7,14 @@
     BAC0BBMDDeviceApplication,
     BAC0ForeignDeviceApplication,
 )
-
-from bacpypes.object import (
-    AnalogInputObject,
-    AnalogValueObject,
-    BinaryValueObject,
-    Property,
-    register_object_type,
-    registered_object_types,
-    DatePatternValueObject,
-    ReadableProperty,
-    WritableProperty,
-    OptionalProperty,
-)
 from bacpypes.basetypes import (
-    EngineeringUnits,
-    DateTime,
     PriorityArray,
-    StatusFlags,
     Reliability,
-    Polarity,
 )
 
+import typing as t
 from collections import namedtuple
-from colorama import Fore, Back, Style
+from colorama import Fore
 
 
 @note_and_log
@@ -55,14 +39,14 @@ class ObjectFactory(object):
 
     """
 
-    instances = {}
+    instances: t.Dict[str, t.Set] = {}
 
-    definition = namedtuple(
+    definition = namedtuple(  # type: ignore[name-match]
         "Definition",
         "name, objectType, instance, properties, description, presentValue, is_commandable, relinquish_default",
     )
 
-    objects = {}
+    objects: t.Dict[str, t.Any] = {}
     # In the future... should think about a way to store relinquish default values because on a restart
     # those should be restored.
 
diff --git a/BAC0/core/devices/mixins/CommandableMixin.py b/BAC0/core/devices/mixins/CommandableMixin.py
index 85e9ea05..42928a77 100644
--- a/BAC0/core/devices/mixins/CommandableMixin.py
+++ b/BAC0/core/devices/mixins/CommandableMixin.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# type: ignore
 
 """
 Rebuilt Commandable
diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py
index 56ff1903..8efdecb9 100755
--- a/BAC0/core/devices/mixins/read_mixin.py
+++ b/BAC0/core/devices/mixins/read_mixin.py
@@ -8,13 +8,13 @@
 read_mixin.py - Add ReadProperty and ReadPropertyMultiple to a device
 """
 # --- standard Python modules ---
+import typing as t
 
 # --- 3rd party modules ---
 
 # --- this application's modules ---
 from ....tasks.Poll import DeviceNormalPoll, DeviceFastPoll
 from ...io.IOExceptions import (
-    ReadPropertyMultipleException,
     NoResponseFromController,
     SegmentationNotSupported,
     BufferOverflow,
@@ -24,7 +24,6 @@
     BooleanPoint,
     EnumPoint,
     StringPoint,
-    OfflinePoint,
     DateTimePoint,
 )
 from ..Trends import TrendLog
@@ -251,7 +250,7 @@ def rp_discovered_values(self, discover_request, points_per_request):
 
 class RPMObjectsProcessing:
     def _process_new_objects(
-        self, obj_cls=None, obj_type=None, objList=None, points_per_request=5
+        self, obj_cls=None, obj_type: str = '', objList=None, points_per_request=5
     ):
         """
         Template to generate BAC0 points instances from information coming from the network.
@@ -302,7 +301,7 @@ def _find_propid_index(key):
 
             pointName = point_infos[_find_propid_index("objectName")]
             presentValue = point_infos[_find_propid_index("presentValue")]
-            if presentValue != None:
+            if presentValue is not None:
                 if obj_type == "analog" or obj_type == "loop":
                     presentValue = float(presentValue)
                 elif obj_type == "multi":
@@ -354,7 +353,7 @@ def _find_propid_index(key):
 
 class RPObjectsProcessing:
     def _process_new_objects(
-        self, obj_cls=NumericPoint, obj_type="analog", objList=None
+        self, obj_cls=NumericPoint, obj_type: str = "analog", objList=None
     ):
         _newpoints = []
         for each in retrieve_type(objList, obj_type):
@@ -565,6 +564,7 @@ def poll(self, command="start", *, delay=10):
         device.poll('stop')
         device.poll(delay = 5)
         """
+        _poll_cls: t.Union[t.Type[DeviceFastPoll], t.Type[DeviceNormalPoll]]
         if delay < 10:
             self.properties.fast_polling = True
             _poll_cls = DeviceFastPoll
@@ -580,7 +580,7 @@ def poll(self, command="start", *, delay=10):
 
         if (
             str(command).lower() == "stop"
-            or command == False
+            or command == False  # noqa E712
             or command == 0
             or delay == 0
         ):
diff --git a/BAC0/core/functions/Calendar.py b/BAC0/core/functions/Calendar.py
index be124d34..66b15cf3 100644
--- a/BAC0/core/functions/Calendar.py
+++ b/BAC0/core/functions/Calendar.py
@@ -4,39 +4,21 @@
 # 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
+import datetime
+import typing as t
 
 # --- 3rd party modules ---
-from bacpypes.pdu import Address, GlobalBroadcast
-from bacpypes.primitivedata import Integer, Date, Time, CharacterString
-from bacpypes.basetypes import DateTime
+from bacpypes.pdu import Address
 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.basetypes import 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
@@ -70,7 +52,7 @@ def create_calendar(self, dict_calendar):
                 if date_entry["recurring"]:
                     weekday = 255
                 else:
-                    weekday = dt.date(year, month, day).weekday() + 1
+                    weekday = datetime.date(year, month, day).weekday() + 1
                     if weekday > 7:
                         weekday = 1
                 _date = (year - 1900, month, day, weekday)
@@ -81,7 +63,7 @@ def create_calendar(self, dict_calendar):
                 year, month, day = (
                     int(x) for x in date_range_entry["startDate"].split("/")
                 )
-                weekday = dt.date(year, month, day).weekday() + 1
+                weekday = datetime.date(year, month, day).weekday() + 1
                 if weekday > 7:
                     weekday = 1
                 start_date = (year - 1900, month, day, weekday)
@@ -89,7 +71,7 @@ def create_calendar(self, dict_calendar):
                 year, month, day = (
                     int(x) for x in date_range_entry["endDate"].split("/")
                 )
-                weekday = dt.date(year, month, day).weekday() + 1
+                weekday = datetime.date(year, month, day).weekday() + 1
                 if weekday > 7:
                     weekday = 1
                 end_date = (year - 1900, month, day, weekday)
@@ -125,7 +107,7 @@ def send_calendar_request(self, request, timeout=10):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
@@ -164,10 +146,10 @@ def read_calendar_dateList(self, address, calendar_instance):
 
         return dict_calendar
 
-    def decode_dateList(self, dateList_object):
+    def decode_dateList(self, dateList_object) -> t.Dict[str, t.List[t.Dict]]:
         dict_calendar = {"dates": [], "dateRanges": []}
         for entry in dateList_object:
-            entry_dict = {}
+            entry_dict: t.Dict[str, t.Union[str, bool]] = {}
             if entry.date:
                 if entry.date[3] == 255:
                     recurring = True
diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py
index f936aa9c..767adb96 100644
--- a/BAC0/core/functions/DeviceCommunicationControl.py
+++ b/BAC0/core/functions/DeviceCommunicationControl.py
@@ -9,12 +9,10 @@
 
 """
 # --- standard Python modules ---
-import datetime as dt
 
 # --- 3rd party modules ---
-from bacpypes.pdu import Address, GlobalBroadcast
-from bacpypes.primitivedata import Date, Time, CharacterString, Unsigned16
-from bacpypes.basetypes import DateTime
+from bacpypes.pdu import Address
+from bacpypes.primitivedata import CharacterString, Unsigned16
 from bacpypes.apdu import (
     DeviceCommunicationControlRequest,
     DeviceCommunicationControlRequestEnableDisable,
@@ -25,9 +23,6 @@
 
 from ...core.io.Read import find_reason
 from ..io.IOExceptions import (
-    SegmentationNotSupported,
-    ReadPropertyException,
-    ReadPropertyMultipleException,
     NoResponseFromController,
     ApplicationNotStarted,
 )
@@ -81,7 +76,7 @@ def dcc(self, address=None, duration=None, password=None, state=None):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py
index 84a11819..cf88a9e0 100644
--- a/BAC0/core/functions/GetIPAddr.py
+++ b/BAC0/core/functions/GetIPAddr.py
@@ -11,28 +11,32 @@
 accepted by every devices (>3.8.38.1 bacnet.jar of Tridium Jace for example)
 
 """
+import socket
+import ipaddress
+import typing as t
+
 from bacpypes.pdu import Address
 
 from ..io.IOExceptions import NetworkInterfaceException
-
-import socket
-import subprocess
-import ipaddress
-import sys
-import re
 from ...core.utils.notes import note_and_log
 
 
+DEFAULT_PORT = 47808
+
+
 @note_and_log
 class HostIP:
     """
     Special class to identify host IP informations
     """
 
-    def __init__(self, port=47808):
+    def __init__(self, port: t.Optional[int] = None) -> None:
         ip = self._findIPAddr()
         mask = self._findSubnetMask(ip)
-        self._port = port
+        if port is not None:
+            self._port = port
+        else:
+            self._port = DEFAULT_PORT
         self.interface = ipaddress.IPv4Interface("{}/{}".format(ip, mask))
 
     @property
@@ -52,7 +56,7 @@ def ip_address(self):
         return "{}".format(self.interface.ip.compressed)
 
     @property
-    def address(self):
+    def address(self) -> Address:
         """
         IP Address using bacpypes Address format
         """
@@ -81,7 +85,7 @@ def port(self):
         """
         return self._port
 
-    def _findIPAddr(self):
+    def _findIPAddr(self) -> str:
         """
         Retrieve the IP address connected to internet... used as
         a default IP address when defining Script
@@ -90,7 +94,7 @@ def _findIPAddr(self):
         """
         s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         try:
-            s.connect(("google.com", 0))
+            s.connect(("google.com", 443))
             addr = s.getsockname()[0]
             s.close()
         except socket.error:
@@ -99,7 +103,7 @@ def _findIPAddr(self):
             )
         return addr
 
-    def _findSubnetMask(self, ip):
+    def _findSubnetMask(self, ip: str) -> str:
         """
         Retrieve the broadcast IP address connected to internet... used as
         a default IP address when defining Script
@@ -124,19 +128,20 @@ def _findSubnetMask(self, ip):
             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'."
+                "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):
+def validate_ip_address(ip: Address) -> bool:
     result = True
     s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     try:
         if not isinstance(ip, Address):
             raise ValueError("Provide Address as bacpypes.Address object")
         s.bind(ip.addrTuple)
-    except OSError as error:
+    except OSError:
         result = False
     finally:
         s.close()
diff --git a/BAC0/core/functions/Reinitialize.py b/BAC0/core/functions/Reinitialize.py
index 513f388d..6f5b5519 100644
--- a/BAC0/core/functions/Reinitialize.py
+++ b/BAC0/core/functions/Reinitialize.py
@@ -10,21 +10,16 @@
 """
 from ...core.io.Read import find_reason
 from ..io.IOExceptions import (
-    SegmentationNotSupported,
-    ReadPropertyException,
-    ReadPropertyMultipleException,
     NoResponseFromController,
     ApplicationNotStarted,
 )
 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 Date, Time, CharacterString
-from bacpypes.basetypes import DateTime
+from bacpypes.pdu import Address
+from bacpypes.primitivedata import CharacterString
 from bacpypes.apdu import (
     ReinitializeDeviceRequest,
     ReinitializeDeviceRequestReinitializedStateOfDevice,
@@ -74,7 +69,7 @@ def reinitialize(self, address=None, password=None, state="coldstart"):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
diff --git a/BAC0/core/functions/Schedule.py b/BAC0/core/functions/Schedule.py
index 3f1d59ce..0f832cb6 100644
--- a/BAC0/core/functions/Schedule.py
+++ b/BAC0/core/functions/Schedule.py
@@ -4,37 +4,24 @@
 # 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 (
-    SegmentationNotSupported,
-    ReadPropertyException,
-    ReadPropertyMultipleException,
-    NoResponseFromController,
-    ApplicationNotStarted,
-)
+from ..io.IOExceptions import NoResponseFromController
 from ...core.utils.notes import note_and_log
 
 # --- standard Python modules ---
-import datetime as dt
+from datetime import time as dt_time
+import typing as t
 
 # --- 3rd party modules ---
-from bacpypes.pdu import Address, GlobalBroadcast
-from bacpypes.primitivedata import Integer, Date, Time, CharacterString
-from bacpypes.basetypes import DateTime
+from bacpypes.pdu import Address
+from bacpypes.primitivedata import Integer
 from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
 from bacpypes.iocb import IOCB
 from bacpypes.core import deferred
-from bacpypes.basetypes import DateTime, DailySchedule, TimeValue, Time
+from bacpypes.basetypes import DailySchedule, TimeValue
 from bacpypes.constructeddata import ArrayOf, Any
 
-from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real, Enumerated
-
-from datetime import time as dt_time
-from datetime import datetime as dt
+from bacpypes.primitivedata import Real, Enumerated
 
 
 @note_and_log
@@ -147,7 +134,7 @@ def send_weeklyschedule_request(self, request, timeout=10):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
         if iocb.ioError:  # unsuccessful: error/reject/abort
@@ -205,8 +192,8 @@ def _read(prop):
             except Exception:
                 raise ()
 
-        schedule = {}
-        _state_text = None
+        schedule: t.Dict[str, t.Union[t.Dict, t.List]] = {}
+        _state_text: t.Union[str, range, t.List[str], None] = None
         offset_MV = 0 if len(object_references) == 0 else 1
 
         try:
@@ -239,30 +226,35 @@ def _read(prop):
             schedule["object_references"] = []
             schedule["references_names"] = []
 
-        schedule["states"] = {}
+        # re-ordered to make type inference possible, but not sure the logic here
+        # is 100% sound (ignored type warnings both look like actual problems).
+        # keeping logic intact and ignoring warnings for now
+        sched_states = {}
         if _state_text == ["inactive", "active"]:
             for i, each in enumerate(_state_text):
-                schedule["states"][each] = i
+                sched_states[each] = i
             presentValue = "{} ({})".format(
-                list(schedule["states"].keys())[int(presentValue.value)],
+                list(sched_states.keys())[int(presentValue.value)],
                 presentValue.value,
             )
-        elif _state_text != "analog":
-            for i, each in enumerate(_state_text):
-                schedule["states"][each] = i + offset_MV
+        elif _state_text == "analog":
+            sched_states = _state_text  # type: ignore[assignment]
+            if presentValue is not None:
+                presentValue = "{}".format(presentValue.value)
+        else:
             try:
+                for i, each in enumerate(_state_text):  # type: ignore[arg-type]
+                    sched_states[each] = i + offset_MV
                 presentValue = "{} ({})".format(
-                    list(schedule["states"].keys())[
+                    list(sched_states.keys())[
                         int(presentValue.value) - offset_MV
                     ],
                     presentValue.value,
                 )
             except TypeError:
                 presentValue = presentValue.value
-        else:
-            schedule["states"] = _state_text
-            if presentValue is not None:
-                presentValue = "{}".format(presentValue.value)
+
+        schedule["states"] = sched_states
         schedule["reliability"] = reliability
         schedule["priority"] = priority
         schedule["presentValue"] = presentValue
diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py
index 8d85a969..b35c3c07 100644
--- a/BAC0/core/functions/TimeSync.py
+++ b/BAC0/core/functions/TimeSync.py
@@ -29,6 +29,7 @@
 from bacpypes.iocb import IOCB
 from bacpypes.core import deferred
 import pytz
+from datetime import datetime
 
 
 def _build_datetime(UTC=False):
@@ -138,14 +139,14 @@ class TimeHandler(object):
     of timezone.
     """
 
-    def __init__(self, tz="America/Montreal"):
+    def __init__(self, tz: str="America/Montreal") -> None:
         self.set_timezone(tz)
 
-    def set_timezone(self, tz):
+    def set_timezone(self, tz: str) -> None:
         self.timezone = pytz.timezone(tz)
 
     @property
-    def now(self):
+    def now(self) -> datetime:
         return dt.datetime.now()
 
     def local_time(self):
@@ -154,11 +155,11 @@ def local_time(self):
     def local_date(self):
         return self.now.date()
 
-    def utcOffset(self):
+    def utcOffset(self) -> float:
         "Returns UTC offset in minutes"
-        return round(self.now.astimezone().utcoffset().total_seconds() / 60)
+        return round(self.now.astimezone().utcoffset().total_seconds() / 60)  # type: ignore[union-attr]
 
-    def is_dst(self):
+    def is_dst(self) -> bool:
         return self.timezone.dst(self.now) != dt.timedelta(0)
 
     def __repr__(self):
diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py
index 667a4eef..46b4f35f 100644
--- a/BAC0/core/io/Read.py
+++ b/BAC0/core/io/Read.py
@@ -21,10 +21,9 @@ def readMultiple()
 """
 
 # --- standard Python modules ---
+import typing as t
 
 # --- 3rd party modules ---
-from bacpypes.debugging import bacpypes_debugging
-
 from bacpypes.pdu import Address
 from bacpypes.object import get_object_class, get_datatype
 from bacpypes.apdu import (
@@ -49,7 +48,7 @@ def readMultiple()
     RangeBySequenceNumber,
     RangeByTime,
 )
-from bacpypes.primitivedata import Tag, ObjectIdentifier, Unsigned, Date, Time
+from bacpypes.primitivedata import Tag, Unsigned, Date, Time
 from bacpypes.constructeddata import Array
 from bacpypes.iocb import IOCB, TimeoutError
 from bacpypes.core import deferred
@@ -65,7 +64,6 @@ def readMultiple()
     SegmentationNotSupported,
     UnknownPropertyError,
     UnknownObjectError,
-    BufferOverflow,
 )
 from bacpypes.object import registered_object_types
 
@@ -74,6 +72,9 @@ def readMultiple()
 # ------------------------------------------------------------------------------
 
 
+ReadValue = t.Union[float, str, t.List]
+
+
 @note_and_log
 class ReadProperty:
     """
@@ -84,13 +85,13 @@ class ReadProperty:
 
     def read(
         self,
-        args,
-        arr_index=None,
-        vendor_id=0,
+        args: str,
+        arr_index: t.Optional[int] = None,
+        vendor_id: int = 0,
         bacoid=None,
-        timeout=10,
-        show_property_name=False,
-    ):
+        timeout: int = 10,
+        show_property_name: bool = False,
+    ) -> t.Union[ReadValue, t.Tuple[ReadValue, str], None]:
         """
         Build a ReadProperty request, wait for the answer and return the value
 
@@ -114,9 +115,6 @@ def read(
 
         self.log_title("Read property", args_split)
 
-        vendor_id = vendor_id
-        bacoid = bacoid
-
         try:
             # build ReadProperty request
             iocb = IOCB(
@@ -141,7 +139,7 @@ def read(
             if not isinstance(apdu, ReadPropertyACK):  # expecting an ACK
                 self._log.warning("Not an ack, see debug for more infos.")
                 self._log.debug("Not an ack. | APDU : {}".format(apdu))
-                return
+                return None
 
             # find the datatype
             datatype = get_datatype(
@@ -173,12 +171,12 @@ def read(
 
             try:
                 int(apdu.propertyIdentifier)
-                objid = apdu.objectIdentifier
                 prop_id = "@prop_{}".format(apdu.propertyIdentifier)
                 value = list(value.items())[0][1]
             except ValueError:
                 prop_id = apdu.propertyIdentifier
             return (value, prop_id)
+
         if iocb.ioError:  # unsuccessful: error/reject/abort
             apdu = iocb.ioError
             reason = find_reason(apdu)
@@ -189,17 +187,20 @@ def read(
                 if reason == "unknownProperty":
                     if "description" in args:
                         self._log.warning(
-                            "The description property is not implemented in the device. Using a default value for internal needs."
+                            "The description property is not implemented in the device. "
+                            "Using a default value for internal needs."
                         )
                         return "Property Not Implemented"
                     elif "inactiveText" in args:
                         self._log.warning(
-                            "The inactiveText property is not implemented in the device. Using a default value of Off for internal needs."
+                            "The inactiveText property is not implemented in the device. "
+                            "Using a default value of Off for internal needs."
                         )
                         return "Off"
                     elif "activeText" in args:
                         self._log.warning(
-                            "The activeText property is not implemented in the device. Using a default value of On for internal needs."
+                            "The activeText property is not implemented in the device. "
+                            "Using a default value of On for internal needs."
                         )
                         return "On"
                     else:
@@ -218,6 +219,7 @@ def read(
                     raise NoResponseFromController(
                         "APDU Abort Reason : {}".format(reason)
                     )
+        return None
 
     def _split_the_read_request(self, args, arr_index):
         """
@@ -230,12 +232,13 @@ def _split_the_read_request(self, args, arr_index):
         number of properties without supporting segmentation
         (FieldServers are a good example)
         """
+        # parameter arr_index appears to be unused in this function?
         nmbr_obj = self.read(args, arr_index=0)
-        return [self.read(args, arr_index=i) for i in range(1, nmbr_obj + 1)]
+        return [self.read(args, arr_index=i) for i in range(1, nmbr_obj + 1)]  # type: ignore
 
     def readMultiple(
-        self, args, request_dict=None, vendor_id=0, timeout=10, show_property_name=False
-    ):
+        self, args: str, request_dict=None, vendor_id: int = 0, timeout: int = 10, show_property_name: bool = False
+    ) -> t.Union[t.Dict, t.List[t.Tuple[t.Any, str]]]:
         """Build a ReadPropertyMultiple request, wait for the answer and return the values
 
         :param args: String with <addr> ( <type> <inst> ( <prop> [ <indx> ] )... )...
@@ -257,9 +260,9 @@ def readMultiple(
         if request_dict is not None:
             request = self.build_rpm_request_from_dict(request_dict, vendor_id)
         else:
-            args = args.split()
-            request = self.build_rpm_request(args, vendor_id=vendor_id)
-            self.log_title("Read Multiple", args)
+            args_list = args.split()
+            request = self.build_rpm_request(args_list, vendor_id=vendor_id)
+            self.log_title("Read Multiple", args_list)
 
         values = []
         dict_values = {}
@@ -281,12 +284,14 @@ def readMultiple(
         if iocb.ioResponse:  # successful response
             apdu = iocb.ioResponse
 
+            # note: the return types along this pass don't appear to be consistent
+            # not sure if this is a real problem or not, leaving as-is and ignoring errors
             if not isinstance(apdu, ReadPropertyMultipleACK):  # expecting an ACK
                 self._log.debug("{:<20}".format("not an ack"))
                 self._log.warning(
-                    "Not an Ack. | APDU : {} / {}".format((apdu, type(apdu)))
+                    "Not an Ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
-                return
+                return  # type: ignore[return-value]
 
             # loop through the results
             for result in apdu.listOfReadAccessResults:
@@ -378,7 +383,7 @@ def readMultiple(
                                         _classname
                                     ].items():
                                         if v["obj_id"] == propertyIdentifier:
-                                            prop_id = (k, propertyIdentifier)
+                                            prop_id = (k, propertyIdentifier)  # type: ignore
                                 if isinstance(value, dict):
                                     value = list(value.items())[0][1]
 
@@ -397,6 +402,8 @@ def readMultiple(
             else:
                 return values
 
+        # note: the return types along this pass don't appear to be consistent
+        # not sure if this is a real problem or not, leaving as-is and ignoring errors
         if iocb.ioError:  # unsuccessful: error/reject/abort
             apdu = iocb.ioError
             reason = find_reason(apdu)
@@ -414,32 +421,36 @@ def readMultiple(
                 raise UnknownObjectError("Unknown object {}".format(args))
             elif reason == "unknownProperty":
                 self._log.warning("Unknown property {}".format(args))
-
-                values.append("")
+                values.append("")  # type: ignore[arg-type]
                 return values
             else:
                 self._log.warning("No response from controller {}".format(reason))
-                values.append("")
+                values.append("")  # type: ignore[arg-type]
                 return values
 
-    def build_rp_request(self, args, arr_index=None, vendor_id=0, bacoid=None):
-        addr, obj_type, obj_inst, prop_id = args[:4]
-        vendor_id = vendor_id
-        bacoid = bacoid
+        return values
 
+    def __get_obj_type(self, obj_type: str, vendor_id) -> int:
         if obj_type.isdigit():
-            obj_type = int(obj_type)
+            return int(obj_type)
         elif "@obj_" in obj_type:
-            obj_type = int(obj_type.split("_")[1])
+            return int(obj_type.split("_")[1])
         elif not get_object_class(obj_type, vendor_id=vendor_id):
             raise ValueError("Unknown object type : {}".format(obj_type))
+        return obj_type  # type: ignore
 
-        obj_inst = int(obj_inst)
+    def build_rp_request(self, args: t.List[str], arr_index=None, vendor_id: int = 0, bacoid=None) -> ReadPropertyRequest:
+        addr, obj_type_str, obj_inst_str, prop_id_str = args[:4]
 
-        if prop_id.isdigit():
-            prop_id = int(prop_id)
-        elif "@prop_" in prop_id:
-            prop_id = int(prop_id.split("_")[1])
+        obj_type = self.__get_obj_type(obj_type_str, vendor_id)
+        obj_inst = int(obj_inst_str)
+
+        if prop_id_str.isdigit():
+            prop_id = int(prop_id_str)
+        elif "@prop_" in prop_id_str:
+            prop_id = int(prop_id_str.split("_")[1])
+        else:
+            prop_id = prop_id_str  # type: ignore
 
         # datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id)
 
@@ -456,7 +467,7 @@ def build_rp_request(self, args, arr_index=None, vendor_id=0, bacoid=None):
         self._log.debug("{:<20} {!r}".format("REQUEST", request))
         return request
 
-    def build_rpm_request(self, args, vendor_id=0):
+    def build_rpm_request(self, args: t.List[str], vendor_id: int = 0) -> ReadPropertyMultipleRequest:
         """
         Build request from args
         """
@@ -468,16 +479,10 @@ def build_rpm_request(self, args, vendor_id=0):
 
         read_access_spec_list = []
         while i < len(args):
-            obj_type = args[i]
+            obj_type_str = args[i]
             i += 1
 
-            if obj_type.isdigit():
-                obj_type = int(obj_type)
-            elif "@obj_" in obj_type:
-                obj_type = int(obj_type.split("_")[1])
-            elif not get_object_class(obj_type, vendor_id=vendor_id):
-                raise ValueError("Unknown object type : {}".format(obj_type))
-
+            obj_type = self.__get_obj_type(obj_type_str, vendor_id)
             obj_inst = int(args[i])
             i += 1
 
@@ -489,7 +494,7 @@ def build_rpm_request(self, args, vendor_id=0):
                 if prop_id not in PropertyIdentifier.enumerations:
                     try:
                         if "@prop_" in prop_id:
-                            prop_id = int(prop_id.split("_")[1])
+                            prop_id = int(prop_id.split("_")[1])  # type: ignore[assignment]
                             self._log.debug(
                                 "Proprietary property : {} | {} -> Vendor : {}".format(
                                     obj_type, prop_id, vendor_id
@@ -713,7 +718,7 @@ def readRange(
             if not isinstance(apdu, ReadRangeACK):  # expecting an ACK
                 self._log.warning("Not an ack, see debug for more infos.")
                 self._log.debug(
-                    "Not an ack. | APDU : {} / {}".format((apdu, type(apdu)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
@@ -777,13 +782,13 @@ def readRange(
                         "APDU Abort Reason : {}".format(reason)
                     )
 
-    def read_priority_array(self, addr, obj, obj_instance):
+    def read_priority_array(self, addr, obj, obj_instance) -> t.List:
         pa = self.read("{} {} {} priorityArray".format(addr, obj, obj_instance))
         res = [pa]
         for each in range(1, 17):
-            _pa = pa[each]
+            _pa = pa[each]  # type: ignore[index]
             for k, v in _pa.__dict__.items():
-                if v != None:
+                if v is not None:
                     res.append(v)
         return res
 
@@ -807,7 +812,7 @@ def find_reason(apdu):
         except IndexError:
             return code
     except KeyError as err:
-        return "KeyError: {} has no key {0!r}".format(type(apdu), err.args[0])
+        return "KeyError: {} has no key {!r}".format(type(apdu), err.args[0])
 
 
 def cast_datatype_from_tag(propertyValue, obj_id, prop_id):
diff --git a/BAC0/core/io/Simulate.py b/BAC0/core/io/Simulate.py
index 0f8039aa..979c4781 100644
--- a/BAC0/core/io/Simulate.py
+++ b/BAC0/core/io/Simulate.py
@@ -1,4 +1,5 @@
 #!/usr/bin/python
+# type: ignore
 # -*- coding: utf-8 -*-
 #
 # Copyright (C) 2015 by Christian Tremblay, P.Eng <christian.tremblay@servisys.com>
diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py
index bea281db..92bc49bf 100644
--- a/BAC0/core/io/Write.py
+++ b/BAC0/core/io/Write.py
@@ -103,7 +103,7 @@ def write(self, args, vendor_id=0, timeout=10):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
@@ -280,7 +280,7 @@ def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10):
             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)))
+                    "Not an ack. | APDU : {} / {}".format(apdu, type(apdu))
                 )
                 return
 
diff --git a/BAC0/core/proprietary_objects/object.py b/BAC0/core/proprietary_objects/object.py
index c66fc022..77332764 100644
--- a/BAC0/core/proprietary_objects/object.py
+++ b/BAC0/core/proprietary_objects/object.py
@@ -4,9 +4,10 @@
     register_object_type,
     registered_object_types,
 )
+from typing import Any, Dict
 
 # Prochaine étape : créer une focntion qui va lire "all" et se redéfinir dynamiquement
-def create_proprietary_object(params):
+def create_proprietary_object(params: Dict[str, Any]) -> None:
 
     try:
         _validate_params(params)
@@ -36,7 +37,7 @@ def create_proprietary_object(params):
     registered_object_types["BAC0"][params["name"]] = params["properties"]
 
 
-def _validate_params(params):
+def _validate_params(params: Dict[str, Any]) -> bool:
     if not params["name"]:
         raise ValueError(
             "Proprietary definition dict must contains a name key with a custom class name"
diff --git a/BAC0/core/utils/notes.py b/BAC0/core/utils/notes.py
index 6f53ae9b..797c0f85 100644
--- a/BAC0/core/utils/notes.py
+++ b/BAC0/core/utils/notes.py
@@ -10,8 +10,9 @@
 from collections import namedtuple
 from datetime import datetime
 import logging
-from logging import FileHandler
+from logging import FileHandler, Logger
 import sys
+import typing as t
 
 import os
 from os.path import expanduser, join
@@ -26,7 +27,7 @@
 
 
 class LogList:
-    LOGGERS = []
+    LOGGERS: t.List[Logger] = []
 
 
 def convert_level(level):
diff --git a/BAC0/db/influxdb.py b/BAC0/db/influxdb.py
index 21e85c22..c084d5ce 100644
--- a/BAC0/db/influxdb.py
+++ b/BAC0/db/influxdb.py
@@ -1,11 +1,10 @@
 try:
     from influxdb_client import InfluxDBClient, Point, WriteOptions
-
+    from influxdb_client.client.write import WriteApi
 except ImportError:
     raise ImportError("Install influxdb to use this feature")
 
 import pytz
-from datetime import datetime
 
 
 class InfluxDB:
@@ -21,7 +20,8 @@ class InfluxDB:
     tags_file = None
     username = None
     password = None
-    client = None
+    client: InfluxDBClient
+    write_api: WriteApi
 
     def __init__(self, params):
         # params should be a dict with name=InfluxDB and bucket=valid_bucket_to_use.
diff --git a/BAC0/db/sql.py b/BAC0/db/sql.py
index 4d19e4a3..87c0c6dc 100644
--- a/BAC0/db/sql.py
+++ b/BAC0/db/sql.py
@@ -5,7 +5,7 @@
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
 """
-sql.py - 
+sql.py -
 """
 
 # --- standard Python modules ---
@@ -271,7 +271,7 @@ def his_from_sql(self, db_name, point):
         """
         Retrive point histories from SQL database
         """
-        his = self._read_from_sql('select * from "{}"'.format("history", db_name))
+        his = self._read_from_sql('select * from "{}"'.format("history"), db_name)
         his.index = his["index"].apply(Timestamp)
         return his.set_index("index")[point]
 
diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py
index 05c55961..2ae82d14 100644
--- a/BAC0/scripts/Base.py
+++ b/BAC0/scripts/Base.py
@@ -21,20 +21,21 @@ def stopApp()
 """
 import random
 import sys
+import typing as t
 
 # --- standard Python modules ---
 from threading import Thread
 
-from bacpypes.basetypes import DeviceStatus, ServicesSupported
+from bacpypes.basetypes import DeviceStatus
 from bacpypes.core import enable_sleeping
 from bacpypes.core import run as startBacnetIPApp
 from bacpypes.core import stop as stopBacnetIPApp
 from bacpypes.local.device import LocalDeviceObject
 from bacpypes.primitivedata import CharacterString
-
-from .. import infos
+from bacpypes.pdu import Address
 
 # --- this application's modules ---
+from .. import infos
 from ..core.app.ScriptApplication import (
     BAC0Application,
     BAC0BBMDDeviceApplication,
@@ -95,7 +96,7 @@ class Base:
     :param segmentationSupported='segmentedBoth':
     """
 
-    _used_ips = set()
+    _used_ips: t.Set[Address] = set()
 
     def __init__(
         self,
@@ -165,7 +166,7 @@ def __init__(
         self.modelName = modelName
         self.description = description
 
-        self.discoveredDevices = None
+        self.discoveredDevices: t.Optional[t.Dict[t.Tuple[str, int], int]] = None
         self.systemStatus = DeviceStatus(1)
 
         self.bbmdAddress = bbmdAddress
diff --git a/BAC0/scripts/Complete.py b/BAC0/scripts/Complete.py
index 27337dc6..9d19396d 100644
--- a/BAC0/scripts/Complete.py
+++ b/BAC0/scripts/Complete.py
@@ -26,6 +26,7 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty)
 import logging
 import pandas as pd
 import time
+import typing as t
 
 
 # --- 3rd party modules ---
@@ -61,9 +62,7 @@ class Stats_Mixin:
     def number_of_devices(self):
         if not self.discoveredDevices:
             return 0
-        s = []
-        [s.append(x) for x in self.discoveredDevices.items() if x[1] > 0]
-        return len(s)
+        return len([x for x in self.discoveredDevices.items() if x[1] > 0])
 
     @property
     def number_of_registered_trends(self):
@@ -105,7 +104,7 @@ def network_stats(self):
         """
         Used by Flask to show informations on the network
         """
-        statistics = {}
+        statistics: t.Dict[str, t.Any] = {}
         mstp_networks = []
         mstp_map = {}
         ip_devices = []
@@ -190,7 +189,7 @@ def __init__(
     @property
     def devices(self):
         lst = []
-        for device in list(self.discoveredDevices):
+        for device in list(self.discoveredDevices or {}):
             try:
                 deviceName, vendorName = self.readMultiple(
                     "{} device {} objectName vendorName".format(device[0], device[1])
diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py
index 26594757..fcb0cbfe 100755
--- a/BAC0/scripts/Lite.py
+++ b/BAC0/scripts/Lite.py
@@ -22,11 +22,9 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty)
 
 """
 # --- standard Python modules ---
-import time
-from datetime import datetime
 import weakref
 from collections import namedtuple
-
+import typing as t
 
 # --- this application's modules ---
 from ..scripts.Base import Base
@@ -100,17 +98,17 @@ class Lite(
 
     def __init__(
         self,
-        ip=None,
-        port=None,
-        mask=None,
+        ip: t.Optional[str] = None,
+        port: t.Optional[int] = None,
+        mask: t.Optional[int] = None,
         bbmdAddress=None,
-        bbmdTTL=0,
+        bbmdTTL: int = 0,
         bdtable=None,
-        ping=True,
-        ping_delay=300,
-        db_params=None,
+        ping: bool = True,
+        ping_delay: int = 300,
+        db_params: t.Optional[t.Dict[str, t.Any]] = None,
         **params
-    ):
+    ) -> None:
         self._log.info(
             "Starting BAC0 version {} ({})".format(
                 version, self.__module__.split(".")[-1]
@@ -137,9 +135,11 @@ def __init__(
             try:
                 ip, subnet_mask_and_port = ip.split("/")
                 try:
-                    mask, port = subnet_mask_and_port.split(":")
+                    mask_s, port_s = subnet_mask_and_port.split(":")
+                    mask = int(mask_s)
+                    port = int(port_s)
                 except ValueError:
-                    mask = subnet_mask_and_port
+                    mask = int(subnet_mask_and_port)
             except ValueError:
                 ip = ip
 
@@ -210,7 +210,10 @@ def known_network_numbers(self):
         return self.this_application.nse._learnedNetworks
 
     def discover(
-        self, networks="known", limits=(0, 4194303), global_broadcast=False, reset=False
+        self, networks: t.Union[str, t.List[int], int] = "known",
+        limits: t.Tuple[int, int] = (0, 4194303),
+        global_broadcast: bool = False,
+        reset: bool = False
     ):
         """
         Discover is meant to be the function used to explore the network when we
@@ -264,7 +267,7 @@ def discover(
             elif networks == "known":
                 _networks = self.known_network_numbers.copy()
             else:
-                if networks < 65535:
+                if isinstance(networks, int) and networks < 65535:
                     _networks.append(networks)
 
         if _networks:
@@ -297,11 +300,11 @@ def discover(
                 found.append(each)
         return found
 
-    def register_device(self, device):
+    def register_device(self, device: t.Union[RPDeviceConnected, RPMDeviceConnected]) -> None:
         oid = id(device)
         self._registered_devices[oid] = device
 
-    def ping_registered_devices(self):
+    def ping_registered_devices(self) -> None:
         """
         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
@@ -365,7 +368,7 @@ def unregister_device(self, device):
         except KeyError:
             pass
 
-    def add_trend(self, point_to_trend):
+    def add_trend(self, point_to_trend: t.Union[Point, TrendLog, VirtualPoint]) -> None:
         """
         Add point to the list of histories that will be handled by Bokeh
 
@@ -375,14 +378,14 @@ def add_trend(self, point_to_trend):
         if (
             isinstance(point_to_trend, Point)
             or isinstance(point_to_trend, TrendLog)
-            or (isinstance(point_to_trend, VirtualPoint))
+            or isinstance(point_to_trend, VirtualPoint)
         ):
             oid = id(point_to_trend)
             self._points_to_trend[oid] = point_to_trend
         else:
             raise TypeError("Please provide point containing history")
 
-    def remove_trend(self, point_to_remove):
+    def remove_trend(self, point_to_remove: t.Union[Point, TrendLog, VirtualPoint]) -> None:
         """
         Remove point from the list of histories that will be handled by Bokeh
 
@@ -401,7 +404,7 @@ def remove_trend(self, point_to_remove):
             del self._points_to_trend[oid]
 
     @property
-    def devices(self):
+    def devices(self) -> t.List[t.Tuple[str, str, str, int]]:
         """
         This property will create a good looking table of all the discovered devices
         seen on the network.
@@ -410,7 +413,7 @@ def devices(self):
         manufacturer, etc and in big network, this could be a long process.
         """
         lst = []
-        for device in list(self.discoveredDevices):
+        for device in list(self.discoveredDevices or {}):
             try:
                 deviceName, vendorName = self.readMultiple(
                     "{} device {} objectName vendorName".format(device[0], device[1])
@@ -433,22 +436,22 @@ def devices(self):
                 self._log.warning("No response from {}".format(device))
                 continue
             lst.append((deviceName, vendorName, device[0], device[1]))
-        return lst
+        return lst  # type: ignore[return-value]
 
     @property
-    def trends(self):
+    def trends(self) -> t.List[t.Any]:
         """
         This will present a list of all registered trends used by Bokeh Server
         """
         return list(self._points_to_trend.values())
 
-    def disconnect(self):
+    def disconnect(self) -> None:
         self._log.debug("Disconnecting")
         for each in self.registered_devices:
             each.disconnect()
         super().disconnect()
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "Bacnet Network using ip {} with device id {}".format(
             self.localIPAddr, self.Boid
         )
diff --git a/BAC0/tasks/Devices.py b/BAC0/tasks/Devices.py
index 5a148c8b..e8d64e71 100644
--- a/BAC0/tasks/Devices.py
+++ b/BAC0/tasks/Devices.py
@@ -4,12 +4,6 @@
 # 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 ---
@@ -18,7 +12,6 @@
 from .TaskManager import Task
 from ..core.io.IOExceptions import BadDeviceDefinition
 from ..core.devices.Device import Device
-from ..core.utils.notes import note_and_log
 
 """
 A way to define a BAC0.device using a task, so it won't block the Notebook or the REPL
diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py
index aff5130a..a6622cea 100644
--- a/BAC0/tasks/Poll.py
+++ b/BAC0/tasks/Poll.py
@@ -10,14 +10,16 @@
 
 # --- standard Python modules ---
 import weakref
-
-# --- 3rd party modules ---
-from bacpypes.core import deferred
+import typing as t
 
 # --- this application's modules ---
 from .TaskManager import Task
 from ..core.utils.notes import note_and_log
 
+if t.TYPE_CHECKING:
+    from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected
+
+
 # ------------------------------------------------------------------------------
 class MultiplePollingFailures(Exception):
     pass
@@ -31,7 +33,7 @@ class SimplePoll(Task):
         device['point_name'].poll(delay=60)
     """
 
-    def __init__(self, point, *, delay=10):
+    def __init__(self, point, *, delay: int = 10) -> None:
         """
         :param point: (BAC0.core.device.Points.Point) name of the point to read
         :param delay: (int) Delay between reads in seconds, defaults = 10sec
@@ -62,7 +64,8 @@ class DevicePoll(Task):
     ReadPropertyMultiple requests.
     """
 
-    def __init__(self, device, delay=10, name="", prefix="basic_poll"):
+    def __init__(self, device: t.Union['RPMDeviceConnected', 'RPDeviceConnected'],
+                 delay: int = 10, name: str = "", prefix: str = "basic_poll") -> None:
         """
         :param device: (BAC0.core.devices.Device.Device) device to poll
         :param delay: (int) Delay between polls in seconds, defaults = 10sec
@@ -79,10 +82,10 @@ def __init__(self, device, delay=10, name="", prefix="basic_poll"):
         self._counter = 0
 
     @property
-    def device(self):
+    def device(self) -> t.Union['RPMDeviceConnected', 'RPDeviceConnected', None]:
         return self._device()
 
-    def task(self):
+    def task(self) -> None:
         if self.device.properties.ping_failures > 0:
             self.device._log.warning(
                 "{} ({}) | Ping failed, skipping polling for now. Resending a ping to speed up things".format(
diff --git a/BAC0/tasks/RecurringTask.py b/BAC0/tasks/RecurringTask.py
index 757368c2..021268b2 100644
--- a/BAC0/tasks/RecurringTask.py
+++ b/BAC0/tasks/RecurringTask.py
@@ -5,11 +5,12 @@
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
 """
-RecurringTask.py - execute a recurring task 
+RecurringTask.py - execute a recurring task
 """
 
 from .TaskManager import Task
 from ..core.utils.notes import note_and_log
+from typing import Callable, Union, Tuple, Any
 
 
 @note_and_log
@@ -18,7 +19,10 @@ class RecurringTask(Task):
     Start a recurring task (a function passed)
     """
 
-    def __init__(self, fnc, delay=60, name="recurring"):
+    def __init__(self,
+                 fnc: Union[Tuple[Callable, Any], Callable],
+                 delay: int = 60,
+                 name: str = "recurring") -> None:
         """
         :param fnc: a function or a tuple (function, args)
         :param delay: (int) Delay between reads executions
@@ -36,7 +40,7 @@ def __init__(self, fnc, delay=60, name="recurring"):
             )
         Task.__init__(self, name=name, delay=delay)
 
-    def task(self):
+    def task(self) -> None:
         if self.fnc_args:
             self.func(self.fnc_args)
         else:
diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py
index d42cd834..96de2a56 100644
--- a/BAC0/tasks/TaskManager.py
+++ b/BAC0/tasks/TaskManager.py
@@ -5,7 +5,7 @@
 # Licensed under LGPLv3, see file LICENSE in this source tree.
 #
 """
-TaskManager.py - creation of threads used for repetitive tasks.  
+TaskManager.py - creation of threads used for repetitive tasks.
 
 A key building block for point simulation.
 """
@@ -62,7 +62,7 @@ def process(cls):
                 time.sleep(1)
             except DeviceNotConnected as error:
                 cls._log.warning(
-                    "Device disconnected. Removing task ({}).".format(error, task)
+                    "Device disconnected with error {}. Removing task ({}).".format(error, task)
                 )
                 cls.tasks.remove(task.id)
             except Exception as error:
@@ -150,7 +150,7 @@ def __init__(self, fn=None, name=None, delay=0):
         self.average_execution_delay = 0
         self.average_latency = 0
         self.next_execution = time.time() + delay + (random() * 10)
-        self.execution_time = 0
+        self.execution_time = 0.0
         self.count = 0
         self.id = id(self)
         self._kwargs = None
@@ -171,7 +171,7 @@ def execute(self):
             self.fn()
         else:
             if self._kwargs is not None:
-                self.task(self._kwargs)
+                self.task(**self._kwargs)
             else:
                 self.task()
         if self.previous_execution:
diff --git a/BAC0/tools/tad_display.py b/BAC0/tools/tad_display.py
index 8e932670..8facb2a9 100644
--- a/BAC0/tools/tad_display.py
+++ b/BAC0/tools/tad_display.py
@@ -1,22 +1,13 @@
+import typing as t
 from . import const
 
 FILE_HEADER = const.FILE_HEADER
 FILE_FOOTER = const.FILE_FOOTER
 TAG = const.TAG
 
-template_example = {
-    "names": [("name", "dev_id")],
-}
 
-
-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.
-    """
+def convert(d, device_name=None):
     list_of_tags = []
-    device_id = None
     OBJECT_TYPE = {
         "analogValue": "AV",
         "analogInput": "AI",
@@ -52,21 +43,18 @@ def convert(d, device_name=None, make_template=False):
         "MV": 10,
     }
 
-    if make_template:
-        device_name = "$DEVICE_NAME"
-        device_id = "$DEVICE_ID"
-
-    data = {}
     for each in d.points:
+        data: t.Dict[str, t.Union[str, int]] = {}
         name = device_name if device_name else d.properties.name
+        obj_type = OBJECT_TYPE[each.properties.type]
         data["name"] = "{}/{}".format(name, each.properties.name)
         data["group"] = ""
-        data["object_type"] = OBJECT_TYPE[each.properties.type]
+        data["object_type"] = obj_type
         data["object_instance"] = each.properties.address
-        data["device_id"] = device_id if device_id else d.properties.device_id
-        data["data_type"] = DATATYPES[data["object_type"]]
+        data["device_id"] = d.properties.device_id
+        data["data_type"] = DATATYPES[obj_type]
         data["object_property"] = 85
-        data["write_priority"] = WRITE_PRIORITY[data["object_type"]]
+        data["write_priority"] = WRITE_PRIORITY[obj_type]
         data["cov"] = "false"
         data["refresh_time"] = 1000
         data["access_mode"] = "READ"
@@ -75,36 +63,13 @@ def convert(d, device_name=None, make_template=False):
         data["array_index"] = -1
         list_of_tags.append(TAG.format(d=data))
 
-    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
+        file = FILE_HEADER
         for tag in list_of_tags:
-            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
+            file += tag
+        file += FILE_FOOTER
+        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
+def write_tags_import_file(name, file):
     with open("{}.xml".format(name), "w") as xml_file:
-        xml_file.write(_content)
+        xml_file.write(file)
\ No newline at end of file
diff --git a/BAC0/web/BokehRenderer.py b/BAC0/web/BokehRenderer.py
index 0030e5be..a0730526 100644
--- a/BAC0/web/BokehRenderer.py
+++ b/BAC0/web/BokehRenderer.py
@@ -13,36 +13,25 @@
     ColumnDataSource,
     HoverTool,
     Range1d,
-    Label,
-    LabelSet,
     LinearAxis,
     Legend,
     LegendItem,
-    Dropdown,
     MultiChoice,
     CustomJS,
 )
-from bokeh.models.widgets import DataTable, TableColumn, Div
-from bokeh.layouts import widgetbox, row, column, gridplot, widgetbox
-from bokeh.palettes import d3, viridis
+from bokeh.models.widgets import DataTable, TableColumn
+from bokeh.layouts import row, column
+from bokeh.palettes import d3
 from bokeh.io import curdoc
 from bokeh.application.handlers import Handler
-from bokeh.models.tools import CustomJSHover
 
-from functools import partial
 
-from tornado import gen
-
-import math
 import pandas as pd
 import weakref
 from queue import Queue
-from collections import deque
 
-from ..tasks.RecurringTask import RecurringTask
 from ..core.utils.notes import note_and_log
 from ..core.devices.Virtuals import VirtualPoint
-from ..core.devices.Trends import TrendLog
 
 
 @note_and_log
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..538c4601
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,7 @@
+[mypy]
+ignore_missing_imports = True
+check_untyped_defs = True
+warn_unused_configs = True
+warn_return_any = False
+disable_error_code = attr-defined,union-attr,var-annotated,misc
+files = BAC0/core/devices/local/object.py,BAC0/core/app/*
diff --git a/requirements-mypy.txt b/requirements-mypy.txt
new file mode 100644
index 00000000..b74f62de
--- /dev/null
+++ b/requirements-mypy.txt
@@ -0,0 +1,2 @@
+mypy==0.971
+types-pytz==2022.1.1


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b431f7dd..009904b1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
@@ -37,6 +37,7 @@ jobs:
         pip install netifaces
         pip install python-dotenv
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+        if [ -f requirements-mypy.txt ]; then pip install -r requirements-mypy.txt; fi
         pip install .
     - name: Lint with flake8
       run: |
@@ -44,6 +45,8 @@ jobs:
         flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
         # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
         flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+    - name: Type check with mypy
+      run: mypy -p BAC0
     - name: Test with pytest
       run: |
         coverage run --source BAC0 -m pytest -v
@@ -55,7 +58,7 @@ jobs:
         GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
         COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
 
-        
+
 #  coveralls:
 #    name: Indicate completion to coveralls.io
 #    needs: build