Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/update tag value #573

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ target/

# MyPy cache
.mypy_cache/

# IDE confs
.idea
14 changes: 14 additions & 0 deletions PIconnect/PI.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class PIServer(object): # pylint: disable=useless-object-inheritance

Args:
server (str, optional): Name of the server to connect to, defaults to None
username (str, optional): can be used only with password as well
password (str, optional): -//-
todo: domain, auth
timeout (int, optional): the maximum seconds an operation can take

.. note::
If the specified `server` is unknown a warning is thrown and the connection
Expand All @@ -66,6 +70,7 @@ def __init__(
password=None,
domain=None,
authentication_mode=AuthenticationMode.PI_USER_AUTHENTICATION,
timeout=None,
):
if server and server not in self.servers:
message = 'Server "{server}" not found, using the default server.'
Expand All @@ -89,8 +94,14 @@ def __init__(
self._credentials = (NetworkCredential(*cred), int(authentication_mode))
else:
self._credentials = None

self.connection = self.servers.get(server, self.default_server)

if timeout:
from System import TimeSpan

self.connection.ConnectionInfo.OperationTimeOut = TimeSpan(0, 0, timeout) # hour, min, sec

def __enter__(self):
if self._credentials:
self.connection.Connect(*self._credentials)
Expand Down Expand Up @@ -223,6 +234,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, af_value_obj, update_mode, buffer_mode):
return self.pi_point.UpdateValue(af_value_obj, update_mode, buffer_mode)

def _recorded_values(self, time_range, boundary_type, filter_expression):
include_filtered_values = False
return self.pi_point.RecordedValues(
Expand Down
7 changes: 7 additions & 0 deletions PIconnect/PIAF.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,13 @@ def units_of_measurement(self):
def _current_value(self):
return self.attribute.GetValue().Value

def _update_value(self, af_value_obj, update_mode, buffer_mode):
return self.attribute.Data.UpdateValue(
af_value_obj,
update_mode,
buffer_mode,
)

def _recorded_values(self, time_range, boundary_type, filter_expression):
include_filtered_values = False
return self.attribute.Data.RecordedValues(
Expand Down
43 changes: 43 additions & 0 deletions PIconnect/PIConsts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 38 additions & 5 deletions PIconnect/PIData.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +52,8 @@
SummaryType,
TimestampCalculation,
get_enumerated_value,
UpdateMode,
BufferMode,
)


Expand All @@ -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
Expand All @@ -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::

Expand All @@ -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,
Expand All @@ -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::

Expand Down Expand Up @@ -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
Expand All @@ -187,6 +195,31 @@ def current_value(self):
Return the current value of the attribute."""
return self._current_value()

def update_value(
self,
value,
ddatetime: Optional[datetime] = None,
update_mode: UpdateMode = UpdateMode.NO_REPLACE,
buffer_mode: BufferMode = 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
ddatetime (datetime, optional): it is not possible to set future value,
it raises PIException: [-11046] Target Date in Future.

You can combine update_mode and ddatetime to change already stored value.
"""

if ddatetime:
assert ddatetime.tzinfo, "No timezone specified for the datetime passed."
ddatetime = AF.Time.AFTime(ddatetime.isoformat())

af_value_obj = AF.Asset.AFValue(value, ddatetime)
return self._update_value(af_value_obj, int(update_mode), int(buffer_mode))

def recorded_values(
self, start_time, end_time, boundary_type="inside", filter_expression=""
):
Expand Down
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
================
Expand Down
25 changes: 25 additions & 0 deletions docs/tutorials/update_value.rst
Original file line number Diff line number Diff line change
@@ -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
ddatetime 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)