Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
spacemanspiff2007 committed Apr 23, 2024
1 parent 10e02b4 commit 0298186
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 97 deletions.
45 changes: 35 additions & 10 deletions docs/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,21 @@ Example
min %: 10
Heartbeat Filter
Throttle Filter
--------------------------------------

.. autopydantic_model:: HeartbeatFilter
.. autopydantic_model:: ThrottleFilter
:inherited-members: BaseModel


Example
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..
YamlModel: HeartbeatFilter
YamlModel: ThrottleFilter
.. code-block:: yaml
heartbeat filter: 60
throttle filter: 60
Actions
Expand All @@ -121,21 +121,21 @@ Example
refresh action: 01:30:00
Throttle Action
Heartbeat Action
--------------------------------------

.. autopydantic_model:: ThrottleAction
.. autopydantic_model:: HeartbeatAction
:inherited-members: BaseModel


Example
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..
YamlModel: ThrottleAction
YamlModel: HeartbeatAction
.. code-block:: yaml
throttle action: 30
heartbeat action: 30
Math
======================================
Expand Down Expand Up @@ -366,7 +366,7 @@ Example
or:
- type: change filter
- heartbeat filter: 60
- heartbeat action: 60
Sequence
Expand Down Expand Up @@ -398,7 +398,8 @@ These are some examples for sml value configurations
Energy consumption today
--------------------------------------

This will report the power consumption of today
This will report the power consumption of today.
The first reported value every day will be 0 and then it will increase for every day.

..
YamlModel: SmlValueConfig
Expand All @@ -416,3 +417,27 @@ This will report the power consumption of today
- round: 1
- type: change filter # Only report on changes
- refresh action: 01:00 # ... but refresh every hour
Downsample current power
--------------------------------------

This will report a power value every max every 30s.
The reported value will be the weighted mean value of the last 30s.

..
YamlModel: SmlValueConfig
.. code-block:: yaml
obis: '0100100700ff' # Obis code for the energy meter
mqtt:
topic: power # MQTT topic for the meter
operations:
- type: mean interval
interval: 30
wait for data: False
- throttle filter: 30 # Let a value pass every 30s
- round: 1
- type: change filter # Only report on changes
- refresh action: 01:00 # ... but refresh every hour
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ pytest-asyncio == 0.23.6
aioresponses == 0.7.6

# Linter
ruff == 0.3.7
ruff == 0.4.1
2 changes: 1 addition & 1 deletion requirements_setup.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ pyserial-asyncio == 0.6
easyconfig == 0.3.2
pydantic == 2.7.0
smllib == 1.4
aiohttp == 3.9.4
aiohttp == 3.9.5
28 changes: 15 additions & 13 deletions src/sml2mqtt/config/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,11 @@ def _check_set(self) -> DeltaFilter:
return self


class HeartbeatFilter(BaseModel):
"""A filter which lets a value pass periodically every specified interval."""

every: DurationType = Field(
alias='heartbeat filter',
description='Interval'
)
class ThrottleFilter(BaseModel):
"""Filter which only lets one value pass in the defined period. If the last passed value is not at least
``period`` old any new value will not be forwarded.
"""
period: DurationType = Field(alias='throttle filter', description='Throttle period')


# -------------------------------------------------------------------------------------------------
Expand All @@ -87,12 +85,16 @@ class RefreshAction(BaseModel):
every: DurationType = Field(alias='refresh action', description='Refresh interval')


class ThrottleAction(BaseModel):
"""Action which only lets one value pass in the defined period. If the last passed value is not at least
``period`` old any new value will not be forwarded.
class HeartbeatAction(BaseModel):
"""Action which lets a value pass periodically every specified interval.
When no value is received (e.g. because an earlier filter blocks)
this action will produce the last received value every interval.
"""
period: DurationType = Field(alias='throttle action', description='Throttle period')

every: DurationType = Field(
alias='heartbeat action',
description='Interval'
)

# -------------------------------------------------------------------------------------------------
# Math
Expand Down Expand Up @@ -259,8 +261,8 @@ class MeanOfInterval(HasIntervalFields):
# -------------------------------------------------------------------------------------------------

OperationsModels = (
OnChangeFilter, DeltaFilter, HeartbeatFilter, RangeFilter,
RefreshAction, ThrottleAction,
OnChangeFilter, DeltaFilter, HeartbeatAction, RangeFilter,
RefreshAction, ThrottleFilter,
Factor, Offset, Round,
NegativeOnEnergyMeterWorkaround,
Or, Sequence,
Expand Down
4 changes: 2 additions & 2 deletions src/sml2mqtt/sml_value/operations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from .actions import RefreshActionOperation, ThrottleActionOperation
from .actions import HeartbeatActionOperation, RefreshActionOperation
from .date_time import DateTimeFinder, MaxValueOperation, MinValueOperation, VirtualMeterOperation
from .filter import (
DeltaFilterOperation,
HeartbeatFilterOperation,
OnChangeFilterOperation,
RangeFilterOperation,
SkipZeroMeterOperation,
ThrottleFilterOperation,
)
from .math import FactorOperation, OffsetOperation, RoundOperation
from .operations import OrOperation, SequenceOperation
Expand Down
22 changes: 11 additions & 11 deletions src/sml2mqtt/sml_value/operations/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,26 @@ def describe(self, indent: str = '') -> Generator[str, None, None]:
yield f'{indent:s}- Refresh Action: {format_period(self.every)}'


class ThrottleActionOperation(ValueOperationBase):
def __init__(self, period: DurationType):
self.period: Final = get_duration(period)
class HeartbeatActionOperation(ValueOperationBase):
def __init__(self, every: DurationType):
self.every: Final = get_duration(every)
self.last_time: float = -1_000_000_000
self.last_value: float | None = None

@override
def process_value(self, value: float | None, info: SmlValueInfo) -> float | None:
if value is None:
return None
if value is not None:
self.last_value = value

now = monotonic()
if self.last_time + self.period > now:
if monotonic() - self.last_time < self.every:
return None

self.last_time = now
return value
self.last_time = monotonic()
return self.last_value

def __repr__(self):
return f'<ThrottleAction: {self.period}s at 0x{id(self):x}>'
return f'<HeartbeatAction: {self.every}s at 0x{id(self):x}>'

@override
def describe(self, indent: str = '') -> Generator[str, None, None]:
yield f'{indent:s}- Throttle Action: {format_period(self.period)}'
yield f'{indent:s}- Heartbeat Action: {format_period(self.every)}'
24 changes: 12 additions & 12 deletions src/sml2mqtt/sml_value/operations/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing_extensions import override

from sml2mqtt.const import DurationType, get_duration
from sml2mqtt.sml_value.base import SmlValueInfo, ValueOperationBase, ValueOperationWithStartupBase
from sml2mqtt.sml_value.base import SmlValueInfo, ValueOperationBase
from sml2mqtt.sml_value.operations._helper import format_period


Expand Down Expand Up @@ -123,26 +123,26 @@ def describe(self, indent: str = '') -> Generator[str, None, None]:
yield f'{indent:s}- Zero Meter Filter'


class HeartbeatFilterOperation(ValueOperationBase):
def __init__(self, every: DurationType):
self.every: Final = get_duration(every)
class ThrottleFilterOperation(ValueOperationBase):
def __init__(self, period: DurationType):
self.period: Final = get_duration(period)
self.last_time: float = -1_000_000_000
self.last_value: float | None = None

@override
def process_value(self, value: float | None, info: SmlValueInfo) -> float | None:
if value is not None:
self.last_value = value
if value is None:
return None

if monotonic() - self.last_time < self.every:
now = monotonic()
if self.last_time + self.period > now:
return None

self.last_time = monotonic()
return self.last_value
self.last_time = now
return value

def __repr__(self):
return f'<HeartbeatFilter: {self.every}s at 0x{id(self):x}>'
return f'<ThrottleFilter: {self.period}s at 0x{id(self):x}>'

@override
def describe(self, indent: str = '') -> Generator[str, None, None]:
yield f'{indent:s}- Heartbeat Filter: {format_period(self.every)}'
yield f'{indent:s}- Throttle Filter: {format_period(self.period)}'
12 changes: 6 additions & 6 deletions src/sml2mqtt/sml_value/setup_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sml2mqtt.config.operations import (
DeltaFilter,
Factor,
HeartbeatFilter,
HeartbeatAction,
MaxOfInterval,
MaxValue,
MeanOfInterval,
Expand All @@ -21,14 +21,14 @@
RefreshAction,
Round,
Sequence,
ThrottleAction,
ThrottleFilter,
VirtualMeter,
)
from sml2mqtt.sml_value.base import OperationContainerBase, ValueOperationBase
from sml2mqtt.sml_value.operations import (
DeltaFilterOperation,
FactorOperation,
HeartbeatFilterOperation,
HeartbeatActionOperation,
MaxOfIntervalOperation,
MaxValueOperation,
MeanOfIntervalOperation,
Expand All @@ -42,7 +42,7 @@
RefreshActionOperation,
RoundOperation,
SequenceOperation,
ThrottleActionOperation,
ThrottleFilterOperation,
VirtualMeterOperation,
)

Expand All @@ -65,11 +65,11 @@ def create_sequence(operations: list[OperationsType]):

MAPPING = {
OnChangeFilter: OnChangeFilterOperation,
HeartbeatFilter: HeartbeatFilterOperation,
HeartbeatAction: HeartbeatActionOperation,
DeltaFilter: DeltaFilterOperation,

RefreshAction: RefreshActionOperation,
ThrottleAction: ThrottleActionOperation,
ThrottleFilter: ThrottleFilterOperation,

Factor: FactorOperation,
Offset: OffsetOperation,
Expand Down
2 changes: 1 addition & 1 deletion tests/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_error_message():
'operations': [
{'negative on energy meter status': True},
{'factor': 3}, {'offset': 100}, {'round': 2},
{'or': [{'change filvter': True}, {'heartbeat filter': 120}]}
{'or': [{'change filvter': True}, {'heartbeat action': 120}]}
]
})

Expand Down
33 changes: 20 additions & 13 deletions tests/sml_values/test_operations/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from tests.sml_values.test_operations.helper import check_description, check_operation_repr

from sml2mqtt.sml_value.operations import RefreshActionOperation, ThrottleActionOperation
from sml2mqtt.sml_value.operations import HeartbeatActionOperation, RefreshActionOperation
from sml2mqtt.sml_value.operations._helper import format_period


Expand Down Expand Up @@ -33,20 +33,27 @@ def test_refresh_action(monotonic):
assert f.process_value(None, None) == 2


def test_throttle_action(monotonic):
f = ThrottleActionOperation(30)
def test_heartbeat_action(monotonic):
f = HeartbeatActionOperation(30)
check_operation_repr(f, '30s')
check_description(f, '- Throttle Action: 30 seconds')

assert f.process_value(None, None) is None
assert f.process_value(1, None) == 1

monotonic.set(29.99)
assert f.process_value(1, None) is None
monotonic.set(30)
assert f.process_value(1, None) == 1
monotonic.add(15)
assert f.process_value(2, None) is None

monotonic.set(59.99)
assert f.process_value(1, None) is None
monotonic.set(60)
assert f.process_value(1, None) == 1
monotonic.add(14.99)
assert f.process_value(3, None) is None

monotonic.add(0.01)
assert f.process_value(None, None) == 3
assert f.process_value(2, None) is None

monotonic.add(30.01)
assert f.process_value(5, None) == 5
assert f.process_value(5, None) is None

check_description(
HeartbeatActionOperation(30),
'- Heartbeat Action: 30 seconds'
)
Loading

0 comments on commit 0298186

Please sign in to comment.