Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
213 commits
Select commit Hold shift + click to select a range
4af2723
abstract device variable class written
Oli-Jones475 Mar 25, 2026
7585807
constant variable class created
Oli-Jones475 Mar 25, 2026
8f08f55
Oscillating variable class created
Oli-Jones475 Mar 25, 2026
b675627
oscillating update loop implemented
Oli-Jones475 Mar 25, 2026
66c6677
Typo: Refresh rate changed to refresh period
Oli-Jones475 Mar 25, 2026
87e6fda
random variable class created
Oli-Jones475 Mar 25, 2026
cbc23b2
random update loop implemented
Oli-Jones475 Mar 25, 2026
412327b
read write variable class created
Oli-Jones475 Mar 25, 2026
8a242b2
docstrings written for device_variable
Oli-Jones475 Mar 25, 2026
0773f82
docstrings written for constant_variable
Oli-Jones475 Mar 25, 2026
b1734f1
docstrings written for oscillating_variable
Oli-Jones475 Mar 25, 2026
8e99de0
bug fix: oscillating variable equation accounts for PI
Oli-Jones475 Mar 25, 2026
e551308
docstring added for random_variable
Oli-Jones475 Mar 25, 2026
448779a
Methods in oscillating and random variable set to private
Oli-Jones475 Mar 25, 2026
3bfa750
docstring written for read_write_variable
Oli-Jones475 Mar 25, 2026
a91ef0b
generic device class created
Oli-Jones475 Mar 25, 2026
5ae4b43
device variable initialisation written
Oli-Jones475 Mar 25, 2026
5e4961f
docstrings added for device class
Oli-Jones475 Mar 25, 2026
f564615
device name stored in generic device
Oli-Jones475 Mar 25, 2026
a421316
bacnet object class created
Oli-Jones475 Mar 25, 2026
1c2f427
Added device variable to mimic to bacnet object
Oli-Jones475 Mar 25, 2026
651f7e0
docstrings added to analog_output_object
Oli-Jones475 Mar 25, 2026
1362682
bacnet device class started
Oli-Jones475 Mar 26, 2026
a7e5710
created add object loop in device
Oli-Jones475 Mar 26, 2026
e43c8f3
docstrings added for the dummy bacnet device
Oli-Jones475 Mar 26, 2026
a489799
privated some variables on device
Oli-Jones475 Mar 26, 2026
bb1c5ed
polling device controller class started
Oli-Jones475 Mar 27, 2026
f140542
device variables made private, getter for name
Oli-Jones475 Mar 27, 2026
2acad71
device variables converted to dictionary
Oli-Jones475 Mar 27, 2026
f45eed2
Added variable getter methods to device
Oli-Jones475 Mar 27, 2026
4e5066c
implemented update and send methods for device controller
Oli-Jones475 Mar 27, 2026
0f0b787
Made device IORef a dataclass
Oli-Jones475 Mar 27, 2026
63bd55b
made constructor for polling_device_controller
Oli-Jones475 Mar 27, 2026
6e49416
subscription device controller class created
Oli-Jones475 Mar 27, 2026
09d65dc
subscription id dataclass added
Oli-Jones475 Mar 26, 2026
c63cbb8
docstring added for subscription id dataclass
Oli-Jones475 Mar 26, 2026
10ce076
object subscription class created
Oli-Jones475 Mar 26, 2026
30f271a
added subscription methods to object subscription
Oli-Jones475 Mar 26, 2026
09bd944
docstrings added to object subscription
Oli-Jones475 Mar 26, 2026
9930920
privated fields of object subscription
Oli-Jones475 Mar 26, 2026
94ece13
added getters and setters for private fields
Oli-Jones475 Mar 26, 2026
56e00d7
fixed typing in methods
Oli-Jones475 Mar 26, 2026
17cd5d3
bacnet client class started
Oli-Jones475 Mar 26, 2026
ab43fd5
Functionality added to stop subscriptions
Oli-Jones475 Mar 26, 2026
05719a6
get and set methods for stopping subscriptions
Oli-Jones475 Mar 26, 2026
aaecedd
methods added to manipulate subscription dict
Oli-Jones475 Mar 26, 2026
7c2aeb2
method added to disconnect non-borrowed bacnet client
Oli-Jones475 Mar 26, 2026
cfa9e6d
typo fixed
Oli-Jones475 Mar 26, 2026
a0cf1ac
docstrings added to bacnet_client
Oli-Jones475 Mar 26, 2026
77dc25b
bacnet_client fields made private
Oli-Jones475 Mar 26, 2026
16bffbf
getters and setters added to bacnet_client
Oli-Jones475 Mar 26, 2026
82244cd
bacnet_client disconnect method updated
Oli-Jones475 Mar 26, 2026
86fe8e0
AttributeIO and Ref created for bacnet_subcontroller
Oli-Jones475 Mar 27, 2026
9eb5602
Made IORef a dataclass
Oli-Jones475 Mar 27, 2026
d790ed0
Bacnet subcontroller class written
Oli-Jones475 Mar 27, 2026
c3f0e34
docstrings added for bacnet_subcontroller
Oli-Jones475 Mar 27, 2026
12ee65c
bacnet controller constructor written
Oli-Jones475 Mar 27, 2026
0afaaa0
sort subscription function written
Oli-Jones475 Mar 27, 2026
6727395
BacnetController redundant parameter removed
Oli-Jones475 Mar 27, 2026
8c2fcd0
docstrings and comments added, variable renamed
Oli-Jones475 Mar 27, 2026
24f8c0f
changed dummy bacnet device objects list to dict
Oli-Jones475 Mar 27, 2026
18967c7
created object property for instance
Oli-Jones475 Mar 27, 2026
ff9cbd1
created a method to get object identifiers
Oli-Jones475 Mar 27, 2026
374a45e
bacnet client example written
Oli-Jones475 Mar 27, 2026
4c5e846
small typing issue fixed
Oli-Jones475 Mar 27, 2026
d72d625
typo in bacnet parameters
Oli-Jones475 Mar 30, 2026
9abbecf
typo in object susbcription
Oli-Jones475 Mar 30, 2026
9bf5b25
added keyword arguments to object_subscription
Oli-Jones475 Mar 30, 2026
6fd86f8
oscillating_variable default argument changed
Oli-Jones475 Mar 30, 2026
5317b4a
typing note added to object_subscription
Oli-Jones475 Mar 30, 2026
6f201a4
single_client_single_device example written
Oli-Jones475 Mar 30, 2026
aaf72f6
assigned default value of None
Oli-Jones475 Mar 30, 2026
4437222
analog object value initialisation bug fixed
Oli-Jones475 Mar 30, 2026
e7def57
default arguments for random_variable cahnged
Oli-Jones475 Mar 30, 2026
9cd81a4
single client multi device example written
Oli-Jones475 Mar 30, 2026
0bcb4f5
example renamed
Oli-Jones475 Mar 30, 2026
dfed16e
CA_polling example written
Oli-Jones475 Mar 30, 2026
69b467b
Usage note added
Oli-Jones475 Mar 30, 2026
dccbfa0
update period bug set for dummy controller attributes
Oli-Jones475 Mar 30, 2026
d89137f
fixed typo in device
Oli-Jones475 Mar 30, 2026
79fbdba
Added note on caput to CA_polling
Oli-Jones475 Mar 30, 2026
0679e63
Extra note added to CA_polling
Oli-Jones475 Mar 30, 2026
8239d8d
CA_subscription example written
Oli-Jones475 Mar 30, 2026
9a06ab5
Set initial value in subscription_device_controller
Oli-Jones475 Mar 30, 2026
62e0f80
Bug fix with calling super in bacnet_controller
Oli-Jones475 Mar 30, 2026
25756ee
converted attribute name to snake case
Oli-Jones475 Mar 30, 2026
51c5131
Added check to only update value of attributes
Oli-Jones475 Mar 30, 2026
9e27547
notes added to CA_full_pipeline
Oli-Jones475 Mar 30, 2026
043b24a
diagnostic callback added to device_variable
Oli-Jones475 Mar 30, 2026
a191f80
Diagnostic callback called in variable slots
Oli-Jones475 Mar 30, 2026
8c8bfd0
added diagnostic callback
Oli-Jones475 Mar 30, 2026
14d9774
docstring added
Oli-Jones475 Mar 30, 2026
d11850d
response timer class started
Oli-Jones475 Mar 30, 2026
f495339
subscription pair class started
Oli-Jones475 Mar 30, 2026
86036ec
subscription pair class written
Oli-Jones475 Mar 30, 2026
a301ea4
privated fields
Oli-Jones475 Mar 30, 2026
8497dc1
getters written for subscription_pair
Oli-Jones475 Mar 30, 2026
6572cd0
docstrings added to subscription_pair
Oli-Jones475 Mar 30, 2026
5a9cfb1
Added method to stop recording
Oli-Jones475 Mar 31, 2026
564dac4
added method to add a pair
Oli-Jones475 Mar 31, 2026
5c66776
added getters for pair objects
Oli-Jones475 Mar 31, 2026
c9429e5
added methods to get index and remove by index
Oli-Jones475 Mar 31, 2026
2609d7c
docstrings added
Oli-Jones475 Mar 31, 2026
5d99ee6
susbcriptions pair list privated
Oli-Jones475 Mar 31, 2026
d0ec900
queue_tracker class started
Oli-Jones475 Mar 31, 2026
33c7f9d
queue probe class written
Oli-Jones475 Mar 31, 2026
9c2a9ac
start method written
Oli-Jones475 Mar 31, 2026
c4b3bff
poll and time set methods implemented
Oli-Jones475 Mar 31, 2026
1706da6
typo fix
Oli-Jones475 Mar 31, 2026
e8d5100
docstrings added to queue_tracker
Oli-Jones475 Mar 31, 2026
8bebe38
privated fields
Oli-Jones475 Mar 31, 2026
06e0eab
getters written for private fields
Oli-Jones475 Mar 31, 2026
973d8ed
tracked_read_attribute class written
Oli-Jones475 Mar 31, 2026
aba7ba9
docstrings added to tracked_read_attribute
Oli-Jones475 Mar 31, 2026
0225fac
attribute_tracker class started
Oli-Jones475 Mar 31, 2026
48ecfef
written update callback method
Oli-Jones475 Mar 31, 2026
cf52bea
docstrings added to attribute_tracker
Oli-Jones475 Mar 31, 2026
616ee73
attributes privated
Oli-Jones475 Mar 31, 2026
41e33bd
getters added for private fields
Oli-Jones475 Mar 31, 2026
c67b1eb
bug fix
Oli-Jones475 Mar 31, 2026
85f6967
added keyword arguments to add attribute method
Oli-Jones475 Mar 31, 2026
0778473
new method to get objects from device
Oli-Jones475 Apr 1, 2026
15d5d16
updated recent recieval times method
Oli-Jones475 Apr 1, 2026
34a8387
method for getting average response time from all subscriptions
Oli-Jones475 Apr 1, 2026
807d692
set intial value of diagnostic callback
Oli-Jones475 Apr 1, 2026
c78e5a0
recplace is not with !=
Oli-Jones475 Apr 1, 2026
665c1eb
removed resubscriptions
Oli-Jones475 Apr 1, 2026
701c156
resubscription notes added
Oli-Jones475 Apr 1, 2026
b84d8f2
added kw argument for autorenew subscriptions
Oli-Jones475 Apr 1, 2026
34be46d
changed equality to an isclose check
Oli-Jones475 Apr 1, 2026
0485403
Added sent buffer to missed messages
Oli-Jones475 Apr 1, 2026
79df089
added reliability score
Oli-Jones475 Apr 1, 2026
a951875
reliabiility added to response timer
Oli-Jones475 Apr 1, 2026
43356e5
calculation bug fix
Oli-Jones475 Apr 1, 2026
e704bb3
average_update_time example added
Oli-Jones475 Apr 1, 2026
8629c38
changed signiture of diagnostic callback
Oli-Jones475 Apr 1, 2026
91a3c4a
accounted for deadzone changed
Oli-Jones475 Apr 1, 2026
1cade3f
parameters changed for example
Oli-Jones475 Apr 1, 2026
c634b8b
fixed none error
Oli-Jones475 Apr 1, 2026
561a220
allowed to test on multiple devices
Oli-Jones475 Apr 1, 2026
4c8a43c
stash
Oli-Jones475 Apr 1, 2026
def6ba5
puppet variable class created
Oli-Jones475 Apr 2, 2026
c271427
puppet controller class written
Oli-Jones475 Apr 2, 2026
d10a0d0
started update loop in puppet controller
Oli-Jones475 Apr 2, 2026
9b43758
docstrings added
Oli-Jones475 Apr 2, 2026
7280a85
puppet variables are available in dummy bacnet devices
Oli-Jones475 Apr 2, 2026
3089399
update loops dont start automatically
Oli-Jones475 Apr 2, 2026
23561d4
Added puppet variables count as parameter
Oli-Jones475 Apr 2, 2026
76df188
removed update loop count in start method
Oli-Jones475 Apr 2, 2026
edbfe6f
low_cpu_stress_test written
Oli-Jones475 Apr 2, 2026
f5abc44
dependencies updated
Oli-Jones475 Apr 2, 2026
7e1c55f
bug fixes
Oli-Jones475 Apr 2, 2026
e35489a
bug fixed in reliability score calculation
Oli-Jones475 Apr 7, 2026
9e16d5c
print statements removed
Oli-Jones475 Apr 8, 2026
9fe8725
list bug fixed
Oli-Jones475 Apr 8, 2026
cb5cebe
reliability calculation fixed
Oli-Jones475 Apr 8, 2026
a33922e
Update src/fastcs_bacnet/examples/BAC0/single_client_single_device.py
Oli-Jones475 Apr 10, 2026
c153b88
type ingore comment added
Oli-Jones475 Apr 10, 2026
db882d8
constant names changed to uppercase
Oli-Jones475 Apr 10, 2026
36b05dd
subcontroller details removed
Oli-Jones475 Apr 10, 2026
31b107f
Changed return None to raising an error
Oli-Jones475 Apr 10, 2026
ffd7fd0
removed redundant assignment
Oli-Jones475 Apr 10, 2026
5af0647
minor typo
Oli-Jones475 Apr 10, 2026
c9ba722
cant remove subscription reference without stopping
Oli-Jones475 Apr 10, 2026
721c497
matplotlib moved to dev dependency
Oli-Jones475 Apr 10, 2026
669a225
removed bacnet client creation inside object
Oli-Jones475 Apr 10, 2026
ff6b57c
socket address helper method added
Oli-Jones475 Apr 10, 2026
eeb52f7
object key helper function added
Oli-Jones475 Apr 10, 2026
6e9d146
comment more descisive
Oli-Jones475 Apr 10, 2026
8cec90a
sort_subscriptions method moved
Oli-Jones475 Apr 10, 2026
ba03676
dictionary changed to default dict
Oli-Jones475 Apr 10, 2026
0722b93
bacnet client added to example
Oli-Jones475 Apr 10, 2026
9108773
defualt generic callback removed
Oli-Jones475 Apr 10, 2026
6fe4bf0
Replace queue_subscription method with call_later
Oli-Jones475 Apr 10, 2026
b042580
Prevent garbage collection of tasks
Oli-Jones475 Apr 10, 2026
bb8197a
Make dataclasses for SubscriptionID parameters
Oli-Jones475 Apr 10, 2026
82e26f7
Capitalise constants
Oli-Jones475 Apr 14, 2026
0f69a69
Correct bacnet_client docstring
Oli-Jones475 Apr 14, 2026
d84da51
Remove miscillaneous print statement
Oli-Jones475 Apr 14, 2026
c396f33
Update subscription_id docstrings
Oli-Jones475 Apr 14, 2026
2ca93f5
Neated dictionary iteration
Oli-Jones475 Apr 14, 2026
8b7e4de
Rename files to avoid triggering pytest
Oli-Jones475 Apr 17, 2026
8933a4b
Start callback_stack class
Oli-Jones475 Apr 17, 2026
4621c2a
Fill in methods for callback_stack
Oli-Jones475 Apr 17, 2026
37af238
Add docstrings to callback_stack
Oli-Jones475 Apr 17, 2026
58de52c
Update object_subscription to use callback_stack
Oli-Jones475 Apr 17, 2026
45b1576
Implement callback stack in device_variable
Oli-Jones475 Apr 17, 2026
de0ae48
Implement callback_stack in device_variable
Oli-Jones475 Apr 17, 2026
cf226c6
Implement callback_stack in all children
Oli-Jones475 Apr 17, 2026
2653d4e
Make subscription status class
Oli-Jones475 Apr 20, 2026
4a303ac
Start CovTracker class
Oli-Jones475 Apr 20, 2026
f5e536c
Add lifetime attribute to subscription_status
Oli-Jones475 Apr 20, 2026
e213a71
Add lock to subscription_status
Oli-Jones475 Apr 20, 2026
f6cfd6c
Add team related functions
Oli-Jones475 Apr 20, 2026
80d1b2a
Implement callback race method
Oli-Jones475 Apr 20, 2026
9b42be7
Implement start_cov method
Oli-Jones475 Apr 20, 2026
6da62a7
Make subscription_status methods more versatile
Oli-Jones475 Apr 20, 2026
9cc158d
Implement on resubscribe method
Oli-Jones475 Apr 20, 2026
e86f694
Implement stop_cov method
Oli-Jones475 Apr 20, 2026
293aa86
Update set_team_up method
Oli-Jones475 Apr 20, 2026
a33ad6a
Set status when subscription confirmation is recieved
Oli-Jones475 Apr 20, 2026
973ee3e
Update object_subscription to use cov_tracker
Oli-Jones475 Apr 20, 2026
c7178b7
Update object_subscription docstrings
Oli-Jones475 Apr 20, 2026
d331dc2
Clean up previous CoV task when resubscribing
Oli-Jones475 Apr 20, 2026
6eff3d4
Add docstrings to cov_tracker
Oli-Jones475 Apr 20, 2026
8b75ea6
Fix bug in resubscription
Oli-Jones475 Apr 20, 2026
9fcd49f
Add docstrings to subscription_status
Oli-Jones475 Apr 20, 2026
bbd79b9
Test git tests
Oli-Jones475 Apr 16, 2026
19aec8f
Add main assertion to example files
Oli-Jones475 Apr 16, 2026
0b84352
Make IP and Port optional in dummy device
Oli-Jones475 Apr 20, 2026
3a1d89d
Remove test tests
Oli-Jones475 Apr 20, 2026
e1975ec
Copy conftest method from PythonSoftIOC
Oli-Jones475 Apr 20, 2026
c0498c3
Add a system test file
Oli-Jones475 Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
description = "Reads bacnet data to an IOC"
dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"]
dependencies = [
"BAC0",
"fastcs[epicsca]",
"epicscorelibs",
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
license.file = "LICENSE"
readme = "README.md"
Expand All @@ -21,6 +25,7 @@ requires-python = ">=3.11"
[dependency-groups]
dev = [
"copier",
"matplotlib",
"pre-commit",
"pyright",
"pytest",
Expand All @@ -45,7 +50,7 @@ version_file = "src/fastcs_bacnet/_version.py"

[tool.pyright]
typeCheckingMode = "standard"
reportMissingImports = false # Ignore missing stubs in imported modules
reportMissingImports = false # Ignore missing stubs in imported modules

[tool.pytest.ini_options]
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
Expand Down
101 changes: 101 additions & 0 deletions src/fastcs_bacnet/diagnostics/BAC0/response_timer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from datetime import timedelta

from fastcs_bacnet.diagnostics.BAC0.subscription_pair import SubscriptionPair
from fastcs_bacnet.dummy.BAC0.analog_output_object import AnalogOutputObject
from fastcs_bacnet.practical.BAC0.object_subscription import ObjectSubscription


class ResponseTimer:
"""
Class for managing a list of SubscriptionPair s
"""

# May want to change to a dictionary??
# Most of the operations invlolve iterating through it
# But getting a specific value using keys is also used
_subscription_pairs: list[SubscriptionPair] = []

def __init__(self, recent_times_buffer_length: int = 20):
self._recent_times_buffer_length = recent_times_buffer_length

def add_subscription_pair(
self,
analog_output_object: AnalogOutputObject,
object_subscription: ObjectSubscription,
):
"""
Creates a subscription pair from the input objects and adds it to the list
"""
self._subscription_pairs.append(
SubscriptionPair(
analog_output_object,
object_subscription,
recent_times_buffer_length=self._recent_times_buffer_length,
)
)

def remove_subscription_pair(self, index: int):
"""
Removes a subscription pair from the list at a specific index
Stops the subscription
Find index of a pair using index_from methods
"""
subscription_pair = self._subscription_pairs.pop(index)
subscription_pair.stop_recording()

def index_from_device_object(
self, analog_output_object: AnalogOutputObject
) -> int | None:
"""
Gets index of a subscription pair from its analog_output_object
"""
for index in range(len(self._subscription_pairs)):
index_analog_output_object = self._subscription_pairs[
index
].get_analog_output_object()
if analog_output_object == index_analog_output_object:
return index
return None

def index_from_object_subscription(
self, object_subscription: ObjectSubscription
) -> int | None:
"""
Gets index of a subscription pair from its object_subscription
"""
for index in range(len(self._subscription_pairs)):
index_object_subscription = self._subscription_pairs[
index
].get_object_subscription()
if object_subscription == index_object_subscription:
return index
return None

def get_average_response_time(self) -> timedelta:
total_average_wait_times = timedelta(0)

for pair in self._subscription_pairs:
pair_average_time = pair.get_average_update_time()
if pair_average_time is not None:
total_average_wait_times += pair_average_time

total_average_wait_times /= len(self._subscription_pairs)

return total_average_wait_times

def get_average_reliability(self) -> float:
total_reliability = 0.0
total_pairs_averaged = 0

for pair in self._subscription_pairs:
pair_reliability = pair.get_reliability()
if pair_reliability is not None:
total_reliability += pair_reliability
total_pairs_averaged += 1

if total_pairs_averaged == 0:
return 0

total_reliability /= total_pairs_averaged

return total_reliability
175 changes: 175 additions & 0 deletions src/fastcs_bacnet/diagnostics/BAC0/subscription_pair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import math
from datetime import datetime, timedelta

from bacpypes3.primitivedata import PropertyIdentifier

from fastcs_bacnet.dummy.BAC0.analog_output_object import AnalogOutputObject
from fastcs_bacnet.practical.BAC0.object_subscription import ObjectSubscription


class SubscriptionPair:
"""
A class to track the time between an AnalogOutputObject being updated and its
corresponding ObjectSubscription recieving the update and related statistics
"""

# subscription messages that have been sent but not recieved yet
_sent_buffer: list[tuple[float, datetime]]

# time between sending and recieving updates
# size limited, only the most recent sent updates are kept
_recent_receival_times: list[timedelta | None]

# max size of recent_recieval_times
_recent_times_buffer_length: int

_total_missed_updates: int = 0
_total_updates_recieved: int = 0
_total_update_wait_time: timedelta

# when we cant match a recieved value to a sent value
# shouldnt ever happed but we should definitely know if it does
_total_mystery_updates: int = 0

def __init__(
self,
analog_output_object: AnalogOutputObject,
object_subscription: ObjectSubscription,
recent_times_buffer_length: int = 20,
):
"""
analog_output_object: analog object of a dummy bacnet device
(cannot replicate this for real devices)
object_subscription: subscription object of a bacnet client device that
subscribes to the analog output object
recent_times_buffer_length: The number of send-recieve time differences to hold
at once. List would quickly get very large if this was not capped
Changes the diagnostic callback function of both objects to _on_send
and _on_recieve respectively
"""
self._recent_times_buffer_length = recent_times_buffer_length
self._total_update_wait_time = timedelta(0)
self._analog_output_object = analog_output_object
self._object_subscription = object_subscription

self._sent_buffer = []
self._recent_receival_times = []

# maybe a bit cheeky to acess the analog_ouput_object underlying
# device variable directly. Should be with a getter??
self._analog_output_object.device_variable.set_diagnostic_callback(
self._on_send
)
self._object_subscription.set_diagnostic_callback(self._on_recieve)

def _on_send(self, old_value: float | None, new_value: float):
"""
Callback to run when an update is sent by the device output object
Adds the update value and the current time to the _sent_buffer list
"""
if old_value is None or abs(old_value - new_value) > 0.1:
self._sent_buffer.append((new_value, datetime.now()))
else:
print("updated value in dead zone")

def _on_recieve(self, property_identifier: str, property_value: float):
"""
Callback to run when an update is recieved by the subscription object
Matches the recieved value to one that was sent by the device object
Values sent before the matched send value are marked as missed
Records the time between the update being sent and recieved
Adds recorded time as well as missed values to the _recent_recieval_times
"""
if property_identifier != PropertyIdentifier.presentValue:
return

recieved_time = datetime.now()

sent_index = self._match(property_value)
if sent_index is None:
print("mystery update")
self._total_mystery_updates += 1
return

sent_time = self._sent_buffer[sent_index][1]

# every sent update before the current one must have been missed
# remove the sent buffer before the one we recieved
# add
for _ in range(sent_index):
self._sent_buffer.pop(0)
self._add_recieval_time(None)

self._sent_buffer.pop(0)
self._add_recieval_time(recieved_time - sent_time)

def _match(self, recieved_value: float) -> int | None:
"""
Matches a recieved update value to the most recent matching value from the
sent updates list
Returns the index of the matching value
"""

for i in range(len(self._sent_buffer)):
sent_value_i = self._sent_buffer[i][0]
if math.isclose(sent_value_i, recieved_value, rel_tol=0.0001):
return i

return None

def _add_recieval_time(self, receival_time: timedelta | None):
"""
Adds a time difference (between sending and recieving)
to the _recent_receival_times list
Also handles changing of total missed updates, recieved updates and wait time
As well as keepin gthe list below its maximum length
"""

if receival_time is None:
print("missed message")
self._total_missed_updates += 1
else:
print("valid receival")
self._total_updates_recieved += 1
self._total_update_wait_time += receival_time

self._recent_receival_times.append(receival_time)

while len(self._recent_receival_times) > self._recent_times_buffer_length:
self._recent_receival_times.pop(0)

def get_recent_recieval_times(self) -> list[timedelta | None]:
"""
Returns a copy of recent_recieval_times
Dont have to deep copy as timedeltas are immutable
"""
return list(self._recent_receival_times)

def get_total_missed_updates(self) -> int:
return self._total_missed_updates + len(self._sent_buffer)

def get_average_update_time(self) -> timedelta | None:
if self._total_updates_recieved == 0:
return None
return self._total_update_wait_time / self._total_updates_recieved

def get_reliability(self) -> float | None:
"""
Total messages recieved / total messages sent
"""
total_updates_sent = (
self._total_updates_recieved + self.get_total_missed_updates()
)
if total_updates_sent == 0:
return None
return self._total_updates_recieved / (total_updates_sent)

def get_analog_output_object(self) -> AnalogOutputObject:
return self._analog_output_object

def get_object_subscription(self) -> ObjectSubscription:
return self._object_subscription

def stop_recording(self):
self._analog_output_object.device_variable.set_diagnostic_callback(None)
self._object_subscription.set_diagnostic_callback(None)
49 changes: 49 additions & 0 deletions src/fastcs_bacnet/diagnostics/FastCS/attribute_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from datetime import datetime, timedelta

from fastcs_bacnet.diagnostics.FastCS.tracked_read_attribute import TrackedAttrR


class AttributeTracker:
"""
Records and stores updates times from a TrackedAttrR
"""

_recent_times: list[timedelta]
_first_update: datetime | None
_last_update: datetime | None

def __init__(self, tracked_attribute: TrackedAttrR, history_size: int = 20):
"""
tracked_attribute: Attribute to track
history_size: Maximum number of update times to store in the recent_times queue
"""

self._tracked_attribute = tracked_attribute
self._history_size = history_size

self._tracked_attribute.set_diagnostic_callback(self.update_callback)

def update_callback(self, start_time: datetime, end_time: datetime):
"""
The callback function that gets called after the attributes
update method is called
start_time is when the update method is first called
end_time is after the original update method has been resolved
"""

self._recent_times.append(end_time - start_time)
while len(self._recent_times) > self._history_size:
self._recent_times.pop(0)

if self._first_update is None:
self._first_update = start_time
self._last_update = start_time

def get_first_update_time(self) -> datetime | None:
return self._first_update

def get_last_update_time(self) -> datetime | None:
return self._last_update

def get_recent_times(self) -> list[timedelta]:
return list(self._recent_times)
Loading
Loading