From d62df43d0ccb9896b6e4ff6b2af7db69a148c2c9 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Tue, 5 Jul 2022 14:36:35 -0600 Subject: [PATCH 01/10] add some type hints generated by MonkeyType --- BAC0/core/app/ScriptApplication.py | 28 +++++++++++++------------ BAC0/core/devices/Points.py | 9 ++++---- BAC0/core/functions/GetIPAddr.py | 10 ++++----- BAC0/core/functions/TimeSync.py | 11 +++++----- BAC0/core/io/Read.py | 23 ++++++++++---------- BAC0/core/proprietary_objects/object.py | 5 +++-- BAC0/tasks/Poll.py | 15 ++++++------- BAC0/tasks/RecurringTask.py | 10 ++++++--- 8 files changed, 61 insertions(+), 50 deletions(-) diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index 425d3f3e..e2239078 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -5,14 +5,14 @@ # 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.] """ @@ -24,7 +24,7 @@ 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 +32,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 +46,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 +170,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 diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py index 9227d612..da5c6cab 100644 --- a/BAC0/core/devices/Points.py +++ b/BAC0/core/devices/Points.py @@ -47,6 +47,7 @@ WritePropertyException, ) from ..utils.notes import note_and_log +from typing import Dict, Union # ------------------------------------------------------------------------------ @@ -263,7 +264,7 @@ def priority(self, priority=None): except ValueError: return None - def _trend(self, res): + def _trend(self, res: float) -> None: # now = datetime.now(tz=pytz.UTC) now = datetime.now().astimezone() self._history.timestamp.append(now) @@ -318,7 +319,7 @@ def lastTimestamp(self): return self._history.timestamp[-1] @property - def history(self): + def history(self) -> Dict[datetime, Union[int, float, str]]: """ returns : (pd.Series) containing timestamp and value of all readings """ @@ -518,7 +519,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 +527,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 ): diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py index 84a11819..43b80e5a 100644 --- a/BAC0/core/functions/GetIPAddr.py +++ b/BAC0/core/functions/GetIPAddr.py @@ -29,7 +29,7 @@ class HostIP: Special class to identify host IP informations """ - def __init__(self, port=47808): + def __init__(self, port: int=47808) -> None: ip = self._findIPAddr() mask = self._findSubnetMask(ip) self._port = port @@ -52,7 +52,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 +81,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 @@ -99,7 +99,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 @@ -129,7 +129,7 @@ def _findSubnetMask(self, ip): 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: diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py index 8d85a969..54ec67ba 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) - 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..89144235 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -70,6 +70,7 @@ def readMultiple() from bacpypes.object import registered_object_types from ..utils.notes import note_and_log +from typing import List # ------------------------------------------------------------------------------ @@ -84,13 +85,13 @@ class ReadProperty: def read( self, - args, - arr_index=None, - vendor_id=0, - bacoid=None, - timeout=10, - show_property_name=False, - ): + args: str, + arr_index: None=None, + vendor_id: int=0, + bacoid: None=None, + timeout: int=10, + show_property_name: bool=False, + ) -> float: """ Build a ReadProperty request, wait for the answer and return the value @@ -234,8 +235,8 @@ def _split_the_read_request(self, args, arr_index): return [self.read(args, arr_index=i) for i in range(1, nmbr_obj + 1)] def readMultiple( - self, args, request_dict=None, vendor_id=0, timeout=10, show_property_name=False - ): + self, args: str, request_dict: None=None, vendor_id: int=0, timeout: int=10, show_property_name: bool=False + ) -> List[float]: """Build a ReadPropertyMultiple request, wait for the answer and return the values :param args: String with ( ( [ ] )... )... @@ -422,7 +423,7 @@ def readMultiple( values.append("") return values - def build_rp_request(self, args, arr_index=None, vendor_id=0, bacoid=None): + def build_rp_request(self, args: List[str], arr_index: None=None, vendor_id: int=0, bacoid: None=None) -> ReadPropertyRequest: addr, obj_type, obj_inst, prop_id = args[:4] vendor_id = vendor_id bacoid = bacoid @@ -456,7 +457,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: List[str], vendor_id: int=0) -> ReadPropertyMultipleRequest: """ Build request from args """ 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/tasks/Poll.py b/BAC0/tasks/Poll.py index aff5130a..fee8ea7d 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -10,13 +10,13 @@ # --- standard Python modules --- import weakref - -# --- 3rd party modules --- -from bacpypes.core import deferred +from typing import Union # --- this application's modules --- from .TaskManager import Task from ..core.utils.notes import note_and_log +from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected + # ------------------------------------------------------------------------------ class MultiplePollingFailures(Exception): @@ -31,7 +31,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 +62,8 @@ class DevicePoll(Task): ReadPropertyMultiple requests. """ - def __init__(self, device, delay=10, name="", prefix="basic_poll"): + def __init__(self, device: 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 +80,10 @@ def __init__(self, device, delay=10, name="", prefix="basic_poll"): self._counter = 0 @property - def device(self): + def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected]: 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: From c679f9fc36437a557e88c1e629f5572399a30961 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Tue, 5 Jul 2022 15:14:27 -0600 Subject: [PATCH 02/10] add some type annotations by hand --- BAC0/scripts/Base.py | 5 ++--- BAC0/scripts/Lite.py | 43 ++++++++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 51e29d77..ccbb27f7 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -25,16 +25,15 @@ def stopApp() # --- 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 - # --- this application's modules --- +from .. import infos from ..core.app.ScriptApplication import ( BAC0Application, BAC0BBMDDeviceApplication, diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index 26594757..dc72a1bd 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] @@ -210,7 +208,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 @@ -297,11 +298,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 +366,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 +376,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 +402,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, t.Any, t.Any]]: """ This property will create a good looking table of all the discovered devices seen on the network. @@ -436,19 +437,19 @@ def devices(self): return lst @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 ) From f3c6a717e22e2e851221803c35315e04e64986e0 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Tue, 5 Jul 2022 15:44:29 -0600 Subject: [PATCH 03/10] add mypy to github workflow and start fixing errors --- .github/workflows/test.yml | 5 ++++- BAC0/core/devices/mixins/CommandableMixin.py | 1 + BAC0/core/io/Simulate.py | 1 + BAC0/tools/__init__.py | 0 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 BAC0/tools/__init__.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b431f7dd..bcd74171 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 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/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 diff --git a/BAC0/tools/__init__.py b/BAC0/tools/__init__.py new file mode 100644 index 00000000..e69de29b From 8d521c7e0f11d93a2f1d95054e769c1b6ebf3fef Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Thu, 8 Sep 2022 16:35:40 -0600 Subject: [PATCH 04/10] start paring down list via mypy.ini and fixing errors, 6 files to go after this commit --- .flake8 | 8 ++++++ .github/workflows/test.yml | 2 +- BAC0/__init__.py | 2 +- BAC0/core/app/ScriptApplication.py | 3 ++- BAC0/core/devices/Trends.py | 22 +--------------- BAC0/core/devices/local/object.py | 26 ++++--------------- BAC0/core/devices/mixins/read_mixin.py | 6 ++--- .../functions/DeviceCommunicationControl.py | 11 +++----- BAC0/core/functions/Reinitialize.py | 11 +++----- BAC0/core/functions/TimeSync.py | 2 +- BAC0/core/io/Write.py | 4 +-- BAC0/core/utils/notes.py | 5 ++-- BAC0/db/influxdb.py | 6 ++--- BAC0/db/sql.py | 4 +-- BAC0/scripts/Base.py | 6 +++-- BAC0/scripts/Complete.py | 9 +++---- BAC0/scripts/Lite.py | 14 +++++----- BAC0/tasks/Devices.py | 7 ----- BAC0/tasks/Poll.py | 2 +- BAC0/tasks/TaskManager.py | 8 +++--- BAC0/web/BokehRenderer.py | 17 +++--------- mypy.ini | 7 +++++ requirements-mypy.txt | 2 ++ 23 files changed, 71 insertions(+), 113 deletions(-) create mode 100644 .flake8 create mode 100644 mypy.ini create mode 100644 requirements-mypy.txt 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/.github/workflows/test.yml b/.github/workflows/test.yml index bcd74171..b9a0b106 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 }} 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 e2239078..0a670bf4 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -18,6 +18,7 @@ """ # --- standard Python modules --- from collections import defaultdict +import typing as t # --- 3rd party modules --- from bacpypes.app import ApplicationIOController @@ -224,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/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 # 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/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/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 56ff1903..3b020c7f 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 @@ -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/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/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/TimeSync.py b/BAC0/core/functions/TimeSync.py index 54ec67ba..b35c3c07 100644 --- a/BAC0/core/functions/TimeSync.py +++ b/BAC0/core/functions/TimeSync.py @@ -157,7 +157,7 @@ def local_date(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) -> bool: return self.timezone.dst(self.now) != dt.timedelta(0) 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/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 ccbb27f7..9964d3f5 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -21,6 +21,7 @@ def stopApp() """ import random import sys +import typing as t # --- standard Python modules --- from threading import Thread @@ -31,6 +32,7 @@ def stopApp() from bacpypes.core import stop as stopBacnetIPApp from bacpypes.local.device import LocalDeviceObject from bacpypes.primitivedata import CharacterString +from bacpypes.pdu import Address # --- this application's modules --- from .. import infos @@ -94,7 +96,7 @@ class Base: :param segmentationSupported='segmentedBoth': """ - _used_ips = set() + _used_ips: t.Set[Address] = set() def __init__( self, @@ -164,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 dc72a1bd..760db513 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -128,16 +128,18 @@ def __init__( if ping: self._ping_task.start() - if ip is None: + if ip is None and port is not None: host = HostIP(port) ip_addr = host.address else: 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 @@ -265,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: @@ -402,7 +404,7 @@ def remove_trend(self, point_to_remove: t.Union[Point, TrendLog, VirtualPoint]) del self._points_to_trend[oid] @property - def devices(self) -> t.List[t.Tuple[str, str, t.Any, t.Any]]: + def devices(self) -> t.List[t.Tuple[float, float, str, int]]: """ This property will create a good looking table of all the discovered devices seen on the network. @@ -411,7 +413,7 @@ def devices(self) -> t.List[t.Tuple[str, str, t.Any, t.Any]]: 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]) 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 # 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 fee8ea7d..7008fbfc 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -80,7 +80,7 @@ def __init__(self, device: Union[RPMDeviceConnected, RPDeviceConnected], self._counter = 0 @property - def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected]: + def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected, None]: return self._device() def task(self) -> None: 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/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 From a127ef3dcfff36c94073fd3eb693838326c95085 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 11:14:19 -0600 Subject: [PATCH 05/10] fix runtime errors in Device and Calendar --- BAC0/core/devices/Device.py | 21 +++++++++--------- BAC0/core/functions/Calendar.py | 38 +++++++++------------------------ 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 8ca60689..2973f2f3 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") @@ -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/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 # 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 From dc0f72153adbc5dcae4b63d0ce2ff976a688417d Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 11:33:33 -0600 Subject: [PATCH 06/10] fix imports, fix some and suppress remaining errors in Schedule.py --- BAC0/core/functions/Schedule.py | 60 ++++++++++++++------------------- 1 file changed, 26 insertions(+), 34 deletions(-) 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 # 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 From 525cf83dbc1ff6e3bd1cee0e4aa86d03761fb2bf Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 11:49:55 -0600 Subject: [PATCH 07/10] try fixing python versions in test matrix setup (3.10 broken) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9a0b106..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, 3.10] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 46419c3a60cbe7688ba4bf4ba693e7ef82b841d9 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 12:29:23 -0600 Subject: [PATCH 08/10] fix type errors in Points --- BAC0/core/devices/Device.py | 2 +- BAC0/core/devices/Points.py | 59 ++++++++++++-------------- BAC0/core/devices/mixins/read_mixin.py | 6 +-- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 2973f2f3..93b1befb 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -249,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): diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py index da5c6cab..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 @@ -47,7 +38,6 @@ WritePropertyException, ) from ..utils.notes import note_and_log -from typing import Dict, Union # ------------------------------------------------------------------------------ @@ -61,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 = {} @@ -137,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): @@ -161,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 @@ -265,7 +255,6 @@ def priority(self, priority=None): return None def _trend(self, res: float) -> None: - # now = datetime.now(tz=pytz.UTC) now = datetime.now().astimezone() self._history.timestamp.append(now) self._history.value.append(res) @@ -281,14 +270,14 @@ def _trend(self, res: float) -> None: 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 @@ -319,7 +308,7 @@ def lastTimestamp(self): return self._history.timestamp[-1] @property - def history(self) -> Dict[datetime, Union[int, float, str]]: + def history(self) -> t.Dict[datetime, t.Union[int, float, str]]: """ returns : (pd.Series) containing timestamp and value of all readings """ @@ -429,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( @@ -735,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, @@ -940,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" @@ -973,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") @@ -1188,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) @@ -1312,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/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 3b020c7f..8efdecb9 100755 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -250,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. @@ -301,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": @@ -353,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): From 0e50dac75ca6ec5867ec36e1e7a5a07b179fda71 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 15:08:09 -0600 Subject: [PATCH 09/10] fix all errors in Read, mypy should pass now --- BAC0/core/functions/GetIPAddr.py | 22 +++--- BAC0/core/io/Read.py | 118 ++++++++++++++++--------------- BAC0/scripts/Lite.py | 6 +- BAC0/tasks/Poll.py | 10 +-- 4 files changed, 83 insertions(+), 73 deletions(-) diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py index 43b80e5a..1ef8d7d7 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: int=47808) -> None: + 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 @@ -90,7 +94,7 @@ def _findIPAddr(self) -> str: """ 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: diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index 89144235..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,16 +64,17 @@ def readMultiple() SegmentationNotSupported, UnknownPropertyError, UnknownObjectError, - BufferOverflow, ) from bacpypes.object import registered_object_types from ..utils.notes import note_and_log -from typing import List # ------------------------------------------------------------------------------ +ReadValue = t.Union[float, str, t.List] + + @note_and_log class ReadProperty: """ @@ -86,12 +86,12 @@ class ReadProperty: def read( self, args: str, - arr_index: None=None, - vendor_id: int=0, - bacoid: None=None, - timeout: int=10, - show_property_name: bool=False, - ) -> float: + arr_index: t.Optional[int] = None, + vendor_id: int = 0, + bacoid=None, + 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 @@ -115,9 +115,6 @@ def read( self.log_title("Read property", args_split) - vendor_id = vendor_id - bacoid = bacoid - try: # build ReadProperty request iocb = IOCB( @@ -142,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( @@ -174,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) @@ -190,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: @@ -219,6 +219,7 @@ def read( raise NoResponseFromController( "APDU Abort Reason : {}".format(reason) ) + return None def _split_the_read_request(self, args, arr_index): """ @@ -231,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: str, request_dict: None=None, vendor_id: int=0, timeout: int=10, show_property_name: bool=False - ) -> List[float]: + 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 ( ( [ ] )... )... @@ -258,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 = {} @@ -282,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: @@ -379,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] @@ -398,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) @@ -415,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: List[str], arr_index: None=None, vendor_id: int=0, bacoid: None=None) -> ReadPropertyRequest: - 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) @@ -457,7 +467,7 @@ def build_rp_request(self, args: List[str], arr_index: None=None, vendor_id: int self._log.debug("{:<20} {!r}".format("REQUEST", request)) return request - def build_rpm_request(self, args: List[str], vendor_id: int=0) -> ReadPropertyMultipleRequest: + def build_rpm_request(self, args: t.List[str], vendor_id: int = 0) -> ReadPropertyMultipleRequest: """ Build request from args """ @@ -469,16 +479,10 @@ def build_rpm_request(self, args: List[str], vendor_id: int=0) -> ReadPropertyMu 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 @@ -490,7 +494,7 @@ def build_rpm_request(self, args: List[str], vendor_id: int=0) -> ReadPropertyMu 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 @@ -714,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 @@ -778,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 @@ -808,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/scripts/Lite.py b/BAC0/scripts/Lite.py index 760db513..fcb0cbfe 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -128,7 +128,7 @@ def __init__( if ping: self._ping_task.start() - if ip is None and port is not None: + if ip is None: host = HostIP(port) ip_addr = host.address else: @@ -404,7 +404,7 @@ def remove_trend(self, point_to_remove: t.Union[Point, TrendLog, VirtualPoint]) del self._points_to_trend[oid] @property - def devices(self) -> t.List[t.Tuple[float, float, str, int]]: + 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. @@ -436,7 +436,7 @@ def devices(self) -> t.List[t.Tuple[float, float, str, int]]: 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) -> t.List[t.Any]: diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py index 7008fbfc..a6622cea 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -10,12 +10,14 @@ # --- standard Python modules --- import weakref -from typing import Union +import typing as t # --- this application's modules --- from .TaskManager import Task from ..core.utils.notes import note_and_log -from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected + +if t.TYPE_CHECKING: + from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected # ------------------------------------------------------------------------------ @@ -62,7 +64,7 @@ class DevicePoll(Task): ReadPropertyMultiple requests. """ - def __init__(self, device: Union[RPMDeviceConnected, RPDeviceConnected], + 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 @@ -80,7 +82,7 @@ def __init__(self, device: Union[RPMDeviceConnected, RPDeviceConnected], self._counter = 0 @property - def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected, None]: + def device(self) -> t.Union['RPMDeviceConnected', 'RPDeviceConnected', None]: return self._device() def task(self) -> None: From 26ba94f2a395bd7286f84d487db7f6b178399912 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Mon, 12 Sep 2022 15:19:56 -0600 Subject: [PATCH 10/10] couple more fixes for 'mypy -p BAC0' --- BAC0/core/devices/local/models.py | 9 ++++----- BAC0/core/functions/GetIPAddr.py | 5 +++-- BAC0/tools/tad_display.py | 11 +++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) 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/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py index 1ef8d7d7..cf88a9e0 100644 --- a/BAC0/core/functions/GetIPAddr.py +++ b/BAC0/core/functions/GetIPAddr.py @@ -128,7 +128,8 @@ def _findSubnetMask(self, ip: str) -> str: 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" @@ -140,7 +141,7 @@ def validate_ip_address(ip: Address) -> bool: 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/tools/tad_display.py b/BAC0/tools/tad_display.py index 05379987..7d6be180 100644 --- a/BAC0/tools/tad_display.py +++ b/BAC0/tools/tad_display.py @@ -1,3 +1,4 @@ +import typing as t from . import const FILE_HEADER = const.FILE_HEADER @@ -41,17 +42,19 @@ def convert(d, device_name=None): "MO": 8, "MV": 10, } - 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"] = d.properties.device_id - data["data_type"] = DATATYPES[data["object_type"]] + 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"