Skip to content

Commit 2173531

Browse files
committed
added limits to Int and Float
1 parent 1ece1a6 commit 2173531

File tree

4 files changed

+80
-18
lines changed

4 files changed

+80
-18
lines changed

src/fastcs/attributes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from enum import Enum
44
from typing import Any, Generic, Protocol, runtime_checkable
55

6-
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
6+
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T, validate_value
77

88

99
class AttrMode(Enum):
@@ -115,7 +115,7 @@ def get(self) -> T:
115115
return self._value
116116

117117
async def set(self, value: T) -> None:
118-
self._value = self._datatype.dtype(value)
118+
self._value = self._datatype.dtype(validate_value(self._datatype, value))
119119

120120
if self._update_callback is not None:
121121
await self._update_callback(self._value)
@@ -202,6 +202,6 @@ def __init__(
202202
)
203203

204204
async def process(self, value: T) -> None:
205-
await self.set(value)
205+
await self.set(validate_value(self._datatype, value))
206206

207207
await super().process(value) # type: ignore

src/fastcs/backends/epics/ioc.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,26 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
330330
match attribute.datatype:
331331
case Bool(znam, onam):
332332
return builder.boolIn(pv, ZNAM=znam, ONAM=onam, **attribute_fields)
333-
case Int():
334-
return builder.longIn(pv, EGU=attribute.datatype.units, **attribute_fields)
335-
case Float(prec):
333+
case Int(units, min, max, min_alarm, max_alarm):
334+
return builder.longIn(
335+
pv,
336+
EGU=units,
337+
DRVL=min,
338+
DRVH=max,
339+
LOPR=min_alarm,
340+
HOPR=max_alarm,
341+
**attribute_fields,
342+
)
343+
case Float(prec, units, min, max, min_alarm, max_alarm):
336344
return builder.aIn(
337-
pv, EGU=attribute.datatype.units, PREC=prec, **attribute_fields
345+
pv,
346+
PREC=prec,
347+
EGU=units,
348+
DRVL=min,
349+
DRVH=max,
350+
LOPR=min_alarm,
351+
HOPR=max_alarm,
352+
**attribute_fields,
338353
)
339354
case String():
340355
return builder.longStringIn(pv, **attribute_fields)
@@ -370,21 +385,29 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
370385
always_update=True,
371386
on_update=on_update,
372387
)
373-
case Int(units=units):
388+
case Int(units, min, max, min_alarm, max_alarm):
374389
return builder.longOut(
375390
pv,
376391
always_update=True,
377392
on_update=on_update,
378393
EGU=units,
394+
DRVL=min,
395+
DRVH=max,
396+
LOPR=min_alarm,
397+
HOPR=max_alarm,
379398
**attribute_fields,
380399
)
381-
case Float(prec=prec, units=units):
400+
case Float(prec, units, min, max, min_alarm, max_alarm):
382401
return builder.aOut(
383402
pv,
384403
always_update=True,
385404
on_update=on_update,
386-
EGU=units,
387405
PREC=prec,
406+
EGU=units,
407+
DRVL=min,
408+
DRVH=max,
409+
LOPR=min_alarm,
410+
HOPR=max_alarm,
388411
**attribute_fields,
389412
)
390413
case String():

src/fastcs/datatypes.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class Int(DataType[int]):
2626
"""`DataType` mapping to builtin ``int``."""
2727

2828
units: str | None = None
29+
min: int | None = None
30+
max: int | None = None
31+
min_alarm: int | None = None
32+
max_alarm: int | None = None
2933

3034
@property
3135
def dtype(self) -> type[int]:
@@ -38,6 +42,10 @@ class Float(DataType[float]):
3842

3943
prec: int = 2
4044
units: str | None = None
45+
min: float | None = None
46+
max: float | None = None
47+
min_alarm: float | None = None
48+
max_alarm: float | None = None
4149

4250
@property
4351
def dtype(self) -> type[float]:
@@ -63,3 +71,15 @@ class String(DataType[str]):
6371
@property
6472
def dtype(self) -> type[str]:
6573
return str
74+
75+
76+
def validate_value(datatype: DataType[T], value: T) -> T:
77+
"""Validate a value against a datatype."""
78+
79+
if isinstance(datatype, (Int | Float)):
80+
assert isinstance(value, (int | float)), f"Value {value} is not a number"
81+
if datatype.min is not None and value < datatype.min:
82+
raise ValueError(f"Value {value} is less than minimum {datatype.min}")
83+
if datatype.max is not None and value > datatype.max:
84+
raise ValueError(f"Value {value} is greater than maximum {datatype.max}")
85+
return value

tests/backends/epics/test_ioc.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ def test_get_output_record_raises(mocker: MockerFixture):
233233
_get_output_record("PV", mocker.MagicMock(), on_update=mocker.MagicMock())
234234

235235

236+
DEFAULT_SCALAR_FIELD_ARGS = {
237+
"EGU": None,
238+
"DRVL": None,
239+
"DRVH": None,
240+
"LOPR": None,
241+
"HOPR": None,
242+
}
243+
244+
236245
def test_ioc(mocker: MockerFixture, mapping: Mapping):
237246
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
238247
add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info")
@@ -244,21 +253,26 @@ def test_ioc(mocker: MockerFixture, mapping: Mapping):
244253

245254
# Check records are created
246255
builder.boolIn.assert_called_once_with(f"{DEVICE}:ReadBool", ZNAM="OFF", ONAM="ON")
247-
builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", EGU=None)
256+
builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", **DEFAULT_SCALAR_FIELD_ARGS)
248257
builder.aIn.assert_called_once_with(
249-
f"{DEVICE}:ReadWriteFloat_RBV", EGU=None, PREC=2
258+
f"{DEVICE}:ReadWriteFloat_RBV", PREC=2, **DEFAULT_SCALAR_FIELD_ARGS
250259
)
251260
builder.aOut.assert_any_call(
252261
f"{DEVICE}:ReadWriteFloat",
253262
always_update=True,
254263
on_update=mocker.ANY,
255-
EGU=None,
256264
PREC=2,
265+
**DEFAULT_SCALAR_FIELD_ARGS,
266+
)
267+
builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", **DEFAULT_SCALAR_FIELD_ARGS)
268+
builder.longIn.assert_any_call(
269+
f"{DEVICE}:ReadWriteInt_RBV", **DEFAULT_SCALAR_FIELD_ARGS
257270
)
258-
builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", EGU=None)
259-
builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV", EGU=None)
260271
builder.longOut.assert_called_with(
261-
f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY, EGU=None
272+
f"{DEVICE}:ReadWriteInt",
273+
always_update=True,
274+
on_update=mocker.ANY,
275+
**DEFAULT_SCALAR_FIELD_ARGS,
262276
)
263277
builder.mbbIn.assert_called_once_with(
264278
f"{DEVICE}:StringEnum_RBV", ZRST="red", ONST="green", TWST="blue"
@@ -417,9 +431,14 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
417431

418432
short_pv_name = "attr_rw_short_name".title().replace("_", "")
419433
builder.longOut.assert_called_once_with(
420-
f"{DEVICE}:{short_pv_name}", always_update=True, on_update=mocker.ANY, EGU=None
434+
f"{DEVICE}:{short_pv_name}",
435+
always_update=True,
436+
on_update=mocker.ANY,
437+
**DEFAULT_SCALAR_FIELD_ARGS,
438+
)
439+
builder.longIn.assert_called_once_with(
440+
f"{DEVICE}:{short_pv_name}_RBV", **DEFAULT_SCALAR_FIELD_ARGS
421441
)
422-
builder.longIn.assert_called_once_with(f"{DEVICE}:{short_pv_name}_RBV", EGU=None)
423442

424443
long_pv_name = long_attr_name.title().replace("_", "")
425444
with pytest.raises(AssertionError):

0 commit comments

Comments
 (0)