From 79ff9f1655e2146255ff0548cc6e0b9995de42c1 Mon Sep 17 00:00:00 2001 From: Noah Oblath Date: Fri, 11 Jul 2025 14:08:51 -0700 Subject: [PATCH 1/5] Changed the check against max_fractional_change to be |x0-x1|/(|x0+x1|/2) and moved the return for when `result[self._check_field])` results in an exception --- dripline/core/entity.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/dripline/core/entity.py b/dripline/core/entity.py index ae37f44b..658d7d18 100644 --- a/dripline/core/entity.py +++ b/dripline/core/entity.py @@ -107,8 +107,10 @@ def __init__(self, self._max_interval = max_interval self._max_fractional_change = max_fractional_change self._check_field = check_field + self._log_action_id = None self._last_log_time = None + self._last_log_value = None @property def get_on_set(self): @@ -160,22 +162,21 @@ def scheduled_log(self): try: this_value = float(result[self._check_field]) except (TypeError, ValueError): - this_value = False + logger.warning(f"cannot check value change for {self.name}") + return + # Various checks for log condition if self._last_log_time is None: - logger.debug("log because no last log") + logger.debug("Logging because this is the first logged value") elif (datetime.datetime.now(datetime.timezone.utc) - self._last_log_time).total_seconds() > self._max_interval: - logger.debug("log because too much time") - elif this_value is False: - logger.warning(f"cannot check value change for {self.name}") - return - elif ((self._last_log_value == 0 and this_value != 0) or - (self._last_log_value != 0 and\ - abs((self._last_log_value - this_value)/self._last_log_value)>self._max_fractional_change)): - logger.debug("log because change magnitude") + logger.debug("Logging because enough time has elapsed") + # this condition is |x1-x0|/(|x1+x0|/2) > max_fractional_change, but safe in case the denominator is 0 + elif 2 * abs(self._last_log_value - this_value) > self._max_fractional_change * abs(self._last_log_value + this_value): + logger.debug("Logging because the value has changed significantly") else: - logger.debug("no log condition met, not logging") + logger.debug("No log condition met, therefore not logging") return + self._last_log_value = this_value self.log_a_value(result) From d8ee30d228e8d35a85f2a7d063bb334b83a0dd98 Mon Sep 17 00:00:00 2001 From: Noah Oblath Date: Fri, 11 Jul 2025 14:36:01 -0700 Subject: [PATCH 2/5] Clarified (hopefully) the docstrings in Entity.__init__(); minor changes to Entity's docstring --- dripline/core/entity.py | 49 ++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/dripline/core/entity.py b/dripline/core/entity.py index 658d7d18..635b7568 100644 --- a/dripline/core/entity.py +++ b/dripline/core/entity.py @@ -54,12 +54,12 @@ def wrapper(*args, **kwargs): __all__.append("Entity") class Entity(Endpoint): ''' - Subclass of Endpoint which adds logic related to logging and confirming values. + Subclass of Endpoint that adds logic related to logging and confirming values. In particular, there is support for: - get_on_set -> setting the endpoint's value returns a get() result rather than an empty success (particularly useful for devices which may round assignment values) - log_on_set -> further extends get_on_set to send an alert message in addtion to returning the value in a reply - log_interval -> leverages the scheduler class to log the on_get result at a regular cadence + get_on_set -> setting the endpoint's value returns an on_get() result rather than an empty success (particularly useful for devices that may round assignment values) + log_on_set -> further extends get_on_set to send an logging alert message in addtion to returning the value in a reply + log_interval -> leverages the scheduler class to log the on_get result at a regular cadence and if the value changes significantly ''' #check_on_set -> allows for more complex logic to confirm successful value updates # (for example, the success condition may be measuring another endpoint) @@ -75,21 +75,34 @@ def __init__(self, **kwargs): ''' Args: - get_on_set: if true, calls to on_set are immediately followed by an on_get, which is returned - log_on_set: if true, always call log_a_value() immediately after on_set + get_on_set: bool (default is False) + If true, calls to on_set() are immediately followed by an on_get(), which is returned + log_on_set: bool (default is False) + If true, always call log_a_value() immediately after on_set() **Note:** requires get_on_set be true, overrides must be equivalent - log_routing_key_prefix: first term in routing key used in alert messages which log values - log_interval: how often to check the Entity's value. If 0 then scheduled logging is disabled; - if a number, interpreted as number of seconds; if a dict, unpacked as arguments - to the datetime.time_delta initializer; if a datetime.timedelta taken as the new value - max_interval: max allowed time interval between logging, allows usage of conditional logging. If 0, - then logging values occurs every log_interval. - max_fractional_change: max allowed fractional difference between subsequent values to trigger log condition. - check_field: result field to check, 'value_cal' or 'value_raw' - calibration (string || dict) : if string, updated with raw on_get() result via str.format() in - @calibrate decorator, used to populate raw and calibrated values - fields of a result payload. If a dictionary, the raw result is used - to index the dict with the calibrated value being the dict's value. + log_routing_key_prefix: string (default is 'sensor_value') + First term in routing key used in alert messages that log values + log_interval: 0 (default), float, dict, datetime.timmedelta + Defines how often to check the Entity's value to determine if it should be logged + If 0, scheduled logging is disabled; + If a number, interpreted as number of seconds; + If a dict, unpacked as arguments to the datetime.time_delta initializer; + If a datetime.timedelta, taken as the new value + max_interval: float + Maximum time interval between logging in seconds. + Logging will take place at the next log_interval after max_interval since the last logged value. + If less than log_interval, then logging values occurs every log_interval. + max_fractional_change: float + Fractional change in the value that will trigger the value to be logged + If 0, then any change in the value will be logged + If < 0, then the value will always be logged + check_field: string + Field in the dict returned by `on_get() that's used to check for a change in the fractional value + Typically is either 'value_cal' or 'value_raw' + calibration: string or dict + If string, updated with raw on_get() result via str.format() in the @calibrate decorator, + used to populate raw and calibrated values fields of a result payload. + If a dictionary, the raw result is used to index the dict with the calibrated value being the dict's value. ''' Endpoint.__init__(self, **kwargs) From d6b378cd7136869a354789313d1cea13f83b7538 Mon Sep 17 00:00:00 2001 From: "Walter C. Pettus" Date: Tue, 12 Aug 2025 12:35:15 -0400 Subject: [PATCH 3/5] Fix logging conditions for string endpoints, add absolute change condition for numeric --- dripline/core/entity.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/dripline/core/entity.py b/dripline/core/entity.py index 635b7568..fae6ae3b 100644 --- a/dripline/core/entity.py +++ b/dripline/core/entity.py @@ -69,6 +69,7 @@ def __init__(self, log_routing_key_prefix='sensor_value', log_interval=0, max_interval=0, + max_absolute_change=0, max_fractional_change=0, check_field='value_cal', calibration=None, @@ -92,10 +93,14 @@ def __init__(self, Maximum time interval between logging in seconds. Logging will take place at the next log_interval after max_interval since the last logged value. If less than log_interval, then logging values occurs every log_interval. + max_absolute_change: float + Absolute change in the numeric value that will trigger the value to be logged + If 0, then any change in the value will be logged + If < 0, then the value will always be logged (recommend instead max_interval=0) max_fractional_change: float Fractional change in the value that will trigger the value to be logged If 0, then any change in the value will be logged - If < 0, then the value will always be logged + If < 0, then the value will always be logged (recommend instead max_interval=0) check_field: string Field in the dict returned by `on_get() that's used to check for a change in the fractional value Typically is either 'value_cal' or 'value_raw' @@ -118,6 +123,7 @@ def __init__(self, self.log_interval = log_interval self._max_interval = max_interval + self._max_absolute_change = max_absolute_change self._max_fractional_change = max_fractional_change self._check_field = check_field @@ -174,20 +180,30 @@ def scheduled_log(self): result = self.on_get() try: this_value = float(result[self._check_field]) - except (TypeError, ValueError): - logger.warning(f"cannot check value change for {self.name}") - return + is_float = True + except ValueError: + is_float = False + this_value = result[self._check_field] # Various checks for log condition if self._last_log_time is None: logger.debug("Logging because this is the first logged value") elif (datetime.datetime.now(datetime.timezone.utc) - self._last_log_time).total_seconds() > self._max_interval: logger.debug("Logging because enough time has elapsed") + # Treatment of non-numeric value + elif not is_float: + if this_value != self._last_log_value: + logger.debug("Logging because the value has changed") + else: + logger.debug("No log condition met for string data, therefore not logging") + return + elif abs(self._last_log_value - this_value) > self._max_absolute_change: + logger.debug("Logging because the value has changed significantly") # this condition is |x1-x0|/(|x1+x0|/2) > max_fractional_change, but safe in case the denominator is 0 elif 2 * abs(self._last_log_value - this_value) > self._max_fractional_change * abs(self._last_log_value + this_value): - logger.debug("Logging because the value has changed significantly") + logger.debug("Logging because the value has fractionally changed significantly") else: - logger.debug("No log condition met, therefore not logging") + logger.debug("No log condition met for numeric data, therefore not logging") return self._last_log_value = this_value From e83cb0bd95a11e0403656046c654fafc4b5879ae Mon Sep 17 00:00:00 2001 From: Noah Oblath Date: Mon, 18 Aug 2025 11:31:36 -0700 Subject: [PATCH 4/5] Add changelog info to GH Release with different release step --- .github/workflows/publish.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 58a9f668..434e5e04 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -204,6 +204,16 @@ jobs: tags: ${{ steps.docker_meta_integration.outputs.tags }} platforms: linux/amd64,linux/arm64,linux/arm/v7 - - name: Release - uses: softprops/action-gh-release@v2 + - name: Release with a changelog + uses: rasmus-saks/release-a-changelog-action@v1 if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' + path: 'changelog.md' + title-template: 'dripline-python v{version} -- Release Notes' + tag-template: 'v{version}' + + # This should be removed if the use of rasmus-saks/release-a-changelog-action works + #- name: Release + # uses: softprops/action-gh-release@v2 + # if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} From ef3052c7727d94bb35e9dd8d7d3cc2db86b39bd6 Mon Sep 17 00:00:00 2001 From: Noah Oblath Date: Mon, 18 Aug 2025 11:48:34 -0700 Subject: [PATCH 5/5] Fixing workflow step version --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 434e5e04..19cabb17 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -205,7 +205,7 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 - name: Release with a changelog - uses: rasmus-saks/release-a-changelog-action@v1 + uses: rasmus-saks/release-a-changelog-action@v1.2.0 if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} with: github-token: '${{ secrets.GITHUB_TOKEN }}'