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/accept datetime on retrieve #574

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ target/
# MyPy cache
.mypy_cache/

# IDE confs
# Pycharm
.idea
venv/
9 changes: 4 additions & 5 deletions PIconnect/PI.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from PIconnect._utils import classproperty
from PIconnect.AFSDK import AF
from PIconnect.PIConsts import AuthenticationMode
from PIconnect.PIData import PISeries, PISeriesContainer
from PIconnect.PIData import PISeriesContainer
from PIconnect.time import timestamp_to_index

_NOTHING = object()

Expand Down Expand Up @@ -103,7 +104,7 @@ def __init__(
if timeout:
from System import TimeSpan

# TimeSpan arguments: hours, minutes, seconds
# System.TimeSpan(hours, minutes, seconds)
self.connection.ConnectionInfo.OperationTimeOut = TimeSpan(0, 0, timeout)

@classproperty
Expand Down Expand Up @@ -221,9 +222,7 @@ def __repr__(self):
@property
def last_update(self):
"""Return the time at which the last value for this PI Point was recorded."""
return PISeries.timestamp_to_index(
self.pi_point.CurrentValue().Timestamp.UtcTime
)
return timestamp_to_index(self.pi_point.CurrentValue().Timestamp.UtcTime)

@property
def raw_attributes(self):
Expand Down
5 changes: 3 additions & 2 deletions PIconnect/PIAF.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@

from PIconnect._operators import OPERATORS, add_operators
from PIconnect.AFSDK import AF
from PIconnect.PIData import PISeries, PISeriesContainer
from PIconnect.PIData import PISeriesContainer
from PIconnect.time import timestamp_to_index
from PIconnect._utils import classproperty

_NOTHING = object()
Expand Down Expand Up @@ -238,7 +239,7 @@ def description(self):
@property
def last_update(self):
"""Return the time at which the current_value was last updated."""
return PISeries.timestamp_to_index(self.attribute.GetValue().Timestamp.UtcTime)
return timestamp_to_index(self.attribute.GetValue().Timestamp.UtcTime)

@property
def units_of_measurement(self):
Expand Down
71 changes: 21 additions & 50 deletions PIconnect/PIData.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
# pragma pylint: enable=unused-import

from datetime import datetime
from typing import Optional

try:
from abc import ABC, abstractmethod
Expand All @@ -41,11 +40,9 @@

ABC = ABCMeta(BuiltinStr("ABC"), (object,), {"__slots__": ()})

import pytz
from pandas import DataFrame, Series

from PIconnect.AFSDK import AF
from PIconnect.config import PIConfig
from PIconnect.PIConsts import (
CalculationBasis,
ExpressionSampleType,
Expand All @@ -55,6 +52,7 @@
UpdateMode,
BufferMode,
)
from PIconnect.time import to_af_time_range, timestamp_to_index


class PISeries(Series):
Expand Down Expand Up @@ -84,33 +82,6 @@ def __init__(self, tag, timestamp, value, uom=None, *args, **kwargs):
self.tag = tag
self.uom = uom

@staticmethod
def timestamp_to_index(timestamp):
"""Convert AFTime object to datetime in local timezone.

.. todo::

Allow to define timezone, default to UTC?

.. todo::

Move outside as separate function?
"""
local_tz = pytz.timezone(PIConfig.DEFAULT_TIMEZONE)
return (
datetime(
timestamp.Year,
timestamp.Month,
timestamp.Day,
timestamp.Hour,
timestamp.Minute,
timestamp.Second,
timestamp.Millisecond * 1000,
)
.replace(tzinfo=pytz.utc)
.astimezone(local_tz)
)


class PISeriesContainer(ABC):
"""PISeriesContainer
Expand Down Expand Up @@ -246,11 +217,11 @@ def recorded_values(
filtered values are always left out entirely.

Args:
start_time (str): String containing the date, and possibly time,
start_time (str or datetime): Containing the date, and possibly time,
from which to retrieve the values. This is parsed, together
with `end_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
end_time (str): String containing the date, and possibly time,
end_time (str or datetime): Containing the date, and possibly time,
until which to retrieve values. This is parsed, together
with `start_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
Expand All @@ -269,7 +240,7 @@ def recorded_values(
`ValueError` is raised.
"""

time_range = AF.Time.AFTimeRange(start_time, end_time)
time_range = to_af_time_range(start_time, end_time)
boundary_type = self.__boundary_types.get(boundary_type.lower())
filter_expression = self._normalize_filter_expression(filter_expression)
if boundary_type is None:
Expand All @@ -280,7 +251,7 @@ def recorded_values(
pivalues = self._recorded_values(time_range, boundary_type, filter_expression)
timestamps, values = [], []
for value in pivalues:
timestamps.append(PISeries.timestamp_to_index(value.Timestamp.UtcTime))
timestamps.append(timestamp_to_index(value.Timestamp.UtcTime))
values.append(value.Value)
return PISeries(
tag=self.name,
Expand All @@ -307,11 +278,11 @@ def interpolated_values(self, start_time, end_time, interval, filter_expression=
and filtered values are always left out entirely.

Args:
start_time (str): String containing the date, and possibly time,
start_time (str or datetime): Containing the date, and possibly time,
from which to retrieve the values. This is parsed, together
with `end_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
end_time (str): String containing the date, and possibly time,
end_time (str or datetime): Containing the date, and possibly time,
until which to retrieve values. This is parsed, together
with `start_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
Expand All @@ -325,13 +296,13 @@ def interpolated_values(self, start_time, end_time, interval, filter_expression=
Returns:
PISeries: Timeseries of the values returned by the SDK
"""
time_range = AF.Time.AFTimeRange(start_time, end_time)
time_range = to_af_time_range(start_time, end_time)
interval = AF.Time.AFTimeSpan.Parse(interval)
filter_expression = self._normalize_filter_expression(filter_expression)
pivalues = self._interpolated_values(time_range, interval, filter_expression)
timestamps, values = [], []
for value in pivalues:
timestamps.append(PISeries.timestamp_to_index(value.Timestamp.UtcTime))
timestamps.append(timestamp_to_index(value.Timestamp.UtcTime))
values.append(value.Value)
return PISeries(
tag=self.name,
Expand All @@ -353,10 +324,10 @@ def summary(
Return one or more summary values over a single time range.

Args:
start_time (str): String containing the date, and possibly time,
start_time (str or datetime): Containing the date, and possibly time,
from which to retrieve the values. This is parsed, together
with `end_time`, using :afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
end_time (str): String containing the date, and possibly time,
end_time (str or datetime): Containing the date, and possibly time,
until which to retrieve values. This is parsed, together
with `start_time`, using :afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
summary_types (int or PIConsts.SummaryType): Type(s) of summaries
Expand All @@ -374,7 +345,7 @@ def summary(
pandas.DataFrame: Dataframe with the unique timestamps as row index
and the summary name as column name.
"""
time_range = AF.Time.AFTimeRange(start_time, end_time)
time_range = to_af_time_range(start_time, end_time)
summary_types = int(summary_types)
calculation_basis = int(calculation_basis)
time_type = int(time_type)
Expand All @@ -385,7 +356,7 @@ def summary(
for summary in pivalues:
key = SummaryType(summary.Key).name
value = summary.Value
timestamp = PISeries.timestamp_to_index(value.Timestamp.UtcTime)
timestamp = timestamp_to_index(value.Timestamp.UtcTime)
value = value.Value
df = df.join(DataFrame(data={key: value}, index=[timestamp]), how="outer")
return df
Expand All @@ -404,11 +375,11 @@ def summaries(
Return one or more summary values for each interval within a time range

Args:
start_time (str): String containing the date, and possibly time,
start_time (str or datetime): Containing the date, and possibly time,
from which to retrieve the values. This is parsed, together
with `end_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
end_time (str): String containing the date, and possibly time,
end_time (str or datetime): Containing the date, and possibly time,
until which to retrieve values. This is parsed, together
with `start_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
Expand All @@ -430,7 +401,7 @@ def summaries(
pandas.DataFrame: Dataframe with the unique timestamps as row index
and the summary name as column name.
"""
time_range = AF.Time.AFTimeRange(start_time, end_time)
time_range = to_af_time_range(start_time, end_time)
interval = AF.Time.AFTimeSpan.Parse(interval)
summary_types = int(summary_types)
calculation_basis = int(calculation_basis)
Expand All @@ -443,7 +414,7 @@ def summaries(
key = SummaryType(summary.Key).name
timestamps, values = zip(
*[
(PISeries.timestamp_to_index(value.Timestamp.UtcTime), value.Value)
(timestamp_to_index(value.Timestamp.UtcTime), value.Value)
for value in summary.Value
]
)
Expand All @@ -467,11 +438,11 @@ def filtered_summaries(
Return one or more summary values for each interval within a time range

Args:
start_time (str): String containing the date, and possibly time,
start_time (str or datetime): String containing the date, and possibly time,
from which to retrieve the values. This is parsed, together
with `end_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
end_time (str): String containing the date, and possibly time,
end_time (str or datetime): String containing the date, and possibly time,
until which to retrieve values. This is parsed, together
with `start_time`, using
:afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.
Expand Down Expand Up @@ -504,7 +475,7 @@ def filtered_summaries(
pandas.DataFrame: Dataframe with the unique timestamps as row index
and the summary name as column name.
"""
time_range = AF.Time.AFTimeRange(start_time, end_time)
time_range = to_af_time_range(start_time, end_time)
interval = AF.Time.AFTimeSpan.Parse(interval)
filter_expression = self._normalize_filter_expression(filter_expression)
calculation_basis = get_enumerated_value(
Expand Down Expand Up @@ -538,7 +509,7 @@ def filtered_summaries(
key = SummaryType(summary.Key).name
timestamps, values = zip(
*[
(PISeries.timestamp_to_index(value.Timestamp.UtcTime), value.Value)
(timestamp_to_index(value.Timestamp.UtcTime), value.Value)
for value in summary.Value
]
)
Expand Down
49 changes: 49 additions & 0 deletions PIconnect/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from datetime import datetime
import pytz

from PIconnect.AFSDK import AF
from PIconnect.config import PIConfig


def to_af_time_range(start_time, end_time):
"""to_af_time_range

Return AF.Time.AFTimeRange object from datetime or string
using :afsdk:`AF.Time.AFTimeRange <M_OSIsoft_AF_Time_AFTimeRange__ctor_1.htm>`.

If string is used, it is assumed that user knows the format
that should be passed to AF.Time.AFTimeRange.
"""

if isinstance(start_time, datetime):
start_time = start_time.isoformat()
if isinstance(end_time, datetime):
end_time = end_time.isoformat()

return AF.Time.AFTimeRange(start_time, end_time)


def timestamp_to_index(timestamp):
"""Convert AFTime object to datetime in local timezone.

.. todo::

Allow to define timezone, default to UTC?

.. todo::

"""
local_tz = pytz.timezone(PIConfig.DEFAULT_TIMEZONE)
return (
datetime(
timestamp.Year,
timestamp.Month,
timestamp.Day,
timestamp.Hour,
timestamp.Minute,
timestamp.Second,
timestamp.Millisecond * 1000,
)
.replace(tzinfo=pytz.utc)
.astimezone(local_tz)
)