JoelBender / BACpypes3

BACnet communications library
42 stars 9 forks source link

Initialising Device via SQLite #59

Closed kheldaroz402 closed 3 weeks ago

kheldaroz402 commented 1 month ago

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

#!/usr/bin/env python3
import asyncio
import aiosqlite
import logging
import socket
import ipaddress
import psutil
import ast
from itertools import chain

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.primitivedata import Unsigned, OctetString
from bacpypes3.pdu import Address
from Functions_Other import clear, color

# Setup 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_ip_and_netmask():
    """
    Get the IP address and netmask of the first Ethernet interface.
    """
    for iface, addrs in psutil.net_if_addrs().items():
        if iface.startswith(("enp", "eth", "eno")):  # Filter for Ethernet interfaces
            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):
    """
    Convert the netmask to CIDR notation.
    """
    return ipaddress.IPv4Network(f"0.0.0.0/{netmask}").prefixlen

# Combine Application with WhoIsIAmServices
class IOM_Application(Application, WhoIsIAmServices):
    def __init__(self, device, db):
        Application.__init__(self, device)
        WhoIsIAmServices.__init__(self)

        self.db = db
        self.device = device  # Store the 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())

    async def update_values(self):
        """
        Fetch the current values from the database and update the BACnet objects.
        """
        async with self.db.execute('SELECT BACnetID, Name, ObjectType, PresentValue, InactiveText, ActiveText, statusFlags, COV, Description FROM objects') as cursor:
            cursor.row_factory = aiosqlite.Row
            async for row in cursor:
                # Define the unique key using BACnetID and ObjectType
                unique_key = (row['BACnetID'], row['ObjectType'])

                object_id = row['BACnetID']
                name = row['Name']
                object_type = row['ObjectType']
                present_value = row['PresentValue']
                statusFlags = ast.literal_eval(row['statusFlags']) if row['statusFlags'] else [0, 0, 0, 0]
                inactiveText = row['InactiveText']
                activeText = row['ActiveText']
                covIncrement = row['COV']
                description = row['Description']

                # Track changes and log if value has changed
                previous_value = self.object_values.get(unique_key)
                if previous_value is not None and previous_value != present_value:
                    logging.info(f"{color.BLUE}Value changed for {name}: {previous_value} -> {present_value}{color.END}")

                self.object_values[unique_key] = present_value

                # Handle different object types and update BACnet objects
                if object_type == "binaryValue":
                    present_value = "active" if present_value == 1 else "inactive"
                    bacnet_object = BinaryValueObject(
                        objectIdentifier=("binaryValue", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        inactiveText=inactiveText,
                        activeText=activeText,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: BinaryValue{color.END}")

                elif object_type == "analogValue":
                    bacnet_object = AnalogValueObject(
                        objectIdentifier=("analogValue", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        covIncrement=covIncrement,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: AnalogValue{color.END}")

                # Repeat similar logic for other object types like BinaryInput, BinaryOutput, etc.

                elif object_type == "binaryInput":
                    present_value = "active" if present_value == 1 else "inactive"
                    bacnet_object = BinaryInputObject(
                        objectIdentifier=("binaryInput", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        inactiveText=inactiveText,
                        activeText=activeText,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: BinaryInput{color.END}")

                elif object_type == "binaryOutput":
                    present_value = "active" if present_value == 1 else "inactive"
                    bacnet_object = BinaryOutputObject(
                        objectIdentifier=("binaryOutput", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        inactiveText=inactiveText,
                        activeText=activeText,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: BinaryOutput{color.END}")

                elif object_type == "analogInput":
                    bacnet_object = AnalogInputObject(
                        objectIdentifier=("analogInput", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        covIncrement=covIncrement,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: AnalogInput{color.END}")

                elif object_type == "analogOutput":
                    bacnet_object = AnalogOutputObject(
                        objectIdentifier=("analogOutput", object_id),
                        objectName=name,
                        presentValue=present_value,
                        statusFlags=statusFlags,
                        covIncrement=covIncrement,
                        description=description
                    )
                    self.add_object(bacnet_object)
                    logging.info(f"{color.GREEN}Created BACnet Object: {name} Type: AnalogOutput{color.END}")

    async def check_database_for_changes(self):
        """
        Periodically check the database for value changes and update the BACnet objects.
        """
        while True:
            await asyncio.sleep(CHECK_INTERVAL)
            logging.info(f"{color.CYAN}Checking database for changes...{color.END}")

            async with self.db.execute('SELECT BACnetID, PresentValue FROM objects') as cursor:
                cursor.row_factory = aiosqlite.Row  # Set the row factory to allow dictionary-like access
                async for row in cursor:
                    object_id = row['BACnetID']  # Access the row as a dictionary
                    new_value = row['PresentValue']
                    previous_value = self.object_values.get(object_id)

                    if previous_value is not None and previous_value != new_value:
                        logging.info(f"{color.BLUE}Value change detected for Object ID {object_id}: {previous_value} -> {new_value}{color.END}")
                        self.object_values[object_id] = new_value

                        # Update the presentValue of the corresponding BACnet object
                        bacnet_object = self.get_object_id(("analogValue", object_id))
                        if bacnet_object:
                            bacnet_object.presentValue = new_value

async def main():
    db = await aiosqlite.connect('/var/www/html/database/WebAI_IOM_Program.db')

    try:
        # Fetch the IP address and netmask
        ip_address, netmask = get_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 assign it to Application
        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', 'segmented-both'),
            vendorIdentifier=int(config.get('vendor-identifier', 47)),
            modelName=model_name,
            location=config.get('location', 'Unknown Location')
        )

        logging.info(f"{color.CYAN}Starting Device: {model_name}, IP Address: {ip_address}/{cidr}, Device ID: {device_id}{color.END}")

        app = IOM_Application(device=device_object, db=db)

        logging.info(f"{color.CYAN}Sending I-Am message...{color.END}")
        app.i_am()
        logging.info(f"{color.GREEN}I-Am message sent successfully!{color.END}")

        # Keep the application running
        await asyncio.Future()

    except KeyboardInterrupt:
        logging.info(f"{color.ORANGE}Keyboard interrupt{color.END}")
    finally:
        if db:
            await db.close()
            logging.info(f"{color.ORANGE}Database connection closed{color.END}")

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logging.info(f"{color.ORANGE}Keyboard interrupt{color.END}")

`

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

    app = Application(device=device_object)

    # Instantiate IOM_Application
    iom_app = IOM_Application(app=app, db=db)
    logging.info(f"{color.GREEN}IOM_Application instantiated{color.END}")
JoelBender commented 4 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])
kheldaroz402 commented 4 weeks ago

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'
JoelBender commented 4 weeks ago

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.

kheldaroz402 commented 3 weeks ago

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")