Is allowed#
Sometimes you’ll want to prevent certain actions on the coffee machines. For example, you definitely don’t want people trying to brew coffee if the water level is too low - they’ll end up burning out the machine! This kind of business logic and state handling can be partially achieved with Tango’s concept of is allowed methods. Each attribute and command can have a custom method that is automatically called as a check, or guard, before invoking the attribute or command handler.
These methods must return a boolean value (true or false). If the method returns true, the Tango device can continue with reading or writing the attribute, or executing the command. If the method returns false, or raises an exception, the client is informed that their request was denied.
from tango import AttReqType, DevState
from tango.server import Device, attribute, command
MIN_WATER_REQUIRED_FOR_BREWING_L = 0.2
class MegaCoffee3k(Device):
def init_device(self):
super().init_device()
self.set_state(DevState.OFF)
self._water_level = 0.1
self._bean_levels = [82.5, 100.0]
self._brewing_temperature = 94.4
@attribute
def waterLevel(self) -> float:
print("reading water level")
return self._water_level
def is_waterLevel_allowed(self, req_type: AttReqType) -> bool:
print(f"checking if waterLevel attribute allowed: {req_type=}")
return self.get_state() == DevState.ON
@attribute(max_dim_x=2,fisallowed="is_beanLevels_allowed")
def beanLevels(self) -> list[float]:
print("reading bean levels")
return self._bean_levels
def is_beanLevels_allowed(self, req_type: AttReqType) -> bool:
print(f"checking if beanLevels attribute allowed: {req_type=}")
return self.get_state() == DevState.ON
@attribute
def brewingTemperature(self) -> float:
print("reading brewing temperature")
return self._brewing_temperature
@brewingTemperature.setter
def brewingTemperature(self, temperature: float):
print("writing brewing temperature ")
self._brewing_temperature = temperature
@brewingTemperature.is_allowed
def brewingTemperature(self, req_type: AttReqType) -> bool:
print(f"checking if brewing temperature allowed: {req_type=}")
state = self.get_state()
if state != DevState.ON:
action = "check" if req_type == AttReqType.READ_REQ else "change"
raise RuntimeError(
f"Cannot {action} settings! Machine is {state},"
f" but needs to be {DevState.ON}. Try the On() command."
)
else:
return True
@command
def Brew(self):
print("brewing coffee!")
def is_Brew_allowed(self) -> bool:
if self._water_level < MIN_WATER_REQUIRED_FOR_BREWING_L:
raise RuntimeError(
f"Sorry, not enough water to brew your coffee!"
f" There is {self._water_level*1000:.0f} ml, but we need"
f" at least {MIN_WATER_REQUIRED_FOR_BREWING_L * 1000:.0f} ml."
f" Add more water. Quick!"
)
return True
@command
def On(self):
self.set_state(DevState.ON)
@command
def Off(self):
self.set_state(DevState.OFF)
if __name__ == "__main__":
MegaCoffee3k.run_server()
Sorry, still TODO!
Sorry, still TODO!
The convention for naming these guard methods follows the pattern is_<name>_allowed.
Where <name> is replaced with the Tango attribute or command name.
In Python high-level devices there are a few ways to link these guard methods to a specific attribute or command:
Naming convention. Methods named according to the convention above are automatically discovered and used. For example, the
is_waterLevel_allowedmethod. This is called whenever a client tries to read thewaterLevelattribute. You also seeis_Brew_allowedwhich will be called when theBrewcommand is attempted.The
fisallowedkeyword argument. It can name the method on the device class as a string, like"is_beanLevels_allowed", for thebeanLevelsattribute. If the guard function is already available (defined before the class), then it can be used directly instead of a string.Attribute decorator
.is_allowed. This is used like@brewingTemperature.is_allowed, linking the decorated method to thebrewingTemperatureattribute. There is no equivalent for commands.
Run this example, and in a second terminal, use the device proxy client to check if it is working:
>>> dp.waterLevel
...
tango._tango.DevFailed: DevFailed[
DevError[
desc = It is currently not allowed to read attribute waterLevel
origin = void Tango::Device_3Impl::read_attributes_no_except(const Tango::DevVarStringArray &, Tango::AttributeIdlData &, bool, std::vector<long> &) at (/Users/runner/miniforge3/conda-bld/cpptango_1758200193404/work/src/server/device_3.cpp:983)
reason = API_AttrNotAllowed
severity = ERR
],
DevError[
desc = Failed to read_attribute on device test/nodb/megacoffee3k, attribute waterLevel
origin = virtual DeviceAttribute Tango::DeviceProxy::read_attribute(const std::string &) at (/Users/runner/miniforge3/conda-bld/cpptango_1758200193404/work/src/client/devapi_base.cpp:6285)
reason = API_AttributeFailed
severity = ERR
]
]
Notice that it doesn’t tell use why you cannot read the waterLevel.
It just has a generic “it is currently not allowed” message.
This is what happens if you simply return false from an is-allowed method - Tango cannot give us any further info.
If you want to provide the user with more information, you should raise an exception in the
is-allowed method. That is what happens with brewingTemperature:
>>> dp.brewingTemperature
...
tango._tango.DevFailed: DevFailed[
DevError[
desc = RuntimeError: Cannot check settings! Machine is OFF, but needs to be ON. Try the On() command.
origin = Traceback (most recent call last):
...
reason = PyDs_PythonError
severity = ERR
],
DevError[
desc = Failed to read_attribute on device test/nodb/megacoffee3k, attribute brewingTemperature
origin = virtual DeviceAttribute Tango::DeviceProxy::read_attribute(const std::string &) at (/Users/runner/miniforge3/conda-bld/cpptango_1758200193404/work/src/client/devapi_base.cpp:6285)
reason = API_AttributeFailed
severity = ERR
]
]
>>> dp.brewingTemperature = 95
Traceback (most recent call last):
...
The is-allowed method for attributes gets the request type as a parameter.
This means you can have different behaviour depending on what the caller is doing.
Maybe you always want to allow reading, but have some rules limiting writing.
In the example code, the req_type parameter is checked.
@brewingTemperature.is_allowed
def brewingTemperature(self, req_type: AttReqType) -> bool:
print(f"checking if brewing temperature allowed: {req_type=}")
state = self.get_state()
if state != DevState.ON:
action = "check" if req_type == AttReqType.READ_REQ else "change"
raise RuntimeError(
f"Cannot {action} settings! Machine is {state},"
f" but needs to be {DevState.ON}. Try the On() command."
)
else:
return True
Sorry, still TODO!
Sorry, still TODO!
You can see the device server prints out that the is-allowed methods were called:
$ python -m tango.test_context main.MegaCoffee3k --host 127.0.0.1
Ready to accept request
...
checking if waterLevel attribute allowed: req_type=<AttReqType.READ_REQ: 0>
checking if brewing temperature allowed: req_type=<AttReqType.READ_REQ: 0>
checking if brewing temperature allowed: req_type=<AttReqType.WRITE_REQ: 1>
If you turn the device on first, then the attributes can be read:
>>> dp.On()
>>> dp.waterLevel
0.1
>>> dp.brewingTemperature
94.4
When you use the Brew command, it fails too, as expected.
>>> dp.Brew()
...
tango._tango.DevFailed: DevFailed[
DevError[
desc = RuntimeError: Sorry, not enough water to brew your coffee! There is 100 ml, but we need at least 200 ml. Add more water. Quick!
origin = Traceback (most recent call last):
...
reason = PyDs_PythonError
severity = ERR
],
DevError[
desc = Failed to execute command_inout on device test/nodb/megacoffee3k, command Brew
origin = virtual DeviceData Tango::Connection::command_inout(const std::string &, const DeviceData &) at (/Users/runner/miniforge3/conda-bld/cpptango_1758200193404/work/src/client/devapi_base.cpp:2029)
reason = API_CommandFailed
severity = ERR
]
]
Hooray - our coffee machines are a bit safer!