diff --git a/PIconnect/PI.py b/PIconnect/PI.py index fdc14927..8d77d98d 100644 --- a/PIconnect/PI.py +++ b/PIconnect/PI.py @@ -97,6 +97,7 @@ def __init__( self._credentials = (NetworkCredential(*cred), int(authentication_mode)) else: self._credentials = None + self.connection = self.servers.get(server, self.default_server) if timeout: @@ -264,6 +265,9 @@ def _current_value(self): """Return the last recorded value for this PI Point (internal use only).""" return self.pi_point.CurrentValue().Value + def _update_value(self, value, update_mode, buffer_mode): + return self.pi_point.UpdateValue(value, update_mode, buffer_mode) + def _recorded_values(self, time_range, boundary_type, filter_expression): include_filtered_values = False return self.pi_point.RecordedValues( diff --git a/PIconnect/PIAF.py b/PIconnect/PIAF.py index 3d2e059f..88f99055 100644 --- a/PIconnect/PIAF.py +++ b/PIconnect/PIAF.py @@ -248,6 +248,13 @@ def units_of_measurement(self): def _current_value(self): return self.attribute.GetValue().Value + def _update_value(self, value, update_mode, buffer_mode): + return self.attribute.Data.UpdateValue( + value, + update_mode, + buffer_mode, + ) + def _recorded_values(self, time_range, boundary_type, filter_expression): include_filtered_values = False return self.attribute.Data.RecordedValues( diff --git a/PIconnect/PIConsts.py b/PIconnect/PIConsts.py index 69095f7e..94743c46 100644 --- a/PIconnect/PIConsts.py +++ b/PIconnect/PIConsts.py @@ -6,6 +6,49 @@ IntFlag = IntEnum +class UpdateMode(IntEnum): + """Indicates how to treat duplicate values in the archive, when supported by the Data Reference. + + Detailed information is available at https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/T_OSIsoft_AF_Data_AFUpdateOption.htm + """ + + #: Add the value to the archive. + #: If any values exist at the same time, will overwrite one of them and set its Substituted flag. + REPLACE = 0 + #: Add the value to the archive. Any existing values at the same time are not overwritten. + INSERT = 1 + #: Add the value to the archive only if no value exists at the same time. + #: If a value already exists for that time, the passed value is ignored. + NO_REPLACE = 2 + #: Replace an existing value in the archive at the specified time. + #: If no existing value is found, the passed value is ignored. + REPLACE_ONLY = 3 + #: Add the value to the archive without compression. + #: If this value is written to the snapshot, the previous snapshot value will be written to the archive, + #: without regard to compression settings. + #: Note that if a subsequent snapshot value is written without the InsertNoCompression option, + #: the value added with the InsertNoCompression option is still subject to compression. + INSERT_NO_COMPRESSION = 5 + #: Remove the value from the archive if a value exists at the passed time. + REMOVE = 6 + + +class BufferMode(IntEnum): + """Indicates buffering option in updating values, when supported by the Data Reference. + + Detailed information is available at https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/T_OSIsoft_AF_Data_AFBufferOption.htm + """ + + #: Updating data reference values without buffer. + DO_NOT_BUFFER = 0 + #: Try updating data reference values with buffer. + #: If fails (e.g. data reference AFDataMethods does not support Buffering, or its Buffering system is not available), + #: then try updating directly without buffer. + BUFFER_IF_POSSIBLE = 1 + # Updating data reference values with buffer. + BUFFER = 2 + + class AuthenticationMode(IntEnum): """AuthenticationMode indicates how a user authenticates to a PI Server diff --git a/PIconnect/PIData.py b/PIconnect/PIData.py index 0ece90af..7b18966e 100644 --- a/PIconnect/PIData.py +++ b/PIconnect/PIData.py @@ -30,7 +30,8 @@ # pragma pylint: enable=unused-import -import datetime +from datetime import datetime +from typing import Optional try: from abc import ABC, abstractmethod @@ -51,6 +52,8 @@ SummaryType, TimestampCalculation, get_enumerated_value, + UpdateMode, + BufferMode, ) @@ -61,7 +64,7 @@ class PISeries(Series): Args: tag (str): Name of the new series - timestamp (List[datetime.datetime]): List of datetime objects to + timestamp (List[datetime]): List of datetime objects to create the new index value (List): List of values for the timeseries, should be equally long as the `timestamp` argument @@ -83,7 +86,7 @@ def __init__(self, tag, timestamp, value, uom=None, *args, **kwargs): @staticmethod def timestamp_to_index(timestamp): - """Convert AFTime object to datetime.datetime in local timezone. + """Convert AFTime object to datetime in local timezone. .. todo:: @@ -95,7 +98,7 @@ def timestamp_to_index(timestamp): """ local_tz = pytz.timezone(PIConfig.DEFAULT_TIMEZONE) return ( - datetime.datetime( + datetime( timestamp.Year, timestamp.Month, timestamp.Day, @@ -112,7 +115,8 @@ def timestamp_to_index(timestamp): class PISeriesContainer(ABC): """PISeriesContainer - General class for objects that return :class:`PISeries` objects + With the ABC class we represent a general behaviour with PI Point object + (General class for objects that return :class:`PISeries` objects). .. todo:: @@ -172,6 +176,10 @@ def _filtered_summaries( def _current_value(self): pass + @abstractmethod + def _update_value(self, value, update_mode, buffer_mode): + pass + @abstractmethod def name(self): pass @@ -187,6 +195,30 @@ def current_value(self): Return the current value of the attribute.""" return self._current_value() + def update_value( + self, + value, + time=None, + update_mode=UpdateMode.NO_REPLACE, + buffer_mode=BufferMode.BUFFER_IF_POSSIBLE, + ): + """Update value for existing PI object. + + Args: + value: value type should be in cohesion with PI object or + it will raise PIException: [-10702] STATE Not Found + time (datetime, optional): it is not possible to set future value, + it raises PIException: [-11046] Target Date in Future. + + You can combine update_mode and time to change already stored value. + """ + + if time: + time = AF.Time.AFTime(time.isoformat()) + + value = AF.Asset.AFValue(value, time) + return self._update_value(value, int(update_mode), int(buffer_mode)) + def recorded_values( self, start_time, end_time, boundary_type="inside", filter_expression="" ): diff --git a/README.rst b/README.rst index cbd43b75..88d8cc6b 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,12 @@ Python connector to OSIsoft PI SDK Features -------- -* TODO +* Get PI tag value(s) from both: PI Server or PIAF Database + * recorded values + * time interpolated values +* Update tag value +* Summarize data before extract in OSIsoft PI SDK +* Filter data as well Copyright notice ================ diff --git a/docs/tutorials/update_value.rst b/docs/tutorials/update_value.rst new file mode 100644 index 00000000..1ed909a9 --- /dev/null +++ b/docs/tutorials/update_value.rst @@ -0,0 +1,25 @@ +############################## +Update value in PI +############################## + +Writing a value back to PI using Python is a interesting feature. +Having this capability we can use PIconnect for implementing collecting data +process it someway (e.g. a prediction model) and write back the results someway +it can be used by final users + +After discussion with @Hugovdberg & with contribution of @ldariva we finally implemented an interface for the AFSDK UpdateValue method with 4 parameters +value as AFValue +time as python datetime.datetime with specified timezone +replace_option as AFUpdateOption +buffer_option as AFBufferOption. + + +.. code-block:: python + from datetime import datetime + import pytz + import PIconnect as PI + from PIconnect.PIConsts import UpdateMode, BufferMode + tag = 'foo' + with PI.PIServer(server='foo') as server: + point = server.search(tag)[0] + point.update_value(1.0, datetime.now(pytz.utc), UpdateMode.NO_REPLACE, BufferMode.BUFFER_IF_POSSIBLE)