Closed kheldaroz402 closed 3 weeks ago
Your suspicions are correct! The application instance you created ate the device=...
keyword argument without giving you an error that it was being ignored. Your application can simply derive from Application
and WhoIsIAmServices
is supported by default.
class IOM_Application(Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.db = db
# self.device = device --- application has self.device_object
self.object_values = {} # Store previous values of objects
# Create tasks for updating values and periodically checking the database
self.update_task = asyncio.create_task(self.update_values())
self.db_check_task = asyncio.create_task(self.check_database_for_changes())
In addition to creating a DeviceObject
, you'll also create a NetworkPortObject
(import from bacpypes3.local.networkport
) that contains the properties you want to use, if you pass in a address like Address("12.34.56.78/24")
a lot of work is already done. Then create your IOM_Application passing in the two objects (like the end of that method):
iom_app = IOM_Application.from_object_list([device_object, network_port_object])
thanks for the feedback Joel
async def main():
db = await aiosqlite.connect('/var/www/html/database/WebAI_IOM_Program.db')
try:
# Fetch the IP address and netmask for eth0
ip_address, netmask = get_eth0_ip_and_netmask()
if ip_address:
cidr = ip_prefix_by_netmask(netmask)
logging.info(f"{color.CYAN}IP Address: {ip_address}/{cidr}{color.END}")
else:
raise Exception("Failed to retrieve IP address and netmask.")
# Fetch BACnet device config from the database
async def get_bacnet_device_config(db):
config = {}
async with db.execute('SELECT parameter, value FROM config') as cursor:
async for row in cursor:
config[row[0]] = row[1]
return config
config = await get_bacnet_device_config(db)
model_name = config.get("model-name", "Unknown Model")
device_id = int(config.get("object-identifier", 470001))
# Create DeviceObject and NetworkPortObject
device_object = DeviceObject(
objectIdentifier=("device", device_id),
objectName=config.get('object-name', 'BACnet Device'),
maxApduLengthAccepted=int(config.get('max-apdu-length-accepted', 1024)),
segmentationSupported=config.get('segmentation-supported', 'noSegmentation'),
vendorIdentifier=int(config.get('vendor-identifier', 47)),
modelName=model_name,
location=config.get('location', 'Unknown Location')
)
network_port_object = NetworkPortObject(
objectIdentifier=("networkPort", 1),
objectName="BACnet Network Port",
networkType=Unsigned(0), # BACnet/IP
ipSubnetMask=OctetString(ip_to_bytes(netmask)),
ipDefaultGateway=OctetString(ip_to_bytes(config.get('gateway', '192.168.3.1'))),
ipAddress=OctetString(ip_to_bytes(ip_address)),
linkSpeed=Unsigned(100)
)
# Create IOM_Application instance and add objects
iom_app = IOM_Application.from_object_list([device_object, network_port_object])
logging.info(f"{color.CYAN}Sending I-Am message...{color.END}")
iom_app.i_am()
logging.info(f"{color.GREEN}I-Am message sent successfully!{color.END}")
# Start background tasks after application is fully set up
iom_app.start_background_tasks()
# Keep the application running
await asyncio.Event().wait()
except KeyboardInterrupt:
logging.info(f"{color.ORANGE}Keyboard interrupt{color.END}")
except Exception as e:
logging.error(f"{color.RED}An error occurred: {e}{color.END}")
finally:
if db:
await db.close()
logging.info(f"{color.ORANGE}Database connection closed{color.END}")
I get this error now
2024-09-25 13:29:06,135 - INFO - Starting...
2024-09-25 13:29:06,144 - INFO - IP Address: 192.168.3.160/24
2024-09-25 13:29:06,197 - INFO - Sending I-Am message...
2024-09-25 13:29:06,200 - INFO - I-Am message sent successfully!
2024-09-25 13:29:06,206 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-4' coro=<ApplicationServiceElement.request() done, defined at /home/pico/.local/lib/python3.10/site-packages/bacpypes3/comm.py:132> exception=ConfigurationError('no adapters')>
Traceback (most recent call last):
File "/home/pico/.local/lib/python3.10/site-packages/bacpypes3/comm.py", line 135, in request
await self.elementService.sap_indication(*args)
File "/home/pico/.local/lib/python3.10/site-packages/bacpypes3/appservice.py", line 1764, in sap_indication
await self.request(apdu)
File "/home/pico/.local/lib/python3.10/site-packages/bacpypes3/appservice.py", line 1572, in request
await Client.request(self, pdu)
File "/home/pico/.local/lib/python3.10/site-packages/bacpypes3/comm.py", line 59, in request
await self.clientPeer.indication(pdu)
File "/home/pico/.local/lib/python3.10/site-packages/bacpypes3/netservice.py", line 661, in indication
raise ConfigurationError("no adapters")
bacpypes3.errors.ConfigurationError: no adapters
2024-09-25 13:29:07,228 - INFO - Checking database for changes...
^C2024-09-25 13:29:12,938 - INFO - Database connection closed
2024-09-25 13:29:12,943 - INFO - Keyboard interrupt
2024-09-25 13:29:13,067 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-5' coro=<IOM_Application.update_values() done, defined at /home/pico/./iom_device.py:68> exception=AttributeError("'NoneType' object has no attribute 'execute'")>
Traceback (most recent call last):
File "/home/pico/./iom_device.py", line 73, in update_values
async with self.db.execute('SELECT BACnetID, Name, ObjectType, PresentValue, InactiveText, ActiveText, statusFlags, COV, Description FROM objects') as cursor:
AttributeError: 'NoneType' object has no attribute 'execute'
2024-09-25 13:29:13,073 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-6' coro=<IOM_Application.check_database_for_changes() done, defined at /home/pico/./iom_device.py:175> exception=AttributeError("'NoneType' object has no attribute 'execute'")>
Traceback (most recent call last):
File "/home/pico/./iom_device.py", line 184, in check_database_for_changes
async with self.db.execute('SELECT BACnetID, ObjectType, PresentValue FROM objects') as cursor:
AttributeError: 'NoneType' object has no attribute 'execute'
That "no adapters" is a clue that your network port object isn't correct. If you know that you're always going to be referencing eth0
and you have ifaddr
installed, just pass in the interface name like this:
>>> from bacpypes3.local.networkport import NetworkPortObject
>>> npo = NetworkPortObject("eth0", objectIdentifier="network-port,1", objectName="Network Port 1")
>>> npo.debug_contents()
objectIdentifier = <property object at 0x7250952eab10>
objectName = <property object at 0x7250952eaac0>
objectType = <ObjectType: network-port>
propertyList = <property object at 0x7250952eab60>
statusFlags = <property object at 0x7250952eabb0>
reliability = <Reliability: no-fault-detected>
outOfService = 0
networkType = <NetworkType: ipv4>
protocolLevel = <ProtocolLevel: bacnet-application>
networkNumber = 0
networkNumberQuality = <NetworkNumberQuality: unknown>
changesPending = 0
macAddress = b'\n\x00\x01Z\xba\xc0'
linkSpeed = 0.0
bacnetIPMode = <IPMode: normal>
ipAddress = 10.0.1.90
bacnetIPUDPPort = 47808
ipSubnetMask = 255.255.255.0
Note that the funky <property object ...>
things are thunks for attributes/properties with special behavior.
yay :) got it working - thanks Joel
#!/usr/bin/env python3
import asyncio
import aiosqlite
import logging
import socket
import ipaddress
import psutil
import ast
import ifaddr
import traceback
from bacpypes3.comm import ApplicationServiceElement, bind
from bacpypes3.app import Application, WhoIsIAmServices
from bacpypes3.local.analog import AnalogValueObject, AnalogInputObject, AnalogOutputObject
from bacpypes3.local.binary import BinaryValueObject, BinaryInputObject, BinaryOutputObject
from bacpypes3.local.device import DeviceObject
from bacpypes3.local.networkport import NetworkPortObject
from bacpypes3.primitivedata import Unsigned, OctetString
from bacpypes3.basetypes import PriorityValue, BinaryPV, Real
from bacpypes3.pdu import Address
from bacpypes3.object import ObjectType
from Functions_Other import clear, color
# Mapping of object type strings to ObjectType enums
object_type_mapping = {
'binaryValue': ObjectType.binaryValue,
'binaryInput': ObjectType.binaryInput,
'binaryOutput': ObjectType.binaryOutput,
'analogValue': ObjectType.analogValue,
'analogInput': ObjectType.analogInput, # Ensure analogInput is included
'analogOutput': ObjectType.analogOutput,
}
# Set up logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Clear screen and start message
clear()
logging.info(f"{color.CYAN}Starting...{color.END}")
CHECK_INTERVAL = 1.0 # Interval to check the database for changes
def get_interface_info(interface_name):
adapters = ifaddr.get_adapters()
for adapter in adapters:
if adapter.nice_name == interface_name:
for ip in adapter.ips:
if isinstance(ip.ip, str): # IPv4 address
return ip.ip, ip.network_prefix
raise Exception(f"No such interface: {interface_name}")
def ip_to_bytes(ip_string):
"""Convert a dotted decimal IP string into bytes."""
return socket.inet_aton(ip_string)
def get_eth0_ip_and_netmask():
for iface, addrs in psutil.net_if_addrs().items():
if iface == "eth0": # Specifically look for eth0
for addr in addrs:
if addr.family == socket.AF_INET: # IPv4 address
return addr.address, addr.netmask
return None, None
def ip_prefix_by_netmask(netmask):
return ipaddress.IPv4Network(f"0.0.0.0/{netmask}").prefixlen
# Function to get BACnet device configuration from the database
async def get_bacnet_device_config(db):
config = {}
async with db.execute('SELECT parameter, value FROM config') as cursor:
async for row in cursor:
config[row[0]] = row[1]
return config
class IOM_Application(Application):
def __init__(self, db=None, **kwargs):
super().__init__(**kwargs)
self.db = db
self.object_values = {}
self.lock = asyncio.Lock()
self.update_task = None
self.db_check_task = None
def start_background_tasks(self):
self.update_task = asyncio.create_task(self.update_values())
self.db_check_task = asyncio.create_task(self.check_database_for_changes())
async def update_values(self):
while True:
try:
async with self.lock:
async with self.db.execute('''
SELECT ObjectID, Name, TRIM(ObjectType) as ObjectType, PresentValue, InactiveText, ActiveText, statusFlags, COV, Description, Priority
FROM objects
''') as cursor:
cursor.row_factory = aiosqlite.Row
row_count = 0
async for row in cursor:
row_count += 1
object_id = row['ObjectID']
name = row['Name']
object_type_str = row['ObjectType']
present_value = row['PresentValue']
inactiveText = row['InactiveText']
activeText = row['ActiveText']
covIncrement = row['COV']
description = row['Description']
priority = row['Priority']
# Parse statusFlags
try:
statusFlags = ast.literal_eval(row['statusFlags']) if row['statusFlags'] else [0, 0, 0, 0]
except Exception as e:
logging.error(f"Error parsing statusFlags for object {name}: {e}")
continue
# Map object_type_str to ObjectType enum
object_type_enum = object_type_mapping.get(object_type_str)
if not object_type_enum:
logging.warning(f"{color.ORANGE}Unsupported object type: '{object_type_str}'{color.END}")
continue
# Define unique key
unique_key = (object_id, object_type_enum)
# Check if object exists
bacnet_object = self.get_object_id((object_type_enum, object_id))
if bacnet_object is None:
logging.info(f"{color.YELLOW}Creating BACnet Object: {name} Type: {object_type_str}{color.END}")
try:
if object_type_enum == ObjectType.binaryValue:
present_value_binary = 'active' if present_value == 1 else 'inactive'
bacnet_object = BinaryValueObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
inactiveText=inactiveText,
activeText=activeText,
presentValue=present_value_binary,
description=description
)
elif object_type_enum == ObjectType.binaryOutput:
present_value_binary = 'active' if present_value == 1 else 'inactive'
bacnet_object = BinaryOutputObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
inactiveText=inactiveText,
activeText=activeText,
presentValue=present_value_binary,
description=description
)
elif object_type_enum == ObjectType.binaryInput:
present_value_binary = 'active' if present_value == 1 else 'inactive'
bacnet_object = BinaryOutputObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
inactiveText=inactiveText,
activeText=activeText,
presentValue=present_value_binary,
description=description
)
elif object_type_enum == ObjectType.analogValue:
bacnet_object = AnalogValueObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
covIncrement=covIncrement,
presentValue=float(present_value),
description=description
)
elif object_type_enum == ObjectType.analogInput:
bacnet_object = AnalogInputObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
covIncrement=covIncrement,
presentValue=float(present_value),
description=description
)
elif object_type_enum == ObjectType.analogOutput:
bacnet_object = AnalogOutputObject(
objectIdentifier=(object_type_enum, object_id),
objectName=name,
statusFlags=statusFlags,
covIncrement=covIncrement,
presentValue=float(present_value),
description=description
)
else:
logging.warning(f"{color.ORANGE}Unsupported object type: {object_type_str}{color.END}")
continue
self.add_object(bacnet_object)
self.device_object.objectList.append(bacnet_object.objectIdentifier)
logging.info(f"{color.GREEN}Successfully created object: {bacnet_object.objectName} with ObjectID: {object_id}{color.END}")
except Exception as e:
logging.error(f"Error creating object {name}: {e}")
continue
if unique_key not in self.object_values or self.object_values[unique_key] != present_value:
self.object_values[unique_key] = present_value
bacnet_object.presentValue = present_value
logging.info(f"{color.BLUE}Updated {name} to {present_value}{color.END}")
except Exception as e:
logging.error(f"{color.RED}Error updating values: {e}{color.END}")
await asyncio.sleep(CHECK_INTERVAL)
async def check_database_for_changes(self):
while True:
logging.info(f"{color.CYAN}Checking database for changes...{color.END}")
await asyncio.sleep(CHECK_INTERVAL)
async def main():
db = await aiosqlite.connect('/var/www/html/database/WebAI_IOM_Program.db')
try:
eth0_ip, eth0_prefix = get_interface_info("eth0")
netmask = get_eth0_ip_and_netmask()[1]
logging.info(f"IP Address: {eth0_ip}/{ip_prefix_by_netmask(netmask)}")
config = await get_bacnet_device_config(db)
device_id = int(config.get("object-identifier", 470001))
model_name = config.get("model-name", "Unknown Model")
device_object = DeviceObject(
objectIdentifier=("device", device_id),
objectName=config.get('object-name', 'BACnet Device'),
maxApduLengthAccepted=int(config.get('max-apdu-length-accepted', 1024)),
segmentationSupported=config.get('segmentation-supported', 'noSegmentation'),
vendorIdentifier=int(config.get('vendor-identifier', 47)),
modelName=model_name,
location=config.get('location', 'Unknown Location'),
vendorName = config.get('vendor-name', 'Sphere'),
firmwareRevision = config.get('firmware-revision', '0.1'),
serialNumber = config.get('application-software-version', '1.0'),
systemStatus = config.get('system-status', 'operational'),
)
network_port_object = NetworkPortObject(
"eth0",
objectIdentifier=("networkPort", 1),
objectName="Network Port 1",
)
network_port_object.debug_contents()
# Create the application using from_object_list
iom_app = IOM_Application.from_object_list([device_object, network_port_object])
iom_app.db = db
logging.info(f"Sending I-Am message...")
iom_app.i_am()
iom_app.start_background_tasks()
await asyncio.Event().wait()
except Exception as e:
logging.error(f"An error occurred: {e}")
finally:
if db:
await db.close()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Keyboard interrupt")
so when i run mini_device_revisted.py i can find the mini device in YABE,
when i try and do it as per below
`
it all looks fine as far as my logging goes - having no issues getting the data from SQLite
2024-09-23 13:43:43,302 - INFO - Starting... 2024-09-23 13:43:43,343 - INFO - Starting Device: Model Name: WebAI DO8, IP Address: 192.168.3.160 2024-09-23 13:43:43,345 - INFO - Device ID: 470160 2024-09-23 13:43:43,365 - INFO - Device Object Details: 2024-09-23 13:43:43,367 - INFO - Object Identifier: device,470160 2024-09-23 13:43:43,369 - INFO - Object Name: DO8-1 2024-09-23 13:43:43,371 - INFO - Max APDU Length Accepted: 1024 2024-09-23 13:43:43,373 - INFO - Segmentation Supported: segmented-both 2024-09-23 13:43:43,375 - INFO - Vendor Identifier: 47 2024-09-23 13:43:43,378 - INFO - Vendor Name: Sphere Systems 2024-09-23 13:43:43,380 - INFO - Model Name: WebAI DO8 2024-09-23 13:43:43,382 - INFO - Firmware Revision: 1.0 2024-09-23 13:43:43,384 - INFO - Serial Number: 24.04.01.01.01 2024-09-23 13:43:43,386 - INFO - System Status: operational 2024-09-23 13:43:43,388 - INFO - Location: So you can find IOM 2024-09-23 13:43:43,390 - INFO - IOM_Application instantiated 2024-09-23 13:43:43,447 - INFO - Created BACnet Object: DO.1, Digital Output 1 Type: BinaryOutput 2024-09-23 13:43:43,499 - INFO - Created BACnet Object: DO.2, Digital Output 2 Type: BinaryOutput 2024-09-23 13:43:43,550 - INFO - Created BACnet Object: DO.3, Digital Output 3 Type: BinaryOutput 2024-09-23 13:43:43,600 - INFO - Created BACnet Object: DO.4, Digital Output 4 Type: BinaryOutput 2024-09-23 13:43:43,651 - INFO - Created BACnet Object: DO.5, Digital Output 5 Type: BinaryOutput 2024-09-23 13:43:43,702 - INFO - Created BACnet Object: DO.6, Digital Output 6 Type: BinaryOutput 2024-09-23 13:43:43,753 - INFO - Created BACnet Object: DO.7, Digital Output 7 Type: BinaryOutput 2024-09-23 13:43:43,815 - INFO - Created BACnet Object: DO.8, Digital Output 8 Type: BinaryOutput 2024-09-23 13:43:43,832 - INFO - Created BACnet Object: BV.1, Binary Value 1 Type: BinaryValue 2024-09-23 13:43:43,850 - INFO - Created BACnet Object: AV.1, Analog Value 1 Type: AnalogValue 2024-09-23 13:43:43,900 - INFO - Created BACnet Object: UIO.1, UIO 1 Type: AnalogOutput 2024-09-23 13:43:43,916 - INFO - Created BACnet Object: UIO.2, UIO 2 Type: AnalogInput
i suspect my issue is around this
Instantiate the application with the device object