from datetime import datetime, timedelta
from typing import Any
from .declarative import _value_is_live, is_tag_reference, normalize_ui_value
from .element import Element
from .misc import Range, Threshold, Widget, NotSet
[docs]
class Variable(Element):
"""Base class for UI variables. All variables should inherit from this class.
A variable is a read-only value in the UI which is updated by a device periodically.
Parameters
----------
display_name: str
The display name of the variable.
var_type: str
The type of the variable (e.g., "float", "string", "bool", "time").
value: Any
The current value of the variable. If not set, defaults to NotSet.
precision: int, optional
The number of decimal places to round the current value to. Defaults to None.
ranges: list[Range]
A list of ranges associated with the variable, used for display purposes.
thresholds: list[Threshold], optional
Thresholds drawn as horizontal lines on the variable's plot.
default_range_view: str, optional
The initial range view (``"line"``, ``"zone"`` or ``"off"``) for the
plot's hamburger menu. If unset, the site falls back to ``"line"``
when thresholds are defined, ``"zone"`` when ranges are defined,
otherwise ``"off"``.
earliest_data_time: datetime, optional
The earliest time for which data is available for this variable. Defaults to None.
default_range_since: timedelta, optional
The timedelta which defines how many seconds the plot should show on load. Defaults to a week if not set.
default_zoom: str, optional
The default zoom setting for the inbuilt plot viewer. Defaults to None.
log_threshold: float, optional
The change threshold for logging the variable. Defaults to None (no threshold). 0 means log every change.
"""
type = "uiVariable"
def __init__(
self,
display_name: str,
var_type: str,
value: Any = NotSet,
precision: int = NotSet,
ranges: list[Range] = NotSet,
thresholds: list[Threshold] = NotSet,
default_range_view: str = NotSet,
earliest_data_time: datetime = NotSet,
default_range_since: timedelta = NotSet,
default_zoom: str = NotSet,
log_threshold: float = NotSet,
graphable: bool = NotSet,
**kwargs,
):
# kwargs: verbose_str=verbose_str, show_activity=show_activity, form=form, graphic=graphic, layout=layout
super().__init__(display_name, **kwargs)
self.var_type = var_type
self.value = value
self.precision = precision
self.earliest_data_time = earliest_data_time
self.default_range_since = default_range_since
self.default_zoom = default_zoom
self.log_threshold = log_threshold
self.graphable = graphable
self.ranges = ranges
self.thresholds = thresholds
self.default_range_view = default_range_view
[docs]
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
result["type"] = self.type
result["varType"] = self.var_type
if self.value is not NotSet:
result["currentValue"] = self.value
if self.precision is not NotSet:
result["decPrecision"] = self.precision
# fixme: this should be in milliseconds
if self.earliest_data_time is not NotSet:
if isinstance(self.earliest_data_time, datetime):
result["earliestDataDate"] = int(self.earliest_data_time.timestamp())
else:
result["earliestDataDate"] = self.earliest_data_time
if self.default_range_since is not NotSet:
result["defaultRangeSince"] = (
int(self.default_range_since.total_seconds()) * 1000
)
if self.default_zoom is not NotSet:
result["defaultZoom"] = self.default_zoom
if self.ranges is not NotSet:
result["ranges"] = [r.to_dict() for r in self.ranges]
if self.thresholds is not NotSet:
result["thresholds"] = [t.to_dict() for t in self.thresholds]
if self.default_range_view is not NotSet:
result["defaultRangeView"] = self.default_range_view
if self.graphable is not NotSet:
if isinstance(self.graphable, bool):
result["notGraphable"] = not self.graphable
else:
# allow a tag I suppose??
result["notGraphable"] = self.graphable
if _value_is_live(self.value):
result["live"] = True
return normalize_ui_value(result)
[docs]
class NumericVariable(Variable):
"""Represents a numeric variable in the UI, which can be an integer or a float.
Parameters
----------
name: str
The name of the variable.
display_name: str
The display name of the variable.
curr_val: Union[int, float], optional
The current value of the variable. Defaults to None.
precision: int, optional
The number of decimal places to round the current value to. Defaults to None.
ranges: list[Range], optional
A list of ranges associated with the variable, used for display purposes. Defaults to None.
thresholds: list[Threshold], optional
Thresholds drawn as horizontal lines on the variable's plot.
default_range_view: str, optional
Initial range view for the plot (``"line"``, ``"zone"`` or ``"off"``).
form: Widget, optional
A widget or string representing the form for this variable. Defaults to None.
"""
def __init__(
self,
display_name: str,
value: Any = None,
precision: int = NotSet,
ranges: list[Range] = NotSet,
thresholds: list[Threshold] = NotSet,
default_range_view: str = NotSet,
form: Widget = NotSet,
**kwargs,
):
super().__init__(
display_name,
var_type="float",
value=value,
precision=precision,
ranges=ranges,
thresholds=thresholds,
default_range_view=default_range_view,
**kwargs,
)
self.form = form
[docs]
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if self.form is not NotSet:
result["form"] = self.form
return normalize_ui_value(result)
[docs]
class TextVariable(Variable):
"""Represents a text variable in the UI, which can be used to display or input string values.
Parameters
----------
display_name: str
The display name of the variable.
value: str, optional
The current value of the variable. Defaults to None.
"""
def __init__(self, display_name: str, value: str, **kwargs):
# fixme: double check this type
super().__init__(display_name, var_type="string", value=value, **kwargs)
[docs]
class BooleanVariable(Variable):
"""Represents a boolean variable in the UI, which can be used to represent true/false values.
Parameters
----------
display_name: str
The display name of the variable.
curr_val: bool, optional
The current value of the variable. Defaults to None.
log_threshold: float, optional
The change threshold for logging the variable. Defaults to 0 (log every change). None means don't log on change.
"""
def __init__(
self,
display_name: str,
value: bool,
log_threshold: float | None = 0,
**kwargs,
):
super().__init__(
display_name,
var_type="bool",
value=value,
log_threshold=log_threshold,
**kwargs,
)
[docs]
class DateTimeVariable(Variable):
"""Represents a date/time variable in the UI, which can be used to display or input datetime values.
Parameters
----------
name: str
The name of the variable.
display_name: str
The display name of the variable.
value: datetime, optional
The current value of the variable, which can be a datetime object or a timestamp (int). Defaults to None.
"""
def __init__(
self,
display_name: str,
value: datetime,
**kwargs,
):
# fixme: double check this type, and how to handle different date / time / datetime
super().__init__(display_name, var_type="time", value=value, **kwargs)
class Timestamp(Variable):
type = "uiTimestamp"
def __init__(self, display_name: str, value: datetime, **kwargs):
# this might do some weird stuff where people think they can have ranges and what not, but yeah...
# this will do for now...
super().__init__(display_name, value=value, var_type="timestamp", **kwargs)
def to_dict(self):
result = super().to_dict()
if is_tag_reference(self.value):
result["currentValue"] = self.value
else:
result["currentValue"] = self.value and int(self.value.timestamp() * 1000)
return normalize_ui_value(result)