Tutorial
========

This tutorial goes through most of the library features by presenting several
facade devices with increasing complexity.

Creating and running a facade device
------------------------------------

A facade device is an enhanced pytango HLAPI device. It provides the same
methods and supports the same pytango object (device properties, attributes,
commands, etc.). In order to create a new facade device class, simply inherit
from the `Facade` base class:

.. literalinclude:: ../examples/empty.py

This example is already a working (empty) device. It is possible to run it
without a database using the `tango.test_context` module:

.. sourcecode:: console

  $ cd examples/
  $ python -m tango.test_context examples.empty.Empty --debug=3
  Ready to accept request
  Empty started on port 8888 with properties {}
  Device access: tango://hostname:8888/test/nodb/empty#dbase=no
  Server access: tango://hostname:8888/dserver/Empty/empty#dbase=no

It is now accessible through `itango`::

  In [1]: d = Device('tango://hostname:8888/test/nodb/empty#dbase=no')
  In [2]: d.state()
  Out[2]: tango._tango.DevState.UNKNOWN


Adding extra logic at initialization
------------------------------------

The default state is **UNKNOWN**. Since facade devices are regular devices,
we can change it using the `set_state` method. However, the `init_device`
method shouldn't be overridden because it performs specific exception handling.
Instead, override `safe_init_device` if you have to add some extra logic. Don't
forget to call the parent method since it performs other useful steps:

.. literalinclude:: ../examples/on.py
   :pyobject: On

Now let's check the state::

  In [2]: d.state()
  Out[2]: tango._tango.DevState.ON


Local attributes
----------------

Good, but a static state is not really useful. Instead we'd like it to react to
the values of other attributes. Let's create a device with a local counter using
the `local_attribute` object.

.. literalinclude:: ../examples/counter1.py
   :pyobject: Counter1

Note that `local_attribute` can be used as a decorator to set a default value
for the attribute, although it is not mandatory. Also, `local_attribute` (and
other facade-specific attributes) supports all the arguments of the standard
pytango attribute object (e.g. `access` and `dtype` in the example above). Now
let's try our counter::

  In [2]: d.count
  Out[2]: 0
  In [3]: d.count += 1
  In [4]: d.count
  Out[4]: 1

See how the `count` attribute has been incremented successfully. Also note that
the facade devices have a full support for events, meaning a change event has
been pushed when the `count` value has been updated (no polling is required on
the attribute).


Data model
----------

Now, instead of a writable attribute, we'd like to use a command to increment
the value of `count`. But first, we need to learn about the data model that
allows reactivity and the propagation of changes. Every facade device instance
has a graph of nodes that represents the different values that the device has
to manage. For instance, every local attribute has a corresponding node that
can be accessed through `self.graph[attr_name]`. A node can contain either:

- nothing
- a `triplet` result (value, stamp, quality)
- an exception

Accessing the node state is done through the following methods:

- `node.result() == None` if the node contains nothing
- `value, stamp, quality = node.result()` if the node contains a result
- `node.exception() == None` if the node doesn't contain an exception
- `exc = node.exception()` if the node contains an exception

Also note that calling `node.result()` on a node containing an exception
will raise the corresponding exception. The node state is set using the
following methods:

- `node.set_result(None)`
- `node.set_result(triplet(value, stamp, quality))`
- `node.set_exception(exc)`

Note that stamp and quality are optional. They respectively default to the
current time and the **VALID** quality. The `increment` tango command can
now be implemented:

.. literalinclude:: ../examples/counter2.py
   :pyobject: Counter2

Let's give it a try::

  In [2]: d.count
  Out[2]: 0
  In [3]: d.increment()
  In [4]: d.count
  Out[4]: 1


State attribute
---------------

Now, we'd like to have the state react to the value of `count`. This can be
achieved using the `state_attribute` facade object. It is used as a decorator
and takes the list of the nodes to bind to as an argument:

.. literalinclude:: ../examples/counter3.py
   :pyobject: Counter3

Note that it's possible to return the status along with the state, although it
is not mandatory. Let's run the counter::

  In [2]: d.state()
  Out[2]: tango._tango.DevState.OFF
  In [3]: d.status()
  Out[3]: 'The count is 0'
  In [4]: d.increment()
  In [5]: d.state()
  Out[5]: tango._tango.DevState.ON
  In [6]: d.status()
  Out[6]: 'The count is 1'

See how the state is updated automatically. Remember that there is no polling
or periodic update involved: the changes are simply propagated through the
device graph.


Logical attributes
------------------

`State` and `Status` are not the only attributes that can react to changes.
It is possible to declare logical attributes using the same binding approach.
Let's write a device that performs a division:

.. literalinclude:: ../examples/division1.py
   :pyobject: Division1

Here we defined the relationship `C = A / B`. Note how the arguments of the
method `C` are simply the value `A` and `B`. Let's give it a try::

  In [2]: d.A = 1
  In [3]: d.B = 4
  In [4]: d.C
  Out[4]: 0.25
  In [5]: d.B = 0
  In [6]: d.C
  PyDs_PythonError: Exception while updating node <C>:
    float division by zero

Remember that the computation of `C` does not happen when the attribute `C` is
being read but when the values of `A` and `B` are changing. For instance, the
zero division exception has been set to the node `C` right after we set `B` to
zero.

They are special rules about aggregation depending on the state of the
different input nodes:

- if node `A` or node `B` is empty, node `C` is empty too
- if node `A` or node `B` contains an exception, it's propagated to `C`
- if the quality of `A` or the quality of `B` is invalid, the quality of `C`
  is invalid
- otherwise, the `C` method is executed and the return value is used as a
  result

Note that the return value of the `C` method can be:

- a single value (timestamp and quality are computed from the input nodes)
- a `triplet` result, in order to set the timestamp and/or the quality


The triplet structure
---------------------

The triplet is a named tuple provided by the facade device. All the node
results are guaranteed to be a triplet when they exist. This is how it is
used::

  from time import time
  from tango import AttrQuality
  from facadedevice import triplet

  # A triplet from a single value
  result = triplet(1.)

  # A triplet from a value and a stamp
  result = triplet(1., stamp=time())

  # A triplet from a value and a quality
  result = triplet(1, quality=AttrQuality.ATTR_ALARM)

  # A triplet from value, a stamp and a quality
  result = triplet(1, time(), AttrQuality.ATTR_CHANGING)

  # Triplets can be unpacked
  value, stamp, quality = result

  # The values can be accessed through attributes
  result.value, result.stamp, result.quality


The default quality is **VALID** and the default stamp is the time at the
triplet creation. It has another interesting property: a `None` value will
cause the quality to be **INVALID** and an **INVALID** quality will cause
the value to be `None`. This is enforced at triplet creation.

.. warning::
   An empty node and a none (invalid) triplet can easily be confused! They
   are however very different:

   - `node.set_result(None)` empty the node
   - `node.set_result(triplet(None))` set an **INVALID** result with a
     timestamp

   The both behave differently when reading the corresponding attribute or
   when used as an input node to propagate changes.


Proxy attribute
---------------

The division device is working nicely but it doesn't really communicate with
the outside world. More precisely, the `A` and `B` might come from another
device. In this case, we can simply replace the local attributes with proxy
attributes:

.. literalinclude:: ../examples/division2.py
   :pyobject: Division2

The only special argument we need to provide a proxy attribute with is
`property_name`: its the name of the device property that will contain
the access to the remote attribute. In this case, the device properties
could be:

  - `AAttribute`: some/device/somewhere/x
  - `BAttribute`: some/other/device/y


Those remote attributes are expected to push either change or periodic events.
Facade devices have an expert command called `GetInfo` that provides extra
information about the event subscription, e.g::

  In [2]: print(d.getinfo())
  The device is currently connected.
  It subscribed to event channel of the following attribute(s):
  - some/device/somewhere/x (CHANGE_EVENT)
  - some/other/device/y (PERIODIC_EVENT)
  -----
  No errors in history since Tue Apr 25 18:26:47 2017 (last initialization).

Once properly set up, any event comming from those remote attributes will
cause `A` (or `B`) and `C` to be updated. Note that facade devices can easily
be chained together since they both publish and subscribe.

It is also possible to apply a conversion to the input data by using
`proxy_attribute` as a decorator::

  @proxy_attribute(
      dtype=float,
      property_name='AAttribute')
  def A(self, a):
      return a * 10

Here, the data coming from the event channel is multiplied by 10. Note that
the device property can also be a value if the remote attribute doesn't
exist:

.. sourcecode:: console

  $ python -m tango.test_context --prop "{'AAttribute': 1.0, 'BAttribute': 4.0}" \
    division2.Division2
  Ready to accept request
  Division2 started on port 8888 with properties {'AAttribute': 1.0, 'BAttribute': 4.0}
  Device access: tango://vinmic-t440p:8888/test/nodb/division2#dbase=no
  Server access: tango://vinmic-t440p:8888/dserver/Division2/division2#dbase=no

Let's check the values::

  In [2]: d.A = 1
  In [3]: d.B = 4
  In [4]: d.C
  Out[4]: 0.25


Combined attributes
-------------------

In some cases, it is interesting to access remote attributes in a more dynamic
way. The `facadedevice` library does not support dymanic attributes directly,
but it provides a `combined_attributes` object that can be used for similar
purposes. Let's say we'd like to compute the average of the values of an
arbitrary list of attributes:

.. literalinclude:: ../examples/average.py
   :pyobject: Average

Here, the `AttributesToAverage` device property is simply the list of all the
attributes that should be used for the computation. The attributes may come
from the same device, or different devices. If that device property is a single
line, it's used a pattern for listing the attributes. For instance, the pattern
`a/b/*/x[12]` might yield:

- a/b/c/x1
- a/b/c/x2
- a/b/whatever/x1
- a/b/whatever/x2
- etc.


It includes all the attributes called `x1` or `x2` from any device starting
with `a/b/`. Note that the aggregation works the same as for logical
attributes.

Timestamps for events and attribute readings
--------------------------------------------

Tango provides a timestamp for the value within each event, and when reading attributes.
What times will the facade device use?
In general, the timestamps come from the triplet's `stamp` field.
This is set when the node is updated.
If you don't provide a time, the current time is used.
When events are emitted, they use the node's timestamp.

For `local_attribute`, the timestamp of the event is the time the attribute was written to.
However, when reading the attribute, the timestamp will be the current time (by default).

Continuing the example from the *Local Attribute* section::

  In [5]: t1 = d.read_attribute("count").time.totime()
  In [6]: t2 = d.read_attribute("count").time.totime()
  In [7]: assert t2 > t1

This default behaviour can be changed by creating the `local_attribute` with `use_current_time=False`.

State and Status in Tango don't maintain a timestamp, so `state_attribute` will use the current time.

For a `logical_attribute`, the timestamp will be based on the nodes that it aggregates.
Note that the aggregation is only done when a dependent node is updated (not on read).
With standard aggregation, the latest timestamp of the dependent nodes is used.
With custom aggregation, it is up to you.
If you provide a triplet, then that time is used.
If you return a simple value, then the current time will be used.
If a dependent node changes, an event is emitted with time determined as above.
Reads of the attribute will not redo the aggregation of value or of quality, however the timestamp is special.
If any of the dependent nodes use the current time (e.g., default for `local_attribute`),
then the `logical_attribute` reading will always report the current time.
If none of the dependent nodes use the current time, then the reading will report the time of the last update.

For a `proxy_attribute`, events are pushed when an event is received from the proxied attribute
(i.e., the underlying attribute on a different Tango device that we subscribe to).
Such an event will reuse the timestamp of the event pushed from the proxied attribute.
Reading the attribute will also report this time,
i.e., the timestamp contained in the most recent event received.

For a `combined_attribute` the aggregation is the same as a `logical_attribute`, but we are combining
multiple proxied attributes.
With the standard aggregation, the event timestamp will be the latest timestamp received
from the proxied attribute events.
Reading the attribute will also report this time,
i.e., the most recent timestamp of the events pushed from the proxied attributes.

Why is it useful to use the current time when reading a `local_attribute` or a `logical_attribute` that depends on `local_attribute`?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Control system users like to see time-series graphs with regular updates, even if values haven't changed for weeks.
These graphs typically get their values from an archiving system, which gets timestamps from the Tango device.
The archiver relies on periodic updates from the Tango server (reading the attribute via polling).
If we keep using the old update time then the archiver cannot store the new data points since they are duplicates.

Static configuration parameters are typically implemented using `local_attribute`,
so they rarely change, thus we want to use the current time for reads.
For `proxy_attribute` and `combined_attribute` we get updates on events from those devices
and they typically represent measurements which change more often due to noise.
It is important to keep the original timestamps for these.

Proxy commands
--------------

The library also provides an interface for proxy attributes, although it doesn't
use of the concepts explained earlier (graph, node, triplets, etc.). It's simply a
helper to bind a tango command to a command on a remote device. Consider the
following example:

.. literalinclude:: ../examples/commands.py
   :pyobject: Commands

The `reset` command here simply delegates to the `ResetCommand` provided in the
device properties. It has no input argument, no return value, and the remote
command is expected to have the same interface.

The `echo` command delegates to the `EchoCommand` provided in the device properties
by passing the input string argument to the remote command and returning its
return value. Again, both interfaces are expected to match (otherwise an exception
will be raised at runtime).

It is also possible to write a remote attribute instead of running a remote command.
The `set_level` command does exactly that by setting `write_attribute=True`. Note
that value to write is directly given by the float input argument.

In some cases, we need a finer control over the command behavior. For instance, we
might need to apply some conversion before or after running the remote command. It
is then possible to use `proxy_command` as a decorator of a method implementing this
extra bit of logic.

The `identity` command in the code above is one example of that:
the remote command can only handle string, while we'd like our command to work with
integers. See how the `identity` method receives the remote command and the input
argument, and how it converts the different values to make the types match.
