Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to detect where a change came from in the BACnet Stack? #528

Open
kheldaroz402 opened this issue Jun 19, 2024 · 2 comments
Open

Comments

@kheldaroz402
Copy link

The issue that I'm having is that an external device makes a change to a Point and before the database has been updated, the BACnet stack has been updated by the process that checks the database, so the external change gets "lost" (its intermitent of course, since sometime the database is updated)

`# Function to create Binary Output Object
def create_binary_output(row):
try:
ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier)
property_list = ArrayOfPropertyIdentifier([
'presentValue', 'statusFlags', 'eventState', 'inactiveText', 'activeText',
'outOfService', 'description', 'reliability', 'polarity'
])

    # Convert the Present_Value to 'active' or 'inactive'
    present_value = 'active' if row['Present_Value'] in [1, 1.0, 'active', True] else 'inactive'

    # Initialize the BinaryOutputFeedbackObject
    binary_output = BinaryOutputFeedbackObject(
        objectName=row['Object_Name'],
        objectIdentifier=(BinaryOutputObject.objectType, row['Object_Identifier']),
        presentValue=present_value,
        inactiveText=row['On_lable'],
        activeText=row['Off_lable'],
        statusFlags=calculate_status_flags(row),
        eventState=row.get('Event_State', 0),
        outOfService=bool(row['Out_Of_Service']),
        polarity=row['Polarity'],
        description=row['Description'],
        reliability=row.get('Reliability', None),
        propertyList=property_list,
    )

    # Initialize the priority array with default values
    priority_array = PriorityArray([None] * 16)

    # Set the Present_Value at the specified priority position if within range
    if 'Priority' in row and 1 <= row['Priority'] <= 16:
        priority_index = row['Priority']   # Adjust for 0-based index
        priority_array[priority_index] = PriorityValue(characterString=CharacterString(present_value))

    # Assign the priority array to the object
    binary_output.priorityArray = priority_array



    return binary_output

except Exception as e:
    logging.error(f"{color.RED}An error occurred in create_binary_output: {e}{color.END}")
    return None

@bacpypes_debugging
@register_object_type(vendor_id=47)
class BinaryOutputFeedbackObject(BinaryOutputCmdObject):
def init(self, *args, **kwargs):
super().init(*args, **kwargs)

    # Initialize the objectIdentifier monitor
    self._property_monitors.setdefault('objectIdentifier', []).append(self.objectIdentifier)

    # listen for changes to the present value
    self._property_monitors["presentValue"].append(self.check_feedback)
    logging.info(f"{color.GREEN}BinaryOutputFeedbackObject initialized for {self.objectName}{color.END}")

def get_lowest_priority_value(self):
    """
    Get the lowest non-null priority and its value from the priorityArray.
    """
    for i, value in enumerate(self.priorityArray):
        if value is not None:
            # Handle different BACnet value types
            if hasattr(value, 'real'):
                value = value.real
            elif hasattr(value, 'integer'):
                value = value.integer
            elif hasattr(value, 'unsigned'):
                value = value.unsigned
            elif hasattr(value, 'boolean'):
                value = value.boolean

            # logging.info(f"{color.YELLOW} Priority {i + 1} value is {value}{color.END}")

            if value is not None:
                return i + 1, value  # BACnet priorities are 1-indexed
    return 16, None

def check_feedback(self, old_value, new_value):
    if new_value == old_value:
        return

    # Ensure the objectIdentifier is available
    if 'objectIdentifier' not in self._property_monitors or not self._property_monitors['objectIdentifier']:
        logging.error(f"{color.RED}BinaryOutputFeedbackObject: objectIdentifier not available in property monitors{color.END}")
        return

    object_identifier = self._property_monitors['objectIdentifier'][0]
    object_identifier_value = object_identifier[1]
    feedback_value = 1 if new_value == 'active' else 0

    # Get the lowest non-null priority and its value
    priority, priority_value = self.get_lowest_priority_value()

    # Determine the value to update in the database
    present_value_to_update = priority_value if priority != 16 else feedback_value

    # Set Override_Value if priority is not 16
    override_value = present_value_to_update if priority != 16 else None
    # Set Override to 1 in the database if priority is 8
    override_flag = 1 if priority == 8 else 0

    logging.info(f"{color.GREEN}BinaryOutputFeedbackObject (ID: {object_identifier_value}, Name: {self.objectName}): Detected change from {old_value} to {new_value}{color.END}")

    mydbProgram = None
    mycursorProgram = None
    try:
        mydbProgram = poolProgram.get_connection()
        mycursorProgram = mydbProgram.cursor(dictionary=True, buffered=True)

        sql = (
            "UPDATE Objects "
            "SET Present_Value = %s, Updated_By = 'Bacnet Feedback', Priority = %s, Override_Value = %s, Override = %s "
            "WHERE Object_Identifier = %s"
        )

        mycursorProgram.execute(sql, (present_value_to_update, priority, override_value, override_flag, object_identifier_value))
        mydbProgram.commit()
        logging.info(f"{color.GREEN}BinaryOutputFeedbackObject: Database updated for Object_Identifier {object_identifier_value} with Present_Value {present_value_to_update}, Priority {priority}, Override_Value {override_value}, Override {override_flag}{color.END}")

    except (MySQLError, Exception) as e:
        logging.error(f"{color.RED}BinaryOutputFeedbackObject: An error occurred while updating the database: {e}{color.END}")
    finally:
        if mycursorProgram:
            mycursorProgram.close()
        if mydbProgram:
            mydbProgram.close()

`
image
(green text)

I've tried to implement a "debounce" feature, but that just complains that its unknown property

`from bacpypes.local.object import AnalogOutputCmdObject
import time
import logging

class AnalogOutputFeedbackObject(AnalogOutputCmdObject):
def init(self, *args, **kwargs):
self.last_update_time = 0
self.debounce_interval = 1 # seconds
if _debug:
AnalogOutputFeedbackObject._debug("init %r %r", args, kwargs)
super().init(*args, **kwargs)

    # Initialize the objectIdentifier monitor
    self._property_monitors.setdefault('objectIdentifier', [])
    self._property_monitors['objectIdentifier'].append(self.objectIdentifier)

    # listen for changes to the present value
    self._property_monitors["presentValue"].append(self.check_feedback)
    logging.info(f"{color.GREEN}AnalogOutputFeedbackObject initialized for {self.objectName}{color.END}")

def get_lowest_priority_value(self):
    """
    Get the lowest non-null priority value from the priorityArray.
    """
    for i, value in enumerate(self.priorityArray):
        if value is not None:
            # Handle different BACnet value types
            if hasattr(value, 'real'):
                value = value.real
            elif hasattr(value, 'integer'):
                value = value.integer
            elif hasattr(value, 'unsigned'):
                value = value.unsigned
            elif hasattr(value, 'boolean'):
                value = value.boolean

            if value is not None:
                return i + 1, value  # BACnet priorities are 1-indexed
    return 16, None

def check_feedback(self, old_value, new_value):
    current_time = time.time()
    if new_value == old_value:
        return
    if current_time - self.last_update_time < self.debounce_interval:
        logging.info(f"{color.YELLOW}Debouncing... Skipping update for {self.objectName}{color.END}")
        return
    self.last_update_time = current_time

    # Ensure the objectIdentifier is available
    if 'objectIdentifier' not in self._property_monitors or not self._property_monitors['objectIdentifier']:
        logging.error(f"{color.RED}AnalogOutputFeedbackObject: objectIdentifier not available in property monitors{color.END}")
        return

    object_identifier = self._property_monitors['objectIdentifier'][0]
    object_identifier_value = object_identifier[1]

    # Get the lowest non-null priority value
    priority, priority_value = self.get_lowest_priority_value()

    logging.info(f"{color.GREEN}AnalogOutputFeedbackObject (ID: {object_identifier_value}, Name: {self.objectName}): Detected change from {old_value} to {new_value} with priority {priority} and priority value {priority_value}{color.END}")

    # Determine the value to update in the database
    if priority != 16:
        present_value_to_update = priority_value
    else:
        present_value_to_update = new_value

    # Set Override_Value if priority is not 16
    override_value = present_value_to_update if priority != 16 else None
    # Set Override to 1 in the database if priority is 8
    override_flag = 1 if priority == 8 else 0

    mydbProgram = None
    mycursorProgram = None
    try:
        mydbProgram = poolProgram.get_connection()
        mycursorProgram = mydbProgram.cursor(dictionary=True, buffered=True)

        sql = (
            "UPDATE Objects "
            "SET Present_Value = %s, Updated_By = 'Bacnet Feedback', Priority = %s, Override_Value = %s, Override = %s "
            "WHERE Object_Identifier = %s"
        )

        mycursorProgram.execute(sql, (present_value_to_update, priority, override_value, override_flag, object_identifier_value))
        mydbProgram.commit()
        logging.info(f"{color.YELLOW}AnalogOutputFeedbackObject: Database updated for Object_Identifier {object_identifier_value} with Present_Value {present_value_to_update}, Priority {priority}, Override_Value {override_value}, Override {override_flag}{color.END}")

    except (MySQLError, Exception) as e:
        logging.error(f"{color.RED}AnalogOutputFeedbackObject: An error occurred while updating the database: {e}{color.END}")
    finally:
        if mycursorProgram:
            mycursorProgram.close()
        if mydbProgram:
            mydbProgram.close()

`

@kheldaroz402
Copy link
Author

Got the debounce working but would still like to know if i can detect where a change is coming from

`class AnalogOutputFeedbackObject(AnalogOutputCmdObject):
debounce_interval = 3 # 500 milliseconds debounce interval

def __init__(self, *args, **kwargs):
    if _debug:
        AnalogOutputFeedbackObject._debug("__init__ %r %r", args, kwargs)
    super().__init__(*args, **kwargs)

    # Initialize the objectIdentifier monitor
    self._property_monitors.setdefault('objectIdentifier', []).append(self.objectIdentifier)

    # Listen for changes to the present value
    self._property_monitors["presentValue"].append(self.check_feedback)
    logging.info(f"{color.GREEN}AnalogOutputFeedbackObject initialized for {self.objectName}{color.END}")

    # Initialize debounce timer attribute using a private name to avoid conflicts
    self._last_change_time = time.time()

def get_lowest_priority_value(self):
    """
    Get the lowest non-null priority value from the priorityArray.
    """
    for i, value in enumerate(self.priorityArray):
        if value is not None:
            # Handle different BACnet value types
            if hasattr(value, 'real'):
                value = value.real
            elif hasattr(value, 'integer'):
                value = value.integer
            elif hasattr(value, 'unsigned'):
                value = value.unsigned
            elif hasattr(value, 'boolean'):
                value = value.boolean

            if value is not None:
                return i + 1, value  # BACnet priorities are 1-indexed
    return 16, None

def check_feedback(self, old_value, new_value):
    current_time = time.time()
    if new_value == old_value or (current_time - self._last_change_time) < self.debounce_interval:
        return

    self._last_change_time = current_time

    # Ensure the objectIdentifier is available
    if 'objectIdentifier' not in self._property_monitors or not self._property_monitors['objectIdentifier']:
        logging.error(f"{color.RED}AnalogOutputFeedbackObject: objectIdentifier not available in property monitors{color.END}")
        return

    object_identifier = self._property_monitors['objectIdentifier'][0]
    object_identifier_value = object_identifier[1]

    # Get the lowest non-null priority value
    priority, priority_value = self.get_lowest_priority_value()

    logging.info(f"{color.GREEN}AnalogOutputFeedbackObject (ID: {object_identifier_value}, Name: {self.objectName}): Detected change from {old_value} to {new_value} with priority {priority} and priority value {priority_value}{color.END}")

    # Determine the value to update in the database
    if priority != 16:
        present_value_to_update = priority_value
    else:
        present_value_to_update = new_value

    # Set Override_Value if priority is not 16
    override_value = present_value_to_update if priority != 16 else None
    # Set Override to 1 in the database if priority is 8
    override_flag = 1 if priority == 8 else 0

    mydbProgram = None
    mycursorProgram = None
    try:
        mydbProgram = poolProgram.get_connection()
        mycursorProgram = mydbProgram.cursor(dictionary=True, buffered=True)

        sql = (
            "UPDATE Objects "
            "SET Present_Value = %s, Updated_By = 'Bacnet Feedback', Priority = %s, Override_Value = %s, Override = %s "
            "WHERE Object_Identifier = %s"
        )

        mycursorProgram.execute(sql, (present_value_to_update, priority, override_value, override_flag, object_identifier_value))
        mydbProgram.commit()
        logging.info(f"{color.YELLOW}AnalogOutputFeedbackObject: Database updated for Object_Identifier {object_identifier_value} with Present_Value {present_value_to_update}, Priority {priority}, Override_Value {override_value}, Override {override_flag}{color.END}")

    except (Exception) as e:
        logging.error(f"{color.RED}AnalogOutputFeedbackObject: An error occurred while updating the database: {e}{color.END}")
    finally:
        if mycursorProgram:
            mycursorProgram.close()
        if mydbProgram:
            mydbProgram.close()`

@bbartling
Copy link

maybe see this git discussion about trying to see where client requests come from.

#504

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants