├── .flake8 ├── .gitignore ├── .gitlab-ci.yml ├── Pipfile ├── README.md ├── bacpypes3 ├── __init__.py ├── __main__.py ├── apdu.py ├── app.py ├── appservice.py ├── argparse.py ├── basetypes.py ├── cmd.py ├── comm.py ├── console.py ├── constructeddata.py ├── debugging.py ├── errors.py ├── ipv4 │ ├── __init__.py │ ├── app.py │ ├── bvll.py │ ├── link.py │ └── service.py ├── ipv6 │ ├── __init__.py │ ├── bvll.py │ └── service.py ├── json │ ├── __init__.py │ ├── __main__.py │ └── util.py ├── lib │ ├── __init__.py │ └── batchread.py ├── local │ ├── __init__.py │ ├── analog.py │ ├── binary.py │ ├── cmd.py │ ├── cov.py │ ├── device.py │ ├── event.py │ ├── fault.py │ ├── multistate.py │ ├── networkport.py │ ├── object.py │ ├── oos.py │ └── schedule.py ├── netservice.py ├── npdu.py ├── object.py ├── pdu.py ├── primitivedata.py ├── rdf │ ├── __init__.py │ ├── __main__.py │ ├── core.py │ └── util.py ├── sc │ ├── __init__.py │ ├── bvll.py │ └── service.py ├── service │ ├── __init__.py │ ├── cov.py │ ├── device.py │ └── object.py ├── settings.py ├── vendor.py └── vlan │ ├── __init__.py │ └── link.py ├── bdist.sh ├── check-install.py ├── check-install.sh ├── doc ├── Makefile ├── make.bat └── source │ ├── _figures │ ├── template.odg │ ├── template.png │ ├── template.svg │ └── template.tif │ ├── _static │ └── deleteme │ ├── _templates │ └── deleteme │ ├── conf.py │ ├── gettingstarted │ ├── addresses.rst │ ├── debugging.rst │ ├── index.rst │ ├── objectidentifiers.rst │ ├── running.rst │ ├── setup.rst │ └── udpcommunications.rst │ ├── glossary.rst │ ├── index.rst │ └── samples │ ├── apdu-hex-decode.rst │ ├── client-server.odg │ ├── client-server.png │ ├── cmd-address.rst │ ├── cmd-debugging.rst │ ├── cmd-samples.rst │ ├── console-ase-sap.rst │ ├── console-ini.rst │ ├── console-json.rst │ ├── console-prompt.rst │ ├── console-yaml.rst │ ├── console.rst │ ├── cov-client.rst │ ├── cov-server.rst │ ├── custom-cache.rst │ ├── custom-client.rst │ ├── custom-server.rst │ ├── custom.rst │ ├── discover-devices.rst │ ├── discover-objects.rst │ ├── docker │ └── index.rst │ ├── index.rst │ ├── read-batch.rst │ ├── read-bbmd.rst │ ├── read-property.rst │ ├── router-json.rst │ ├── start-here.rst │ ├── vlan-console-1.rst │ ├── vlan-console-2.rst │ ├── vlan-console-3.rst │ ├── who-has.rst │ └── write-property.rst ├── pyproject.toml ├── pytest.ini ├── release_to_pypi.sh ├── release_to_testpypi.sh ├── samples ├── BACpypes.ini ├── BACpypes.json ├── BACpypes.yml ├── apdu-hex-decode.py ├── cmd-address.py ├── cmd-debugging.py ├── cmd-samples.py ├── console-ase-sap.py ├── console-ini.py ├── console-json.py ├── console-prompt.py ├── console-yaml.py ├── console.py ├── cov-client-shell.py ├── cov-client.py ├── cov-server.py ├── custom-client.py ├── custom-device-cache.py ├── custom-router-cache.py ├── custom-server.py ├── custom.py ├── device-object-init-1.py ├── device-object-init-2.py ├── discover-devices.py ├── discover-objects-rdf.py ├── discover-objects-rdf.ttl ├── discover-objects.py ├── docker │ ├── README.md │ ├── bacpypes-build.sh │ ├── bacpypes-greetings-build.sh │ ├── bacpypes-greetings-run.sh │ ├── bacpypes-greetings-save.sh │ ├── bacpypes-greetings.dockerfile │ ├── bacpypes-greetings.py │ ├── bacpypes-run.sh │ ├── bacpypes-save.sh │ ├── bacpypes.dockerfile │ ├── requirements.txt │ ├── who-is-build.sh │ ├── who-is-console-build.sh │ ├── who-is-console-run-mini.sh │ ├── who-is-console-run.sh │ ├── who-is-console-save.sh │ ├── who-is-console.dockerfile │ ├── who-is-console.py │ ├── who-is-run.sh │ ├── who-is-save.sh │ ├── who-is.dockerfile │ └── who-is.py ├── find-device-by-address.py ├── find-device-by-instance.py ├── ip-to-vlan.json ├── ip-to-vlan.py ├── ipv4-to-ipv4.json ├── ipv4-to-ipv4.py ├── link-layer │ ├── sc-echo-client.py │ ├── sc-echo-server.py │ ├── udp-ipv4-echo-client-build.sh │ ├── udp-ipv4-echo-client-run.sh │ ├── udp-ipv4-echo-client.dockerfile │ ├── udp-ipv4-echo-client.py │ ├── udp-ipv4-echo-server.py │ ├── udp-ipv6-echo-client.py │ └── udp-ipv6-echo-server.py ├── mini-device-revisited.py ├── multiple-stacks.json ├── multiple-stacks.py ├── network-port-1.py ├── read-batch-point-list.py ├── read-batch.py ├── read-bbmd.py ├── read-property-console.py ├── read-property-multiple-console.py ├── read-property.py ├── read-range-console.py ├── router-config.json ├── router-json.py ├── rpc-server.py ├── start-here.py ├── vlan-console-1.py ├── vlan-console-2.py ├── vlan-console-3.py ├── who-has.py ├── write-property-console.py └── write-property.py ├── sandbox ├── calendar-server.py ├── discover-ipv4-foreign-build.sh ├── discover-ipv4-foreign-run.sh ├── discover-ipv4-foreign-save.sh ├── discover-ipv4-foreign.dockerfile ├── discover-ipv4-foreign.py ├── discover-ipv4-normal.py ├── event-notification-recipient.py ├── event-server-avo1-fd.py ├── event-server-avo1-ir.py ├── event-server-avo2.py ├── event-server-avo3.py ├── event-server-bio1.py ├── event-server-bvo1.py ├── event-server-bvo2.py ├── event-server.py ├── get-ip-address.py ├── get-netmask.py ├── initialize-routing-table.py ├── link-layer │ ├── ipv4-bbmd-server.py │ ├── ipv4-foreign-client.py │ ├── ipv4-normal-client.py │ ├── ipv4-normal-server-broadcast.py │ ├── ipv4-normal-server.py │ ├── ipv6-bbmd-server-broadcast.py │ ├── ipv6-bbmd-server.py │ ├── ipv6-foreign-client.py │ ├── ipv6-normal-client.py │ └── ipv6-normal-server.py ├── mqtt-a-side.py ├── mqtt-b-side.py ├── net-normal-client.py ├── net-normal-server.py ├── normal-discover.py ├── octetstring-server.py ├── redis-server.py └── write-property-multiple.py ├── setup.py ├── testpypi_install_bacpypes3.sh └── tests ├── __init__.py ├── clocked_test.py ├── conftest.py ├── mypy.ini ├── state_machine.py ├── test_1.py ├── test__template.py ├── test_app ├── __init__.py ├── test_parse_reference.py └── test_read_write_api.py ├── test_basetypes ├── __init__.py └── test_datetime.py ├── test_constructed_data ├── __init__.py ├── test_any.py ├── test_array.py ├── test_choice.py ├── test_list.py ├── test_read_property_multiple.py ├── test_sequence.py └── test_sequence_of.py ├── test_pdu ├── __init__.py ├── test_address.py ├── test_pci.py └── test_pdu.py ├── test_primitive_data ├── __init__.py ├── test_bit_string.py ├── test_boolean.py ├── test_character_string.py ├── test_date.py ├── test_double.py ├── test_enumerated.py ├── test_integer.py ├── test_null.py ├── test_object_identifier.py ├── test_octet_string.py ├── test_real.py ├── test_tag.py ├── test_time.py └── test_unsigned.py ├── test_utilities ├── __init__.py └── test_state_machine.py ├── test_vlan ├── __init__.py ├── test_ipv4_network.py ├── test_ipv4_router.py └── test_network.py ├── trapped_classes.py └── utilities.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, E501, E262, E265 4 | per-file-ignores = 5 | setup.py:E121 6 | bacpypes3/__init__.py:F401, E402 7 | bacpypes3/ipv4/__init__.py:F401, E402 8 | bacpypes3/ipv6/__init__.py:F401, E402 9 | bacpypes3/json/__init__.py:F401, E402 10 | bacpypes3/lib/__init__.py:F401, E402 11 | bacpypes3/local/__init__.py:F401, E402 12 | bacpypes3/rdf/__init__.py:F401, E402 13 | bacpypes3/sc/__init__.py:F401, E402 14 | bacpypes3/service/__init__.py:F401, E402 15 | bacpypes3/vlan/__init__.py:F401, E402 16 | tests/test_vlan/__init__.py:F401, E402 17 | tests/test_utilities/__init__.py:F401, E402 18 | tests/test_pdu/__init__.py:F401, E402 19 | tests/test_primitive_data/__init__.py:F401, E402 20 | tests/test_constructed_data/__init__.py:F401, E402 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Editor backups 6 | *~ 7 | *.bak 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | # lib/ 19 | # lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | *.whl 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Log files 53 | *.log 54 | 55 | # History files 56 | *.history 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # SERVISYS inc. Gitlab-CI Configuration file 2 | # Official language image. Look for the different tagged releases at: 3 | # https://hub.docker.com/r/library/python/tags/ 4 | image: python:latest 5 | 6 | # Change pip's cache directory to be inside the project directory since we can 7 | # only cache local items. 8 | variables: 9 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 10 | 11 | # Pip's cache doesn't store the python packages 12 | # https://pip.pypa.io/en/stable/reference/pip_install/#caching 13 | # 14 | # If you want to also cache the installed packages, you have to install 15 | # them in a virtualenv and cache it as well. 16 | cache: 17 | paths: 18 | - .cache/pip 19 | - venv/ 20 | 21 | before_script: 22 | - python -V # Print out python version for debugging 23 | - pip install virtualenv 24 | - pip install websockets 25 | - pip install colorama 26 | - pip install atomicwrites 27 | - pip install pytest 28 | - pip install pytest-asyncio 29 | - virtualenv venv 30 | - source venv/bin/activate 31 | 32 | test_python: 33 | script: 34 | - pip install . 35 | - pytest -v -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | verify_ssl = true 3 | name = "pypi" 4 | url = "https://pypi.python.org/simple" 5 | 6 | [packages] 7 | bacpypes3 = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | build = "*" 11 | ifaddr = "*" 12 | rdflib = "*" 13 | websockets = "*" 14 | pyyaml = "*" 15 | black = "*" 16 | flake8 = "*" 17 | mypy = "*" 18 | pytest = "*" 19 | pytest-asyncio = "*" 20 | sphinx = "*" 21 | sphinx-rtd-theme = "*" 22 | twine = "*" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BACpypes3 - Python library for BACnet applications 2 | 3 | BACpypes provides a BACnet application layer and network layer written in 4 | Python for daemons, scripting, and graphical interfaces. This version is the 5 | next generation for Python3.8+, completely overhauled from the Python2.5+ 6 | version using async/await methods and asyncio for communications. 7 | -------------------------------------------------------------------------------- /bacpypes3/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | BACnet Python Package 3 | """ 4 | 5 | # 6 | # Platform Check 7 | # 8 | 9 | import sys as _sys 10 | import warnings as _warnings 11 | 12 | _supported_platforms = ("linux", "win32", "darwin") 13 | 14 | if _sys.platform not in _supported_platforms: 15 | _warnings.warn("unsupported platform", RuntimeWarning) 16 | 17 | # 18 | # Project Metadata 19 | # 20 | 21 | __version__ = "0.0.102" 22 | __author__ = "Joel Bender" 23 | __email__ = "joel@carrickbender.com" 24 | 25 | # 26 | # Settings and Debugging 27 | # 28 | 29 | from . import settings 30 | from . import debugging 31 | from . import errors 32 | 33 | # 34 | # Communications Core Modules 35 | # 36 | 37 | from . import pdu 38 | from . import comm 39 | 40 | # 41 | # Shell 42 | # 43 | 44 | from . import argparse 45 | from . import console 46 | 47 | # from . import singleton 48 | # from . import capability 49 | 50 | # 51 | # Link Layer Modules 52 | # 53 | 54 | from . import ipv4 55 | from . import ipv6 56 | from . import vlan 57 | 58 | try: 59 | import websockets 60 | from . import sc 61 | except ImportError: 62 | pass 63 | 64 | # 65 | # Network Layer Modules 66 | # 67 | 68 | from . import npdu 69 | from . import netservice 70 | 71 | # 72 | # Application Layer Modules 73 | # 74 | 75 | from . import primitivedata 76 | from . import constructeddata 77 | from . import basetypes 78 | from . import object 79 | from . import apdu 80 | 81 | from . import app 82 | from . import appservice 83 | 84 | from . import local 85 | from . import service 86 | from . import vendor 87 | 88 | # 89 | # Library of useful functions 90 | # 91 | 92 | from . import lib 93 | from . import json 94 | 95 | try: 96 | import rdflib # type: ignore[import] 97 | from . import rdf 98 | except ImportError: 99 | pass 100 | 101 | # 102 | # Analysis 103 | # 104 | 105 | # from . import analysis 106 | -------------------------------------------------------------------------------- /bacpypes3/json/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | BACnet/JSON 3 | """ 4 | 5 | from .util import apdu_to_json, json_to_apdu, json_to_sequence, sequence_to_json 6 | -------------------------------------------------------------------------------- /bacpypes3/json/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command Line Interpreter 3 | """ 4 | 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | print("bacpypes json cli placeholder") 9 | sys.exit(1) 10 | -------------------------------------------------------------------------------- /bacpypes3/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Library 3 | # 4 | 5 | from . import batchread 6 | -------------------------------------------------------------------------------- /bacpypes3/local/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Local Objects 3 | # 4 | 5 | from ..basetypes import PropertyIdentifier 6 | from ..vendor import VendorInfo 7 | 8 | 9 | class LocalPropertyIdentifier(PropertyIdentifier): 10 | settings = 512 11 | 12 | 13 | local_vendor_info = VendorInfo(999, property_identifier=LocalPropertyIdentifier) 14 | 15 | from . import object 16 | from . import cmd 17 | from . import cov 18 | from . import device 19 | from . import analog 20 | from . import binary 21 | from . import event 22 | from . import fault 23 | from . import networkport 24 | from . import schedule 25 | -------------------------------------------------------------------------------- /bacpypes3/local/multistate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multistate Input, Output, and Value Object 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Callable 8 | 9 | from ..debugging import ModuleLogger, bacpypes_debugging 10 | 11 | # object module provides basic AnalogInputObject 12 | from ..object import MultiStateInputObject as _MultiStateInputObject 13 | from ..object import MultiStateOutputObject as _MultiStateOutputObject 14 | from ..object import MultiStateValueObject as _MultiStateValueObject 15 | from .cmd import Commandable 16 | from .cov import COVIncrementCriteria 17 | from .event import OutOfRangeEventAlgorithm 18 | from .fault import OutOfRangeFaultAlgorithm 19 | 20 | # local object provides dynamically generated propertyList property 21 | from .object import Object as _Object 22 | 23 | # some debugging 24 | _debug = 0 25 | _log = ModuleLogger(globals()) 26 | 27 | 28 | # this is for sample applications 29 | _vendor_id = 999 30 | 31 | 32 | @bacpypes_debugging 33 | class MultiStateInputObject(_Object, _MultiStateInputObject): 34 | """ 35 | A local multistate input object. 36 | """ 37 | 38 | _debug: Callable[..., None] 39 | _cov_criteria = COVIncrementCriteria 40 | _required = ( 41 | "presentValue", 42 | "statusFlags", 43 | "eventState", 44 | "outOfService", 45 | "numberOfStates", 46 | ) 47 | 48 | 49 | @bacpypes_debugging 50 | class MultiStateOutputObject(Commandable, _Object, _MultiStateOutputObject): 51 | """ 52 | A local multistate output object. 53 | """ 54 | 55 | _debug: Callable[..., None] 56 | _cov_criteria = COVIncrementCriteria 57 | _required = ( 58 | "presentValue", 59 | "statusFlags", 60 | "eventState", 61 | "outOfService", 62 | "numberOfStates", 63 | "priorityArray", 64 | "relinquishDefault", 65 | "currentCommandPriority", 66 | ) 67 | 68 | 69 | @bacpypes_debugging 70 | class MultiStateValueObject(_Object, _MultiStateValueObject): 71 | """ 72 | Vanilla Multistate Value Object 73 | """ 74 | 75 | _debug: Callable[..., None] 76 | _cov_criteria = COVIncrementCriteria 77 | _required = ( 78 | "presentValue", 79 | "priorityArray", 80 | "statusFlags", 81 | "eventState", 82 | "outOfService", 83 | "numberOfStates", 84 | ) 85 | -------------------------------------------------------------------------------- /bacpypes3/local/oos.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, Callable, Optional, Union 2 | 3 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 4 | 5 | from bacpypes3.errors import PropertyError 6 | from bacpypes3.basetypes import PropertyIdentifier 7 | 8 | # some debugging 9 | _debug = 0 10 | _log = ModuleLogger(globals()) 11 | 12 | 13 | @bacpypes_debugging 14 | class OutOfService: 15 | """ 16 | This mix-in class is used to make present-value readonly unless out-of-service 17 | is true. 18 | """ 19 | 20 | _debug: Callable[..., None] 21 | 22 | async def write_property( # type: ignore[override] 23 | self, 24 | attr: Union[int, str], 25 | value: _Any, 26 | index: Optional[int] = None, 27 | priority: Optional[int] = None, 28 | ) -> None: 29 | if _debug: 30 | OutOfService._debug( 31 | "write_property %r %r %r %r", attr, value, index, priority 32 | ) 33 | if (attr == PropertyIdentifier.presentValue) and (not self.outOfService): 34 | raise PropertyError("writeAccessDenied") 35 | 36 | # pass along 37 | await super().write_property(attr, value, index, priority) 38 | -------------------------------------------------------------------------------- /bacpypes3/rdf/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | BACnet/RDF Python Package 3 | """ 4 | 5 | # 6 | # Platform Check 7 | # 8 | 9 | import sys as _sys 10 | import warnings as _warnings 11 | 12 | _supported_platforms = ("linux", "win32", "darwin") 13 | 14 | if _sys.platform not in _supported_platforms: 15 | _warnings.warn("unsupported platform", RuntimeWarning) 16 | 17 | # 18 | # 19 | # 20 | 21 | from .core import BACnetNS, BACnetGraph 22 | 23 | from . import core 24 | from . import util 25 | -------------------------------------------------------------------------------- /bacpypes3/rdf/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command Line Interpreter 3 | """ 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | print("bacpypes rdf cli placeholder") 8 | sys.exit(1) 9 | -------------------------------------------------------------------------------- /bacpypes3/sc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Secure Connect 3 | """ 4 | 5 | from . import bvll 6 | from . import service 7 | -------------------------------------------------------------------------------- /bacpypes3/service/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Services 3 | # 4 | 5 | from . import device 6 | from . import object 7 | from . import cov 8 | -------------------------------------------------------------------------------- /bacpypes3/vlan/link.py: -------------------------------------------------------------------------------- 1 | """ 2 | Link Layer Module 3 | 4 | Classes in this module extend the BVLLServiceAccessPoint subclasses with 5 | additional components necessary to send packets down to a virtual network. 6 | 7 | The clients of instances of these object (next up the stack) is a NetworkAdapter 8 | instance which references a NetworkServiceAccessPoint. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | from ..debugging import bacpypes_debugging, ModuleLogger 14 | 15 | from ..comm import bind, Client, Server 16 | from ..pdu import PDU, LocalStation 17 | from ..vlan import VirtualNode 18 | 19 | # some debugging 20 | _debug = 0 21 | _log = ModuleLogger(globals()) 22 | 23 | # 24 | # VirtualLinkLayer 25 | # 26 | 27 | 28 | @bacpypes_debugging 29 | class VirtualLinkLayer(Client[PDU], Server[PDU]): 30 | """ 31 | Create a link layer mini-stack ... 32 | """ 33 | 34 | def __init__( 35 | self, local_address: LocalStation, network_interface_name: str, **kwargs 36 | ) -> None: 37 | if _debug: 38 | VirtualLinkLayer._debug( 39 | "__init__ %r %r %r", 40 | local_address, 41 | network_interface_name, 42 | kwargs, 43 | ) 44 | 45 | # create a node 46 | self.node = VirtualNode(local_address, network_interface_name) 47 | 48 | # add it below this in the stack 49 | bind(self, self.node) 50 | 51 | async def indication(self, pdu: PDU) -> None: 52 | if _debug: 53 | VirtualLinkLayer._debug("indication %r", pdu) 54 | 55 | # continue down the stack 56 | await self.request(pdu) 57 | 58 | async def confirmation(self, pdu: PDU) -> None: 59 | if _debug: 60 | VirtualLinkLayer._debug("confirmation %r", pdu) 61 | 62 | # countinue up the stack 63 | await self.response(pdu) 64 | 65 | def close(self): 66 | if _debug: 67 | VirtualLinkLayer._debug("close") 68 | -------------------------------------------------------------------------------- /bdist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # remove everything in the current dist/ directory 4 | [ -d dist ] && rm -Rfv dist 5 | 6 | # start with a clean build directory 7 | [ -d build ] && rm -Rfv build 8 | 9 | # use the build package 10 | python3 -m build --no-isolation 11 | 12 | echo 13 | echo This is what was built... 14 | echo 15 | ls -1 dist/ 16 | echo 17 | 18 | # copy the wheel into the docker samples 19 | cp -v dist/*.whl samples/docker/ 20 | 21 | echo 22 | -------------------------------------------------------------------------------- /check-install.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check to see what versions of modules are installed. 3 | """ 4 | import sys 5 | 6 | try: 7 | import bacpypes3 8 | 9 | print("bacpypes3:", bacpypes3.__version__, bacpypes3.__file__) 10 | except: 11 | print("bacpypes3: not installed") 12 | 13 | try: 14 | import websockets 15 | 16 | print("websockets:", websockets.__version__, websockets.__file__) 17 | except: 18 | print("websockets: not installed") 19 | 20 | try: 21 | import ifaddr 22 | 23 | print("ifaddr:", ifaddr.__file__) 24 | except: 25 | print("ifaddr: not installed") 26 | 27 | try: 28 | import yaml 29 | 30 | print("pyyaml:", yaml.__version__, yaml.__file__) 31 | except: 32 | print("pyyaml: not installed") 33 | 34 | try: 35 | import rdflib 36 | 37 | print("rdflib:", rdflib.__version__, rdflib.__file__) 38 | except: 39 | print("rdflib: not installed") 40 | -------------------------------------------------------------------------------- /check-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for version in 3.7 3.8 3.9 3.10; 4 | do 5 | if [ -a "`which python$version`" ]; then 6 | echo "===== $version =====" 7 | python$version check-install.py 8 | echo 9 | fi 10 | done 11 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/source/_figures/template.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/_figures/template.odg -------------------------------------------------------------------------------- /doc/source/_figures/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/_figures/template.png -------------------------------------------------------------------------------- /doc/source/_figures/template.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/_figures/template.tif -------------------------------------------------------------------------------- /doc/source/_static/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/_static/deleteme -------------------------------------------------------------------------------- /doc/source/_templates/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/_templates/deleteme -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'BACpypes3' 10 | copyright = '2023, Joel Bender' 11 | author = 'Joel Bender' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [] 17 | 18 | templates_path = ['_templates'] 19 | exclude_patterns = [] 20 | 21 | 22 | 23 | # -- Options for HTML output ------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 25 | 26 | html_theme = 'sphinx_rtd_theme' 27 | html_static_path = ['_static'] 28 | -------------------------------------------------------------------------------- /doc/source/gettingstarted/addresses.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes3 Addresses 2 | 3 | BACpypes3 Addresses 4 | =================== 5 | 6 | BACpypes3 addresses are used to communicate between BACnet devices directly 7 | (one local station to another) or through a router (a local station through one 8 | or more routers to another local station). 9 | 10 | There are also special addresses to send a message to all of the stations on 11 | the local network (called a "local broadcast"), all of the stations on a 12 | specific network through one of more routers (called a "remote broadcast") or 13 | to all of the stations on all of the networks (called a "global broadcast"). 14 | 15 | .. list-table:: Address Patterns 16 | :widths: 25 25 50 17 | :header-rows: 1 18 | 19 | * - Type 20 | - Example 21 | - Context 22 | * - Local Station 23 | - 12 24 | - ARCNET, MS/TP 25 | * - 26 | - 192.168.0.10 27 | - IPv4, standard port 47808 28 | * - 29 | - 192.168.0.11/24 30 | - IPv4 CIDR, standard port 47808 31 | * - 32 | - 192.168.0.12/255.255.255.0 33 | - IPv4 subnet mask, standard port 47808 34 | * - 35 | - 192.168.0.13:47809 36 | - IPv4, alternate port 37 | * - 38 | - 192.168.0.14/24:47809 39 | - IPv4 CIDR, alternate port 40 | * - 41 | - 192.168.0.15/255.255.255.0:47809 42 | - IPv4 subnet mask, alternate port 47809 43 | * - 44 | - 01:02:03:04:05:06 45 | - Ethernet address 46 | * - 47 | - 0x010203 48 | - VLAN address 49 | * - 50 | - [fe80::9873:c319] 51 | - IPv6 52 | * - 53 | - [fe80::9873:c319]:47809 54 | - IPv6, alternate port 47809 55 | * - 56 | - [fe80::9873:c319/64] 57 | - IPv6 network mask, standard port 47808 58 | * - 59 | - enp0s25 60 | - Interface name (ifaddr) 61 | * - Local Broadcast 62 | - \* 63 | - 64 | * - Remote Station 65 | - 100:12 66 | - ARCNET, MS/TP 67 | * - 68 | - 100:192.168.0.16 69 | - IPv4, standard port 47808 70 | * - 71 | - 100:192.168.0.17:47809 72 | - IPv4, alternate port 47809 73 | * - 74 | - 100:0x010203 75 | - VLAN address 76 | * - Remote Broadcast 77 | - 100:\* 78 | - 79 | * - Global Broadcast 80 | - \*:\* 81 | - 82 | 83 | BACpypes3 has a special addressing mode called **route aware** that allows 84 | applications to bypass the router-to-network discovery and resolution process 85 | and send a message to a specific router. 86 | 87 | .. list-table:: Route Aware Address Patterns 88 | :widths: 25 25 50 89 | :header-rows: 1 90 | 91 | * - Type 92 | - Example 93 | - Context 94 | * - Remote Station 95 | - 200:12\@192.168.0.18 96 | - ARCNET, MS/TP via IPv4 97 | * - 98 | - 200:192.168.0.19\@192.168.0.20 99 | - IPv4 via IPv4 100 | * - 101 | - 200:0x030405\@192.168.0.20 102 | - VLAN via IPv4 103 | * - Remote Broadcast 104 | - 200:\*\@192.168.0.21 105 | - ARCNET, MS/TP via IPv4 106 | * - Global Broadcast 107 | - \*:\*\@192.168.0.22 108 | - all devices via IPv4 109 | 110 | -------------------------------------------------------------------------------- /doc/source/gettingstarted/index.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes Getting Started 1 2 | 3 | Getting Started 4 | =============== 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | setup.rst 10 | udpcommunications.rst 11 | addresses.rst 12 | objectidentifiers.rst 13 | running.rst 14 | debugging.rst 15 | 16 | This tutorial starts with just enough of the basics of BACnet to get a 17 | workstation communicating with another device. If you are already familiar 18 | with BACnet, skip to the next section. 19 | 20 | -------------------------------------------------------------------------------- /doc/source/gettingstarted/objectidentifiers.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes3 Addresses 2 | 3 | Object and Property Identifiers 4 | =============================== 5 | 6 | Object Identifiers 7 | ------------------ 8 | 9 | Every object in a BACnet device has an object identifier consisting of two 10 | parts, an object type and an instance number: 11 | 12 | .. code-block:: text 13 | 14 | object-type,instance 15 | 16 | The *object-type* is the enumeration name in the ANS.1 definition of the 17 | **BACnetObjectType** in Clause 21 such as `binary-input`, `calendar`, or 18 | `device`. The object type may also be an unsigned integer in the range 0..1023. 19 | 20 | .. note:: 21 | 22 | Enumerated values 0-127 are reserved for definition by ASHRAE. Enumerated 23 | values 128-1023 may be used by others subject to the procedures and 24 | constraints described in Clause 23. 25 | 26 | The *instance* is an unsigned integer in the range 0..4194303, note that 4194303 27 | is reserved for "uninitialized." 28 | 29 | .. caution:: 30 | 31 | BACpypes3 also supports the legacy BACpypes format where the object type 32 | and instance are separated by a colon ':' and the object type names are 33 | lower-camel-case such as `binaryInput`. This format is discouraged and may 34 | be deprecated in a future version of BACpypes3. 35 | 36 | Property Identifiers 37 | -------------------- 38 | 39 | Every property of a BACnet object has a property identifier: 40 | 41 | .. code-block:: text 42 | 43 | property-identifier 44 | 45 | The *property-identifier* is the enumeration name in the ASN.1 definition of the 46 | **BACnetPropertyIdentifier** in Clause 21 such as `object-name`, `description`, 47 | or `present-value`. The property identifier may also be an unsigned integer in 48 | the range 49 | 50 | .. note:: 51 | 52 | Enumerated values 0-511 and enumerated values 4194304 and up are reserved 53 | for definition by ASHRAE. Enumerated values 512-4194303 may be used by 54 | others subject to the procedures and constraints described in Clause 23. 55 | 56 | -------------------------------------------------------------------------------- /doc/source/gettingstarted/udpcommunications.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes3 Addresses 2 | 3 | UDP Communications 4 | ================== 5 | 6 | BACnet devices communicate using UDP rather than TCP. This is so 7 | devices do not need to implement a full IP stack (although 8 | many of them do because they support multiple protocols, including 9 | having embedded web servers). 10 | 11 | There are two types of UDP messages; *unicast* which is a message 12 | from one specific IP address (and port) to another device's IP address 13 | (and port); and *broadcast* messages which are sent by one device 14 | and received and processed by all other devices that are listening 15 | on that port. BACnet uses both types of messages and your workstation 16 | will need to receive both types. 17 | 18 | To receive both unicast and broadcast addresses, BACpypes3 19 | opens two sockets, one for unicast traffic and one that only listens 20 | for broadcast messages. The operating system will not allow two applications 21 | to open the same socket at the same time so to run two BACnet applications at 22 | the same time they need to be configured with different ports. 23 | 24 | .. note:: 25 | 26 | The BACnet protocol has been assigned port 47808 (hex 0xBAC0) by 27 | by the `Internet Assigned Numbers Authority `_, and sequentially 28 | higher numbers are used in many applications (i.e. 47809, 47810,...). 29 | There are some BACnet routing and networking issues related to using these higher unoffical 30 | ports, but that is a topic for another tutorial. 31 | 32 | -------------------------------------------------------------------------------- /doc/source/glossary.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes glossary 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | 8 | BACnet 9 | BACnet (Building Automation and Control Network) is the global data 10 | communications standard for building automation and control networks. 11 | It provides a vendor-independent networking solution to enable 12 | interoperability among equipment and control devices for a wide range 13 | of building automation applications. BACnet enables interoperability 14 | by defining communications messages, formats and rules for exchanging 15 | data, commands, and status information. BACnet provides the data 16 | communications infrastructure for intelligent buildings and is 17 | implemented in hundreds of thousands of buildings around the world. 18 | 19 | The BACnet standard was developed and is continuously maintained by the 20 | BACnet Committee, more formally known as SSPC 135 (a Standing Standards 21 | Project Committee) of the American Society of Heating, Refrigerating 22 | and Air-Conditioning Engineers (ASHRAE). 23 | 24 | BACnet is an ISO standard (EN ISO 16484-5), a European standard (DIN EN 25 | ISO 16484-5:2017-12) and a national standard in many countries. 26 | 27 | BACnet device 28 | Any device, real or virtual, that supports digital communication using 29 | the BACnet protocol. 30 | 31 | BACnet network 32 | A network of BACnet devices that share the MAC or VMAC address space 33 | under a particular BACnet network number. 34 | 35 | BACnet internetwork 36 | A set of two or more networks interconnected by BACnet routers. In a 37 | BACnet internetwork interconnected by BACnet routers, there exists 38 | exactly one message path between any two nodes. 39 | 40 | BACnet Device 41 | Any device, real or virtual, that supports digital communication using 42 | the BACnet protocol. 43 | 44 | ephemeral port 45 | An ephemeral port is a communications endpoint (port) of a transport 46 | layer protocol of the Internet protocol suite that is used for only a 47 | short period of time for the duration of a communication session. 48 | Such short-lived ports are allocated automatically within a predefined 49 | range of port numbers by the IP stack software of a computer operating 50 | system. `Wikipedia `_ 51 | 52 | upstream 53 | Something going up a stack from a server to client. 54 | 55 | downstream 56 | Something going down a stack from a client to a server. 57 | 58 | stack 59 | A sequence of communication objects organized in a semi-linear sequence 60 | from the application layer at the top to the physical networking layer(s) 61 | at the bottom. 62 | 63 | discoverable 64 | Something that can be determined using a combination of BACnet objects, 65 | properties and services. For example, discovering the network topology 66 | by using Who-Is-Router-To-Network, or knowing what objects are defined 67 | in a device by reading the *object-list* property. 68 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. BACpypes3 documentation master file, created by 2 | sphinx-quickstart on Sat Mar 11 00:14:30 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to BACpypes3! 7 | ===================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | BACpypes3 is a library for building Python applications that communicate using 14 | the :term:`BACnet` protocol. Installation is easy:: 15 | 16 | $ pip install bacpypes3 17 | 18 | You will be installing the latest released version from 19 | `PyPI `_. You can also check out the 20 | latest version from `GitHub `_:: 21 | 22 | $ git clone https://github.com/JoelBender/BACpypes3.git 23 | $ cd BACpypes3 24 | 25 | And then use the `pipenv `_ utility to create 26 | a virtual environment, install all of the developer tools, then activate it:: 27 | 28 | $ pipenv install --dev 29 | $ pipenv shell 30 | 31 | .. note:: 32 | 33 | If you would like to participate in development, please join 34 | the chat room on `Gitter `_. 35 | 36 | Getting Started 37 | --------------- 38 | 39 | The **bacpypes3** module can be run directly. It will create a small 40 | :term:`stack` of communications objects that are bound together as a 41 | :term:`BACnet device` and present a shell-like prompt:: 42 | 43 | $ python3 -m bacpypes3 44 | > 45 | 46 | The module has a variety options for configuring the small stack for different 47 | environments:: 48 | 49 | $ python3 -m bacpypes3 --help 50 | 51 | And the shell has commands that are useful for examining the local 52 | :term:`BACnet internetwork`, its topology, the connected devices, and their 53 | objects:: 54 | 55 | > help 56 | 57 | .. toctree:: 58 | :maxdepth: 2 59 | 60 | gettingstarted/index.rst 61 | samples/index.rst 62 | 63 | 64 | Glossary 65 | -------- 66 | 67 | .. toctree:: 68 | :maxdepth: 2 69 | 70 | glossary.rst 71 | 72 | 73 | Indices and tables 74 | ================== 75 | 76 | * :ref:`genindex` 77 | * :ref:`modindex` 78 | * :ref:`search` 79 | -------------------------------------------------------------------------------- /doc/source/samples/apdu-hex-decode.rst: -------------------------------------------------------------------------------- 1 | .. apdu-hex-decode.py sample application 2 | 3 | .. _apdu-hex-decode.py: 4 | 5 | apdu-hex-decode.py 6 | ================== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/client-server.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/samples/client-server.odg -------------------------------------------------------------------------------- /doc/source/samples/client-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelBender/BACpypes3/0b5c12c3a8f9cdf0226a63dc3e711b11dfd696d2/doc/source/samples/client-server.png -------------------------------------------------------------------------------- /doc/source/samples/cmd-address.rst: -------------------------------------------------------------------------------- 1 | .. cmd-address.py sample application 2 | 3 | cmd-address.py 4 | ============== 5 | 6 | This is a long line of text. 7 | -------------------------------------------------------------------------------- /doc/source/samples/cmd-debugging.rst: -------------------------------------------------------------------------------- 1 | .. cmd-debugging.py sample application 2 | 3 | cmd-debugging.py 4 | ================ 5 | 6 | This is a long line of test. 7 | -------------------------------------------------------------------------------- /doc/source/samples/cmd-samples.rst: -------------------------------------------------------------------------------- 1 | .. cmd-samples sample application 2 | 3 | cmd-samples.py 4 | ============== 5 | 6 | This is a long line of text. 7 | -------------------------------------------------------------------------------- /doc/source/samples/console-ase-sap.rst: -------------------------------------------------------------------------------- 1 | .. console-ase-sap.py sample application 2 | 3 | .. _console-ase-sap.py: 4 | 5 | console-ase-sap.py 6 | ================== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/console-ini.rst: -------------------------------------------------------------------------------- 1 | .. console-ini.py sample application 2 | 3 | .. _console-ini.py: 4 | 5 | console-ini.py 6 | ============== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/console-json.rst: -------------------------------------------------------------------------------- 1 | .. console-json.py sample application 2 | 3 | .. _console-json.py: 4 | 5 | console-json.py 6 | =============== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/console-prompt.rst: -------------------------------------------------------------------------------- 1 | .. console-prompt.py sample application 2 | 3 | .. _console-prompt.py: 4 | 5 | console-prompt.py 6 | ================= 7 | 8 | This is almost identical to the :ref:`console.py` example, all this does is 9 | add a `\-\-prompt` option. 10 | 11 | The `ArgumentParser` class in BACpypes3 is a subclass of the built-in class 12 | with the same name, so extending it is simple:: 13 | 14 | args = ArgumentParser().parse_args() 15 | 16 | The code is replaced with this:: 17 | 18 | # add a way to change the console prompt 19 | parser = ArgumentParser() 20 | parser.add_argument( 21 | "--prompt", 22 | type=str, 23 | help="change the prompt", 24 | default="> ", 25 | ) 26 | args = parser.parse_args() 27 | 28 | And the `Console` constructor changes from this:: 29 | 30 | console = Console() 31 | 32 | to include the prompt:: 33 | 34 | console = Console(prompt=args.prompt) 35 | 36 | Turning on debugging shows the new argument along with the others that are 37 | built-in:: 38 | 39 | $ python3 console-prompt.py --prompt '? ' --debug 40 | DEBUG:__main__:args: Namespace(..., prompt='? ') 41 | DEBUG:__main__:settings: {'debug': ['__main__'], 'color': False, ... } 42 | ? 43 | 44 | -------------------------------------------------------------------------------- /doc/source/samples/console-yaml.rst: -------------------------------------------------------------------------------- 1 | .. console-yaml.py sample application 2 | 3 | .. _console-yaml.py: 4 | 5 | console-yaml.py 6 | =============== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/cov-client.rst: -------------------------------------------------------------------------------- 1 | .. cov-client.py sample application 2 | 3 | .. _cov-client.py: 4 | 5 | cov-client.py 6 | ============= 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/cov-server.rst: -------------------------------------------------------------------------------- 1 | .. cov-server.py sample application 2 | 3 | .. _cov-server.py: 4 | 5 | cov-server.py 6 | ============= 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/custom-cache.rst: -------------------------------------------------------------------------------- 1 | .. custom-cache.py sample application 2 | 3 | .. _custom-cache.py: 4 | 5 | custom-cache.py 6 | =============== 7 | 8 | This application *is a start* of a custom device information cache implementation 9 | that uses Redis as a shared cache for DeviceInfo records. 10 | 11 | The Redis keys will be `bacnet:dev:address` where `address` is the address of 12 | the device and may include the network like `10:1.2.3.4` for an IPv4 device 13 | on network 10. 14 | 15 | This application is also *a start* of a custom routing information cache 16 | implementation that also uses Redis as a shared cache for Who-Is-Router-To-Network 17 | content. When a request is sent to a remote station or remote broadcast, the 18 | cache is used to return the address of the router to the destination network. 19 | 20 | The Redis keys will be `bacnet:rtn:snet:dnet` and the key value is the address 21 | of the router on the `snet` that is the router to the `dnet`. 22 | 23 | -------------------------------------------------------------------------------- /doc/source/samples/custom-client.rst: -------------------------------------------------------------------------------- 1 | .. custom-client.py sample application 2 | 3 | .. _custom-client.py: 4 | 5 | custom-client.py 6 | ================ 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/custom-server.rst: -------------------------------------------------------------------------------- 1 | .. custom-server.py sample application 2 | 3 | .. _custom-server.py: 4 | 5 | custom-server.py 6 | ================ 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/custom.rst: -------------------------------------------------------------------------------- 1 | .. custom.py sample application 2 | 3 | .. _custom.py: 4 | 5 | custom.py 6 | ========= 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/discover-devices.rst: -------------------------------------------------------------------------------- 1 | .. discover-devices.py sample application 2 | 3 | .. _discover-devices.py: 4 | 5 | discover-devices.py 6 | =================== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/discover-objects.rst: -------------------------------------------------------------------------------- 1 | .. discover-objects.py sample application 2 | 3 | .. _discover-objects.py: 4 | 5 | discover-objects.py 6 | =================== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/docker/index.rst: -------------------------------------------------------------------------------- 1 | .. docker samples 2 | 3 | Docker Samples 4 | ============== 5 | 6 | Running BACnet applications in docker is a challenge because the 7 | environment that is normally presented to applications is not the 8 | one that the host has because the intent is to protect containers 9 | (running images) from interfering with or being interfered with 10 | by other devices than the host. 11 | 12 | Docker has a `\-\-network host `_ 13 | option so the container does not get its own IP address allocated, 14 | which is helpful, but only for unicast traffic. Attempts to "bind" to 15 | broadcast addresses of the host fail. 16 | 17 | The solution is to have BACpypes3 applications be configured as 18 | a foreign device and register with a BBMD on the network. To allow 19 | more than one application running on the same host, the application 20 | uses an :term:`ephemeral port` and the operating system will proved 21 | provide an unused socket. 22 | 23 | A side effect is that subsequent runs of the same image will present 24 | most likely not have the same port number, so they will have different 25 | BACnet/IPv4 addresses each time they are run. During debugging it 26 | might be beneficial to assign a fixed socket number. 27 | 28 | This also means that the container can register as a foreign device 29 | with some BBMD in the BACnet intranet. 30 | 31 | Setting Up 32 | ---------- 33 | 34 | The docker samples are often used with unpublished versions of 35 | BACpypes3, so the build scripts include a local build of the 36 | package as a wheel. 37 | 38 | The root folder of the BACpypes3 project has a `bdist.sh` script 39 | which builds Python eggs and a wheel. 40 | 41 | -------------------------------------------------------------------------------- /doc/source/samples/read-batch.rst: -------------------------------------------------------------------------------- 1 | .. read-batch.py sample application 2 | 3 | .. _read-batch.py: 4 | 5 | read-batch.py 6 | ============= 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/read-bbmd.rst: -------------------------------------------------------------------------------- 1 | .. read-bbmd.py sample application 2 | 3 | .. _read-bbmd.py: 4 | 5 | read-bbmd.py 6 | ============ 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/read-property.rst: -------------------------------------------------------------------------------- 1 | .. read-property.py sample application 2 | 3 | .. _read-property.py: 4 | 5 | read-property.py 6 | ================ 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/router-json.rst: -------------------------------------------------------------------------------- 1 | .. router-json.py sample application 2 | 3 | .. _router-json.py: 4 | 5 | router-json.py 6 | ============== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/start-here.rst: -------------------------------------------------------------------------------- 1 | .. start-here.py sample application 2 | 3 | .. _start-here.py: 4 | 5 | start-here.py 6 | ============= 7 | 8 | This is a good place to start. 9 | 10 | BACnet is a peer-to-peer protocol so devices can be clients (issue requests) and 11 | servers (provide responses) and are often both at the same time. This sample 12 | has just enough code to make itself known on the network and can be used as 13 | a starting point for predominantly client-like applications (reading data from 14 | other devices) or server-like applications (gateways). 15 | 16 | The `SimpleArgumentParser` is the same one the module uses, see :ref:`running`:: 17 | 18 | args = SimpleArgumentParser().parse_args() 19 | if _debug: 20 | _log.debug("args: %r", args) 21 | 22 | The `Application` class has different class methods for building an application 23 | stack, the `from_args()` method is looks for the options from the simple 24 | argument parser. Custom applications can add additional options and/or use 25 | their own subclass:: 26 | 27 | # build an application 28 | app = Application.from_args(args) 29 | if _debug: 30 | _log.debug("app: %r", app) 31 | 32 | Server-like applications just run:: 33 | 34 | # like running forever 35 | await asyncio.Future() 36 | 37 | .. note:: 38 | 39 | The `ReinitializeDevice` service can be used to quit the current application 40 | which is in turn wrapped by some supervisory service that automatically 41 | restarts it such as 42 | `systemd `_ 43 | or `docker `_. 44 | -------------------------------------------------------------------------------- /doc/source/samples/vlan-console-1.rst: -------------------------------------------------------------------------------- 1 | .. vlan-console-1.py sample application 2 | 3 | vlan-console-1.py 4 | ================= 5 | 6 | This is a long line of text. 7 | -------------------------------------------------------------------------------- /doc/source/samples/vlan-console-2.rst: -------------------------------------------------------------------------------- 1 | .. vlan-console-2.py sample application 2 | 3 | vlan-console-2.py 4 | ================= 5 | 6 | This is a long line of text. 7 | -------------------------------------------------------------------------------- /doc/source/samples/vlan-console-3.rst: -------------------------------------------------------------------------------- 1 | .. vlan-console-3.py sample application 2 | 3 | vlan-console-3.py 4 | ================= 5 | 6 | This is a long line of text. 7 | -------------------------------------------------------------------------------- /doc/source/samples/who-has.rst: -------------------------------------------------------------------------------- 1 | .. who-has.py sample application 2 | 3 | .. _who-has.py: 4 | 5 | who-has.py 6 | ========== 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /doc/source/samples/write-property.rst: -------------------------------------------------------------------------------- 1 | .. write-property.py sample application 2 | 3 | .. _write-property.py: 4 | 5 | write-property.py 6 | ================= 7 | 8 | This is a long line of text. 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = auto 3 | asyncio_default_fixture_loop_scope = function 4 | -------------------------------------------------------------------------------- /release_to_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build a distribution 4 | . bdist.sh 5 | 6 | read -p "Upload to PyPI? [y/n/x] " yesno || exit 1 7 | 8 | if [ "$yesno" = "y" ] ; 9 | then 10 | python3 -m twine upload dist/* 11 | elif [ "$yesno" = "n" ] ; 12 | then 13 | echo "Skipped..." 14 | else 15 | echo "exit..." 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /release_to_testpypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build a distribution 4 | . build_dist.sh 5 | 6 | read -p "Upload to Test PyPI? [y/n/x] " yesno || exit 1 7 | 8 | if [ "$yesno" = "y" ] ; 9 | then 10 | twine upload -r testpypi --config-file .pypirc dist/* 11 | elif [ "$yesno" = "n" ] ; 12 | then 13 | echo "Skipped..." 14 | else 15 | echo "exit..." 16 | exit 1 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /samples/BACpypes.ini: -------------------------------------------------------------------------------- 1 | [BACpypes] 2 | color: false 3 | -------------------------------------------------------------------------------- /samples/BACpypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "BACpypes": { 4 | "color": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/BACpypes.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | BACpypes: 3 | color: false 4 | -------------------------------------------------------------------------------- /samples/apdu-hex-decode.py: -------------------------------------------------------------------------------- 1 | """ 2 | Given a hex-binary encoded string from the variable portion of an APDU, decode 3 | it into into a tag list and dump out the result. 4 | """ 5 | 6 | import sys 7 | from bacpypes3.debugging import xtob 8 | from bacpypes3.pdu import PDUData 9 | from bacpypes3.primitivedata import TagList 10 | 11 | b = xtob(sys.argv[1]) 12 | d = PDUData(b) 13 | 14 | tl = TagList.decode(d) 15 | for i, t in enumerate(tl): 16 | print(f"[{i}]") 17 | t.debug_contents() 18 | -------------------------------------------------------------------------------- /samples/cmd-address.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Address parameters for command line interpreters 5 | """ 6 | 7 | import sys 8 | import asyncio 9 | 10 | from typing import Callable, Optional, Union 11 | 12 | from bacpypes3.settings import settings 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | 16 | from bacpypes3.console import Console 17 | from bacpypes3.cmd import Cmd 18 | from bacpypes3.comm import bind 19 | from bacpypes3.pdu import Address, IPv4Address, IPv6Address 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class SampleCmd(Cmd): 28 | """ 29 | Sample Console Command utility demonstrating addresses support in bacpypes3 30 | Using the typing module, bacpypes3 can evaluate at runtime the argument 31 | passed to the function then return the right type (or an exception). 32 | """ 33 | 34 | _debug: Callable[..., None] 35 | 36 | async def do_test1(self, addr: Address) -> None: 37 | """ 38 | test1 requires an address argument that will be evaluated as a 39 | bacpypes3.pdu.Address, and if that interpretation is successful, 40 | provided as the parameter value. 41 | 42 | usage: test1 addr 43 | 44 | ex. test1 2:4 or test1 192.168.1.2 45 | """ 46 | if _debug: 47 | SampleCmd._debug("do_test1 %r", addr) 48 | 49 | await self.response(f"test1 {addr!r}") 50 | 51 | async def do_test2(self, addr: Optional[Address] = None) -> None: 52 | """ 53 | test2 is similar to test1 with an optional address. 54 | 55 | usage: test2 [ addr:Address ] 56 | 57 | ex. test2 2:4 or test2 192.168.1.2 or test2 58 | """ 59 | if _debug: 60 | SampleCmd._debug("do_test2 %r", addr) 61 | 62 | await self.response(f"test2 {addr!r}") 63 | 64 | async def do_test3(self, addr: Union[IPv4Address, IPv6Address]) -> None: 65 | """ 66 | This varient accepts only IPv4 or IPv6 addresses or will raise an 67 | exception. 68 | 69 | usage: test3 addr 70 | """ 71 | if _debug: 72 | SampleCmd._debug("do_test3 %r", addr) 73 | 74 | await self.response(f"test3 {addr!r}") 75 | 76 | 77 | async def main() -> None: 78 | try: 79 | console = None 80 | args = ArgumentParser().parse_args() 81 | if _debug: 82 | _log.debug("args: %r", args) 83 | _log.debug("settings: %r", settings) 84 | 85 | # build a very small stack 86 | console = Console() 87 | cmd = SampleCmd() 88 | bind(console, cmd) 89 | 90 | # run until the console is done, canceled or EOF 91 | await console.fini.wait() 92 | 93 | finally: 94 | if console and console.exit_status: 95 | sys.exit(console.exit_status) 96 | 97 | 98 | if __name__ == "__main__": 99 | asyncio.run(main()) 100 | -------------------------------------------------------------------------------- /samples/cmd-debugging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This application adds the CmdDebugging mixin class to provide support for 5 | additional debugging commands. 6 | """ 7 | 8 | import sys 9 | import asyncio 10 | 11 | from typing import Callable 12 | 13 | from bacpypes3.settings import settings 14 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 15 | from bacpypes3.argparse import ArgumentParser 16 | 17 | from bacpypes3.console import Console 18 | from bacpypes3.cmd import Cmd, CmdDebugging 19 | from bacpypes3.comm import bind 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class SampleCmd(Cmd, CmdDebugging): 28 | _debug: Callable[..., None] 29 | 30 | """ 31 | Sample Cmd 32 | """ 33 | 34 | async def do_hello(self) -> None: 35 | """ 36 | usage: hello 37 | """ 38 | await self.response("Hello!") 39 | 40 | 41 | async def main() -> None: 42 | try: 43 | console = None 44 | args = ArgumentParser().parse_args() 45 | if _debug: 46 | _log.debug("args: %r", args) 47 | _log.debug("settings: %r", settings) 48 | 49 | # build a very small stack 50 | console = Console() 51 | cmd = SampleCmd() 52 | bind(console, cmd) 53 | 54 | # run until the console is done, canceled or EOF 55 | await console.fini.wait() 56 | 57 | finally: 58 | if console and console.exit_status: 59 | sys.exit(console.exit_status) 60 | 61 | 62 | if __name__ == "__main__": 63 | asyncio.run(main()) 64 | -------------------------------------------------------------------------------- /samples/console-ase-sap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that forwards the input to a service and echos the 5 | response. The service converts the requests to uppercase. 6 | """ 7 | 8 | import sys 9 | import asyncio 10 | 11 | from typing import Callable 12 | 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | from bacpypes3.console import Console, ConsolePDU 16 | from bacpypes3.comm import Server, ApplicationServiceElement, ServiceAccessPoint, bind 17 | 18 | # some debugging 19 | _debug = 0 20 | _log = ModuleLogger(globals()) 21 | 22 | 23 | @bacpypes_debugging 24 | class Echo(Server[ConsolePDU], ApplicationServiceElement): 25 | """ 26 | Simple example server that echos the downstream strings as uppercase 27 | strings going upstream. If the PDU is None the console is finished, 28 | and this could send an integer status code upstream to exit. 29 | """ 30 | 31 | _debug: Callable[..., None] 32 | 33 | async def indication(self, pdu: ConsolePDU) -> None: 34 | if _debug: 35 | Echo._debug("indication {!r}".format(pdu)) 36 | if pdu is None: 37 | return 38 | 39 | await self.request(pdu) 40 | 41 | async def confirmation(self, pdu: ConsolePDU) -> None: 42 | if _debug: 43 | Echo._debug("confirmation {!r}".format(pdu)) 44 | if pdu is None: 45 | return 46 | 47 | await self.response(pdu.upper()) 48 | 49 | 50 | @bacpypes_debugging 51 | class EchoServiceAccessPoint(ServiceAccessPoint): 52 | 53 | _debug: Callable[..., None] 54 | 55 | async def sap_indication(self, pdu: str) -> None: 56 | if _debug: 57 | EchoServiceAccessPoint._debug("sap_indication {!r}".format(pdu)) 58 | await self.sap_response(pdu.upper()) 59 | 60 | 61 | async def main() -> None: 62 | try: 63 | console = None 64 | ArgumentParser().parse_args() 65 | 66 | # build a very small stack 67 | console = Console() 68 | echo = Echo() 69 | echo_service = EchoServiceAccessPoint() 70 | bind(console, echo, echo_service) # type: ignore[misc] 71 | 72 | # run until the console is done, canceled or EOF 73 | await console.fini.wait() 74 | 75 | except KeyboardInterrupt: 76 | if _debug: 77 | _log.debug("keyboard interrupt") 78 | finally: 79 | if console and console.exit_status: 80 | sys.exit(console.exit_status) 81 | 82 | 83 | if __name__ == "__main__": 84 | asyncio.run(main()) 85 | -------------------------------------------------------------------------------- /samples/console-ini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos the input converted to uppercase. This is a 5 | derivative of the console.py sample that looks for a BACpypes.ini settings 6 | file, or the INI file specified with the --ini option. 7 | """ 8 | 9 | import sys 10 | import asyncio 11 | 12 | from typing import Callable 13 | 14 | from bacpypes3.settings import settings 15 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 16 | from bacpypes3.argparse import INIArgumentParser 17 | from bacpypes3.console import Console, ConsolePDU 18 | from bacpypes3.comm import Server, bind 19 | 20 | # some debugging 21 | _debug = 0 22 | _log = ModuleLogger(globals()) 23 | 24 | 25 | @bacpypes_debugging 26 | class Echo(Server[ConsolePDU]): 27 | """ 28 | Simple example server that echos the downstream strings as uppercase 29 | strings going upstream. If the PDU is None the console is finished, 30 | and this could send an integer status code upstream to exit. 31 | """ 32 | 33 | _debug: Callable[..., None] 34 | 35 | async def indication(self, pdu: ConsolePDU) -> None: 36 | if _debug: 37 | Echo._debug("indication {!r}".format(pdu)) 38 | if pdu is None: 39 | return 40 | 41 | await self.response(pdu.upper()) 42 | 43 | 44 | async def main() -> None: 45 | try: 46 | console = None 47 | parser = INIArgumentParser() 48 | args = parser.parse_args() 49 | if _debug: 50 | _log.debug("args: %r", args) 51 | _log.debug("settings: %r", settings) 52 | 53 | # build a very small stack 54 | console = Console() 55 | echo = Echo() 56 | bind(console, echo) 57 | 58 | # run until the console is done, canceled or EOF 59 | await console.fini.wait() 60 | 61 | finally: 62 | if console and console.exit_status: 63 | sys.exit(console.exit_status) 64 | 65 | 66 | if __name__ == "__main__": 67 | asyncio.run(main()) 68 | -------------------------------------------------------------------------------- /samples/console-json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos the input converted to uppercase. This is a 5 | derivative of the console.py sample that looks for a BACpypes.json settings 6 | file, or the JSON file specified with the --json option. 7 | """ 8 | 9 | import sys 10 | import asyncio 11 | 12 | from typing import Callable 13 | 14 | from bacpypes3.settings import settings 15 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 16 | from bacpypes3.argparse import JSONArgumentParser 17 | from bacpypes3.console import Console, ConsolePDU 18 | from bacpypes3.comm import Server, bind 19 | 20 | # some debugging 21 | _debug = 0 22 | _log = ModuleLogger(globals()) 23 | 24 | 25 | @bacpypes_debugging 26 | class Echo(Server[ConsolePDU]): 27 | """ 28 | Simple example server that echos the downstream strings as uppercase 29 | strings going upstream. If the PDU is None the console is finished, 30 | and this could send an integer status code upstream to exit. 31 | """ 32 | 33 | _debug: Callable[..., None] 34 | 35 | async def indication(self, pdu: ConsolePDU) -> None: 36 | if _debug: 37 | Echo._debug("indication {!r}".format(pdu)) 38 | if pdu is None: 39 | return 40 | 41 | await self.response(pdu.upper()) 42 | 43 | 44 | async def main() -> None: 45 | try: 46 | console = None 47 | parser = JSONArgumentParser() 48 | args = parser.parse_args() 49 | if _debug: 50 | _log.debug("args: %r", args) 51 | _log.debug("settings: %r", settings) 52 | 53 | # build a very small stack 54 | console = Console() 55 | echo = Echo() 56 | bind(console, echo) 57 | 58 | # run until the console is done, canceled or EOF 59 | await console.fini.wait() 60 | 61 | finally: 62 | if console and console.exit_status: 63 | sys.exit(console.exit_status) 64 | 65 | 66 | if __name__ == "__main__": 67 | asyncio.run(main()) 68 | -------------------------------------------------------------------------------- /samples/console-prompt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Same as the `console.py` application but adds a `--prompt` option to 3 | change the prompt. 4 | """ 5 | 6 | import sys 7 | import asyncio 8 | 9 | from typing import Callable 10 | 11 | from bacpypes3.settings import settings 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.argparse import ArgumentParser 14 | from bacpypes3.console import Console, ConsolePDU 15 | from bacpypes3.comm import Server, bind 16 | 17 | # some debugging 18 | _debug = 0 19 | _log = ModuleLogger(globals()) 20 | 21 | 22 | @bacpypes_debugging 23 | class Echo(Server[ConsolePDU]): 24 | _debug: Callable[..., None] 25 | 26 | async def indication(self, pdu: ConsolePDU) -> None: 27 | if _debug: 28 | Echo._debug("indication {!r}".format(pdu)) 29 | if pdu is None: 30 | return 31 | 32 | assert isinstance(pdu, str) 33 | await self.response(pdu.upper()) 34 | 35 | 36 | async def main() -> None: 37 | try: 38 | console = None 39 | 40 | # add a way to change the console prompt 41 | parser = ArgumentParser() 42 | parser.add_argument( 43 | "--prompt", 44 | type=str, 45 | help="change the prompt", 46 | default="> ", 47 | ) 48 | 49 | args = parser.parse_args() 50 | if _debug: 51 | _log.debug("args: %r", args) 52 | _log.debug("settings: %r", settings) 53 | 54 | # build a very small stack 55 | console = Console(prompt=args.prompt) 56 | echo = Echo() 57 | bind(console, echo) 58 | 59 | # run until the console is done, canceled or EOF 60 | await console.fini.wait() 61 | 62 | finally: 63 | if console and console.exit_status: 64 | sys.exit(console.exit_status) 65 | 66 | 67 | if __name__ == "__main__": 68 | asyncio.run(main()) 69 | -------------------------------------------------------------------------------- /samples/console-yaml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos the input converted to uppercase. This is a 5 | derivative of the console.py sample that looks for a BACpypes.yml settings 6 | file, or the YAML file specified with the --yaml option. 7 | """ 8 | 9 | import sys 10 | import asyncio 11 | 12 | from typing import Callable 13 | 14 | from bacpypes3.settings import settings 15 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 16 | from bacpypes3.argparse import YAMLArgumentParser 17 | from bacpypes3.console import Console, ConsolePDU 18 | from bacpypes3.comm import Server, bind 19 | 20 | # some debugging 21 | _debug = 0 22 | _log = ModuleLogger(globals()) 23 | 24 | 25 | @bacpypes_debugging 26 | class Echo(Server[ConsolePDU]): 27 | """ 28 | Simple example server that echos the downstream strings as uppercase 29 | strings going upstream. If the PDU is None the console is finished, 30 | and this could send an integer status code upstream to exit. 31 | """ 32 | 33 | _debug: Callable[..., None] 34 | 35 | async def indication(self, pdu: ConsolePDU) -> None: 36 | if _debug: 37 | Echo._debug("indication {!r}".format(pdu)) 38 | if pdu is None: 39 | return 40 | 41 | assert isinstance(pdu, str) 42 | await self.response(pdu.upper()) 43 | 44 | 45 | async def main() -> None: 46 | try: 47 | console = None 48 | parser = YAMLArgumentParser() 49 | args = parser.parse_args() 50 | if _debug: 51 | _log.debug("args: %r", args) 52 | _log.debug("settings: %r", settings) 53 | 54 | # build a very small stack 55 | console = Console() 56 | echo = Echo() 57 | bind(console, echo) 58 | 59 | # run until the console is done, canceled or EOF 60 | await console.fini.wait() 61 | 62 | finally: 63 | if console and console.exit_status: 64 | sys.exit(console.exit_status) 65 | 66 | 67 | if __name__ == "__main__": 68 | asyncio.run(main()) 69 | -------------------------------------------------------------------------------- /samples/console.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that echos the input converted to uppercase. 3 | """ 4 | 5 | import sys 6 | import asyncio 7 | 8 | from typing import Callable 9 | 10 | from bacpypes3.settings import settings 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.argparse import ArgumentParser 13 | from bacpypes3.console import Console, ConsolePDU 14 | from bacpypes3.comm import Server, bind 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | 21 | @bacpypes_debugging 22 | class Echo(Server[ConsolePDU]): 23 | """ 24 | This example server echos downstream strings as uppercase strings going 25 | upstream. If the PDU is None the console is finished, and this could send 26 | an integer status code upstream to exit. 27 | """ 28 | 29 | _debug: Callable[..., None] 30 | 31 | async def indication(self, pdu: ConsolePDU) -> None: 32 | """ 33 | This function is called with each line of text from the console (or 34 | from a file or pipe) and called with None at end-of-file. It is 35 | "downstream" of the Console() instance and gets this "indication" when 36 | the console is making a "request". 37 | """ 38 | if _debug: 39 | Echo._debug("indication {!r}".format(pdu)) 40 | if pdu is None: 41 | return 42 | 43 | # send the uppercase content back up the stack 44 | await self.response(pdu.upper()) 45 | 46 | 47 | async def main() -> None: 48 | try: 49 | console = None 50 | args = ArgumentParser().parse_args() 51 | if _debug: 52 | _log.debug("args: %r", args) 53 | _log.debug("settings: %r", settings) 54 | 55 | # build a very small stack 56 | console = Console() 57 | echo = Echo() 58 | if _debug: 59 | _log.debug("console, echo: %r, %r", console, echo) 60 | 61 | # bind the two objects together, top down 62 | bind(console, echo) 63 | 64 | # run until the console is done, canceled or EOF 65 | await console.fini.wait() 66 | 67 | finally: 68 | if console and console.exit_status: 69 | sys.exit(console.exit_status) 70 | 71 | 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /samples/cov-client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that creates a change-of-value context and dumps out the 3 | property identifier and its value from the notifications it receives. 4 | 5 | The device address and monitored object identifier are the only required 6 | parameters to `change_of_value()`. 7 | """ 8 | 9 | import asyncio 10 | import signal 11 | 12 | from bacpypes3.debugging import ModuleLogger 13 | from bacpypes3.argparse import SimpleArgumentParser 14 | from bacpypes3.app import Application 15 | 16 | from bacpypes3.pdu import Address 17 | from bacpypes3.primitivedata import ObjectIdentifier 18 | 19 | # some debugging 20 | _debug = 0 21 | _log = ModuleLogger(globals()) 22 | 23 | 24 | async def main() -> None: 25 | app = None 26 | try: 27 | parser = SimpleArgumentParser() 28 | parser.add_argument( 29 | "device_address", 30 | help="address of the server (B-device)", 31 | ) 32 | parser.add_argument( 33 | "object_identifier", 34 | help="object identifier", 35 | ) 36 | parser.add_argument( 37 | "--process-identifier", 38 | help="subscriber process identifier", 39 | ) 40 | parser.add_argument( 41 | "--confirmed", 42 | action="store_true", 43 | help="issues confirmed notifications", 44 | ) 45 | parser.add_argument( 46 | "--lifetime", 47 | help="lifetime", 48 | ) 49 | args = parser.parse_args() 50 | if _debug: 51 | _log.debug("args: %r", args) 52 | 53 | # interpret the address 54 | device_address = Address(args.device_address) 55 | if _debug: 56 | _log.debug("device_address: %r", device_address) 57 | 58 | # interpret the object identifier 59 | object_identifier = ObjectIdentifier(args.object_identifier) 60 | if _debug: 61 | _log.debug("object_identifier: %r", object_identifier) 62 | 63 | # build an application 64 | app = Application.from_args(args) 65 | if _debug: 66 | _log.debug("app: %r", app) 67 | 68 | # add a Ctrl-C signal handler to terminate the application 69 | fini = asyncio.Event() 70 | loop: asyncio.events.AbstractEventLoop = asyncio.get_running_loop() 71 | loop.add_signal_handler(signal.SIGINT, lambda: fini.set()) 72 | 73 | try: 74 | # use a SubscriptionContextManager 75 | async with app.change_of_value( 76 | device_address, 77 | object_identifier, 78 | args.process_identifier, 79 | args.confirmed, 80 | args.lifetime, 81 | ) as scm: 82 | if _debug: 83 | _log.debug(" - scm: %r", scm) 84 | 85 | # do something with what is received 86 | while not fini.is_set(): 87 | property_identifier, property_value = await scm.get_value() 88 | print(f"{property_identifier} {property_value}") 89 | 90 | if _debug: 91 | _log.debug("exited context") 92 | except Exception as err: 93 | if _debug: 94 | _log.debug(" - exception: %r", err) 95 | finally: 96 | if app: 97 | app.close() 98 | 99 | 100 | if __name__ == "__main__": 101 | asyncio.run(main()) 102 | -------------------------------------------------------------------------------- /samples/custom-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that has a device object and an additional custom object. 3 | """ 4 | 5 | import asyncio 6 | 7 | from bacpypes3.debugging import ModuleLogger 8 | from bacpypes3.argparse import SimpleArgumentParser 9 | from bacpypes3.ipv4.app import Application 10 | 11 | # this server has custom objects 12 | import custom 13 | 14 | # some debugging 15 | _debug = 0 16 | _log = ModuleLogger(globals()) 17 | 18 | 19 | async def main() -> None: 20 | try: 21 | app = None 22 | args = SimpleArgumentParser().parse_args() 23 | 24 | # make sure the vendor identifier is the custom one 25 | args.vendoridentifier = custom._vendor_id 26 | if _debug: 27 | _log.debug("args: %r", args) 28 | 29 | # build an application 30 | app = Application.from_args(args) 31 | if _debug: 32 | _log.debug("app: %r", app) 33 | 34 | # create a custom object 35 | custom_object = custom.ProprietaryObject( 36 | objectIdentifier=("custom_object", 12), 37 | objectName="Wowzers!", 38 | custom_property=13, 39 | ) 40 | if _debug: 41 | _log.debug("custom_object: %r", custom_object) 42 | 43 | app.add_object(custom_object) 44 | 45 | # like running forever 46 | await asyncio.Future() 47 | 48 | finally: 49 | if app: 50 | app.close() 51 | 52 | 53 | if __name__ == "__main__": 54 | asyncio.run(main()) 55 | -------------------------------------------------------------------------------- /samples/custom.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom Objects and Properties 3 | """ 4 | 5 | from bacpypes3.debugging import ModuleLogger 6 | from bacpypes3.primitivedata import Integer, ObjectType 7 | from bacpypes3.basetypes import PropertyIdentifier 8 | from bacpypes3.vendor import VendorInfo 9 | 10 | from bacpypes3.local.object import _Object 11 | from bacpypes3.local.device import DeviceObject as _DeviceObject 12 | from bacpypes3.local.networkport import NetworkPortObject as _NetworkPortObject 13 | 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | # this vendor identifier reference is used when registering custom classes 21 | _vendor_id = 888 22 | 23 | 24 | class ProprietaryObjectType(ObjectType): 25 | """ 26 | This is a list of the object type enumerations for proprietary object types, 27 | see Clause 23.4.1. 28 | """ 29 | 30 | custom_object = 128 31 | 32 | 33 | class ProprietaryPropertyIdentifier(PropertyIdentifier): 34 | """ 35 | This is a list of the property identifiers that are used in custom object 36 | types or are used in custom properties of standard types. 37 | """ 38 | 39 | custom_property = 512 40 | 41 | 42 | # create a VendorInfo object for this custom application before registering 43 | # specialize object classes 44 | custom_vendor_info = VendorInfo( 45 | _vendor_id, ProprietaryObjectType, ProprietaryPropertyIdentifier 46 | ) 47 | 48 | 49 | class DeviceObject(_DeviceObject): 50 | """ 51 | When running as an instance of this custom device, the DeviceObject is 52 | an extension of the one defined in bacpypes3.local.device (in this case 53 | doesn't add any proprietary properties). 54 | """ 55 | 56 | pass 57 | 58 | 59 | class NetworkPortObject(_NetworkPortObject): 60 | """ 61 | When running as an instance of this custom device, the NetworkPortObject is 62 | an extension of the one defined in bacpypes3.local.networkport (in this 63 | case doesn't add any proprietary properties). 64 | """ 65 | 66 | pass 67 | 68 | 69 | class ProprietaryObject(_Object): 70 | """ 71 | This is a proprietary object type. 72 | """ 73 | 74 | # object identifiers are interpreted from this customized subclass of the 75 | # standard ObjectIdentifier that leverages the ProprietaryObjectType 76 | # enumeration in the vendor information 77 | objectIdentifier: custom_vendor_info.object_identifier 78 | 79 | # all objects get the object-type property to be this value 80 | objectType = ProprietaryObjectType("custom_object") 81 | 82 | # all objects have an object-name property, provided by the parent class 83 | # with special hooks if an instance of this class is bound to an application 84 | # objectName: CharacterString 85 | 86 | # the property-list property of this object is provided by the getter 87 | # method defined in the parent class and computed dynamically 88 | # propertyList: ArrayOf(PropertyIdentifier) 89 | 90 | # this is a custom property using a standard datatype 91 | custom_property: Integer 92 | -------------------------------------------------------------------------------- /samples/device-object-init-1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create a local device object instance, convert it to JSON and print the result, 3 | then round-trip the JSON into another device object. 4 | """ 5 | from pprint import pprint 6 | 7 | from bacpypes3.debugging import ModuleLogger 8 | 9 | # from bacpypes3.argparse import create_log_handler 10 | 11 | from bacpypes3.local.device import DeviceObject 12 | from bacpypes3.json import sequence_to_json, json_to_sequence 13 | 14 | # some debugging 15 | _debug = 0 16 | _log = ModuleLogger(globals()) 17 | 18 | # create_log_handler("__main__") 19 | # create_log_handler("bacpypes3.constructeddata", color=4) 20 | # create_log_handler("bacpypes3.object", color=5) 21 | # create_log_handler("bacpypes3.local.object", color=6) 22 | # create_log_handler("bacpypes3.local.device", color=6) 23 | 24 | do1 = DeviceObject(objectIdentifier="device,1", objectName="Excelsior") 25 | print("do1") 26 | do1.debug_contents() 27 | print("") 28 | 29 | json_content = sequence_to_json(do1) 30 | pprint(json_content) 31 | print("") 32 | 33 | do2 = json_to_sequence(json_content, DeviceObject) 34 | print("do2") 35 | do2.debug_contents() 36 | print("") 37 | -------------------------------------------------------------------------------- /samples/device-object-init-2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Define a new device object for vendor identifier 888, create an instance of it, 3 | dump the contents as JSON, then round-trip the JSON back into a device object. 4 | """ 5 | from pprint import pprint 6 | 7 | from bacpypes3.debugging import ModuleLogger 8 | 9 | # from bacpypes3.argparse import create_log_handler 10 | 11 | from bacpypes3.object import VendorInfo 12 | from bacpypes3.local.device import DeviceObject as _DeviceObject 13 | from bacpypes3.json import sequence_to_json, json_to_sequence 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | # this vendor identifier reference is used when registering custom classes 20 | _vendor_id = 888 21 | 22 | # create a VendorInfo object for this custom application before registering 23 | # specialize object classes 24 | custom_vendor_info = VendorInfo(_vendor_id) 25 | 26 | # create_log_handler("__main__") 27 | # create_log_handler("bacpypes3.constructeddata", color=4) 28 | # create_log_handler("bacpypes3.object", color=5) 29 | # create_log_handler("bacpypes3.local.object", color=6) 30 | # create_log_handler("bacpypes3.local.device", color=6) 31 | 32 | 33 | class DeviceObject(_DeviceObject): 34 | """ 35 | When running as an instance of this custom device, the DeviceObject is 36 | an extension of the one defined in bacpypes3.local.device (in this case 37 | doesn't add any proprietary properties). 38 | 39 | The vendor-identifier property isn't set from the module for device 40 | objects, it is simpler to provide the value here in the class definition. 41 | """ 42 | 43 | vendorIdentifier = _vendor_id 44 | 45 | 46 | do1 = DeviceObject(objectIdentifier="device,1", objectName="Excelsior") 47 | print("do1") 48 | do1.debug_contents() 49 | print("") 50 | 51 | json_content = sequence_to_json(do1) 52 | pprint(json_content) 53 | print("") 54 | 55 | do2 = json_to_sequence(json_content, DeviceObject) 56 | print("do2") 57 | do2.debug_contents() 58 | print("") 59 | -------------------------------------------------------------------------------- /samples/discover-devices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends a Who-Is request and prints out the device identifier, 3 | address, and description for the devices that respond. 4 | """ 5 | 6 | import sys 7 | import asyncio 8 | 9 | from bacpypes3.debugging import ModuleLogger 10 | from bacpypes3.argparse import SimpleArgumentParser 11 | 12 | from bacpypes3.pdu import Address 13 | from bacpypes3.primitivedata import ObjectIdentifier 14 | from bacpypes3.apdu import ErrorRejectAbortNack 15 | from bacpypes3.app import Application 16 | 17 | # some debugging 18 | _debug = 0 19 | _log = ModuleLogger(globals()) 20 | 21 | # globals 22 | show_warnings: bool = False 23 | 24 | 25 | async def main() -> None: 26 | app = None 27 | try: 28 | parser = SimpleArgumentParser() 29 | parser.add_argument( 30 | "low_limit", 31 | type=int, 32 | help="device instance range low limit", 33 | ) 34 | parser.add_argument( 35 | "high_limit", 36 | type=int, 37 | help="device instance range high limit", 38 | ) 39 | args = parser.parse_args() 40 | if _debug: 41 | _log.debug("args: %r", args) 42 | 43 | # build an application 44 | app = Application.from_args(args) 45 | if _debug: 46 | _log.debug("app: %r", app) 47 | 48 | # run the query 49 | i_ams = await app.who_is(args.low_limit, args.high_limit) 50 | for i_am in i_ams: 51 | if _debug: 52 | _log.debug(" - i_am: %r", i_am) 53 | 54 | device_address: Address = i_am.pduSource 55 | device_identifier: ObjectIdentifier = i_am.iAmDeviceIdentifier 56 | print(f"{device_identifier} @ {device_address}") 57 | 58 | try: 59 | device_description: str = await app.read_property( 60 | device_address, device_identifier, "description" 61 | ) 62 | print(f" description: {device_description}") 63 | 64 | except ErrorRejectAbortNack as err: 65 | if show_warnings: 66 | sys.stderr.write(f"{device_identifier} description error: {err}\n") 67 | finally: 68 | if app: 69 | app.close() 70 | 71 | 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /samples/discover-objects-rdf.ttl: -------------------------------------------------------------------------------- 1 | @prefix bacnet: . 2 | @prefix rdfs: . 3 | @prefix xsd: . 4 | 5 | a bacnet:Device ; 6 | bacnet:device-address [ rdfs:label "10.0.1.90" ; 7 | bacnet:mac-address "0a00015abac0"^^xsd:hexBinary ; 8 | bacnet:network-number "0"^^xsd:nonNegativeInteger ] ; 9 | bacnet:device-instance 999 ; 10 | bacnet:hasObject , 11 | . 12 | 13 | bacnet:object-identifier "device,999" ; 14 | bacnet:object-name "Excelsior" ; 15 | bacnet:object-type bacnet:ObjectType.device ; 16 | bacnet:property-list "object-identifier;object-name;object-type;property-list;system-status;vendor-name;vendor-identifier;model-name;firmware-revision;application-software-version;protocol-version;protocol-revision;protocol-services-supported;protocol-object-types-supported;object-list;max-apdu-length-accepted;segmentation-supported;max-segments-accepted;local-time;local-date;apdu-segment-timeout;apdu-timeout;number-of-apdu-retries;device-address-binding;database-revision;active-cov-subscriptions;status-flags" . 17 | 18 | bacnet:object-identifier "network-port,1" ; 19 | bacnet:object-name "NetworkPort-1" ; 20 | bacnet:object-type bacnet:ObjectType.network-port ; 21 | bacnet:property-list "object-identifier;object-name;object-type;property-list;status-flags;reliability;out-of-service;network-type;protocol-level;network-number;network-number-quality;changes-pending;mac-address;link-speed;bacnet-ip-mode;ip-address;bacnet-ip-udp-port;ip-subnet-mask" . 22 | 23 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the script needs to be run from its directory 4 | DIR=`dirname $0` 5 | pushd $DIR > /dev/null 6 | 7 | # find the latest wheel put in this directory 8 | BACPYPES_WHEEL=`ls -1 bacpypes3-*-py3-none-any.whl 2> /dev/null | tail -n 1` 9 | 10 | if [[ -z "${BACPYPES_WHEEL}" ]] 11 | then 12 | python3 -m pip download bacpypes3 13 | BACPYPES_WHEEL=`ls -1 bacpypes3-*-py3-none-any.whl | tail -n 1` 14 | fi 15 | echo Building from $BACPYPES_WHEEL 16 | 17 | # build the image passing in the file name 18 | docker build --tag bacpypes:latest \ 19 | --file bacpypes.dockerfile \ 20 | --build-arg BACPYPES_WHEEL=`basename $BACPYPES_WHEEL` \ 21 | . 22 | 23 | popd 24 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-greetings-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the script needs to be run from its directory 4 | DIR=`dirname $0` 5 | pushd $DIR > /dev/null 6 | 7 | # find the latest wheel put in this directory 8 | BACPYPES_WHEEL=`ls -1 bacpypes3-*-py3-none-any.whl | tail -n 1` 9 | 10 | if [[ -z "${BACPYPES_WHEEL}" ]] 11 | then 12 | echo "missing wheel" 13 | exit 1 14 | fi 15 | 16 | # build the image passing in the file name 17 | docker build --tag bacpypes-greetings \ 18 | --file bacpypes-greetings.dockerfile \ 19 | --build-arg BACPYPES_WHEEL=`basename $BACPYPES_WHEEL` \ 20 | . 21 | 22 | popd 23 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-greetings-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -it --rm bacpypes-greetings 4 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-greetings-save.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker save bacpypes-greetings:latest > bacpypes-greetings.tar 4 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-greetings.dockerfile: -------------------------------------------------------------------------------- 1 | FROM bacpypes:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY bacpypes-greetings.py . 6 | 7 | CMD ["python3", "bacpypes-greetings.py"] 8 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-greetings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Greetings from BACpypes 3 | """ 4 | 5 | import bacpypes3 6 | 7 | print("Greetings from BACpypes, version", bacpypes3.__version__) 8 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "x" ]] 4 | then 5 | if [[ -z "${BACPYPES_FOREIGN_BBMD}" ]] 6 | then 7 | echo "The address of the BBMD for foreign device registration" 8 | echo "" 9 | read -p "BBMD Address " BACPYPES_FOREIGN_BBMD || exit 1 10 | export BACPYPES_FOREIGN_BBMD 11 | fi 12 | if [[ -z "${BACPYPES_FOREIGN_TTL}" ]] 13 | then 14 | read -p "Time-to-live " BACPYPES_FOREIGN_TTL || exit 1 15 | export BACPYPES_FOREIGN_TTL 16 | fi 17 | fi 18 | 19 | docker run -it --rm \ 20 | --network host \ 21 | --env BACPYPES_DEVICE_ADDRESS \ 22 | --env BACPYPES_DEVICE_NAME \ 23 | --env BACPYPES_DEVICE_INSTANCE \ 24 | --env BACPYPES_NETWORK \ 25 | --env BACPYPES_VENDOR_IDENTIFIER \ 26 | --env BACPYPES_FOREIGN_BBMD \ 27 | --env BACPYPES_FOREIGN_TTL \ 28 | --env BACPYPES_BBMD_BDT \ 29 | --env BACPYPES_DEBUG \ 30 | bacpypes:latest 31 | -------------------------------------------------------------------------------- /samples/docker/bacpypes-save.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker save bacpypes:latest > bacpypes.tar 4 | -------------------------------------------------------------------------------- /samples/docker/bacpypes.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | WORKDIR /app 4 | RUN pip install --root-user-action=ignore --upgrade pip 5 | 6 | COPY requirements.txt . 7 | RUN pip install --root-user-action=ignore -r requirements.txt 8 | 9 | ARG BACPYPES_WHEEL 10 | COPY ${BACPYPES_WHEEL} . 11 | RUN pip install --root-user-action=ignore ${BACPYPES_WHEEL} 12 | 13 | CMD ["python3", "-m", "bacpypes3"] 14 | 15 | -------------------------------------------------------------------------------- /samples/docker/requirements.txt: -------------------------------------------------------------------------------- 1 | ifaddr 2 | pyyaml 3 | rdflib 4 | websockets 5 | -------------------------------------------------------------------------------- /samples/docker/who-is-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the script needs to be run from its directory 4 | DIR=`dirname $0` 5 | pushd $DIR > /dev/null 6 | 7 | docker build --tag who-is:latest \ 8 | --file who-is.dockerfile \ 9 | . 10 | 11 | popd 12 | -------------------------------------------------------------------------------- /samples/docker/who-is-console-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the script needs to be run from its directory 4 | DIR=`dirname $0` 5 | pushd $DIR > /dev/null 6 | 7 | docker build \ 8 | --tag who-is-console:latest \ 9 | --file who-is-console.dockerfile \ 10 | . 11 | 12 | popd 13 | -------------------------------------------------------------------------------- /samples/docker/who-is-console-run-mini.sh: -------------------------------------------------------------------------------- 1 | export HOST_PORT=47808 2 | export BBMD_ADDRESS=10.0.1.90 3 | export TTL=30 4 | export DEBUG="--debug bacpypes3.ipv4.IPv4DatagramServer bacpypes3.ipv4.service.BIPForeign --color" 5 | 6 | docker run \ 7 | -it \ 8 | --rm \ 9 | -p $HOST_PORT:47808/udp \ 10 | --env BBMD_ADDRESS \ 11 | --env TTL \ 12 | --env DEBUG \ 13 | who-is-console:latest 14 | -------------------------------------------------------------------------------- /samples/docker/who-is-console-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -it --rm \ 4 | --network host \ 5 | who-is-console:latest 6 | -------------------------------------------------------------------------------- /samples/docker/who-is-console-save.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker save who-is-console:latest > who-is-console.tar 4 | -------------------------------------------------------------------------------- /samples/docker/who-is-console.dockerfile: -------------------------------------------------------------------------------- 1 | FROM bacpypes:latest 2 | 3 | COPY who-is-console.py . 4 | 5 | CMD ["python3", "who-is-console.py"] 6 | 7 | -------------------------------------------------------------------------------- /samples/docker/who-is-console.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends Who-Is requests. 3 | """ 4 | 5 | import asyncio 6 | from typing import Callable 7 | 8 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 9 | from bacpypes3.argparse import SimpleArgumentParser 10 | from bacpypes3.app import Application 11 | from bacpypes3.console import Console 12 | from bacpypes3.cmd import Cmd 13 | 14 | from bacpypes3.pdu import Address 15 | from bacpypes3.comm import bind 16 | 17 | # some debugging 18 | _debug = 0 19 | _log = ModuleLogger(globals()) 20 | 21 | 22 | @bacpypes_debugging 23 | class SampleCmd(Cmd): 24 | """ 25 | Simple console example that sends Who-Is requests. 26 | """ 27 | 28 | _debug: Callable[..., None] 29 | 30 | async def do_whois( 31 | self, 32 | address: Address = None, 33 | low_limit: int = None, 34 | high_limit: int = None, 35 | ) -> None: 36 | """ 37 | Send a Who-Is request and wait for the responses. 38 | 39 | usage: whois [ address [ low_limit high_limit ] ] 40 | """ 41 | if _debug: 42 | SampleCmd._debug("do_whois %r %r %r", address, low_limit, high_limit) 43 | 44 | i_ams = await this_application.who_is(low_limit, high_limit, address) 45 | if not i_ams: 46 | await self.response("No responses") 47 | else: 48 | for i_am in i_ams: 49 | if _debug: 50 | SampleCmd._debug(" - i_am: %r", i_am) 51 | await self.response(f"{i_am.iAmDeviceIdentifier[1]} {i_am.pduSource}") 52 | 53 | 54 | async def main() -> None: 55 | global this_application 56 | 57 | this_application = None 58 | try: 59 | parser = SimpleArgumentParser() 60 | args = parser.parse_args() 61 | if _debug: 62 | _log.debug("args: %r", args) 63 | 64 | # build a very small stack 65 | console = Console() 66 | cmd = SampleCmd() 67 | bind(console, cmd) 68 | 69 | # build an application 70 | this_application = Application.from_args(args) 71 | if _debug: 72 | _log.debug("this_application: %r", this_application) 73 | 74 | # wait until the user is done 75 | await console.fini.wait() 76 | 77 | except KeyboardInterrupt: 78 | if _debug: 79 | _log.debug("keyboard interrupt") 80 | finally: 81 | if this_application: 82 | this_application.close() 83 | 84 | 85 | if __name__ == "__main__": 86 | asyncio.run(main()) 87 | -------------------------------------------------------------------------------- /samples/docker/who-is-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "${LOW_LIMIT}" ]] 4 | then 5 | read -p "Low limit " LOW_LIMIT || exit 1 6 | export LOW_LIMIT 7 | fi 8 | if [[ -z "${HIGH_LIMIT}" ]] 9 | then 10 | read -p "High limit " HIGH_LIMIT || exit 1 11 | export HIGH_LIMIT 12 | fi 13 | 14 | docker run -it --rm \ 15 | --network host \ 16 | --env LOW_LIMIT \ 17 | --env HIGH_LIMIT \ 18 | who-is:latest 19 | -------------------------------------------------------------------------------- /samples/docker/who-is-save.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker save who-is:latest > who-is.tar 4 | -------------------------------------------------------------------------------- /samples/docker/who-is.dockerfile: -------------------------------------------------------------------------------- 1 | FROM bacpypes:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY who-is.py . 6 | 7 | ARG LOW_LIMIT 8 | ARG HIGH_LIMIT 9 | 10 | CMD python3 who-is.py $LOW_LIMIT $HIGH_LIMIT 11 | 12 | -------------------------------------------------------------------------------- /samples/docker/who-is.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends a Who-Is request and prints out the device identifier 3 | and address for the devices that respond. 4 | """ 5 | 6 | import asyncio 7 | 8 | from bacpypes3.debugging import ModuleLogger 9 | from bacpypes3.argparse import SimpleArgumentParser 10 | from bacpypes3.app import Application 11 | 12 | # some debugging 13 | _debug = 0 14 | _log = ModuleLogger(globals()) 15 | 16 | 17 | async def main() -> None: 18 | app = None 19 | try: 20 | parser = SimpleArgumentParser() 21 | parser.add_argument( 22 | "low_limit", 23 | type=int, 24 | help="device instance range low limit", 25 | ) 26 | parser.add_argument( 27 | "high_limit", 28 | type=int, 29 | help="device instance range high limit", 30 | ) 31 | args = parser.parse_args() 32 | if _debug: 33 | _log.debug("args: %r", args) 34 | 35 | # build an application 36 | app = Application.from_args(args) 37 | if _debug: 38 | _log.debug("app: %r", app) 39 | 40 | # run the query 41 | i_ams = await app.who_is(args.low_limit, args.high_limit) 42 | for i_am in i_ams: 43 | if _debug: 44 | _log._debug(" - i_am: %r", i_am) 45 | print(f"{i_am.iAmDeviceIdentifier[1]} {i_am.pduSource}") 46 | 47 | finally: 48 | if app: 49 | app.close() 50 | 51 | 52 | if __name__ == "__main__": 53 | asyncio.run(main()) 54 | -------------------------------------------------------------------------------- /samples/find-device-by-address.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example uses a prepared query to find all of the BACnet 3 | devices in a graph, optionally with an address. By specifying 4 | different initial bindings then different collections of 5 | devices can be found. 6 | """ 7 | import sys 8 | from rdflib import Graph 9 | 10 | from bacpypes3.pdu import Address 11 | from bacpypes3.rdf.core import find_device_by_address 12 | from bacpypes3.rdf.util import unsigned_encode, octetstring_encode 13 | 14 | g = Graph() 15 | g.parse(sys.argv[1]) 16 | 17 | init_bindings = {} 18 | if len(sys.argv) > 2: 19 | device_address = Address(sys.argv[2]) 20 | 21 | if device_address.is_localbroadcast: 22 | init_bindings["net"] = unsigned_encode(g, 0) 23 | elif device_address.is_remotestation or device_address.is_remotebroadcast: 24 | init_bindings["net"] = unsigned_encode(g, device_address.addrNet) 25 | if device_address.is_localstation or device_address.is_remotestation: 26 | init_bindings["addr"] = octetstring_encode(g, device_address.addrAddr) 27 | 28 | for row in g.query(find_device_by_address, initBindings=init_bindings): 29 | print(row[0]) 30 | -------------------------------------------------------------------------------- /samples/find-device-by-instance.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example uses a prepared query to find all of the BACnet 3 | devices in a graph, optionally with a specific device instance 4 | number. 5 | """ 6 | import sys 7 | from rdflib import Graph, Literal 8 | from bacpypes3.rdf.core import find_device_by_instance 9 | 10 | 11 | g = Graph() 12 | g.parse(sys.argv[1]) 13 | 14 | init_bindings = {} 15 | if len(sys.argv) > 2: 16 | init_bindings["device_instance"] = Literal(int(sys.argv[2])) 17 | 18 | for row in g.query(find_device_by_instance, initBindings=init_bindings): 19 | print(row[0]) 20 | -------------------------------------------------------------------------------- /samples/ipv4-to-ipv4.json: -------------------------------------------------------------------------------- 1 | { 2 | "BACpypes": { 3 | "backup_count": 5, 4 | "color": false, 5 | "config": {}, 6 | "cov_lifetime": 60, 7 | "debug": [], 8 | "debug_file": "", 9 | "max_bytes": 1048576, 10 | "route_aware": false 11 | }, 12 | "router": [ 13 | { 14 | "apdu-segment-timeout": 1000, 15 | "apdu-timeout": 3000, 16 | "application-software-version": "1.0", 17 | "database-revision": 1, 18 | "firmware-revision": "N/A", 19 | "max-apdu-length-accepted": 1024, 20 | "model-name": "N/A", 21 | "number-of-apdu-retries": 3, 22 | "object-identifier": "device,998", 23 | "object-name": "Exeter", 24 | "object-type": "device", 25 | "protocol-revision": 22, 26 | "protocol-version": 1, 27 | "segmentation-supported": "segmented-both", 28 | "system-status": "operational", 29 | "vendor-identifier": 999, 30 | "vendor-name": "BACpypes" 31 | }, 32 | { 33 | "bacnet-ip-mode": "normal", 34 | "bacnet-ip-udp-port": 47808, 35 | "changes-pending": false, 36 | "ip-address": "10.0.1.90", 37 | "ip-subnet-mask": "255.255.255.0", 38 | "link-speed": 0.0, 39 | "mac-address": "10.0.1.90:47808", 40 | "network-number": 100, 41 | "network-number-quality": "configured", 42 | "network-type": "ipv4", 43 | "object-identifier": "network-port,1", 44 | "object-name": "NetworkPort-1", 45 | "object-type": "network-port", 46 | "out-of-service": false, 47 | "protocol-level": "bacnet-application", 48 | "reliability": "no-fault-detected" 49 | }, 50 | { 51 | "bacnet-ip-mode": "normal", 52 | "bacnet-ip-udp-port": 47809, 53 | "changes-pending": false, 54 | "ip-address": "10.0.1.90", 55 | "ip-subnet-mask": "255.255.255.0", 56 | "link-speed": 0.0, 57 | "mac-address": "10.0.1.90:47809", 58 | "network-number": 200, 59 | "network-number-quality": "configured", 60 | "network-type": "ipv4", 61 | "object-identifier": "network-port,2", 62 | "object-name": "NetworkPort-2", 63 | "object-type": "network-port", 64 | "out-of-service": false, 65 | "protocol-level": "bacnet-application", 66 | "reliability": "no-fault-detected" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /samples/link-layer/sc-echo-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that sends websocket messages. 5 | """ 6 | 7 | import sys 8 | import asyncio 9 | import logging 10 | 11 | from typing import Callable 12 | 13 | from bacpypes3.settings import settings 14 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 15 | from bacpypes3.argparse import ArgumentParser 16 | from bacpypes3.console import Console 17 | from bacpypes3.cmd import Cmd 18 | 19 | from bacpypes3.comm import Client, bind 20 | from bacpypes3.pdu import PDU 21 | from bacpypes3.sc.service import SCNodeSwitch 22 | 23 | # some debugging 24 | _debug = 0 25 | _log = ModuleLogger(globals()) 26 | 27 | direct_connection = None 28 | 29 | 30 | @bacpypes_debugging 31 | class SampleCmd(Cmd, Client[PDU]): 32 | """ 33 | Sample Cmd 34 | """ 35 | 36 | _debug: Callable[..., None] 37 | 38 | async def do_send(self, data: str) -> None: 39 | """ 40 | usage: send data:str 41 | """ 42 | if _debug: 43 | SampleCmd._debug("do_send %r", data) 44 | global direct_connection 45 | 46 | pdu = PDU(data.encode(), destination=direct_connection) 47 | if _debug: 48 | SampleCmd._debug(" - pdu: %r", pdu) 49 | 50 | await self.request(pdu) 51 | 52 | async def confirmation(self, pdu: PDU) -> None: 53 | if _debug: 54 | SampleCmd._debug("confirmation %r", pdu) 55 | 56 | await self.response(pdu.pduData.decode()) 57 | 58 | 59 | async def main() -> None: 60 | global direct_connection 61 | 62 | switch = None 63 | console = None 64 | try: 65 | parser = ArgumentParser() 66 | parser.add_argument( 67 | "--host", 68 | type=str, 69 | default="localhost", 70 | help="listening host address", 71 | ) 72 | parser.add_argument( 73 | "--port", 74 | type=int, 75 | default=8765, 76 | help="listening port", 77 | ) 78 | args = parser.parse_args() 79 | if _debug: 80 | _log.debug("settings: %r", settings) 81 | _log.debug("args: %r", args) 82 | 83 | # build a very small stack 84 | console = Console() 85 | cmd = SampleCmd() 86 | switch = SCNodeSwitch() 87 | bind(console, cmd, switch) # type: ignore[misc] 88 | 89 | # establish a direct connection 90 | direct_connection = switch.connect_to_device(f"ws://{args.host}:{args.port}") 91 | if _debug: 92 | _log.debug("direct_connection: %r", direct_connection) 93 | 94 | # run until the console is done, canceled or EOF 95 | await console.fini.wait() 96 | _log.debug("console fini") 97 | 98 | except KeyboardInterrupt: 99 | if _debug: 100 | _log.debug("keyboard interrupt") 101 | finally: 102 | if direct_connection: 103 | _log.debug("direct_connection.close()") 104 | await direct_connection.close() 105 | if switch: 106 | _log.debug("switch.close()") 107 | await switch.close() 108 | if console and console.exit_status: 109 | _log.debug("sys.exit()") 110 | sys.exit(console.exit_status) 111 | 112 | 113 | if __name__ == "__main__": 114 | asyncio.run(main()) 115 | -------------------------------------------------------------------------------- /samples/link-layer/sc-echo-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos websocket messages, upper cased. 5 | """ 6 | 7 | import asyncio 8 | import logging 9 | 10 | from typing import Callable 11 | 12 | from bacpypes3.settings import settings 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | 16 | from bacpypes3.comm import Client, bind 17 | from bacpypes3.pdu import PDU 18 | from bacpypes3.sc.service import SCNodeSwitch 19 | 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class Echo(Client[PDU]): 28 | """ 29 | Echo 30 | """ 31 | 32 | _debug: Callable[..., None] 33 | 34 | async def confirmation(self, pdu: PDU) -> None: 35 | if _debug: 36 | Echo._debug("confirmation %r", pdu) 37 | 38 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 39 | if _debug: 40 | Echo._debug(" - ack: %r", ack) 41 | 42 | await self.request(ack) 43 | 44 | 45 | async def main() -> None: 46 | switch = None 47 | 48 | try: 49 | parser = ArgumentParser() 50 | parser.add_argument( 51 | "--host", 52 | type=str, 53 | default="localhost", 54 | help="listening host address", 55 | ) 56 | parser.add_argument( 57 | "--port", 58 | type=int, 59 | default=8765, 60 | help="listening port", 61 | ) 62 | args = parser.parse_args() 63 | if _debug: 64 | _log.debug("settings: %r", settings) 65 | 66 | # build a very small stack 67 | echo = Echo() 68 | switch = SCNodeSwitch( 69 | host=args.host, port=args.port, dc_support=True, hub_support=True 70 | ) 71 | bind(echo, switch) 72 | 73 | # run a really long time 74 | await asyncio.Future() 75 | 76 | except KeyboardInterrupt: 77 | if _debug: 78 | _log.debug("keyboard interrupt") 79 | finally: 80 | if switch: 81 | await switch.close() 82 | 83 | 84 | if __name__ == "__main__": 85 | try: 86 | asyncio.run(main()) 87 | except KeyboardInterrupt: 88 | pass 89 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv4-echo-client-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build \ 4 | --tag bacpypes3-udp-ipv4-echo-client:latest \ 5 | --file udp-ipv4-echo-client.dockerfile . 6 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv4-echo-client-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "${HOST_ADDRESS}" ]] 4 | then 5 | echo "The HOST_ADDRESS is the UDP port number of the docker host" 6 | echo "like '47808' or an IPv4 address and port number if the host" 7 | echo "has more than one home, like '10.0.1.70:47808' and may also" 8 | echo "have an alternative port number like '10.0.1.99:47809'" 9 | echo "" 10 | read -p "Host Address " HOST_ADDRESS || exit 1 11 | export HOST_ADDRESS 12 | echo "" 13 | fi 14 | if [[ -z "${LOCAL_PORT}" ]] 15 | then 16 | echo "The LOCAL_PORT is the UDP port number the application" 17 | echo "uses on the docker network, normally '47808'. The" 18 | echo "IPv4 address is assigned by docker" 19 | echo "" 20 | read -p "Local Port " LOCAL_PORT || exit 1 21 | export LOCAL_PORT 22 | echo "" 23 | fi 24 | if [[ -z "${DEBUG}" ]] 25 | then 26 | export DEBUG="" 27 | fi 28 | 29 | docker run \ 30 | -it \ 31 | --rm \ 32 | -p $HOST_ADDRESS:${LOCAL_PORT} \ 33 | --env LOCAL_PORT \ 34 | --env DEBUG \ 35 | bacpypes3-udp-ipv4-echo-client:latest 36 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv4-echo-client.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | RUN python3 -m pip install --upgrade pip 6 | RUN python3 -m pip install pipenv 7 | RUN pipenv --python 3.8 8 | RUN pipenv run python3 -m pip install --upgrade --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ bacpypes3 9 | 10 | COPY udp-ipv4-echo-client.py . 11 | 12 | ARG LOCAL_PORT 13 | ARG DEBUG= 14 | 15 | CMD pipenv run python3 udp-ipv4-echo-client.py host:${LOCAL_PORT} ${DEBUG} 16 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv4-echo-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that sends IPv4 UDP messages. 5 | """ 6 | 7 | import sys 8 | import asyncio 9 | 10 | from typing import Callable 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.argparse import ArgumentParser 14 | from bacpypes3.console import Console 15 | from bacpypes3.cmd import Cmd 16 | 17 | from bacpypes3.comm import Client, bind 18 | from bacpypes3.pdu import Address, LocalBroadcast, IPv4Address, PDU 19 | from bacpypes3.ipv4 import IPv4DatagramServer 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class SampleCmd(Cmd, Client[PDU]): 28 | """ 29 | Sample Cmd 30 | """ 31 | 32 | _debug: Callable[..., None] 33 | 34 | async def do_send(self, address: Address, data: str) -> None: 35 | """ 36 | usage: send address:Address data:str 37 | """ 38 | if _debug: 39 | SampleCmd._debug("do_send %r %r", address, data) 40 | 41 | if not isinstance(address, (IPv4Address, LocalBroadcast)): 42 | if _debug: 43 | SampleCmd._debug(" - invalid address: %r", address) 44 | await self.response("invalid address: " + str(address)) 45 | return 46 | 47 | pdu = PDU(data.encode(), destination=address) 48 | if _debug: 49 | SampleCmd._debug(" - pdu: %r", pdu) 50 | 51 | await self.request(pdu) 52 | 53 | async def confirmation(self, pdu: PDU) -> None: 54 | if _debug: 55 | SampleCmd._debug("confirmation %r", pdu) 56 | 57 | await self.response(str(pdu.pduData.decode())) 58 | 59 | 60 | async def main() -> None: 61 | try: 62 | console = cmd = server = None 63 | parser = ArgumentParser() 64 | parser.add_argument( 65 | "local_address", 66 | type=str, 67 | help="local address (e.g., 'host:47808')", 68 | ) 69 | 70 | args = parser.parse_args() 71 | if _debug: 72 | _log.debug("args: %r", args) 73 | 74 | # evaluate the address 75 | local_address = IPv4Address(args.local_address) 76 | if _debug: 77 | _log.debug("local_address: %r", local_address) 78 | 79 | # build a very small stack 80 | server = IPv4DatagramServer(local_address) 81 | cmd = SampleCmd() 82 | console = Console() 83 | bind(console, cmd, server) # type: ignore[misc] 84 | 85 | # run until the console is done, canceled or EOF 86 | await console.fini.wait() 87 | 88 | except KeyboardInterrupt: 89 | if _debug: 90 | _log.debug("keyboard interrupt") 91 | finally: 92 | if server: 93 | server.close() 94 | if console and console.exit_status: 95 | sys.exit(console.exit_status) 96 | 97 | 98 | if __name__ == "__main__": 99 | asyncio.run(main()) 100 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv4-echo-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos IPv4 UDP messages, upper cased. 5 | """ 6 | 7 | import asyncio 8 | 9 | from typing import Callable 10 | 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.argparse import ArgumentParser 13 | 14 | from bacpypes3.comm import Client, bind 15 | from bacpypes3.pdu import IPv4Address, PDU 16 | from bacpypes3.ipv4 import IPv4DatagramServer 17 | 18 | 19 | # some debugging 20 | _debug = 0 21 | _log = ModuleLogger(globals()) 22 | 23 | 24 | @bacpypes_debugging 25 | class Echo(Client[PDU]): 26 | """ 27 | Echo 28 | """ 29 | 30 | _debug: Callable[..., None] 31 | 32 | async def confirmation(self, pdu: PDU) -> None: 33 | if _debug: 34 | Echo._debug("confirmation %r", pdu) 35 | 36 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 37 | if _debug: 38 | Echo._debug(" - ack: %r", ack) 39 | 40 | await self.request(ack) 41 | 42 | 43 | async def main() -> None: 44 | try: 45 | echo = server = None 46 | parser = ArgumentParser() 47 | parser.add_argument( 48 | "local_address", 49 | type=str, 50 | # nargs="?", 51 | help="local address", 52 | ) 53 | 54 | args = parser.parse_args() 55 | if _debug: 56 | _log.debug("args: %r", args) 57 | 58 | # evaluate the address 59 | local_address = IPv4Address(args.local_address) 60 | if _debug: 61 | _log.debug("local_address: %r", local_address) 62 | 63 | # build a very small stack 64 | echo = Echo() 65 | server = IPv4DatagramServer(local_address) 66 | bind(echo, server) 67 | 68 | # run a really long time 69 | await asyncio.Future() 70 | 71 | except KeyboardInterrupt: 72 | if _debug: 73 | _log.debug("keyboard interrupt") 74 | finally: 75 | if server: 76 | server.close() 77 | 78 | 79 | if __name__ == "__main__": 80 | asyncio.run(main()) 81 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv6-echo-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that sends IPv6 UDP messages. 5 | """ 6 | 7 | import sys 8 | import asyncio 9 | 10 | from typing import Callable 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.argparse import ArgumentParser 14 | from bacpypes3.console import Console 15 | from bacpypes3.cmd import Cmd 16 | 17 | from bacpypes3.comm import Client, bind 18 | from bacpypes3.pdu import Address, LocalBroadcast, IPv6Address, PDU 19 | from bacpypes3.ipv6 import IPv6DatagramServer 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class SampleCmd(Cmd, Client[PDU]): 28 | """ 29 | Sample Cmd 30 | """ 31 | 32 | _debug: Callable[..., None] 33 | 34 | async def do_send(self, address: Address, data: str) -> None: 35 | """ 36 | usage: send address:IPv6Address data:str 37 | """ 38 | if _debug: 39 | SampleCmd._debug("do_send %r %r", address, data) 40 | 41 | if not isinstance(address, (IPv6Address, LocalBroadcast)): 42 | if _debug: 43 | SampleCmd._debug(" - invalid address: %r", address) 44 | await self.response("invalid address: " + str(address)) 45 | return 46 | 47 | pdu = PDU(data.encode(), destination=address) 48 | if _debug: 49 | SampleCmd._debug(" - pdu: %r", pdu) 50 | 51 | await self.request(pdu) 52 | 53 | async def confirmation(self, pdu: PDU) -> None: 54 | if _debug: 55 | SampleCmd._debug("confirmation %r", pdu) 56 | 57 | await self.response(str(pdu.pduData.decode())) 58 | 59 | 60 | async def main() -> None: 61 | try: 62 | console = server = None 63 | parser = ArgumentParser() 64 | parser.add_argument( 65 | "address", 66 | type=str, 67 | nargs="?", 68 | default="[::]", 69 | help="listening address (default [::])", 70 | ) 71 | 72 | args = parser.parse_args() 73 | if _debug: 74 | _log.debug("args: %r", args) 75 | 76 | # evaluate the address 77 | address = IPv6Address(args.address) 78 | if _debug: 79 | _log.debug("address: %r", address) 80 | 81 | # build a very small stack 82 | server = IPv6DatagramServer(address) 83 | cmd = SampleCmd() 84 | console = Console() 85 | bind(console, cmd, server) # type: ignore[misc] 86 | 87 | # run until the console is done, canceled or EOF 88 | await console.fini.wait() 89 | 90 | except KeyboardInterrupt: 91 | if _debug: 92 | _log.debug("keyboard interrupt") 93 | finally: 94 | if server: 95 | server.close() 96 | if console and console.exit_status: 97 | sys.exit(console.exit_status) 98 | 99 | 100 | if __name__ == "__main__": 101 | asyncio.run(main()) 102 | -------------------------------------------------------------------------------- /samples/link-layer/udp-ipv6-echo-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple console example that echos IPv6 UDP messages, upper cased. Given this 5 | IP configuration: 6 | 7 | $ ip addr 8 | 2: enp0s25: mtu 1500 ... 9 | link/ether 00:23:ae:95:c2:82 brd ff:ff:ff:ff:ff:ff 10 | inet 10.0.1.90/24 brd 10.0.1.255 scope global dynamic noprefixroute enp0s25 11 | valid_lft 82093sec preferred_lft 82093sec 12 | inet6 fe80::1b99:de63:cdd6:67a9/64 scope link noprefixroute 13 | valid_lft forever preferred_lft forever 14 | 15 | Examples: 16 | 17 | $ python3 samples/udp-ipv6-echo-server.py [::]:47809 18 | $ python3 samples/udp-ipv6-echo-server.py [fe80::1b99:de63:cdd6:67a9/64]:47809%enp0s25 19 | """ 20 | 21 | import asyncio 22 | 23 | from typing import Callable 24 | 25 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 26 | from bacpypes3.argparse import ArgumentParser 27 | 28 | from bacpypes3.comm import Client, bind 29 | from bacpypes3.pdu import IPv6Address, PDU 30 | from bacpypes3.ipv6 import IPv6DatagramServer 31 | 32 | # some debugging 33 | _debug = 0 34 | _log = ModuleLogger(globals()) 35 | 36 | 37 | @bacpypes_debugging 38 | class Echo(Client[PDU]): 39 | """ 40 | Echo 41 | """ 42 | 43 | _debug: Callable[..., None] 44 | 45 | async def confirmation(self, pdu: PDU) -> None: 46 | if _debug: 47 | Echo._debug("confirmation %r", pdu) 48 | 49 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 50 | if _debug: 51 | Echo._debug(" - ack: %r", ack) 52 | 53 | await self.request(ack) 54 | 55 | 56 | async def main() -> None: 57 | try: 58 | server = None 59 | parser = ArgumentParser() 60 | parser.add_argument( 61 | "address", 62 | type=str, 63 | nargs="?", 64 | default="[::]", 65 | help="listening address (default [::])", 66 | ) 67 | 68 | args = parser.parse_args() 69 | if _debug: 70 | _log.debug("args: %r", args) 71 | 72 | # evaluate the address 73 | address = IPv6Address(args.address) 74 | if _debug: 75 | _log.debug("address: %r", address) 76 | 77 | # build a very small stack 78 | echo = Echo() 79 | server = IPv6DatagramServer(address) 80 | bind(echo, server) 81 | 82 | # run a really long time 83 | await asyncio.Future() 84 | 85 | except KeyboardInterrupt: 86 | if _debug: 87 | _log.debug("keyboard interrupt") 88 | finally: 89 | if server: 90 | server.close() 91 | 92 | 93 | if __name__ == "__main__": 94 | asyncio.run(main()) 95 | -------------------------------------------------------------------------------- /samples/multiple-stacks.json: -------------------------------------------------------------------------------- 1 | { 2 | "BACpypes": { 3 | "backup_count": 5, 4 | "color": false, 5 | "config": {}, 6 | "cov_lifetime": 60, 7 | "debug": [], 8 | "debug_file": "", 9 | "max_bytes": 1048576, 10 | "route_aware": false 11 | }, 12 | "applications": [ 13 | [ 14 | { 15 | "apdu-segment-timeout": 1000, 16 | "apdu-timeout": 3000, 17 | "application-software-version": "1.0", 18 | "database-revision": 1, 19 | "firmware-revision": "N/A", 20 | "max-apdu-length-accepted": 1024, 21 | "model-name": "N/A", 22 | "number-of-apdu-retries": 3, 23 | "object-identifier": "device,998", 24 | "object-name": "Exeter-0", 25 | "object-type": "device", 26 | "protocol-revision": 22, 27 | "protocol-version": 1, 28 | "segmentation-supported": "segmented-both", 29 | "system-status": "operational", 30 | "vendor-identifier": 999, 31 | "vendor-name": "BACpypes" 32 | }, 33 | { 34 | "bacnet-ip-mode": "normal", 35 | "bacnet-ip-udp-port": 47808, 36 | "changes-pending": false, 37 | "ip-address": "10.0.1.90", 38 | "ip-subnet-mask": "255.255.255.0", 39 | "link-speed": 0.0, 40 | "mac-address": "10.0.1.90:47808", 41 | "network-number": 100, 42 | "network-number-quality": "configured", 43 | "network-type": "ipv4", 44 | "object-identifier": "network-port,1", 45 | "object-name": "NetworkPort-1", 46 | "object-type": "network-port", 47 | "out-of-service": false, 48 | "protocol-level": "bacnet-application", 49 | "reliability": "no-fault-detected" 50 | } 51 | ], 52 | [ 53 | { 54 | "apdu-segment-timeout": 1000, 55 | "apdu-timeout": 3000, 56 | "application-software-version": "1.0", 57 | "database-revision": 1, 58 | "firmware-revision": "N/A", 59 | "max-apdu-length-accepted": 1024, 60 | "model-name": "N/A", 61 | "number-of-apdu-retries": 3, 62 | "object-identifier": "device,999", 63 | "object-name": "Exeter-1", 64 | "object-type": "device", 65 | "protocol-revision": 22, 66 | "protocol-version": 1, 67 | "segmentation-supported": "segmented-both", 68 | "system-status": "operational", 69 | "vendor-identifier": 999, 70 | "vendor-name": "BACpypes" 71 | }, 72 | { 73 | "bacnet-ip-mode": "normal", 74 | "bacnet-ip-udp-port": 47809, 75 | "changes-pending": false, 76 | "ip-address": "10.0.1.90", 77 | "ip-subnet-mask": "255.255.255.0", 78 | "link-speed": 0.0, 79 | "mac-address": "10.0.1.90:47809", 80 | "network-number": 200, 81 | "network-number-quality": "configured", 82 | "network-type": "ipv4", 83 | "object-identifier": "network-port,1", 84 | "object-name": "NetworkPort-1", 85 | "object-type": "network-port", 86 | "out-of-service": false, 87 | "protocol-level": "bacnet-application", 88 | "reliability": "no-fault-detected" 89 | } 90 | ] 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /samples/network-port-1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create a network port object from an address, print some interesting stuff, 3 | convert it to JSON and YAML. Variations of this sample are used to check the 4 | results of various patterns of addresses. 5 | """ 6 | from pprint import pprint 7 | import yaml 8 | 9 | # from bacpypes3.argparse import create_log_handler 10 | from bacpypes3.local.networkport import NetworkPortObject 11 | from bacpypes3.json import sequence_to_json 12 | 13 | # create_log_handler("bacpypes3.object") 14 | # create_log_handler("bacpypes3.local.networkport") 15 | 16 | npo = NetworkPortObject( 17 | "5:192.168.0.99/24", 18 | objectName="Network Port 1", 19 | objectIdentifier=("network-port", 1), 20 | ) 21 | 22 | print("npo:", npo) 23 | npo.debug_contents() 24 | print("") 25 | 26 | npo_address = npo.address 27 | print(f"{npo_address = }") 28 | print(f"{npo_address.network = }") 29 | print("") 30 | 31 | npo_json = sequence_to_json(npo) 32 | pprint(npo_json) 33 | print("") 34 | 35 | print(yaml.dump({"application": [npo_json]})) 36 | -------------------------------------------------------------------------------- /samples/read-batch-point-list.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that reads batches of values with the point list 3 | coded into the application. See the read-batch.py sample for a version that 4 | gets the point list from tab-delimited text input. 5 | 6 | This version waits for a 'read' command in the console to start the batch, or 7 | 'stop' if it's already running. 8 | """ 9 | 10 | import sys 11 | import asyncio 12 | 13 | from typing import Callable 14 | 15 | from bacpypes3.settings import settings 16 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 17 | from bacpypes3.argparse import SimpleArgumentParser 18 | from bacpypes3.app import Application 19 | 20 | from bacpypes3.console import Console 21 | from bacpypes3.cmd import Cmd 22 | from bacpypes3.comm import bind 23 | 24 | from bacpypes3.lib.batchread import DeviceAddressObjectPropertyReference, BatchRead 25 | 26 | # some debugging 27 | _debug = 0 28 | _log = ModuleLogger(globals()) 29 | 30 | # globals 31 | app: Application 32 | batch_read: BatchRead 33 | 34 | # stuff to read, parameters to DeviceAddressObjectPropertyReference 35 | stuff_to_read = [ 36 | (1, "100:2", "device,10002", "object-name"), 37 | (2, "100:2", "device,10002", "local-date"), 38 | (3, "100:2", "device,10002", "local-time"), 39 | (4, "100:2", "analog-value,1", "object-name"), 40 | (5, "100:2", "analog-value,1", "present-value"), 41 | (6, "100:3", "device,10003", "object-name"), 42 | (7, "100:4", "device,10004", "object-name"), 43 | (8, "100:5", "device,10005", "object-name"), 44 | (9, "200:2", "device,20002", "object-name"), 45 | (10, "200:2", "analog-value,1", "present-value"), 46 | (11, "200:2", "analog-value,2", "present-value"), 47 | (12, "200:3", "device:20003", "object-name"), 48 | ] 49 | 50 | 51 | @bacpypes_debugging 52 | class SampleCmd(Cmd): 53 | """ 54 | Sample Cmd 55 | """ 56 | 57 | _debug: Callable[..., None] 58 | 59 | async def do_read(self) -> None: 60 | """ 61 | usage: read 62 | """ 63 | if _debug: 64 | SampleCmd._debug("do_read") 65 | 66 | asyncio.ensure_future(batch_read.run(app, self.callback)) 67 | 68 | def callback(self, key, value) -> None: 69 | asyncio.ensure_future(self.response(f"{key} = {value}")) 70 | 71 | async def do_stop(self) -> None: 72 | """ 73 | usage: stop 74 | """ 75 | global batch_read 76 | batch_read.stop() 77 | 78 | 79 | async def main() -> None: 80 | global app, batch_read 81 | 82 | try: 83 | app = None 84 | args = SimpleArgumentParser().parse_args() 85 | if _debug: 86 | _log.debug("settings: %r", settings) 87 | 88 | # build a very small stack 89 | console = Console() 90 | cmd = SampleCmd() 91 | bind(console, cmd) 92 | 93 | # build the application 94 | app = Application.from_args(args) 95 | 96 | # transform the list of stuff to read 97 | daopr_list = [ 98 | DeviceAddressObjectPropertyReference(*parms) for parms in stuff_to_read 99 | ] 100 | batch_read = BatchRead(daopr_list) 101 | 102 | # run until the console is done, canceled or EOF 103 | await console.fini.wait() 104 | 105 | except KeyboardInterrupt: 106 | if _debug: 107 | _log.debug("keyboard interrupt") 108 | finally: 109 | if app: 110 | app.close() 111 | if console and console.exit_status: 112 | sys.exit(console.exit_status) 113 | 114 | 115 | if __name__ == "__main__": 116 | asyncio.run(main()) 117 | -------------------------------------------------------------------------------- /samples/read-batch.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example reads in a tab-delimited list of DeviceAddressObjectPropertyReference 3 | and reads them as a batch. 4 | 5 | The text file contains: 6 | key - any simple value to identify the row 7 | device address - an Address, like '192.168.0.12' or '5101:16' 8 | object identifier - an ObjectIdentifier, like 'analog-value,12' 9 | property reference - a PropertyReference, like 'present-value' 10 | 11 | The property reference can also include an array index like 'priority-array[6]'. 12 | """ 13 | 14 | import sys 15 | import asyncio 16 | 17 | from typing import Union 18 | 19 | from bacpypes3.settings import settings 20 | from bacpypes3.debugging import ModuleLogger 21 | from bacpypes3.argparse import SimpleArgumentParser 22 | from bacpypes3.app import Application 23 | 24 | from bacpypes3.apdu import ErrorRejectAbortNack 25 | from bacpypes3.lib.batchread import DeviceAddressObjectPropertyReference, BatchRead 26 | 27 | # some debugging 28 | _debug = 0 29 | _log = ModuleLogger(globals()) 30 | 31 | # globals 32 | app: Application 33 | batch_read: BatchRead 34 | 35 | 36 | def callback(key: str, value: Union[float, ErrorRejectAbortNack]) -> None: 37 | """ 38 | This is called when the batch has found a value or an error. The alternative 39 | to using a callback is to wait for the batch to complete and zip() the 40 | results with the list of keys. 41 | """ 42 | if isinstance(value, ErrorRejectAbortNack): 43 | print(f"{key}: {value.errorClass}, {value.errorCode}") 44 | else: 45 | print(f"{key} = {value}") 46 | 47 | 48 | async def main() -> None: 49 | global app, batch_read 50 | 51 | try: 52 | app = None 53 | args = SimpleArgumentParser().parse_args() 54 | if _debug: 55 | _log.debug("settings: %r", settings) 56 | 57 | # build the application 58 | app = Application.from_args(args) 59 | 60 | # transform the list of stuff to read 61 | daopr_list = [] 62 | while line := sys.stdin.readline(): 63 | line_args = line[:-1].split("\t") 64 | daopr_list.append(DeviceAddressObjectPropertyReference(*line_args)) 65 | batch_read = BatchRead(daopr_list) 66 | 67 | # run until the batch is done 68 | await batch_read.run(app, callback) 69 | 70 | except KeyboardInterrupt: 71 | if _debug: 72 | _log.debug("keyboard interrupt") 73 | finally: 74 | if app: 75 | app.close() 76 | 77 | 78 | if __name__ == "__main__": 79 | asyncio.run(main()) 80 | -------------------------------------------------------------------------------- /samples/read-property-console.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends Read Property requests. 3 | """ 4 | 5 | import asyncio 6 | import re 7 | 8 | from typing import Callable 9 | 10 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 11 | from bacpypes3.argparse import SimpleArgumentParser 12 | from bacpypes3.console import Console 13 | from bacpypes3.cmd import Cmd 14 | from bacpypes3.comm import bind 15 | 16 | from bacpypes3.pdu import Address 17 | from bacpypes3.primitivedata import ObjectIdentifier 18 | from bacpypes3.constructeddata import AnyAtomic 19 | from bacpypes3.apdu import ErrorRejectAbortNack 20 | from bacpypes3.app import Application 21 | 22 | # some debugging 23 | _debug = 0 24 | _log = ModuleLogger(globals()) 25 | 26 | # 'property[index]' matching 27 | property_index_re = re.compile(r"^([0-9A-Za-z-]+)(?:\[([0-9]+)\])?$") 28 | 29 | # globals 30 | app: Application 31 | 32 | 33 | @bacpypes_debugging 34 | class SampleCmd(Cmd): 35 | """ 36 | Sample Cmd 37 | """ 38 | 39 | _debug: Callable[..., None] 40 | 41 | async def do_read( 42 | self, 43 | address: Address, 44 | object_identifier: ObjectIdentifier, 45 | property_identifier: str, 46 | ) -> None: 47 | """ 48 | usage: read address objid prop[indx] 49 | """ 50 | if _debug: 51 | SampleCmd._debug( 52 | "do_read %r %r %r", address, object_identifier, property_identifier 53 | ) 54 | 55 | try: 56 | property_value = await app.read_property( 57 | address, object_identifier, property_identifier 58 | ) 59 | if _debug: 60 | SampleCmd._debug(" - property_value: %r", property_value) 61 | except ErrorRejectAbortNack as err: 62 | if _debug: 63 | SampleCmd._debug(" - exception: %r", err) 64 | property_value = err 65 | 66 | if isinstance(property_value, AnyAtomic): 67 | if _debug: 68 | SampleCmd._debug(" - schedule objects") 69 | property_value = property_value.get_value() 70 | 71 | await self.response(str(property_value)) 72 | 73 | 74 | async def main() -> None: 75 | global app 76 | 77 | app = None 78 | try: 79 | parser = SimpleArgumentParser() 80 | args = parser.parse_args() 81 | if _debug: 82 | _log.debug("args: %r", args) 83 | 84 | # build a very small stack 85 | console = Console() 86 | cmd = SampleCmd() 87 | bind(console, cmd) 88 | 89 | # build an application 90 | app = Application.from_args(args) 91 | if _debug: 92 | _log.debug("app: %r", app) 93 | 94 | # wait until the user is done 95 | await console.fini.wait() 96 | 97 | except KeyboardInterrupt: 98 | if _debug: 99 | _log.debug("keyboard interrupt") 100 | finally: 101 | if app: 102 | app.close() 103 | 104 | 105 | if __name__ == "__main__": 106 | asyncio.run(main()) 107 | -------------------------------------------------------------------------------- /samples/read-property.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends a Read Property request and decodes the response. 3 | """ 4 | 5 | import asyncio 6 | import re 7 | 8 | from bacpypes3.debugging import ModuleLogger 9 | from bacpypes3.argparse import SimpleArgumentParser 10 | from bacpypes3.app import Application 11 | 12 | from bacpypes3.constructeddata import AnyAtomic 13 | from bacpypes3.apdu import ErrorRejectAbortNack 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | # 'property[index]' matching 20 | property_index_re = re.compile(r"^([0-9A-Za-z-]+)(?:\[([0-9]+)\])?$") 21 | 22 | 23 | async def main() -> None: 24 | app = None 25 | try: 26 | parser = SimpleArgumentParser() 27 | parser.add_argument( 28 | "device_address", 29 | help="address of the server (B-device)", 30 | ) 31 | parser.add_argument( 32 | "object_identifier", 33 | help="object identifier, like 'analog-input,1'", 34 | ) 35 | parser.add_argument( 36 | "property_identifier", 37 | help="property identifier with optional array index, like 'present-value'", 38 | ) 39 | args = parser.parse_args() 40 | if _debug: 41 | _log.debug("args: %r", args) 42 | 43 | # build an application 44 | app = Application.from_args(args) 45 | if _debug: 46 | _log.debug("app: %r", app) 47 | 48 | try: 49 | response = await app.read_property( 50 | args.device_address, 51 | args.object_identifier, 52 | args.property_identifier, 53 | ) 54 | if _debug: 55 | _log.debug(" - response: %r", response) 56 | except ErrorRejectAbortNack as err: 57 | if _debug: 58 | _log.debug(" - exception: %r", err) 59 | response = err 60 | 61 | if isinstance(response, AnyAtomic): 62 | if _debug: 63 | _log.debug(" - schedule objects") 64 | response = response.get_value() 65 | 66 | print(str(response)) 67 | 68 | finally: 69 | if app: 70 | app.close() 71 | 72 | 73 | if __name__ == "__main__": 74 | asyncio.run(main()) 75 | -------------------------------------------------------------------------------- /samples/start-here.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example. 3 | """ 4 | 5 | import asyncio 6 | 7 | from bacpypes3.debugging import ModuleLogger 8 | from bacpypes3.argparse import SimpleArgumentParser 9 | from bacpypes3.app import Application 10 | 11 | # some debugging 12 | _debug = 0 13 | _log = ModuleLogger(globals()) 14 | 15 | 16 | async def main() -> None: 17 | app = None 18 | try: 19 | args = SimpleArgumentParser().parse_args() 20 | if _debug: 21 | _log.debug("args: %r", args) 22 | 23 | # build an application 24 | app = Application.from_args(args) 25 | if _debug: 26 | _log.debug("app: %r", app) 27 | 28 | # like running forever 29 | await asyncio.Future() 30 | 31 | finally: 32 | if app: 33 | app.close() 34 | 35 | 36 | if __name__ == "__main__": 37 | try: 38 | asyncio.run(main()) 39 | except KeyboardInterrupt: 40 | if _debug: 41 | _log.debug("keyboard interrupt") 42 | -------------------------------------------------------------------------------- /samples/who-has.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends a Who-Has request for an object identifier and prints 3 | out the device instance number, object identifier and object name of the 4 | responses. 5 | """ 6 | 7 | import asyncio 8 | 9 | from bacpypes3.debugging import ModuleLogger 10 | from bacpypes3.argparse import SimpleArgumentParser 11 | from bacpypes3.primitivedata import ObjectIdentifier 12 | from bacpypes3.app import Application 13 | 14 | # some debugging 15 | _debug = 0 16 | _log = ModuleLogger(globals()) 17 | 18 | 19 | async def main() -> None: 20 | app = None 21 | try: 22 | parser = SimpleArgumentParser() 23 | parser.add_argument( 24 | "low_limit", 25 | type=int, 26 | help="device instance range low limit", 27 | ) 28 | parser.add_argument( 29 | "high_limit", 30 | type=int, 31 | help="device instance range high limit", 32 | ) 33 | parser.add_argument( 34 | "object_identifier", 35 | help="object identifier", 36 | ) 37 | args = parser.parse_args() 38 | if _debug: 39 | _log.debug("args: %r", args) 40 | 41 | # build an application 42 | app = Application.from_args(args) 43 | if _debug: 44 | _log.debug("app: %r", app) 45 | 46 | object_identifier = ObjectIdentifier(args.object_identifier) 47 | if _debug: 48 | _log.debug("object_identifier: %r", object_identifier) 49 | 50 | # run the query 51 | i_haves = await app.who_has( 52 | args.low_limit, 53 | args.high_limit, 54 | object_identifier, 55 | ) 56 | if _debug: 57 | _log.debug(" - i_haves: %r", i_haves) 58 | for i_have in i_haves: 59 | if _debug: 60 | _log.debug(" - i_have: %r", i_have) 61 | print( 62 | f"{i_have.deviceIdentifier[1]} {i_have.objectIdentifier} {i_have.objectName!r}" 63 | ) 64 | 65 | finally: 66 | if app: 67 | app.close() 68 | 69 | 70 | if __name__ == "__main__": 71 | asyncio.run(main()) 72 | -------------------------------------------------------------------------------- /samples/write-property.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that sends a Write Property request. 3 | """ 4 | 5 | import asyncio 6 | import re 7 | 8 | from bacpypes3.debugging import ModuleLogger 9 | from bacpypes3.argparse import SimpleArgumentParser 10 | from bacpypes3.app import Application 11 | 12 | from bacpypes3.pdu import Address 13 | from bacpypes3.primitivedata import ObjectIdentifier 14 | from bacpypes3.apdu import ErrorRejectAbortNack 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | # 'property[index]' matching 21 | property_index_re = re.compile(r"^([0-9A-Za-z-]+)(?:\[([0-9]+)\])?$") 22 | 23 | 24 | async def main() -> None: 25 | app = None 26 | try: 27 | parser = SimpleArgumentParser() 28 | parser.add_argument( 29 | "device_address", 30 | help="address of the server (B-device)", 31 | ) 32 | parser.add_argument( 33 | "object_identifier", 34 | help="object identifier", 35 | ) 36 | parser.add_argument( 37 | "property_identifier", 38 | help="property identifier with optional array index", 39 | ) 40 | parser.add_argument( 41 | "value", 42 | help="value to write", 43 | ) 44 | parser.add_argument( 45 | "priority", 46 | nargs="?", 47 | help="optional priority", 48 | ) 49 | args = parser.parse_args() 50 | if _debug: 51 | _log.debug("args: %r", args) 52 | 53 | # interpret the address 54 | device_address = Address(args.device_address) 55 | if _debug: 56 | _log.debug("device_address: %r", device_address) 57 | 58 | # interpret the object identifier 59 | object_identifier = ObjectIdentifier(args.object_identifier) 60 | if _debug: 61 | _log.debug("object_identifier: %r", object_identifier) 62 | 63 | # split the property identifier and its index 64 | property_index_match = property_index_re.match(args.property_identifier) 65 | if not property_index_match: 66 | raise ValueError("property specification incorrect") 67 | property_identifier, property_array_index = property_index_match.groups() 68 | if property_identifier.isdigit(): 69 | property_identifier = int(property_identifier) 70 | if property_array_index is not None: 71 | property_array_index = int(property_array_index) 72 | 73 | # check the priority 74 | priority = None 75 | if args.priority: 76 | priority = int(args.priority) 77 | if (priority < 1) or (priority > 16): 78 | raise ValueError(f"priority: {priority}") 79 | if _debug: 80 | _log.debug("priority: %r", priority) 81 | 82 | # build an application 83 | app = Application.from_args(args) 84 | if _debug: 85 | _log.debug("app: %r", app) 86 | 87 | try: 88 | response = await app.write_property( 89 | device_address, 90 | object_identifier, 91 | property_identifier, 92 | args.value, 93 | property_array_index, 94 | priority, 95 | ) 96 | if _debug: 97 | _log.debug("response: %r", response) 98 | except ErrorRejectAbortNack as err: 99 | print(str(err)) 100 | 101 | finally: 102 | if app: 103 | app.close() 104 | 105 | 106 | if __name__ == "__main__": 107 | asyncio.run(main()) 108 | -------------------------------------------------------------------------------- /sandbox/discover-ipv4-foreign-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build \ 4 | --tag bacpypes3-discover-ipv4-foreign:latest \ 5 | --file discover-ipv4-foreign.dockerfile . 6 | -------------------------------------------------------------------------------- /sandbox/discover-ipv4-foreign-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "${HOST_ADDRESS}" ]] 4 | then 5 | echo "The HOST_ADDRESS is the UDP port number of the docker host" 6 | echo "like '47808' or an IPv4 address and port number if the host" 7 | echo "has more than one home, like '10.0.1.70:47808' and may also" 8 | echo "have an alternative port number like '10.0.1.99:47809'" 9 | echo "" 10 | read -p "Host Address " HOST_ADDRESS || exit 1 11 | export HOST_ADDRESS 12 | echo "" 13 | fi 14 | if [[ -z "${BBMD_ADDRESS}" ]] 15 | then 16 | echo "The address of the BBMD for foreign device registration" 17 | echo "" 18 | read -p "BBMD Address " BBMD_ADDRESS || exit 1 19 | export BBMD_ADDRESS 20 | fi 21 | if [[ -z "${TTL}" ]] 22 | then 23 | read -p "Time-to-live " TTL || exit 1 24 | export TTL 25 | fi 26 | if [[ -z "${DEBUG}" ]] 27 | then 28 | export DEBUG="" 29 | fi 30 | 31 | docker run \ 32 | -it \ 33 | --rm \ 34 | -p ${HOST_ADDRESS}:47808/udp \ 35 | --env BBMD_ADDRESS \ 36 | --env TTL \ 37 | --env DEBUG \ 38 | bacpypes3-discover-ipv4-foreign:latest 39 | -------------------------------------------------------------------------------- /sandbox/discover-ipv4-foreign-save.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker save bacpypes3-discover-ipv4-foreign:latest > discover-ipv4-foreign.tar 4 | -------------------------------------------------------------------------------- /sandbox/discover-ipv4-foreign.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | WORKDIR /app 4 | 5 | RUN pip install --upgrade pip 6 | RUN pip install --upgrade --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ bacpypes3 7 | 8 | COPY discover-ipv4-foreign.py . 9 | 10 | ARG BBMD_ADDRESS 11 | ARG TTL 12 | ARG DEBUG= 13 | 14 | CMD python3 discover-ipv4-foreign.py host:47808 ${BBMD_ADDRESS} ${TTL} --route-aware ${DEBUG} 15 | -------------------------------------------------------------------------------- /sandbox/event-notification-recipient.py: -------------------------------------------------------------------------------- 1 | """ 2 | Event Notification Recipient 3 | """ 4 | 5 | import asyncio 6 | 7 | from typing import Callable 8 | 9 | from bacpypes3.debugging import ModuleLogger, bacpypes_debugging 10 | from bacpypes3.argparse import SimpleArgumentParser 11 | from bacpypes3.apdu import SimpleAckPDU 12 | from bacpypes3.app import Application 13 | 14 | # some debugging 15 | _debug = 0 16 | _log = ModuleLogger(globals()) 17 | 18 | 19 | @bacpypes_debugging 20 | class EventRecipientApplication(Application): 21 | """ 22 | Event Recipient Application 23 | """ 24 | 25 | _debug: Callable[..., None] 26 | 27 | async def do_ConfirmedEventNotificationRequest(self, apdu): 28 | print("ConfirmedEventNotificationRequest") 29 | apdu.debug_contents() 30 | 31 | await self.response(SimpleAckPDU(context=apdu)) 32 | 33 | async def do_UnconfirmedEventNotificationRequest(self, apdu): 34 | print("UnconfirmedEventNotificationRequest") 35 | apdu.debug_contents() 36 | 37 | 38 | async def main() -> None: 39 | app = None 40 | try: 41 | args = SimpleArgumentParser().parse_args() 42 | if _debug: 43 | _log.debug("args: %r", args) 44 | 45 | # build an application 46 | app = EventRecipientApplication.from_args(args) 47 | if _debug: 48 | _log.debug("app: %r", app) 49 | 50 | # like running forever 51 | await asyncio.Future() 52 | 53 | finally: 54 | if app: 55 | app.close() 56 | 57 | 58 | if __name__ == "__main__": 59 | try: 60 | asyncio.run(main()) 61 | except KeyboardInterrupt: 62 | if _debug: 63 | _log.debug("keyboard interrupt") 64 | -------------------------------------------------------------------------------- /sandbox/get-ip-address.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the IPv4 address of an interface. 3 | 4 | https://gist.github.com/socketz/fc9bbbba7be561852ae8905e277402b8 5 | """ 6 | import sys 7 | import socket 8 | import fcntl 9 | import struct 10 | 11 | SIOCGIFADDR = 0x8915 12 | 13 | 14 | def get_ip_address(ifname): 15 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 16 | fdsock = s.fileno() 17 | 18 | ifreq = struct.pack("16sH14s", ifname.encode(), socket.AF_INET, b"\x00" * 14) 19 | try: 20 | res = fcntl.ioctl(fdsock, SIOCGIFADDR, ifreq) 21 | except: 22 | return None 23 | return socket.inet_ntoa(struct.unpack("16sH2x4s8x", res)[2]) 24 | 25 | 26 | print(get_ip_address(sys.argv[1])) 27 | -------------------------------------------------------------------------------- /sandbox/get-netmask.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the network mask of the interface. 3 | """ 4 | import sys 5 | import socket 6 | import fcntl 7 | import struct 8 | 9 | SIOCGIFNETMASK = 0x891B 10 | 11 | 12 | def get_netmask(ifname): 13 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 14 | return socket.inet_ntoa( 15 | fcntl.ioctl(s.fileno(), SIOCGIFNETMASK, struct.pack("256s", ifname.encode()))[ 16 | 20:24 17 | ] 18 | ) 19 | 20 | 21 | print(get_netmask(sys.argv[1])) 22 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv4-bbmd-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple IPv4 BBMD server that does _not_ have a network layer or application 3 | layer. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import asyncio 9 | 10 | from typing import Callable 11 | 12 | from bacpypes3.settings import settings 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | 16 | from bacpypes3.pdu import IPv4Address, PDU 17 | from bacpypes3.comm import Client, bind 18 | 19 | from bacpypes3.ipv4.service import UDPMultiplexer, BIPBBMD 20 | from bacpypes3.ipv4.bvll import BVLLCodec 21 | from bacpypes3.ipv4 import IPv4DatagramServer 22 | 23 | 24 | # some debugging 25 | _debug = 0 26 | _log = ModuleLogger(globals()) 27 | 28 | 29 | def main() -> None: 30 | try: 31 | loop = server = None 32 | parser = ArgumentParser() 33 | parser.add_argument( 34 | "address", 35 | type=str, 36 | help="listening address", 37 | ) 38 | parser.add_argument( 39 | "peers", 40 | type=str, 41 | nargs="*", 42 | help="peer addresses", 43 | ) 44 | 45 | args = parser.parse_args() 46 | if _debug: 47 | _log.debug("settings: %r", settings) 48 | 49 | # get the event loop 50 | loop = asyncio.get_event_loop() 51 | 52 | # evaluate the address 53 | address = IPv4Address(args.address) 54 | if _debug: 55 | _log.debug("address: %r", address) 56 | 57 | # build a very small stack 58 | bbmd = BIPBBMD(address) 59 | for peer in args.peers: 60 | peer_address = IPv4Address(peer) 61 | bbmd.add_peer(peer_address) 62 | 63 | codec = BVLLCodec() 64 | multiplexer = UDPMultiplexer() 65 | server = IPv4DatagramServer(loop, address) 66 | 67 | bind(bbmd, codec, multiplexer.annexJ) 68 | bind(multiplexer, server) 69 | 70 | # run a really long time 71 | loop.run_forever() 72 | 73 | except KeyboardInterrupt: 74 | if _debug: 75 | _log.debug("keyboard interrupt") 76 | finally: 77 | if server: 78 | server.close() 79 | if loop: 80 | loop.close() 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv4-normal-client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that sends messages. 3 | """ 4 | 5 | import sys 6 | import asyncio 7 | 8 | from typing import Callable 9 | 10 | from bacpypes3.settings import settings 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.argparse import ArgumentParser 13 | from bacpypes3.console import Console 14 | from bacpypes3.cmd import Cmd 15 | 16 | from bacpypes3.pdu import Address, LocalBroadcast, IPv4Address, PDU 17 | from bacpypes3.comm import Client, bind 18 | 19 | from bacpypes3.ipv4.service import UDPMultiplexer, BIPNormal 20 | from bacpypes3.ipv4.bvll import BVLLCodec 21 | from bacpypes3.ipv4 import IPv4DatagramServer 22 | 23 | 24 | # some debugging 25 | _debug = 0 26 | _log = ModuleLogger(globals()) 27 | 28 | 29 | @bacpypes_debugging 30 | class SampleCmd(Cmd, Client[PDU]): 31 | """ 32 | Sample Cmd 33 | """ 34 | 35 | _debug: Callable[..., None] 36 | 37 | async def do_send(self, address: Address, data: str) -> None: 38 | """ 39 | usage: send address:Address data:str 40 | """ 41 | if _debug: 42 | SampleCmd._debug("do_send %r %r", address, data) 43 | 44 | if not isinstance(address, (IPv4Address, LocalBroadcast)): 45 | if _debug: 46 | SampleCmd._debug(" - invalid address: %r", address) 47 | await self.response("invalid address: " + str(address)) 48 | return 49 | 50 | pdu = PDU(data.encode(), destination=address) 51 | if _debug: 52 | SampleCmd._debug(" - pdu: %r", pdu) 53 | 54 | await self.request(pdu) 55 | 56 | async def confirmation(self, pdu: PDU) -> None: 57 | if _debug: 58 | SampleCmd._debug("confirmation %r", pdu) 59 | 60 | await self.response(pdu.pduData.decode()) 61 | 62 | 63 | def main() -> None: 64 | try: 65 | loop = console = cmd = server = None 66 | parser = ArgumentParser() 67 | parser.add_argument( 68 | "address", 69 | type=str, 70 | help="address", 71 | ) 72 | 73 | args = parser.parse_args() 74 | if _debug: 75 | _log.debug("settings: %r", settings) 76 | 77 | # get the event loop 78 | loop = asyncio.get_event_loop() 79 | 80 | # evaluate the address 81 | address = IPv4Address(args.address) 82 | if _debug: 83 | _log.debug("address: %r", address) 84 | 85 | # build a very small stack 86 | console = Console() 87 | cmd = SampleCmd() 88 | simple = BIPNormal() 89 | codec = BVLLCodec() 90 | multiplexer = UDPMultiplexer() 91 | server = IPv4DatagramServer(loop, address) 92 | 93 | bind(console, cmd, simple, codec, multiplexer.annexJ) # type: ignore[arg-type] 94 | bind(multiplexer, server) # type: ignore[arg-type] 95 | 96 | # run until the console is done, canceled or EOF 97 | loop.run_until_complete(console.fini.wait()) 98 | 99 | except KeyboardInterrupt: 100 | if _debug: 101 | _log.debug("keyboard interrupt") 102 | finally: 103 | if server: 104 | server.close() 105 | if loop: 106 | loop.close() 107 | if console and console.exit_status: 108 | sys.exit(console.exit_status) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv4-normal-server-broadcast.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that echos PDU messages, upper cased, and broadcasts 3 | the response. 4 | """ 5 | 6 | import asyncio 7 | 8 | from typing import Callable 9 | 10 | from bacpypes3.settings import settings 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.argparse import ArgumentParser 13 | 14 | from bacpypes3.pdu import LocalBroadcast, IPv4Address, PDU 15 | from bacpypes3.comm import Client, bind 16 | 17 | from bacpypes3.ipv4.service import UDPMultiplexer, BIPNormal 18 | from bacpypes3.ipv4.bvll import BVLLCodec 19 | from bacpypes3.ipv4 import IPv4DatagramServer 20 | 21 | 22 | # some debugging 23 | _debug = 0 24 | _log = ModuleLogger(globals()) 25 | 26 | 27 | @bacpypes_debugging 28 | class Echo(Client[PDU]): 29 | """ 30 | Echo 31 | """ 32 | 33 | _debug: Callable[..., None] 34 | 35 | async def confirmation(self, pdu: PDU) -> None: 36 | if _debug: 37 | Echo._debug("confirmation %r", pdu) 38 | 39 | ack = PDU(pdu.pduData.upper(), destination=LocalBroadcast()) 40 | if _debug: 41 | Echo._debug(" - ack: %r", ack) 42 | 43 | await self.request(ack) 44 | 45 | 46 | def main() -> None: 47 | try: 48 | loop = echo = server = None 49 | parser = ArgumentParser() 50 | parser.add_argument( 51 | "address", 52 | type=str, 53 | help="listening address", 54 | ) 55 | 56 | args = parser.parse_args() 57 | if _debug: 58 | _log.debug("settings: %r", settings) 59 | 60 | # get the event loop 61 | loop = asyncio.get_event_loop() 62 | 63 | # evaluate the address 64 | address = IPv4Address(args.address) 65 | if _debug: 66 | _log.debug("address: %r", address) 67 | 68 | # build a very small stack 69 | echo = Echo() 70 | simple = BIPNormal() 71 | codec = BVLLCodec() 72 | multiplexer = UDPMultiplexer() 73 | server = IPv4DatagramServer(loop, address) 74 | 75 | bind(echo, simple, codec, multiplexer.annexJ) 76 | bind(multiplexer, server) 77 | 78 | # run a really long time 79 | loop.run_forever() 80 | 81 | except KeyboardInterrupt: 82 | if _debug: 83 | _log.debug("keyboard interrupt") 84 | finally: 85 | if server: 86 | server.close() 87 | if loop: 88 | loop.close() 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv4-normal-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that echos PDU messages, upper cased. 3 | """ 4 | 5 | import asyncio 6 | 7 | from typing import Callable 8 | 9 | from bacpypes3.settings import settings 10 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 11 | from bacpypes3.argparse import ArgumentParser 12 | 13 | from bacpypes3.pdu import IPv4Address, PDU 14 | from bacpypes3.comm import Client, bind 15 | 16 | from bacpypes3.ipv4.service import UDPMultiplexer, BIPNormal 17 | from bacpypes3.ipv4.bvll import BVLLCodec 18 | from bacpypes3.ipv4 import IPv4DatagramServer 19 | 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class Echo(Client[PDU]): 28 | """ 29 | Echo 30 | """ 31 | 32 | _debug: Callable[..., None] 33 | 34 | async def confirmation(self, pdu: PDU) -> None: 35 | if _debug: 36 | Echo._debug("confirmation %r", pdu) 37 | 38 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 39 | if _debug: 40 | Echo._debug(" - ack: %r", ack) 41 | 42 | await self.request(ack) 43 | 44 | 45 | def main() -> None: 46 | try: 47 | loop = echo = server = None 48 | parser = ArgumentParser() 49 | parser.add_argument( 50 | "address", 51 | type=str, 52 | help="listening address", 53 | ) 54 | 55 | args = parser.parse_args() 56 | if _debug: 57 | _log.debug("settings: %r", settings) 58 | 59 | # get the event loop 60 | loop = asyncio.get_event_loop() 61 | 62 | # evaluate the address 63 | address = IPv4Address(args.address) 64 | if _debug: 65 | _log.debug("address: %r", address) 66 | 67 | # build a very small stack 68 | echo = Echo() 69 | simple = BIPNormal() 70 | codec = BVLLCodec() 71 | multiplexer = UDPMultiplexer() 72 | server = IPv4DatagramServer(loop, address) 73 | 74 | bind(echo, simple, codec, multiplexer.annexJ) 75 | bind(multiplexer, server) 76 | 77 | # run a really long time 78 | loop.run_forever() 79 | 80 | except KeyboardInterrupt: 81 | if _debug: 82 | _log.debug("keyboard interrupt") 83 | finally: 84 | if server: 85 | server.close() 86 | if loop: 87 | loop.close() 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv6-bbmd-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Simple IPv6 BBMD server that does _not_ have a network layer or application 5 | layer. 6 | """ 7 | 8 | import asyncio 9 | 10 | from typing import Callable 11 | 12 | from bacpypes3.settings import settings 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | 16 | from bacpypes3.pdu import IPv6Address, VirtualAddress, PDU 17 | from bacpypes3.comm import Client, bind 18 | 19 | from bacpypes3.ipv6.service import BIPBBMD, BVLLServiceElement 20 | from bacpypes3.ipv6.bvll import BVLLCodec 21 | from bacpypes3.ipv6 import IPv6DatagramServer 22 | 23 | 24 | # some debugging 25 | _debug = 0 26 | _log = ModuleLogger(globals()) 27 | 28 | 29 | def main() -> None: 30 | try: 31 | loop = server = None 32 | parser = ArgumentParser() 33 | parser.add_argument( 34 | "address", 35 | type=str, 36 | help="listening address", 37 | ) 38 | parser.add_argument( 39 | "virtual_address", 40 | type=str, 41 | help="virtual address", 42 | ) 43 | 44 | args = parser.parse_args() 45 | if _debug: 46 | _log.debug("settings: %r", settings) 47 | 48 | # get the event loop 49 | loop = asyncio.get_event_loop() 50 | 51 | # evaluate the addresses 52 | address = IPv6Address(args.address) 53 | if _debug: 54 | _log.debug("address: %r", address) 55 | virtual_address = VirtualAddress(args.virtual_address) 56 | if _debug: 57 | _log.debug("virtual_address: %r", virtual_address) 58 | 59 | # build a very small stack 60 | bbmd = BIPBBMD(bbmd_address=address, virtual_address=virtual_address) 61 | codec = BVLLCodec() 62 | server = IPv6DatagramServer(loop, address) 63 | 64 | bind(bbmd, codec, server) 65 | 66 | bvll_service_element = BVLLServiceElement() 67 | bind(bvll_service_element, normal) 68 | 69 | # run a really long time 70 | loop.run_forever() 71 | 72 | except KeyboardInterrupt: 73 | if _debug: 74 | _log.debug("keyboard interrupt") 75 | finally: 76 | if server: 77 | server.close() 78 | if loop: 79 | loop.close() 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /sandbox/link-layer/ipv6-normal-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that echos PDU messages, upper cased. 3 | """ 4 | 5 | import asyncio 6 | 7 | from typing import Callable 8 | 9 | from bacpypes3.settings import settings 10 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 11 | from bacpypes3.argparse import ArgumentParser 12 | 13 | from bacpypes3.pdu import IPv6Address, VirtualAddress, PDU 14 | from bacpypes3.comm import Client, bind 15 | 16 | from bacpypes3.ipv6.service import BIPNormal, BVLLServiceElement 17 | from bacpypes3.ipv6.bvll import BVLLCodec 18 | from bacpypes3.ipv6 import IPv6DatagramServer 19 | 20 | 21 | # some debugging 22 | _debug = 0 23 | _log = ModuleLogger(globals()) 24 | 25 | 26 | @bacpypes_debugging 27 | class Echo(Client[PDU]): 28 | """ 29 | Echo 30 | """ 31 | 32 | _debug: Callable[..., None] 33 | 34 | async def confirmation(self, pdu: PDU) -> None: 35 | if _debug: 36 | Echo._debug("confirmation %r", pdu) 37 | 38 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 39 | if _debug: 40 | Echo._debug(" - ack: %r", ack) 41 | 42 | await self.request(ack) 43 | 44 | 45 | def main() -> None: 46 | try: 47 | loop = echo = server = None 48 | parser = ArgumentParser() 49 | parser.add_argument( 50 | "address", 51 | type=str, 52 | help="listening address", 53 | ) 54 | parser.add_argument( 55 | "virtual_address", 56 | type=str, 57 | help="virtual address", 58 | ) 59 | 60 | args = parser.parse_args() 61 | if _debug: 62 | _log.debug("settings: %r", settings) 63 | 64 | # get the event loop 65 | loop = asyncio.get_event_loop() 66 | 67 | # evaluate the addresses 68 | address = IPv6Address(args.address) 69 | if _debug: 70 | _log.debug("address: %r", address) 71 | virtual_address = VirtualAddress(args.virtual_address) 72 | if _debug: 73 | _log.debug("virtual_address: %r", virtual_address) 74 | 75 | # build a very small stack 76 | echo = Echo() 77 | normal = BIPNormal(virtual_address=virtual_address) 78 | codec = BVLLCodec() 79 | server = IPv6DatagramServer(loop, address) 80 | 81 | bind(echo, normal, codec, server) 82 | 83 | bvll_service_element = BVLLServiceElement() 84 | bind(bvll_service_element, normal) 85 | 86 | # run a really long time 87 | loop.run_forever() 88 | 89 | except KeyboardInterrupt: 90 | if _debug: 91 | _log.debug("keyboard interrupt") 92 | finally: 93 | if server: 94 | server.close() 95 | if loop: 96 | loop.close() 97 | 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /sandbox/mqtt-b-side.py: -------------------------------------------------------------------------------- 1 | """ 2 | MQTT B-side Example 3 | 4 | When the present-value of an Analog Object is changed, publish a message to the 5 | broker using the object name as the topic. 6 | """ 7 | 8 | import asyncio 9 | 10 | from typing import Dict, Tuple 11 | 12 | from bacpypes3.debugging import ModuleLogger 13 | from bacpypes3.argparse import SimpleArgumentParser 14 | from bacpypes3.primitivedata import ObjectIdentifier, Real 15 | from bacpypes3.app import Application 16 | from bacpypes3.local.analog import AnalogValueObject 17 | 18 | import aiomqtt 19 | 20 | # some debugging 21 | _debug = 0 22 | _log = ModuleLogger(globals()) 23 | 24 | MQTT_HOST = "test.mosquitto.org" 25 | MQTT_TOPIC_PREFIX = "bacpypes3/mqtt" 26 | 27 | # globals 28 | args = None 29 | client = None 30 | 31 | topic_map: Dict[str, ObjectIdentifier] = { 32 | "oat": ObjectIdentifier("analog-value,1"), 33 | "rh": ObjectIdentifier("analog-value,2"), 34 | } 35 | 36 | 37 | class MQTTAnalogValueObject(AnalogValueObject): 38 | _present_value: float = 0.0 39 | 40 | @property 41 | async def presentValue(self) -> Real: 42 | if _debug: 43 | _log.debug("presentValue (getter)") 44 | 45 | return Real(self._present_value) 46 | 47 | @presentValue.setter 48 | async def presentValue(self, value: Real) -> None: 49 | """Change the present value.""" 50 | if _debug: 51 | _log.debug("presentValue (setter) %r", value) 52 | global client 53 | 54 | self._present_value = value 55 | 56 | await client.publish(args.topic + "/" + self.objectName, payload=value) 57 | 58 | 59 | async def main() -> None: 60 | global args, client 61 | 62 | app = None 63 | try: 64 | parser = SimpleArgumentParser() 65 | parser.add_argument( 66 | "--topic", 67 | help="topic prefix", 68 | default=MQTT_TOPIC_PREFIX, 69 | ) 70 | parser.add_argument( 71 | "--host", 72 | help="host name of the broker", 73 | default=MQTT_HOST, 74 | ) 75 | args = parser.parse_args() 76 | if _debug: 77 | _log.debug("args: %r", args) 78 | 79 | # build an application 80 | app = Application.from_args(args) 81 | if _debug: 82 | _log.debug("app: %r", app) 83 | 84 | # create objects 85 | for object_name, object_identifier in topic_map.items(): 86 | # create a custom object 87 | analog_value_object = MQTTAnalogValueObject( 88 | objectName=object_name, 89 | objectIdentifier=(object_identifier), 90 | presentValue=0.0, 91 | ) 92 | if _debug: 93 | _log.debug("analog_value_object: %r", analog_value_object) 94 | 95 | app.add_object(analog_value_object) 96 | 97 | # connect and run forever 98 | async with aiomqtt.Client(args.host) as client: 99 | await asyncio.Future() 100 | 101 | finally: 102 | if app: 103 | app.close() 104 | 105 | 106 | if __name__ == "__main__": 107 | try: 108 | asyncio.run(main()) 109 | except KeyboardInterrupt: 110 | if _debug: 111 | _log.debug("keyboard interrupt") 112 | -------------------------------------------------------------------------------- /sandbox/net-normal-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple console example that echos PDU messages, upper cased. 3 | """ 4 | 5 | import asyncio 6 | 7 | from typing import Callable 8 | 9 | from bacpypes3.settings import settings 10 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 11 | from bacpypes3.argparse import ArgumentParser 12 | 13 | from bacpypes3.pdu import IPv4Address, PDU 14 | from bacpypes3.comm import Client, bind 15 | 16 | from bacpypes3.netservice import NetworkServiceAccessPoint, NetworkServiceElement 17 | 18 | from bacpypes3.ipv4.service import UDPMultiplexer, BIPNormal 19 | from bacpypes3.ipv4.bvll import BVLLCodec 20 | from bacpypes3.ipv4 import IPv4DatagramServer 21 | 22 | 23 | # some debugging 24 | _debug = 0 25 | _log = ModuleLogger(globals()) 26 | 27 | 28 | @bacpypes_debugging 29 | class Echo(Client[PDU]): 30 | """ 31 | Echo 32 | """ 33 | 34 | _debug: Callable[..., None] 35 | 36 | async def confirmation(self, pdu: PDU) -> None: 37 | if _debug: 38 | Echo._debug("confirmation %r", pdu) 39 | 40 | ack = PDU(pdu.pduData.upper(), destination=pdu.pduSource) 41 | if _debug: 42 | Echo._debug(" - ack: %r", ack) 43 | 44 | await self.request(ack) 45 | 46 | 47 | def main() -> None: 48 | try: 49 | loop = echo = server = None 50 | parser = ArgumentParser() 51 | parser.add_argument( 52 | "address", 53 | type=str, 54 | help="listening address", 55 | ) 56 | 57 | args = parser.parse_args() 58 | if _debug: 59 | _log.debug("settings: %r", settings) 60 | 61 | # get the event loop 62 | loop = asyncio.get_event_loop() 63 | 64 | # evaluate the address 65 | ipv4_address = IPv4Address(args.address) 66 | if _debug: 67 | _log.debug("ipv4_address: %r", ipv4_address) 68 | 69 | # application layer 70 | echo = Echo() 71 | 72 | # network layer 73 | npdu_nsap = NetworkServiceAccessPoint() 74 | npdu_nse = NetworkServiceElement() 75 | bind(npdu_nse, npdu_nsap) # type: ignore[arg-type] 76 | 77 | # bind the upper layers together 78 | bind(echo, npdu_nsap) 79 | 80 | # create a link layer 81 | bvll_normal = BIPNormal() 82 | bvll_codec = BVLLCodec() 83 | multiplexer = UDPMultiplexer() 84 | server = IPv4DatagramServer(loop, ipv4_address) 85 | bind(multiplexer, server) # type: ignore[arg-type] 86 | bind(bvll_normal, bvll_codec, multiplexer.annexJ) # type: ignore[arg-type] 87 | 88 | # connect the network layer to the link layer 89 | npdu_nsap.bind(bvll_normal) 90 | 91 | # run a really long time 92 | loop.run_forever() 93 | 94 | except KeyboardInterrupt: 95 | if _debug: 96 | _log.debug("keyboard interrupt") 97 | finally: 98 | if server: 99 | server.close() 100 | if loop: 101 | loop.close() 102 | 103 | 104 | if __name__ == "__main__": 105 | main() 106 | -------------------------------------------------------------------------------- /sandbox/redis-server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example that has a device object and an additional analog value object 3 | that matches a key in Redis. 4 | """ 5 | 6 | import asyncio 7 | import os 8 | 9 | from bacpypes3.debugging import ModuleLogger 10 | from bacpypes3.argparse import SimpleArgumentParser 11 | 12 | from bacpypes3.primitivedata import Real 13 | from bacpypes3.ipv4.app import Application 14 | from bacpypes3.local.analog import AnalogValueObject 15 | 16 | from redis import asyncio as aioredis 17 | from redis.asyncio import Redis 18 | 19 | # some debugging 20 | _debug = 0 21 | _log = ModuleLogger(globals()) 22 | 23 | # globals 24 | redis: Redis 25 | 26 | # settings 27 | REDIS_HOST = os.getenv("REDIS_HOST", "localhost") 28 | REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) 29 | REDIS_DB = int(os.getenv("REDIS_DB", 0)) 30 | 31 | 32 | class RedisAnalogValueObject(AnalogValueObject): 33 | @property 34 | async def presentValue(self) -> Real: 35 | if _debug: 36 | _log.debug("presentValue (getter)") 37 | 38 | value = await redis.get(str(self.objectName)) 39 | if _debug: 40 | _log.debug(f"{value = !r}") 41 | if value is None: 42 | return None 43 | 44 | return Real(float(value)) 45 | 46 | @presentValue.setter 47 | async def presentValue(self, value: Real) -> None: 48 | """Change the present value.""" 49 | if _debug: 50 | _log.debug("presentValue (setter) %r", value) 51 | 52 | await redis.set(str(self.objectName), value) 53 | 54 | 55 | async def main() -> None: 56 | global redis 57 | 58 | try: 59 | app = None 60 | args = SimpleArgumentParser().parse_args() 61 | if _debug: 62 | _log.debug("args: %r", args) 63 | 64 | # connect to Redis 65 | redis = aioredis.from_url(f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}") 66 | await redis.ping() 67 | 68 | # build an application 69 | app = Application.from_args(args) 70 | if _debug: 71 | _log.debug("app: %r", app) 72 | 73 | # create a custom object 74 | custom_object = RedisAnalogValueObject( 75 | objectIdentifier=("analog-value", 1), 76 | objectName="Wowzers", 77 | ) 78 | if _debug: 79 | _log.debug("custom_object: %r", custom_object) 80 | 81 | app.add_object(custom_object) 82 | 83 | # like running forever 84 | await asyncio.Future() 85 | 86 | finally: 87 | if app: 88 | app.close() 89 | 90 | 91 | if __name__ == "__main__": 92 | asyncio.run(main()) 93 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | 7 | from setuptools import setup, find_packages 8 | 9 | # load in the project metadata 10 | init_py = open(os.path.join("bacpypes3", "__init__.py")).read() 11 | metadata = dict(re.findall("""__([a-z]+)__ = ["]([^"]+)["]""", init_py)) 12 | 13 | requirements = [] 14 | 15 | setup( 16 | name="bacpypes3", 17 | version=metadata["version"], 18 | description="BACnet Communications Library", 19 | long_description="BACpypes3 provides a BACnet application layer and network layer written in Python3 for daemons, scripting, and graphical interfaces.", 20 | long_description_content_type="text/x-rst", 21 | author=metadata["author"], 22 | author_email=metadata["email"], 23 | url="https://github.com/JoelBender/bacpypes3", 24 | packages=find_packages(), 25 | package_data={"bacpypes3": ["py.typed"]}, 26 | include_package_data=True, 27 | install_requires=requirements, 28 | license="MIT", 29 | zip_safe=False, 30 | classifiers=[ 31 | "Development Status :: 2 - Pre-Alpha", 32 | "Intended Audience :: Developers", 33 | "License :: OSI Approved :: BSD License", 34 | "Natural Language :: English", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3.8", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /testpypi_install_bacpypes3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m pip install --upgrade \ 4 | --index-url https://test.pypi.org/simple/ \ 5 | --extra-index-url https://pypi.org/simple/ \ 6 | bacpypes3 7 | python3 -c 'import bacpypes3; print("bacpypes3:", bacpypes3.__version__, bacpypes3.__file__)' 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | BACpypes3 Testing 6 | ---------------- 7 | """ 8 | 9 | from . import test__template # noqa: F401 10 | from . import test_pdu # noqa: F401 11 | from . import test_vlan # noqa: F401 12 | from . import test_utilities # noqa: F401 13 | 14 | from . import test_primitive_data # noqa: F401 15 | from . import test_basetypes # noqa: F401 16 | from . import test_constructed_data # noqa: F401 17 | -------------------------------------------------------------------------------- /tests/clocked_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import pytest 5 | 6 | from typing import Any, Optional, AsyncGenerator, cast 7 | 8 | 9 | class ClockedTest: 10 | """ 11 | A context wrapper around an event loop that allows tests to advance the 12 | clock which is inspired by the ClockedTestCase of the asynctest library (no 13 | longer receiving updates for Python 3.8+). 14 | """ 15 | 16 | time: float = 0.0 17 | 18 | def __init__(self, loop: asyncio.BaseEventLoop) -> None: 19 | self.loop = loop 20 | 21 | def __enter__(self) -> ClockedTest: 22 | # incorporate offset timing into the event loop 23 | self._original_time_fn = self.loop.time 24 | self.loop.time = lambda: self.time # type: ignore[assignment] 25 | return self 26 | 27 | def __exit__(self, *_: Any, **__: Any) -> None: 28 | # restore the loop time function 29 | self.loop.time = self._original_time_fn # type: ignore[assignment] 30 | 31 | async def __call__(self, seconds: float) -> None: 32 | await self.advance(seconds) 33 | 34 | async def advance(self, seconds: float) -> None: 35 | if seconds < 0: 36 | raise ValueError("Advance time must not be negative") 37 | await self._drain_loop() 38 | 39 | target_time = self.time + seconds 40 | while True: 41 | next_time = self._next_scheduled() 42 | if next_time is None or next_time > target_time: 43 | break 44 | 45 | self.time = next_time 46 | await self._drain_loop() 47 | 48 | self.time = target_time 49 | await self._drain_loop() 50 | 51 | def _next_scheduled(self) -> Optional[float]: 52 | try: 53 | return cast(float, self.loop._scheduled[0]._when) # type: ignore[attr-defined] 54 | except IndexError: 55 | return None 56 | 57 | async def _drain_loop(self) -> None: 58 | while True: 59 | next_time = self._next_scheduled() 60 | if not self.loop._ready and ( # type: ignore[attr-defined] 61 | next_time is None or next_time > self.time 62 | ): 63 | break 64 | await asyncio.sleep(0) 65 | 66 | 67 | @pytest.fixture(scope="function") 68 | async def clocked_test() -> AsyncGenerator[ClockedTest, None]: 69 | """ 70 | This function scoped fixture returns an instance of a ClockedTest. 71 | """ 72 | with ClockedTest(asyncio.get_running_loop()) as clocked_test: 73 | yield clocked_test 74 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Glue routines to simulate package setup and teardown. 5 | """ 6 | 7 | import _pytest # type: ignore[import] 8 | 9 | from .utilities import setup_package, teardown_package 10 | from .clocked_test import clocked_test # noqa: F401 11 | 12 | 13 | def pytest_configure(config: _pytest.config.Config) -> None: 14 | setup_package() 15 | 16 | 17 | def pytest_unconfigure() -> None: 18 | teardown_package() 19 | -------------------------------------------------------------------------------- /tests/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | mypy_path = .. 3 | show_error_codes = True 4 | -------------------------------------------------------------------------------- /tests/test_1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_something 6 | -------------- 7 | """ 8 | 9 | import unittest 10 | from typing import Callable 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | 14 | # some debugging 15 | _debug = 0 16 | _log = ModuleLogger(globals()) 17 | 18 | 19 | @bacpypes_debugging 20 | class TestSomething(unittest.TestCase): 21 | 22 | _debug: Callable[..., None] 23 | 24 | def setUp(self) -> None: 25 | if _debug: 26 | TestSomething._debug("setUp") 27 | 28 | def test_something(self) -> None: 29 | if _debug: 30 | TestSomething._debug("test_something") 31 | 32 | def tearDown(self) -> None: 33 | if _debug: 34 | TestSomething._debug("tearDown") 35 | -------------------------------------------------------------------------------- /tests/test__template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Module Template 6 | -------------------- 7 | """ 8 | 9 | import unittest 10 | 11 | from typing import Callable, Any 12 | 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def setup_module() -> None: 22 | """This function is called once at the beginning of all of the tests 23 | in this module.""" 24 | if _debug: 25 | setup_module._debug("setup_module") # type: ignore[attr-defined] 26 | 27 | 28 | @bacpypes_debugging 29 | def teardown_module() -> None: 30 | """This function is called once at the end of the tests in this module.""" 31 | if _debug: 32 | teardown_module._debug("teardown_module") # type: ignore[attr-defined] 33 | 34 | 35 | @bacpypes_debugging 36 | def setup_function(function: Callable[..., Any]) -> None: 37 | """This function is called before each module level test function.""" 38 | if _debug: 39 | setup_function._debug( # type: ignore[attr-defined] 40 | "setup_function %r", function 41 | ) 42 | 43 | 44 | @bacpypes_debugging 45 | def teardown_function(function: Callable[..., Any]) -> None: 46 | """This function is called after each module level test function.""" 47 | if _debug: 48 | teardown_function._debug( # type: ignore[attr-defined] 49 | "teardown_function %r", function 50 | ) 51 | 52 | 53 | @bacpypes_debugging 54 | def test_some_function(*args: Any, **kwargs: Any) -> None: 55 | """This is a module level test function.""" 56 | if _debug: 57 | setup_function._debug( # type: ignore[attr-defined] 58 | "test_some_function %r %r", args, kwargs 59 | ) 60 | 61 | 62 | @bacpypes_debugging 63 | class TestCaseTemplate(unittest.TestCase): 64 | 65 | _debug: Callable[..., None] 66 | 67 | @classmethod 68 | def setup_class(cls) -> None: 69 | """This function is called once before the test case is instantiated 70 | for each of the tests.""" 71 | if _debug: 72 | TestCaseTemplate._debug("setup_class") 73 | 74 | @classmethod 75 | def teardown_class(cls) -> None: 76 | """This function is called once at the end after the last instance 77 | of the test case has been abandon.""" 78 | if _debug: 79 | TestCaseTemplate._debug("teardown_class") 80 | 81 | def setup_method(self, method: Callable[..., Any]) -> None: 82 | """This function is called before each test method is called as is 83 | given a reference to the test method.""" 84 | if _debug: 85 | TestCaseTemplate._debug("setup_method %r", method) 86 | 87 | def teardown_method(self, method: Callable[..., Any]) -> None: 88 | """This function is called after each test method has been called and 89 | is given a reference to the test method.""" 90 | if _debug: 91 | TestCaseTemplate._debug("teardown_method %r", method) 92 | 93 | def test_something(self) -> None: 94 | """This is a method level test function.""" 95 | if _debug: 96 | TestCaseTemplate._debug("test_something") 97 | 98 | def test_something_else(self) -> None: 99 | """This is another method level test function.""" 100 | if _debug: 101 | TestCaseTemplate._debug("test_something_else") 102 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test BACpypes3 Application Module 3 | """ 4 | 5 | from . import test_parse_reference 6 | from . import test_read_write_api 7 | -------------------------------------------------------------------------------- /tests/test_basetypes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Test BACpypes Base Types Module 5 | """ 6 | 7 | from . import test_datetime -------------------------------------------------------------------------------- /tests/test_basetypes/test_datetime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Date 6 | --------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Date, Time 14 | from bacpypes3.basetypes import DateTime 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | 21 | @bacpypes_debugging 22 | class TestDateTime(unittest.TestCase): 23 | def test_ctor(self): 24 | if _debug: 25 | TestDateTime._debug("test_ctor") 26 | 27 | # no ctor parameters 28 | obj = DateTime() 29 | assert obj.date is None 30 | assert obj.time is None 31 | 32 | # ctor simple parameters 33 | assert DateTime("2025-01-01") == DateTime(date=(125, 1, 1, 3), time=(0, 0, 0, 0)) 34 | assert DateTime("2025-01-01 12:34:56") == DateTime(date=(125, 1, 1, 3), time=(12, 34, 56, 0)) 35 | assert DateTime("2025-01-01 12:34:56.7") == DateTime(date=(125, 1, 1, 3), time=(12, 34, 56, 70)) 36 | assert DateTime("2025-01-01 12:34:56.78") == DateTime(date=(125, 1, 1, 3), time=(12, 34, 56, 78)) 37 | assert DateTime("2025-01-01 12:34:56.789") == DateTime(date=(125, 1, 1, 3), time=(12, 34, 56, 78)) 38 | 39 | assert DateTime("2025-01-01 mon") == DateTime(date=(125, 1, 1, 1), time=(0, 0, 0, 0)) 40 | -------------------------------------------------------------------------------- /tests/test_constructed_data/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test BACpypes Constructed Data Module 3 | """ 4 | 5 | from . import test_any 6 | from . import test_array 7 | from . import test_choice 8 | from . import test_list 9 | from . import test_sequence 10 | from . import test_sequence_of 11 | 12 | from . import test_read_property_multiple 13 | -------------------------------------------------------------------------------- /tests/test_constructed_data/test_any.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Any 6 | -------- 7 | """ 8 | 9 | import inspect 10 | import unittest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.primitivedata import Integer 14 | from bacpypes3.constructeddata import Any, Sequence 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | 21 | @bacpypes_debugging 22 | def any_endec(cls, *args, **kwargs): 23 | """ 24 | Encode an instance of the class using the arguments and keyword arguments, 25 | then decode the tag list using the class and confirm they are identical. 26 | """ 27 | if _debug: 28 | any_endec._debug("test_endec %r %r", cls, kwargs) 29 | 30 | obj1 = cls(*args, **kwargs) 31 | if _debug: 32 | any_endec._debug(" - obj1: %r", obj1) 33 | 34 | tag_list = obj1.encode() 35 | if _debug: 36 | any_endec._debug(" - tag_list: %r", tag_list) 37 | 38 | obj2 = cls.decode(tag_list) 39 | if _debug: 40 | any_endec._debug(" - obj2: %r", obj2) 41 | 42 | assert obj1 == obj2 43 | 44 | 45 | @bacpypes_debugging 46 | class TestAny000(unittest.TestCase): 47 | def test_ctor(self): 48 | if _debug: 49 | TestAny000._debug("test_ctor") 50 | 51 | # no ctor parameters is a class 52 | cls = Any() 53 | assert inspect.isclass(cls) 54 | 55 | # context decorator still a class 56 | cls = Any(_context=1) 57 | assert inspect.isclass(cls) 58 | 59 | # pass None is uninitialized 60 | obj = Any(None) 61 | assert isinstance(obj, Any) 62 | assert obj.tagList is None 63 | 64 | def test_atomic_ctor(self): 65 | if _debug: 66 | TestAny000._debug("test_atomic_ctor") 67 | 68 | # ctor atomic parameter 69 | obj = Any(Integer(1)) 70 | assert obj.tagList 71 | 72 | def test_cast(self): 73 | if _debug: 74 | TestAny000._debug("test_cast") 75 | 76 | value = Integer(2) 77 | 78 | obj1 = Any(None) 79 | obj1.cast_in(value) 80 | obj2 = obj1.cast_out(Integer) 81 | assert obj2 == value 82 | 83 | def test_copy(self): 84 | if _debug: 85 | TestAny000._debug("test_copy") 86 | 87 | obj1 = Any(Integer(1)) 88 | obj2 = Any(obj1) 89 | assert obj1 == obj2 90 | 91 | 92 | # 93 | # Thing001 94 | # 95 | 96 | 97 | class Thing001(Sequence): 98 | _order = ("i",) 99 | i: Integer(_context=1) 100 | 101 | 102 | @bacpypes_debugging 103 | class TestAny001(unittest.TestCase): 104 | def test_ctor(self): 105 | if _debug: 106 | TestAny001._debug("test_ctor") 107 | 108 | # ctor nonatomic parameter 109 | obj = Any(Thing001(i=1)) 110 | assert obj.tagList 111 | 112 | def test_cast(self): 113 | if _debug: 114 | TestAny001._debug("test_cast") 115 | 116 | value = Thing001(i=2) 117 | 118 | obj1 = Any(None) 119 | obj1.cast_in(value) 120 | obj2 = obj1.cast_out(Thing001) 121 | assert obj2 == value 122 | -------------------------------------------------------------------------------- /tests/test_constructed_data/test_array.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test ArrayOf 6 | ------------ 7 | """ 8 | 9 | import unittest 10 | 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.primitivedata import Integer 13 | from bacpypes3.constructeddata import ArrayOf 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def array_of_endec(cls, *args, **kwargs): 22 | """ 23 | Encode an instance of the class using the keyword arguments, then 24 | decode the tag list using the class and confirm they are identical. 25 | """ 26 | if _debug: 27 | array_of_endec._debug("test_endec %r %r %r", cls, args, kwargs) 28 | 29 | obj1 = cls(*args, **kwargs) 30 | if _debug: 31 | array_of_endec._debug(" - obj1: %r", obj1) 32 | 33 | tag_list = obj1.encode() 34 | if _debug: 35 | array_of_endec._debug(" - tag_list: %r", tag_list) 36 | 37 | obj2 = cls.decode(tag_list) 38 | if _debug: 39 | array_of_endec._debug(" - obj2: %r", obj2) 40 | 41 | assert obj1 == obj2 42 | 43 | 44 | # 45 | # Thing000 46 | # 47 | 48 | 49 | Thing000 = ArrayOf(Integer) 50 | 51 | 52 | @bacpypes_debugging 53 | class TestThing000(unittest.TestCase): 54 | def test_ctor(self): 55 | if _debug: 56 | TestThing000._debug("test_ctor") 57 | 58 | # no ctor parameters 59 | obj = Thing000() 60 | assert len(obj) == 0 61 | 62 | # empty array 63 | obj = Thing000([]) 64 | assert len(obj) == 0 65 | 66 | # single element 67 | obj = Thing000([1]) 68 | assert len(obj) == 1 69 | assert isinstance(obj[0], Integer) 70 | 71 | def test_copy(self): 72 | if _debug: 73 | TestThing000._debug("test_copy") 74 | 75 | obj1 = Thing000([1, 2, 3]) 76 | obj2 = Thing000(obj1) 77 | assert obj1 == obj2 78 | 79 | def test_index(self): 80 | if _debug: 81 | TestThing000._debug("test_index") 82 | 83 | # no ctor parameters 84 | obj = Thing000() 85 | assert len(obj) == 0 86 | 87 | def test_endec_1(self): 88 | if _debug: 89 | TestThing000._debug("test_endec_1") 90 | 91 | # encode and decode 92 | array_of_endec(Thing000, [4, 5]) 93 | 94 | def test_extend(self): 95 | if _debug: 96 | TestThing000._debug("test_extend") 97 | 98 | # simple array 99 | obj = Thing000([1, 2, 3]) 100 | assert len(obj) == 3 101 | 102 | # add an item to the end 103 | obj.append(4) 104 | assert len(obj) == 4 105 | 106 | 107 | # 108 | # Thing001 109 | # 110 | 111 | 112 | Thing001 = ArrayOf(Integer, _context=1) 113 | 114 | 115 | @bacpypes_debugging 116 | class TestThing001(unittest.TestCase): 117 | def test_endec_1(self): 118 | if _debug: 119 | TestThing001._debug("test_endec_1") 120 | 121 | # encode and decode 122 | array_of_endec(Thing001, [4, 5]) 123 | -------------------------------------------------------------------------------- /tests/test_constructed_data/test_choice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Choice 6 | ----------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.primitivedata import Integer, Real 14 | from bacpypes3.constructeddata import Choice 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | 21 | @bacpypes_debugging 22 | def choice_endec(cls, **kwargs): 23 | """ 24 | Encode an instance of the class using the keyword arguments, then 25 | decode the tag list using the class and confirm they are identical. 26 | """ 27 | if _debug: 28 | choice_endec._debug("test_endec %r %r", cls, kwargs) 29 | 30 | obj1 = cls(**kwargs) 31 | if _debug: 32 | choice_endec._debug(" - obj1: %r", obj1) 33 | 34 | tag_list = obj1.encode() 35 | if _debug: 36 | choice_endec._debug(" - tag_list: %r", tag_list) 37 | 38 | obj2 = cls.decode(tag_list) 39 | if _debug: 40 | choice_endec._debug(" - obj2: %r", obj2) 41 | 42 | assert obj1 == obj2 43 | 44 | 45 | # 46 | # Thing000 47 | # 48 | 49 | 50 | class Thing000(Choice): 51 | i: Integer 52 | j: Real 53 | 54 | 55 | @bacpypes_debugging 56 | class TestThing000(unittest.TestCase): 57 | def test_ctor(self): 58 | if _debug: 59 | TestThing000._debug("test_ctor") 60 | 61 | # no ctor parameters 62 | obj = Thing000() 63 | assert obj.i is None 64 | assert obj.j is None 65 | assert obj._choice is None 66 | 67 | # set the choice 68 | obj.i = 1 69 | assert obj.i == 1 70 | assert isinstance(obj.i, Integer) 71 | assert obj._choice == "i" 72 | assert obj.j is None 73 | 74 | # change the choice 75 | obj.j = 2.5 76 | assert obj.i is None 77 | assert isinstance(obj.j, Real) 78 | assert obj._choice == "j" 79 | assert obj.j == 2.5 80 | 81 | # clear the choice 82 | obj.j = None 83 | assert obj.i is None 84 | assert obj.j is None 85 | 86 | def test_ctor_kwarg(self): 87 | if _debug: 88 | TestThing000._debug("test_ctor_kwarg") 89 | 90 | # pick a parameter 91 | obj = Thing000(i=2) 92 | assert obj.i == 2 93 | assert obj._choice == "i" 94 | 95 | # multiple parameters an error 96 | with pytest.raises(RuntimeError): 97 | Thing000(i=2, j=3.5) 98 | 99 | def test_ctor_dict(self): 100 | if _debug: 101 | TestThing000._debug("test_ctor_dict") 102 | 103 | # pick a parameter 104 | obj = Thing000({"i": 3}) 105 | assert obj.i == 3 106 | assert obj._choice == "i" 107 | 108 | # wrong attribute 109 | with pytest.raises(AttributeError): 110 | Thing000({"k": 4}) 111 | 112 | # multiple parameters an error 113 | with pytest.raises(RuntimeError): 114 | Thing000({"i": 4, "j": 5.5}) 115 | 116 | def test_copy(self): 117 | if _debug: 118 | TestThing000._debug("test_copy") 119 | 120 | obj1 = Thing000(i=1) 121 | obj2 = Thing000(obj1) 122 | assert obj1 == obj2 123 | 124 | def test_endec_1(self): 125 | if _debug: 126 | TestThing000._debug("test_endec_1") 127 | 128 | # encode and decode 129 | choice_endec(Thing000, j=4.5) 130 | -------------------------------------------------------------------------------- /tests/test_constructed_data/test_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test ListOf 6 | ----------- 7 | """ 8 | 9 | import unittest 10 | 11 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 12 | from bacpypes3.primitivedata import Integer 13 | from bacpypes3.constructeddata import ListOf 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def list_of_endec(cls, *args, **kwargs): 22 | """ 23 | Encode an instance of the class using the keyword arguments, then 24 | decode the tag list using the class and confirm they are identical. 25 | """ 26 | if _debug: 27 | list_of_endec._debug("test_endec %r %r %r", cls, args, kwargs) 28 | 29 | obj1 = cls(*args, **kwargs) 30 | if _debug: 31 | list_of_endec._debug(" - obj1: %r", obj1) 32 | 33 | tag_list = obj1.encode() 34 | if _debug: 35 | list_of_endec._debug(" - tag_list: %r", tag_list) 36 | 37 | obj2 = cls.decode(tag_list) 38 | if _debug: 39 | list_of_endec._debug(" - obj2: %r", obj2) 40 | 41 | assert obj1 == obj2 42 | 43 | 44 | # 45 | # Thing000 46 | # 47 | 48 | 49 | Thing000 = ListOf(Integer) 50 | 51 | 52 | @bacpypes_debugging 53 | class TestThing000(unittest.TestCase): 54 | def test_ctor(self): 55 | if _debug: 56 | TestThing000._debug("test_ctor") 57 | 58 | # no ctor parameters 59 | obj = Thing000() 60 | assert len(obj) == 0 61 | 62 | # empty list 63 | obj = Thing000([]) 64 | assert len(obj) == 0 65 | 66 | # single element 67 | obj = Thing000([1]) 68 | assert len(obj) == 1 69 | assert isinstance(obj[0], Integer) 70 | 71 | def test_copy(self): 72 | if _debug: 73 | TestThing000._debug("test_copy") 74 | 75 | obj1 = Thing000([1, 2, 3]) 76 | obj2 = Thing000(obj1) 77 | assert obj1 == obj2 78 | 79 | def test_endec_1(self): 80 | if _debug: 81 | TestThing000._debug("test_endec_1") 82 | 83 | # encode and decode 84 | list_of_endec(Thing000, [4, 5]) 85 | 86 | 87 | # 88 | # Thing001 89 | # 90 | 91 | 92 | Thing001 = ListOf(Integer, _context=1) 93 | 94 | 95 | @bacpypes_debugging 96 | class TestThing001(unittest.TestCase): 97 | def test_endec_1(self): 98 | if _debug: 99 | TestThing001._debug("test_endec_1") 100 | 101 | # encode and decode 102 | list_of_endec(Thing001, [4, 5]) 103 | -------------------------------------------------------------------------------- /tests/test_pdu/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Test BACpypes PDU Module 5 | """ 6 | 7 | from . import test_address 8 | from . import test_pci 9 | from . import test_pdu 10 | -------------------------------------------------------------------------------- /tests/test_pdu/test_pci.py: -------------------------------------------------------------------------------- 1 | # placeholder 2 | -------------------------------------------------------------------------------- /tests/test_pdu/test_pdu.py: -------------------------------------------------------------------------------- 1 | # placeholder 2 | -------------------------------------------------------------------------------- /tests/test_primitive_data/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Test BACpypes Primitive Data Module 5 | """ 6 | 7 | from . import test_tag 8 | from . import test_null 9 | from . import test_boolean 10 | from . import test_unsigned 11 | from . import test_integer 12 | from . import test_real 13 | from . import test_double 14 | from . import test_octet_string 15 | from . import test_character_string 16 | from . import test_bit_string 17 | from . import test_enumerated 18 | from . import test_date 19 | from . import test_time 20 | from . import test_object_identifier 21 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_boolean.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Boolean 6 | ------------ 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Boolean 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def boolean_tag(x): 22 | """Convert a hex string to a null application tag.""" 23 | if _debug: 24 | boolean_tag._debug("boolean_tag %r", x) 25 | 26 | tag = Tag(TagClass.application, TagNumber.boolean, x, b"") 27 | if _debug: 28 | boolean_tag._debug(" - tag: %r", tag) 29 | 30 | return tag 31 | 32 | 33 | @bacpypes_debugging 34 | def boolean_encode(obj): 35 | """Encode a Boolean object into a tag.""" 36 | if _debug: 37 | boolean_encode._debug("boolean_encode %r", obj) 38 | 39 | tag = obj.encode()[0] 40 | if _debug: 41 | boolean_encode._debug(" - tag: %r", tag) 42 | 43 | return tag 44 | 45 | 46 | @bacpypes_debugging 47 | def boolean_decode(tag): 48 | """Decode a null from a tag.""" 49 | if _debug: 50 | boolean_decode._debug("boolean_decode %r", tag) 51 | 52 | obj = Boolean.decode(TagList([tag])) 53 | if _debug: 54 | boolean_decode._debug(" - obj: %r", obj) 55 | 56 | return obj 57 | 58 | 59 | @bacpypes_debugging 60 | def boolean_endec(v, x): 61 | """Pass the value to Boolean, construct a tag from the hex string, 62 | and compare results of encoding and decoding.""" 63 | if _debug: 64 | boolean_endec._debug("boolean_endec %r %r", v, x) 65 | 66 | obj = Boolean(v) 67 | if _debug: 68 | boolean_endec._debug(" - obj: %r", obj) 69 | 70 | tag = boolean_tag(x) 71 | if _debug: 72 | boolean_endec._debug(" - tag: %r, %r", tag, tag.tag_data) 73 | 74 | assert boolean_encode(obj) == tag 75 | assert boolean_decode(tag) == obj 76 | 77 | 78 | @bacpypes_debugging 79 | class TestBoolean(unittest.TestCase): 80 | def test_boolean(self): 81 | if _debug: 82 | TestBoolean._debug("test_boolean") 83 | 84 | # booleans cannot extend bool, so they are int values 1 or 0 85 | obj = Boolean(0) 86 | assert obj == 0 87 | 88 | with pytest.raises(ValueError): 89 | Boolean("some string") 90 | with pytest.raises(TypeError): 91 | Boolean(1.0) 92 | 93 | def test_boolean_value(self): 94 | if _debug: 95 | TestBoolean._debug("test_boolean_value") 96 | 97 | obj = Boolean(True) 98 | assert obj == 1 99 | 100 | def test_boolean_copy(self): 101 | if _debug: 102 | TestBoolean._debug("test_boolean_copy") 103 | 104 | obj1 = Boolean(0) 105 | obj2 = Boolean(obj1) 106 | assert obj1 == obj2 107 | 108 | def test_boolean_endec(self): 109 | if _debug: 110 | TestBoolean._debug("test_boolean_endec") 111 | 112 | boolean_endec(False, 0) 113 | boolean_endec(True, 1) 114 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_double.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Double 6 | ----------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Double 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def double_tag(x): 22 | """Convert a hex string to a real application tag.""" 23 | if _debug: 24 | double_tag._debug("double_tag %r", x) 25 | 26 | b = xtob(x) 27 | tag = Tag(TagClass.application, TagNumber.double, len(b), b) 28 | if _debug: 29 | double_tag._debug(" - tag: %r", tag) 30 | 31 | return tag 32 | 33 | 34 | @bacpypes_debugging 35 | def double_encode(obj): 36 | """Encode a Double object into a tag.""" 37 | if _debug: 38 | double_encode._debug("double_encode %r", obj) 39 | 40 | tag = obj.encode()[0] 41 | if _debug: 42 | double_encode._debug(" - tag: %r", tag) 43 | 44 | return tag 45 | 46 | 47 | @bacpypes_debugging 48 | def double_decode(tag): 49 | """Decode a Double from a tag.""" 50 | if _debug: 51 | double_decode._debug("double_decode %r", tag) 52 | 53 | obj = Double.decode(TagList([tag])) 54 | if _debug: 55 | double_decode._debug(" - obj: %r", obj) 56 | 57 | return obj 58 | 59 | 60 | @bacpypes_debugging 61 | def double_endec(v, x): 62 | """Pass the value to Double, construct a tag from the hex string, 63 | and compare results of encoding and decoding.""" 64 | if _debug: 65 | double_endec._debug("double_endec %r %r", v, x) 66 | 67 | obj = Double(v) 68 | if _debug: 69 | double_endec._debug(" - obj: %r", obj) 70 | 71 | tag = double_tag(x) 72 | if _debug: 73 | double_endec._debug(" - tag: %r", tag) 74 | tag.debug_contents() 75 | 76 | assert double_encode(obj) == tag 77 | assert double_decode(tag) == obj 78 | 79 | 80 | @bacpypes_debugging 81 | class TestDouble(unittest.TestCase): 82 | def test_double(self): 83 | if _debug: 84 | TestDouble._debug("test_double") 85 | 86 | obj = Double(0.0) 87 | assert obj == 0.0 88 | 89 | obj = Double(1.5) 90 | assert obj == 1.5 91 | 92 | obj = Double("2.5") 93 | assert obj == 2.5 94 | 95 | with pytest.raises(ValueError): 96 | Double("some string") 97 | 98 | def test_double_copy(self): 99 | if _debug: 100 | TestDouble._debug("test_double_copy") 101 | 102 | obj1 = Double(3.4) 103 | obj2 = Double(obj1) 104 | assert obj1 == obj2 105 | 106 | def test_double_endec(self): 107 | if _debug: 108 | TestDouble._debug("test_double_endec") 109 | 110 | double_endec(0, "0000000000000000") 111 | double_endec(1, "3ff0000000000000") 112 | double_endec(-1, "bff0000000000000") 113 | 114 | double_endec(73.5, "4052600000000000") 115 | 116 | inf = float("inf") 117 | double_endec(inf, "7ff0000000000000") 118 | double_endec(-inf, "fff0000000000000") 119 | 120 | # nan not the same nan 121 | # nan = float('nan') 122 | # double_endec(nan, '7ff8000000000000') 123 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_integer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Integer 6 | ------------ 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Integer 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def integer_tag(x): 22 | """Convert a hex string to an integer application tag.""" 23 | if _debug: 24 | integer_tag._debug("integer_tag %r", x) 25 | 26 | b = xtob(x) 27 | tag = Tag(TagClass.application, TagNumber.integer, len(b), b) 28 | if _debug: 29 | integer_tag._debug(" - tag: %r", tag) 30 | 31 | return tag 32 | 33 | 34 | @bacpypes_debugging 35 | def integer_encode(obj): 36 | """Encode a Integer object into a tag.""" 37 | if _debug: 38 | integer_encode._debug("integer_encode %r", obj) 39 | 40 | tag = obj.encode()[0] 41 | if _debug: 42 | integer_encode._debug(" - tag: %r", tag) 43 | 44 | return tag 45 | 46 | 47 | @bacpypes_debugging 48 | def integer_decode(tag): 49 | """Decode a null from a tag.""" 50 | if _debug: 51 | integer_decode._debug("integer_decode %r", tag) 52 | 53 | obj = Integer.decode(TagList([tag])) 54 | if _debug: 55 | integer_decode._debug(" - obj: %r", obj) 56 | 57 | return obj 58 | 59 | 60 | @bacpypes_debugging 61 | def integer_endec(v, x): 62 | """Pass the value to Integer, construct a tag from the hex string, 63 | and compare results of encoding and decoding.""" 64 | if _debug: 65 | integer_endec._debug("integer_endec %r %r", v, x) 66 | 67 | obj = Integer(v) 68 | if _debug: 69 | integer_endec._debug(" - obj: %r", obj) 70 | 71 | tag = integer_tag(x) 72 | if _debug: 73 | integer_endec._debug(" - tag: %r", tag) 74 | 75 | assert integer_encode(obj) == tag 76 | assert integer_decode(tag) == obj 77 | 78 | 79 | @bacpypes_debugging 80 | class TestInteger(unittest.TestCase): 81 | def test_integer(self): 82 | if _debug: 83 | TestInteger._debug("test_integer") 84 | 85 | obj = Integer(0) 86 | assert obj == 0 87 | 88 | with pytest.raises(ValueError): 89 | Integer("some string") 90 | with pytest.raises(TypeError): 91 | Integer(1.0) 92 | 93 | def test_integer_value(self): 94 | if _debug: 95 | TestInteger._debug("test_integer_value") 96 | 97 | obj = Integer(1) 98 | assert obj == 1 99 | 100 | def test_integer_copy(self): 101 | if _debug: 102 | TestInteger._debug("test_integer_copy") 103 | 104 | obj1 = Integer(2) 105 | obj2 = Integer(obj1) 106 | assert obj1 == obj2 107 | 108 | def test_integer_endec(self): 109 | if _debug: 110 | TestInteger._debug("test_integer_endec") 111 | 112 | integer_endec(0, "00") 113 | integer_endec(1, "01") 114 | integer_endec(127, "7F") 115 | integer_endec(-1, "FF") 116 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_null.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Null 6 | --------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Null 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def null_tag(x): 22 | """Convert a hex string to a null application tag.""" 23 | if _debug: 24 | null_tag._debug("null_tag %r", x) 25 | 26 | b = xtob(x) 27 | tag = Tag(TagClass.application, TagNumber.null, len(b), b) 28 | if _debug: 29 | null_tag._debug(" - tag: %r", tag) 30 | 31 | return tag 32 | 33 | 34 | @bacpypes_debugging 35 | def null_encode(obj): 36 | """Encode a Null object into a tag.""" 37 | if _debug: 38 | null_encode._debug("null_encode %r", obj) 39 | 40 | tag = obj.encode()[0] 41 | if _debug: 42 | null_encode._debug(" - tag: %r", tag) 43 | 44 | return tag 45 | 46 | 47 | @bacpypes_debugging 48 | def null_decode(tag): 49 | """Decode a null from a tag.""" 50 | if _debug: 51 | null_decode._debug("null_decode %r", tag) 52 | 53 | obj = Null.decode(TagList([tag])) 54 | if _debug: 55 | null_decode._debug(" - obj: %r", obj) 56 | 57 | return obj 58 | 59 | 60 | @bacpypes_debugging 61 | def null_endec(v, x): 62 | """Pass the value to Null, construct a tag from the hex string, 63 | and compare results of encoding and decoding.""" 64 | if _debug: 65 | null_endec._debug("null_endec %r %r", v, x) 66 | 67 | obj = Null(v) 68 | if _debug: 69 | null_endec._debug(" - obj: %r", obj) 70 | 71 | tag = null_tag(x) 72 | if _debug: 73 | null_endec._debug(" - tag: %r, %r", tag, tag.tag_data) 74 | 75 | assert null_encode(obj) == tag 76 | assert null_decode(tag) == obj 77 | 78 | 79 | @bacpypes_debugging 80 | class TestNull(unittest.TestCase): 81 | def test_null(self): 82 | if _debug: 83 | TestNull._debug("test_null") 84 | 85 | obj = Null(()) 86 | assert obj == () 87 | 88 | obj = Null([]) 89 | assert obj == () 90 | 91 | obj = Null("null") 92 | assert obj == () 93 | 94 | with pytest.raises(ValueError): 95 | Null("some string") 96 | with pytest.raises(TypeError): 97 | Null(1.0) 98 | 99 | def test_null_copy(self): 100 | if _debug: 101 | TestNull._debug("test_null_copy") 102 | 103 | obj1 = Null(()) 104 | obj2 = Null(obj1) 105 | assert obj2 == () 106 | 107 | def test_null_endec(self): 108 | if _debug: 109 | TestNull._debug("test_null_endec") 110 | 111 | null_endec((), "") 112 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_octet_string.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test OctetString 6 | ---------------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, OctetString 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def octet_string_tag(x): 22 | """Convert a hex string to an octet string application tag.""" 23 | if _debug: 24 | octet_string_tag._debug("octet_string_tag %r", x) 25 | 26 | b = xtob(x) 27 | tag = Tag(TagClass.application, TagNumber.octetString, len(b), b) 28 | if _debug: 29 | octet_string_tag._debug(" - tag: %r", tag) 30 | 31 | return tag 32 | 33 | 34 | @bacpypes_debugging 35 | def octet_string_encode(obj): 36 | """Encode a OctetString object into a tag.""" 37 | if _debug: 38 | octet_string_encode._debug("octet_string_encode %r", obj) 39 | 40 | tag = obj.encode()[0] 41 | if _debug: 42 | octet_string_encode._debug(" - tag: %r", tag) 43 | 44 | return tag 45 | 46 | 47 | @bacpypes_debugging 48 | def octet_string_decode(tag): 49 | """Decode a OctetString from a tag.""" 50 | if _debug: 51 | octet_string_decode._debug("octet_string_decode %r", tag) 52 | 53 | obj = OctetString.decode(TagList([tag])) 54 | if _debug: 55 | octet_string_decode._debug(" - obj: %r", obj) 56 | 57 | return obj 58 | 59 | 60 | @bacpypes_debugging 61 | def octet_string_endec(x): 62 | """Pass the value to OctetString, construct a tag from the hex string, 63 | and compare results of encoding and decoding.""" 64 | if _debug: 65 | octet_string_endec._debug("octet_string_endec %r", x) 66 | 67 | b = xtob(x) 68 | obj = OctetString(b) 69 | if _debug: 70 | octet_string_endec._debug(" - obj: %r", obj) 71 | 72 | tag = octet_string_tag(x) 73 | if _debug: 74 | octet_string_endec._debug(" - tag: %r", tag) 75 | tag.debug_contents() 76 | 77 | assert octet_string_encode(obj) == tag 78 | assert octet_string_decode(tag) == obj 79 | 80 | 81 | @bacpypes_debugging 82 | class TestOctetString(unittest.TestCase): 83 | def test_octet_string(self): 84 | if _debug: 85 | TestOctetString._debug("test_octet_string") 86 | 87 | obj = OctetString(b"") 88 | assert obj == b"" 89 | 90 | with pytest.raises(TypeError): 91 | OctetString(1) 92 | with pytest.raises(TypeError): 93 | OctetString("some string") 94 | 95 | def test_octet_string_copy(self): 96 | if _debug: 97 | TestOctetString._debug("test_octet_string_copy") 98 | 99 | obj1 = OctetString(b"123") 100 | obj2 = OctetString(obj1) 101 | assert obj1 == obj2 102 | 103 | def test_octet_string_endec(self): 104 | if _debug: 105 | TestOctetString._debug("test_octet_string_endec") 106 | 107 | octet_string_endec("") 108 | octet_string_endec("01") 109 | octet_string_endec("0102") 110 | octet_string_endec("010203") 111 | octet_string_endec("01020304") 112 | -------------------------------------------------------------------------------- /tests/test_primitive_data/test_real.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Test Real 6 | --------- 7 | """ 8 | 9 | import unittest 10 | import pytest 11 | 12 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob 13 | from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Real 14 | 15 | # some debugging 16 | _debug = 0 17 | _log = ModuleLogger(globals()) 18 | 19 | 20 | @bacpypes_debugging 21 | def real_tag(x): 22 | """Convert a hex string to a real application tag.""" 23 | if _debug: 24 | real_tag._debug("real_tag %r", x) 25 | 26 | b = xtob(x) 27 | tag = Tag(TagClass.application, TagNumber.real, len(b), b) 28 | if _debug: 29 | real_tag._debug(" - tag: %r", tag) 30 | 31 | return tag 32 | 33 | 34 | @bacpypes_debugging 35 | def real_encode(obj): 36 | """Encode a Real object into a tag.""" 37 | if _debug: 38 | real_encode._debug("real_encode %r", obj) 39 | 40 | tag = obj.encode()[0] 41 | if _debug: 42 | real_encode._debug(" - tag: %r", tag) 43 | 44 | return tag 45 | 46 | 47 | @bacpypes_debugging 48 | def real_decode(tag): 49 | """Decode a Real from a tag.""" 50 | if _debug: 51 | real_decode._debug("real_decode %r", tag) 52 | 53 | obj = Real.decode(TagList([tag])) 54 | if _debug: 55 | real_decode._debug(" - obj: %r", obj) 56 | 57 | return obj 58 | 59 | 60 | @bacpypes_debugging 61 | def real_endec(v, x): 62 | """Pass the value to Real, construct a tag from the hex string, 63 | and compare results of encoding and decoding.""" 64 | if _debug: 65 | real_endec._debug("real_endec %r %r", v, x) 66 | 67 | obj = Real(v) 68 | if _debug: 69 | real_endec._debug(" - obj: %r", obj) 70 | 71 | tag = real_tag(x) 72 | if _debug: 73 | real_endec._debug(" - tag: %r", tag) 74 | tag.debug_contents() 75 | 76 | assert real_encode(obj) == tag 77 | assert real_decode(tag) == obj 78 | 79 | 80 | @bacpypes_debugging 81 | class TestReal(unittest.TestCase): 82 | def test_unsigned(self): 83 | if _debug: 84 | TestReal._debug("test_unsigned") 85 | 86 | obj = Real(0.0) 87 | assert obj == 0 88 | 89 | obj = Real(1.5) 90 | assert obj == 1.5 91 | 92 | obj = Real("2.5") 93 | assert obj == 2.5 94 | 95 | with pytest.raises(ValueError): 96 | Real("some string") 97 | 98 | def test_real_value(self): 99 | if _debug: 100 | TestReal._debug("test_real_value") 101 | 102 | obj = Real(1) 103 | assert obj == 1 104 | 105 | def test_real_copy(self): 106 | if _debug: 107 | TestReal._debug("test_real_copy") 108 | 109 | obj1 = Real(4.6) 110 | obj2 = Real(obj1) 111 | assert obj1 == obj2 112 | 113 | def test_real_endec(self): 114 | if _debug: 115 | TestReal._debug("test_real_endec") 116 | 117 | real_endec(0, "00000000") 118 | real_endec(1, "3f800000") 119 | real_endec(-1, "bf800000") 120 | 121 | real_endec(73.5, "42930000") 122 | 123 | inf = float("inf") 124 | real_endec(inf, "7f800000") 125 | real_endec(-inf, "ff800000") 126 | 127 | # nan not the same nan 128 | # nan = float('nan') 129 | # real_endec(nan, '7fc00000') 130 | -------------------------------------------------------------------------------- /tests/test_utilities/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Test Utilities 5 | -------------- 6 | 7 | This module tests the test utilities. 8 | """ 9 | 10 | from . import test_state_machine 11 | -------------------------------------------------------------------------------- /tests/test_vlan/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Test VLAN Networking 5 | -------------------- 6 | 7 | This module tests the VLAN networking. 8 | """ 9 | 10 | from . import test_network 11 | from . import test_ipv4_network 12 | from . import test_ipv4_router 13 | -------------------------------------------------------------------------------- /tests/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | BACpypes Testing Utilities 5 | -------------------------- 6 | """ 7 | 8 | import os 9 | 10 | from typing import Callable, cast 11 | 12 | from bacpypes3.settings import os_settings 13 | from bacpypes3.debugging import bacpypes_debugging, ModuleLogger 14 | from bacpypes3.argparse import ArgumentParser 15 | 16 | # some debugging 17 | _debug = 0 18 | _log = ModuleLogger(globals()) 19 | 20 | # defaults for testing 21 | BACPYPES_TEST = "" 22 | BACPYPES_TEST_OPTION = "" 23 | 24 | # parsed test options 25 | test_options = None 26 | 27 | 28 | @bacpypes_debugging 29 | def setup_package() -> None: 30 | fn_debug = cast(Callable[..., None], setup_package._debug) # type: ignore[attr-defined] 31 | global test_options 32 | 33 | # get the os settings 34 | os_settings() 35 | 36 | # create an argument parser 37 | parser = ArgumentParser(description=__doc__) 38 | 39 | # add an option 40 | parser.add_argument( 41 | "--option", 42 | help="this is an option", 43 | default=os.getenv("BACPYPES_TEST_OPTION") or BACPYPES_TEST_OPTION, 44 | ) 45 | 46 | # get the debugging args and parse them 47 | arg_str = os.getenv("BACPYPES_TEST") or BACPYPES_TEST 48 | test_options = parser.parse_args(arg_str.split()) 49 | 50 | if _debug: 51 | fn_debug("setup_package") 52 | fn_debug(" - test_options: %r", test_options) 53 | 54 | 55 | @bacpypes_debugging 56 | def teardown_package() -> None: 57 | fn_debug = cast(Callable[..., None], teardown_package._debug) # type: ignore[attr-defined] 58 | 59 | if _debug: 60 | fn_debug("teardown_package") 61 | --------------------------------------------------------------------------------