diff --git a/docs/control_item.rst b/docs/control_item.rst new file mode 100644 index 00000000..54e6d4af --- /dev/null +++ b/docs/control_item.rst @@ -0,0 +1,71 @@ +Control item +============ + +Control items are elements of crontol panels which have two functions by default: + +- display the last value of a variable, +- control the value of a variable. + +To create one in the administration interface, you need at least to: + +- enter a label, +- select a variable OR a variable property. +- chose a type : display the value or control the value of the variable/variable property, + +You can also: + +- select the order in the control panel using the position attribute : lower is at the top of the control panel, +- add options using display value options or control element options. + +Display value options +--------------------- + +It allows adding options to a control item configured to display value. + +To create one in the administration interface, you need at least to: + +- enter a title, +- choose a template to change the graphic rendering, +- choose if you want to replace a timestamp value by a human readable format, +- transform the data before show it in the user interface: see section below, +- apply color levels: see section below. + +### Transform data + +#### Usage (configuration) + +You can use a data transformation to pass the data through a function before displaying it (for example, display the minimum of the variable in the selected time range). + +You may need to specify additional information at the bottom depending on the tranformation needs (as for the Count Value transformation). + +#### Creation (developer) + +A plugin can add a new transform data to the list. + +To do so you can create them automatically in the *ready* function of the *AppConfig* class in *apps.py*. +Have a look at the [*hmi.apps.py*](https://github.com/pyscada/PyScada/blob/main/pyscada/hmi/apps.py). + +The fields of a transform data are : +- inline_model_name : the model name to add an inline to the admin page which can add additional fields needed by the transform data function (as TransformDataCountValue for the Count Value function), +- short_name : the name displayed in the admin interface, +- js_function_name : the name of the JavaScript function which will be called to transform the data, +- js_files : a coma separated list of the JavaScript files to add, +- css_files : a coma separated list of the CSS files to add, +- need_historical_data : set to True if the transform data function needs the variable data for the whole period selected by the date time range picker, set to False if it only needs the last value. + +### Template + +You can choose a specific template to display you control item. + +#### Creation (developer) + +A plugin can add a new control item template. + +To do so you can create them automatically in the *ready* function of the *AppConfig* class in *apps.py*. +Have a look at the [*hmi.apps.py*](https://github.com/pyscada/PyScada/blob/main/pyscada/hmi/apps.py). + +The fields of a template are : +- label the template name to display, +- template_name : the file name to use, +- js_files : a coma separated list of the JavaScript files to add, +- css_files : a coma separated list of the CSS files to add. diff --git a/pyscada/hmi/admin.py b/pyscada/hmi/admin.py index 75462237..2496f6a2 100644 --- a/pyscada/hmi/admin.py +++ b/pyscada/hmi/admin.py @@ -16,6 +16,7 @@ DisplayValueOption, ControlElementOption, DisplayValueColorOption, + DisplayValueOptionTemplate, ) from pyscada.hmi.models import CustomHTMLPanel from pyscada.hmi.models import Widget @@ -25,6 +26,7 @@ from pyscada.hmi.models import Pie from pyscada.hmi.models import Theme from pyscada.hmi.models import CssClass +from pyscada.hmi.models import TransformData from django.utils.translation import gettext_lazy as _ from django.contrib import admin @@ -318,15 +320,46 @@ class DisplayValueColorOptionInline(admin.TabularInline): extra = 0 +class TransformDataAdmin(admin.ModelAdmin): + # only allow viewing and deleting + def has_module_permission(self, request): + return False + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return True + + +class DisplayValueOptionTemplateAdmin(admin.ModelAdmin): + # only allow viewing and deleting + def has_module_permission(self, request): + return False + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return True + + class DisplayValueOptionAdmin(admin.ModelAdmin): fieldsets = ( ( None, { "fields": ( - "name", - "type", + "title", + "template", "timestamp_conversion", + "transform_data", ), }, ), @@ -346,10 +379,31 @@ class DisplayValueOptionAdmin(admin.ModelAdmin): save_as = True save_as_continue = True inlines = [DisplayValueColorOptionInline] + # Add inlines for any model with OneToOne relation with Device + related_models = [ + field + for field in DisplayValueOption._meta.get_fields() + if issubclass(type(field), OneToOneRel) + ] + for m in related_models: + model_dict = dict(model=m.related_model) + if hasattr(m.related_model, "FormSet"): + model_dict["formset"] = m.related_model.FormSet + cl = type(m.name, (admin.StackedInline,), model_dict) # classes=['collapse'] + inlines.append(cl) def has_module_permission(self, request): return False + class Media: + js = ( + # To be sure the jquery files are loaded before our js file + "admin/js/vendor/jquery/jquery.min.js", + "admin/js/jquery.init.js", + # only the inline corresponding to the transform data selected + "pyscada/js/admin/display_inline_transform_data_display_value_option.js", + ) + class ControlElementOptionAdmin(admin.ModelAdmin): save_as = True @@ -610,3 +664,5 @@ def has_module_permission(self, request): admin_site.register(ProcessFlowDiagramItem, ProcessFlowDiagramItemAdmin) admin_site.register(Theme, ThemeAdmin) admin_site.register(CssClass, CssClassAdmin) +admin_site.register(TransformData, TransformDataAdmin) +admin_site.register(DisplayValueOptionTemplate, DisplayValueOptionTemplateAdmin) diff --git a/pyscada/hmi/apps.py b/pyscada/hmi/apps.py index f69f0cd7..dc02b6c8 100644 --- a/pyscada/hmi/apps.py +++ b/pyscada/hmi/apps.py @@ -3,6 +3,8 @@ from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ +from django.db.utils import ProgrammingError, OperationalError +from django.conf import settings class PyScadaHMIConfig(AppConfig): @@ -12,3 +14,190 @@ class PyScadaHMIConfig(AppConfig): def ready(self): import pyscada.hmi.signals + + try: + from .models import TransformData + + # create the control item transform data display value options + # min, max, mean difference, difference percent... + # TODO: do not get the whole historical data for first, difference, difference percent + TransformData.objects.update_or_create( + short_name="Min", + defaults={ + "inline_model_name": "TransformDataMin", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataMin", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Max", + defaults={ + "inline_model_name": "TransformDataMax", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataMax", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Total", + defaults={ + "inline_model_name": "TransformDataTotal", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataTotal", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Difference", + defaults={ + "inline_model_name": "TransformDataDifference", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataDifference", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="DifferencePercent", + defaults={ + "inline_model_name": "TransformDataDifferencePercent", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataDifferencePercent", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Delta", + defaults={ + "inline_model_name": "TransformDataDelta", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataDelta", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Mean", + defaults={ + "inline_model_name": "TransformDataMean", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataMean", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="First", + defaults={ + "inline_model_name": "TransformDataFirst", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataFirst", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Count", + defaults={ + "inline_model_name": "TransformDataCount", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataCount", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="CountValue", + defaults={ + "inline_model_name": "TransformDataCountValue", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataCountValue", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Range", + defaults={ + "inline_model_name": "TransformDataRange", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataRange", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="Step", + defaults={ + "inline_model_name": "TransformDataStep", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataStep", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="ChangeCount", + defaults={ + "inline_model_name": "TransformDataChangeCount", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataChangeCount", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + TransformData.objects.update_or_create( + short_name="DistinctCount", + defaults={ + "inline_model_name": "TransformDataDistinctCount", + "js_function_name": "PyScadaControlItemDisplayValueTransformDataDistinctCount", + "js_files": "pyscada/js/pyscada/TransformDataHmiPlugin.js", + "need_historical_data": True, + }, + ) + except ProgrammingError: + pass + except OperationalError: + pass + + try: + from .models import DisplayValueOptionTemplate + + STATIC_URL = ( + str(settings.STATIC_URL) + if hasattr(settings, "STATIC_URL") + else "/static/" + ) + + # create the circular gauge for control item display value option + DisplayValueOptionTemplate.objects.update_or_create( + label="Circular gauge", + defaults={ + "template_name": "circular_gauge.html", + "js_files": "pyscada/js/jquery/jquery.tablesorter.min.js," + + "pyscada/js/jquery/parser-input-select.js," + + "pyscada/js/flot/lib/jquery.mousewheel.js," + + "pyscada/js/flot/source/jquery.canvaswrapper.js," + + "pyscada/js/flot/source/jquery.colorhelpers.js," + + "pyscada/js/flot/source/jquery.flot.js," + + "pyscada/js/flot/source/jquery.flot.saturated.js," + + "pyscada/js/flot/source/jquery.flot.browser.js," + + "pyscada/js/flot/source/jquery.flot.drawSeries.js," + + "pyscada/js/flot/source/jquery.flot.errorbars.js," + + "pyscada/js/flot/source/jquery.flot.uiConstants.js," + + "pyscada/js/flot/source/jquery.flot.logaxis.js," + + "pyscada/js/flot/source/jquery.flot.symbol.js," + + "pyscada/js/flot/source/jquery.flot.flatdata.js," + + "pyscada/js/flot/source/jquery.flot.navigate.js," + + "pyscada/js/flot/source/jquery.flot.fillbetween.js," + + "pyscada/js/flot/source/jquery.flot.stack.js," + + "pyscada/js/flot/source/jquery.flot.touchNavigate.js," + + "pyscada/js/flot/source/jquery.flot.hover.js," + + "pyscada/js/flot/source/jquery.flot.touch.js," + + "pyscada/js/flot/source/jquery.flot.time.js," + + "pyscada/js/flot/source/jquery.flot.axislabels.js," + + "pyscada/js/flot/source/jquery.flot.selection.js," + + "pyscada/js/flot/source/jquery.flot.composeImages.js," + + "pyscada/js/flot/source/jquery.flot.legend.js," + + "pyscada/js/flot/source/jquery.flot.pie.js," + + "pyscada/js/flot/source/jquery.flot.crosshair.js," + + "pyscada/js/flot/source/jquery.flot.gauge.js," + + "pyscada/js/jquery.flot.axisvalues.js", + }, + ) + except ProgrammingError: + pass + except OperationalError: + pass diff --git a/pyscada/hmi/migrations/0076_displayvalueoptiontemplate_transformdata_and_more.py b/pyscada/hmi/migrations/0076_displayvalueoptiontemplate_transformdata_and_more.py new file mode 100644 index 00000000..64408d9a --- /dev/null +++ b/pyscada/hmi/migrations/0076_displayvalueoptiontemplate_transformdata_and_more.py @@ -0,0 +1,125 @@ +# Generated by Django 4.2.5 on 2023-11-16 16:05 + +from django.db import migrations, models +import django.db.models.deletion +import pyscada.hmi.models + + +class Migration(migrations.Migration): + dependencies = [ + ("hmi", "0075_alter_processflowdiagram_url_height_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="DisplayValueOptionTemplate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("label", models.CharField(max_length=40, unique=True)), + ( + "template_name", + models.CharField( + blank=True, + help_text="The template to use for the control item. Must ends with '.html'.", + max_length=100, + validators=[pyscada.hmi.models.validate_html], + ), + ), + ( + "js_files", + models.TextField( + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + max_length=400, + ), + ), + ( + "css_files", + models.TextField( + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + max_length=100, + ), + ), + ], + ), + migrations.CreateModel( + name="TransformData", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("inline_model_name", models.CharField(max_length=100)), + ("short_name", models.CharField(max_length=20)), + ("js_function_name", models.CharField(max_length=100)), + ( + "js_files", + models.TextField( + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + max_length=100, + ), + ), + ( + "css_files", + models.TextField( + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + max_length=100, + ), + ), + ( + "need_historical_data", + models.BooleanField( + default=False, + help_text="If true, will query the data corresponding of the date range picker.", + ), + ), + ], + ), + migrations.RenameField( + model_name="displayvalueoption", + old_name="name", + new_name="title", + ), + migrations.RemoveField( + model_name="displayvalueoption", + name="type", + ), + migrations.AddField( + model_name="displayvalueoption", + name="template", + field=models.ForeignKey( + blank=True, + help_text="Select a custom template to use for this control item display value option.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="hmi.displayvalueoptiontemplate", + ), + ), + migrations.AddField( + model_name="displayvalueoption", + name="transform_data", + field=models.ForeignKey( + blank=True, + help_text="Select a function to transform and manipulate data before displaying it.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="hmi.transformdata", + ), + ), + ] diff --git a/pyscada/hmi/migrations/0077_transformdatacountvalue.py b/pyscada/hmi/migrations/0077_transformdatacountvalue.py new file mode 100644 index 00000000..0b48af4b --- /dev/null +++ b/pyscada/hmi/migrations/0077_transformdatacountvalue.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.5 on 2023-11-22 10:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("hmi", "0076_displayvalueoptiontemplate_transformdata_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="TransformDataCountValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.FloatField()), + ( + "display_value_option", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="hmi.displayvalueoption", + ), + ), + ], + ), + ] diff --git a/pyscada/hmi/models.py b/pyscada/hmi/models.py index 49bdfa7e..b77c763a 100644 --- a/pyscada/hmi/models.py +++ b/pyscada/hmi/models.py @@ -12,6 +12,8 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.db.models.query import QuerySet +from django.conf import settings +from django.forms.models import BaseInlineFormSet from six import text_type import traceback @@ -68,6 +70,45 @@ def validate_tempalte(value): pass +# raise a ValidationError if value not endswith .html or if template not found +def validate_html(value): + if not value.endswith(".html"): + raise ValidationError( + _("%(value)s should ends with '.html'"), + params={"value": value}, + ) + try: + get_template(value) + except TemplateDoesNotExist: + raise ValidationError( + _("%(value)s template does not exist."), + params={"value": value}, + ) + + +# return a list of files from a coma separated string +# if :// not in the file name and the filename is not starting with /, add the static url +def get_js_or_css_set_from_str(self, field): + result = list() + if not hasattr(self, field): + logger.warning(f"{field} not in {self}") + return result + files = getattr(self, field) + for file in files.split(","): + if file == "": + continue + if not file.startswith("/") and "://" not in file: + STATIC_URL = ( + str(settings.STATIC_URL) + if hasattr(settings, "STATIC_URL") + else "/static/" + ) + result.append(STATIC_URL + file) + else: + result.append(file) + return result + + class WidgetContentModel(models.Model): @classmethod def __init_subclass__(cls, **kwargs): @@ -206,16 +247,97 @@ class ControlElementOption(models.Model): def __str__(self): return self.name + def get_js(self): + files = list() + return files + + def get_css(self): + files = list() + return files + + def get_daterangepicker(self): + return False + + def get_timeline(self): + return False + + +class TransformData(models.Model): + inline_model_name = models.CharField(max_length=100) + short_name = models.CharField(max_length=20) + js_function_name = models.CharField(max_length=100) + js_files = models.TextField( + max_length=100, + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + ) + css_files = models.TextField( + max_length=100, + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + ) + need_historical_data = models.BooleanField( + default=False, + help_text="If true, will query the data corresponding of the date range picker.", + ) + + def __str__(self): + return self.short_name + + def get_js(self): + return get_js_or_css_set_from_str(self, "js_files") + + def get_css(self): + return get_js_or_css_set_from_str(self, "css_files") + + +class DisplayValueOptionTemplate(models.Model): + label = models.CharField(max_length=40, unique=True) + template_name = models.CharField( + max_length=100, + blank=True, + help_text="The template to use for the control item. Must ends with '.html'.", + validators=[validate_html], + ) + js_files = models.TextField( + max_length=400, + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + ) + css_files = models.TextField( + max_length=100, + blank=True, + help_text="for a file in static, start without /, like : pyscada/js/pyscada/file.js
for a local file not in static, start with /, like : /test/file.js
for a remote file, indicate the url
you can provide a coma separated list", + ) + + def __str__(self): + return self.label + + def get_js(self): + return get_js_or_css_set_from_str(self, "js_files") + + def get_css(self): + return get_js_or_css_set_from_str(self, "css_files") + + # return the template name or template_not_found.html if the template is not found + def get_template_name(self): + try: + validate_html(self.template_name) + return self.template_name + except ValidationError as e: + logger.warning(e) + return "template_not_found.html" + class DisplayValueOption(models.Model): - name = models.CharField(max_length=400) - type_choices = ( - (0, "Classic (Div)"), - (1, "Horizontal gauge"), - (2, "Vertical gauge"), - (3, "Circular gauge"), + title = models.CharField(max_length=400) + template = models.ForeignKey( + DisplayValueOptionTemplate, + null=True, + blank=True, + on_delete=models.CASCADE, + help_text="Select a custom template to use for this control item display value option.", ) - type = models.PositiveSmallIntegerField(default=0, choices=type_choices) color = models.ForeignKey( Color, @@ -248,8 +370,16 @@ class DisplayValueOption(models.Model): default=0, choices=timestamp_conversion_choices ) + transform_data = models.ForeignKey( + TransformData, + null=True, + blank=True, + on_delete=models.CASCADE, + help_text="Select a function to transform and manipulate data before displaying it.", + ) + def __str__(self): - return self.name + return self.title def _get_objects_for_html( self, list_to_append=None, obj=None, exclude_model_names=None @@ -261,6 +391,46 @@ def _get_objects_for_html( ) return list_to_append + def get_js(self): + files = list() + if self.transform_data is not None: + js_files = self.transform_data.get_js() + if type(js_files) == list: + files += js_files + elif type(js_files) == str: + files.append(js_files) + if self.template is not None: + js_files = self.template.get_js() + if type(js_files) == list: + files += js_files + elif type(js_files) == str: + files.append(js_files) + return files + + def get_css(self): + files = list() + if self.transform_data is not None: + css_files = self.transform_data.get_css() + if type(css_files) == list: + files += css_files + elif type(css_files) == str: + files.append(css_files) + if self.template is not None: + css_files = self.template.get_css() + if type(css_files) == list: + files += css_files + elif type(css_files) == str: + files.append(css_files) + return files + + def get_daterangepicker(self): + if self.transform_data is not None: + return self.transform_data.need_historical_data + + def get_timeline(self): + if self.transform_data is not None: + return self.transform_data.need_historical_data + class DisplayValueColorOption(models.Model): display_value_option = models.ForeignKey( @@ -292,6 +462,37 @@ class Meta: ordering = ["color_level", "-color_level_type"] +class TransformDataCountValue(models.Model): + display_value_option = models.OneToOneField( + DisplayValueOption, on_delete=models.CASCADE + ) + value = models.FloatField() # the value to count + + class FormSet(BaseInlineFormSet): + def clean(self): + super().clean() + # get the formset model name, here TransformDataCountValue + class_name = self.model.__name__ + # check if a transform data has been selected in the admin and if a transform data exist with this id + if ( + self.data["transform_data"] != "" + and TransformData.objects.get(id=self.data["transform_data"]) + is not None + ): + # get the selected transform data inline model name + transform_data_name = TransformData.objects.get( + id=self.data["transform_data"] + ).inline_model_name + # if the selected transform data inline model name is this model, check if the value field has been filled in + # otherwhise raise a ValidationError + if ( + class_name == transform_data_name + and self.data[transform_data_name.lower() + "-0-value"] == "" + and self.data["transform_data"] != "" + ): + raise ValidationError("Value is required.") + + class ControlItem(models.Model): id = models.AutoField(primary_key=True) label = models.CharField(max_length=400, default="") @@ -482,6 +683,36 @@ def _get_objects_for_html( list_to_append = get_objects_for_html(list_to_append, self, exclude_model_names) return list_to_append + def get_js(self): + files = list() + if self.type == 1 and self.display_value_options is not None: + files += self.display_value_options.get_js() + if self.type == 0 and self.control_element_options is not None: + files += self.control_element_options.get_js() + return files + + def get_css(self): + files = list() + if self.type == 1 and self.display_value_options is not None: + files += self.display_value_options.get_css() + if self.type == 0 and self.control_element_options is not None: + files += self.control_element_options.get_css() + return files + + def get_daterangepicker(self): + if self.type == 0 and self.control_element_options is not None: + return self.control_element_options.get_daterangepicker() + elif self.type == 1 and self.display_value_options is not None: + return self.display_value_options.get_daterangepicker() + return False + + def get_timeline(self): + if self.type == 0 and self.control_element_options is not None: + return self.control_element_options.get_timeline() + elif self.type == 1 and self.display_value_options is not None: + return self.display_value_options.get_timeline() + return False + class Chart(WidgetContentModel): id = models.AutoField(primary_key=True) @@ -694,6 +925,22 @@ def control_items_list(self): def hidden_control_items_to_true_list(self): return [item.pk for item in self.hidden_control_items_to_true] + def get_js(self): + files = list() + for item in self.control_items.all(): + files += item.get_js() + for item in self.hidden_control_items_to_true.all(): + files += item.get_js() + return files + + def get_css(self): + files = list() + for item in self.control_items.all(): + files += item.get_css() + for item in self.hidden_control_items_to_true.all(): + files += item.get_css() + return files + class Page(models.Model): id = models.AutoField(primary_key=True) @@ -756,19 +1003,24 @@ def gen_html(self, **kwargs): sidebar_content = None opts = dict() opts["flot"] = False + opts["javascript_files_list"] = list() + opts["css_files_list"] = list() + opts["show_daterangepicker"] = False + opts["show_timeline"] = False for item in self.items.all(): - if ( - item.display_value_options is not None - and item.display_value_options.type == 3 - ): - opts["flot"] = True + opts["javascript_files_list"] += item.get_js() + opts["css_files_list"] += item.get_css() + opts["show_daterangepicker"] = ( + opts["show_daterangepicker"] or item.get_daterangepicker() + ) + opts["show_timeline"] = opts["show_timeline"] or item.get_timeline() for form in self.forms.all(): - for item in form.control_items.all(): - if ( - item.display_value_options is not None - and item.display_value_options.type == 3 - ): - opts["flot"] = True + opts["javascript_files_list"] += form.get_js() + opts["css_files_list"] += form.get_css() + opts["show_daterangepicker"] = ( + opts["show_daterangepicker"] or item.get_daterangepicker() + ) + opts["show_timeline"] = opts["show_timeline"] or item.get_timeline() # opts["object_config_list"] = set() # opts["object_config_list"].update(self._get_objects_for_html()) # opts = self.add_custom_fields_list(opts) diff --git a/pyscada/hmi/static/pyscada/js/admin/display_inline_transform_data_display_value_option.js b/pyscada/hmi/static/pyscada/js/admin/display_inline_transform_data_display_value_option.js new file mode 100644 index 00000000..cce09629 --- /dev/null +++ b/pyscada/hmi/static/pyscada/js/admin/display_inline_transform_data_display_value_option.js @@ -0,0 +1,11 @@ +function updateTransformDataInlines() { + document.querySelectorAll("div[id^='transformdata'].js-inline-admin-formset.inline-group").forEach((e) => {e.hidden = true;}); // hide all transform data inlines + var v = document.querySelector("#id_transform_data").selectedOptions[0].innerHTML; // get the selected transform data name + document.querySelectorAll("[id='transformdata" + v.toLowerCase() + "-group'].js-inline-admin-formset.inline-group").forEach((e) => {e.hidden = false;}); // show the correct transform data inline + document.querySelector("#id_transform_data").onchange = function(e) { + document.querySelectorAll("div[id^='transformdata'].js-inline-admin-formset.inline-group").forEach((e) => {e.hidden = true;}); // hide all transform data inlines + var v = e.target.selectedOptions[0].innerHTML; // get the selected transform data name + document.querySelectorAll("[id='transformdata" + v.toLowerCase() + "-group'].js-inline-admin-formset.inline-group").forEach((e) => {e.hidden = false;}); // show the correct transform data inline + }; + }; + document.addEventListener("DOMContentLoaded", function () {updateTransformDataInlines();}); diff --git a/pyscada/hmi/static/pyscada/js/pyscada/TransformDataHmiPlugin.js b/pyscada/hmi/static/pyscada/js/pyscada/TransformDataHmiPlugin.js new file mode 100644 index 00000000..c357d4b3 --- /dev/null +++ b/pyscada/hmi/static/pyscada/js/pyscada/TransformDataHmiPlugin.js @@ -0,0 +1,230 @@ +function PyScadaControlItemDisplayValueTransformDataMin(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataMin : " + variable_id + " not in DATA. ") + return val; + } + var result = null; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + if (result == null) {result = data[d][1]} + else{result = Math.min(result, data[d][1])} + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataMax(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataMax : " + variable_id + " not in DATA. ") + return val; + } + var result = null; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + if (result == null) {result = data[d][1]} + else{result = Math.max(result, data[d][1])} + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataTotal(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataTotal : " + variable_id + " not in DATA. ") + return val; + } + var result = 0; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + result += data[d][1] + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataDifference(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataDifference : " + variable_id + " not in DATA. ") + return val; + } + var result = null; + var data = sliceDATAusingTimestamps(variable_id); + if (data.length > 0) { + result = data[data.length - 1][1] - data[0][1] + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataDifferencePercent(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataDifferencePercent : " + variable_id + " not in DATA. ") + return val; + } + var result = null; + var data = sliceDATAusingTimestamps(variable_id); + if (data.length > 0) { + result = data[data.length - 1][1] - data[0][1] + result = result / Math.abs(data[0][1] * 100) + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataDelta(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataDelta : " + variable_id + " not in DATA. ") + return val; + } + var result = 0; + var prev = null; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + if (prev != null && data[d][1] - prev > 0) { + result += data[d][1] - prev; + } + prev = data[d][1]; + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataMean(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataMean : " + variable_id + " not in DATA. ") + return val; + } + var data = sliceDATAusingTimestamps(variable_id); + if (data.length == 0) {return null;} + var result = 0; + for (d in data) { + result += data[d][1] + } + return result / data.length; +} + +function PyScadaControlItemDisplayValueTransformDataFirst(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataFirst : " + variable_id + " not in DATA. ") + return val; + } + var data = sliceDATAusingTimestamps(variable_id); + if (data.length == 0) {return null;} + return data[0][1]; +} + +function PyScadaControlItemDisplayValueTransformDataCount(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataCount : " + variable_id + " not in DATA. ") + return val; + } + var result = 0; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + result += 1; + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataCountValue(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataCountValue : " + variable_id + " not in DATA. ") + return val; + } + var value = get_config_from_hidden_config('transformdatacountvalue', 'display-value-option', displayvalueoption_id, 'value'); + var result = 0; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + if (data[d][1] == value) { + result += 1; + } + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataRange(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataRange : " + variable_id + " not in DATA. ") + return val; + } + var min = null; + var max = null; + var data = sliceDATAusingTimestamps(variable_id); + for (d in data) { + if (min == null) { + min = data[d][1]; + max = data[d][1]; + } + else{ + min = Math.min(min, data[d][1]); + max = Math.max(max, data[d][1]); + } + } + return max - min; +} + +function PyScadaControlItemDisplayValueTransformDataStep(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataStep : " + variable_id + " not in DATA. ") + return val; + } + var data = sliceDATAusingTimestamps(variable_id); + if (data.length == 0) {return null;} + var result = null; + var prev = null; + for (d in data) { + if (prev != null) { + if (result == null) { + result = Math.abs(data[d][1] - prev); + }else { + result = Math.min(result, Math.abs(data[d][1] - prev)); + } + } + prev = data[d][1]; + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataChangeCount(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataChangeCount : " + variable_id + " not in DATA. ") + return val; + } + var data = sliceDATAusingTimestamps(variable_id); + if (data.length == 0) {return null;} + var result = 0; + var prev = null; + for (d in data) { + if (prev != null && prev != data[d][1]) { + result += 1; + } + prev = data[d][1]; + } + return result; +} + +function PyScadaControlItemDisplayValueTransformDataDistinctCount(key, val, control_item_id, display_value_option_id, transform_data_id) { + var variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable'); + if (DATA[variable_id] == undefined) { + console.log("PyScada HMI : PyScadaControlItemDisplayValueTransformDataDistinctCount : " + variable_id + " not in DATA. ") + return val; + } + var data = sliceDATAusingTimestamps(variable_id); + if (data.length == 0) {return null;} + var result = 0; + var list = []; + for (d in data) { + if (!(data[d][1] in list)) { + result += 1; + list.push(data[d][1]) + } + } + return result; +} diff --git a/pyscada/hmi/static/pyscada/js/pyscada/pyscada_v0-7-0rc14.js b/pyscada/hmi/static/pyscada/js/pyscada/pyscada_v0-7-0rc14.js index 5cf5be48..56e92b1c 100644 --- a/pyscada/hmi/static/pyscada/js/pyscada/pyscada_v0-7-0rc14.js +++ b/pyscada/hmi/static/pyscada/js/pyscada/pyscada_v0-7-0rc14.js @@ -679,6 +679,27 @@ var store_temp_ajax_data = null; // Data's Functions // ----------------------------------------------------------- + /** + * Transform data using control item display value option function + * @param {number} id Control item id + * @param {number} val + * @returns {number} Return the transformed value + */ +function transform_data(control_item_id, val, key) { + var display_value_option_id = get_config_from_hidden_config("controlitem", 'id', control_item_id, 'display-value-options'); + var transform_data_id = get_config_from_hidden_config("displayvalueoption", 'id', display_value_option_id, 'transform-data'); + var transform_data_function_name = get_config_from_hidden_config("transformdata", 'id', transform_data_id, 'js-function-name'); + + if (transform_data_function_name != null && transform_data_function_name != "") { + if (typeof window[transform_data_function_name] === "function") { + var transform_data_function = eval(transform_data_function_name); + val = transform_data_function.call(null , key, val, control_item_id, display_value_option_id, transform_data_id); + }else {console.log("PyScada HMI : " + transform_data_function_name + " function not found.")} + } + return val +} + + /** * Update variable data values and refresh logo * @param {number} key Data id to update @@ -745,53 +766,6 @@ var store_temp_ajax_data = null; // NUMBER and BOOLEAN : if (typeof(val)==="number" || typeof(val)==="boolean"){ - if (typeof(val)==="number") { - var r_val = Number(val); - - // adjusting r_val - if(Math.abs(r_val) == 0 ){ - r_val = 0; - }else if(Math.abs(r_val) < 0.001) { - r_val = r_val.toExponential(2); - }else if (Math.abs(r_val) < 0.01) { - r_val = r_val.toPrecision(1); - }else if(Math.abs(r_val) < 0.1) { - r_val = r_val.toPrecision(2); - }else if(Math.abs(r_val) < 1) { - r_val = r_val.toPrecision(3); - }else if(r_val > 100) { - r_val = r_val.toPrecision(4); - }else{ - r_val = r_val.toPrecision(4); - } - }else { - var r_val = val; - // set button colors - if (r_val === 0 | r_val == false) { - $('button.btn-success.write-task-btn.' + key).addClass("update-able"); - $('button.update-able.write-task-btn.' + key).addClass("btn-default"); - $('button.update-able.write-task-btn.' + key).removeClass("btn-success"); - - r_val = 0; - - //$(".type-numeric." + key).html(0); - if ($('input.'+ key).attr("placeholder") == "") { - $('input.'+ key).attr("placeholder",0); - } - } else if (typeof(val)==="boolean"){ - r_val = 1; - - $('button.btn-default.write-task-btn.' + key).addClass("update-able"); - $('button.update-able.write-task-btn.' + key).removeClass("btn-default"); - $('button.update-able.write-task-btn.' + key).addClass("btn-success"); - - //$(".type-numeric." + key).html(1); - if ($('input.'+ key).attr("placeholder") == "") { - $('input.'+ key).attr("placeholder",1); - } - } - } - // timestamp, dictionary and color document.querySelectorAll(".control-item.type-numeric." + key).forEach(function(e) { var control_item_id = e.id; @@ -802,29 +776,78 @@ var store_temp_ajax_data = null; var var_id = get_config_from_hidden_config("controlitem", 'id', control_item_id.split('-')[1], type); var dictionary_value = get_config_from_hidden_config(type.replace('-', ''), 'id', var_id, 'dictionary'); var ci_label = get_config_from_hidden_config("controlitem", 'id', control_item_id.split('-')[1], 'label'); + var temp_val = transform_data(control_item_id.split("-")[1], val, key); + + if (typeof(temp_val)==="number") { + var r_val = Number(temp_val); + + // adjusting r_val + if(Math.abs(r_val) == 0 ){ + r_val = 0; + }else if(Math.abs(r_val) < 0.001) { + r_val = r_val.toExponential(2); + }else if (Math.abs(r_val) < 0.01) { + r_val = r_val.toPrecision(1); + }else if(Math.abs(r_val) < 0.1) { + r_val = r_val.toPrecision(2); + }else if(Math.abs(r_val) < 1) { + r_val = r_val.toPrecision(3); + }else if(r_val > 100) { + r_val = r_val.toPrecision(4); + }else{ + r_val = r_val.toPrecision(4); + } + }else { + var r_val = val; + // set button colors + if (r_val === 0 | r_val == false) { + $('button.btn-success.write-task-btn.' + key).addClass("update-able"); + $('button.update-able.write-task-btn.' + key).addClass("btn-default"); + $('button.update-able.write-task-btn.' + key).removeClass("btn-success"); + + r_val = 0; + + //$(".type-numeric." + key).html(0); + if ($('input.'+ key).attr("placeholder") == "") { + $('input.'+ key).attr("placeholder",0); + } + } else if (typeof(temp_val)==="boolean"){ + r_val = 1; + + $('button.btn-default.write-task-btn.' + key).addClass("update-able"); + $('button.update-able.write-task-btn.' + key).removeClass("btn-default"); + $('button.update-able.write-task-btn.' + key).addClass("btn-success"); + + //$(".type-numeric." + key).html(1); + if ($('input.'+ key).attr("placeholder") == "") { + $('input.'+ key).attr("placeholder",1); + } + } + } + if (display_value_option_id == 'None' || color_only == 'False') { if (typeof(val)==="number") { if (timestamp_conversion_value != null && timestamp_conversion_value != 0 && typeof(timestamp_conversion_value) != "undefined"){ // Transform timestamps - r_val=dictionary(var_id, val, type.replace('-', '')); + r_val=dictionary(var_id, temp_val, type.replace('-', '')); r_val=timestamp_conversion(timestamp_conversion_value,r_val); }else { // Transform value in dictionaries - r_val=dictionary(var_id, val, type.replace('-', '')); + r_val=dictionary(var_id, temp_val, type.replace('-', '')); } // Set the text value document.querySelector("#" + control_item_id).innerHTML = r_val + " " + unit; - }else if(typeof(val)==="boolean" && e.querySelector('.boolean-value') != null){ + }else if(typeof(temp_val)==="boolean" && e.querySelector('.boolean-value') != null){ // Set the text value - e.querySelector('.boolean-value').innerHTML = ci_label + " : " + dictionary(var_id, val, type.replace('-', '')) + " " + unit; + e.querySelector('.boolean-value').innerHTML = ci_label + " : " + dictionary(var_id, temp_val, type.replace('-', '')) + " " + unit; } } if (display_value_option_id == 'None' || ci_type == 0){ // Change background color if (e.classList.contains("process-flow-diagram-item")) { - e.style.fill = update_data_colors(control_item_id,val); + e.style.fill = update_data_colors(control_item_id,temp_val); }else { - e.style.backgroundColor = update_data_colors(control_item_id,val); + e.style.backgroundColor = update_data_colors(control_item_id,temp_val); } // create event to announce color change for a control item var event = new CustomEvent("changePyScadaControlItemColor_" + control_item_id.split('-')[1], { detail: update_data_colors(control_item_id,val) }); @@ -832,6 +855,53 @@ var store_temp_ajax_data = null; } }) + if (typeof(val)==="number") { + var r_val = Number(val); + + // adjusting r_val + if(Math.abs(r_val) == 0 ){ + r_val = 0; + }else if(Math.abs(r_val) < 0.001) { + r_val = r_val.toExponential(2); + }else if (Math.abs(r_val) < 0.01) { + r_val = r_val.toPrecision(1); + }else if(Math.abs(r_val) < 0.1) { + r_val = r_val.toPrecision(2); + }else if(Math.abs(r_val) < 1) { + r_val = r_val.toPrecision(3); + }else if(r_val > 100) { + r_val = r_val.toPrecision(4); + }else{ + r_val = r_val.toPrecision(4); + } + }else { + var r_val = val; + // set button colors + if (r_val === 0 | r_val == false) { + $('button.btn-success.write-task-btn.' + key).addClass("update-able"); + $('button.update-able.write-task-btn.' + key).addClass("btn-default"); + $('button.update-able.write-task-btn.' + key).removeClass("btn-success"); + + r_val = 0; + + //$(".type-numeric." + key).html(0); + if ($('input.'+ key).attr("placeholder") == "") { + $('input.'+ key).attr("placeholder",0); + } + } else if (typeof(val)==="boolean"){ + r_val = 1; + + $('button.btn-default.write-task-btn.' + key).addClass("update-able"); + $('button.update-able.write-task-btn.' + key).removeClass("btn-default"); + $('button.update-able.write-task-btn.' + key).addClass("btn-success"); + + //$(".type-numeric." + key).html(1); + if ($('input.'+ key).attr("placeholder") == "") { + $('input.'+ key).attr("placeholder",1); + } + } + } + // update chart legend variable value if (DATA_DISPLAY_FROM_TIMESTAMP > 0 && time < DATA_DISPLAY_FROM_TIMESTAMP) { }else if (DATA_DISPLAY_TO_TIMESTAMP > 0 && time > DATA_DISPLAY_TO_TIMESTAMP) { @@ -2340,7 +2410,10 @@ function createOffset(date) { for (var key in keys){ key = keys[key]; if (key in DATA) { - data=[[min_value, DATA[key][DATA[key].length - 1][1]]] + // get the last value using the daterangepicker and the timeline slider values + var value = sliceDATAusingTimestamps(key)[sliceDATAusingTimestamps(key).length - 1][1]; + value = transform_data(id.split("-")[1], value, "var-" + key); + data=[[min_value, value]]; series.push({"data":data, "label":variables[key].label}); } } @@ -3077,6 +3150,33 @@ function setAggregatedPeriodList(widget_id, var_id) { document.querySelector("li-aggregation-all-period-select-" + widget_id + "-" + var_id); } +/** + * select data in DATA for key using the daterangepicker and timeline slider values + */ +function sliceDATAusingTimestamps(key) { + if (!(key in DATA)) { + console.log("PyScada HMI : " + key + " not in DATA."); + return []; + } + if (DATA_DISPLAY_TO_TIMESTAMP > 0 && DATA_DISPLAY_FROM_TIMESTAMP > 0){ + start_id = find_index_sub_gte(DATA[key],DATA_DISPLAY_FROM_TIMESTAMP,0); + stop_id = find_index_sub_lte(DATA[key],DATA_DISPLAY_TO_TIMESTAMP,0) + 1; + }else if (DATA_DISPLAY_FROM_TIMESTAMP > 0 && DATA_DISPLAY_TO_TIMESTAMP < 0){ + start_id = find_index_sub_gte(DATA[key],DATA_DISPLAY_FROM_TIMESTAMP,0); + stop_id = find_index_sub_lte(DATA[key],DATA_TO_TIMESTAMP,0) + 1; + }else if (DATA_DISPLAY_FROM_TIMESTAMP < 0 && DATA_DISPLAY_TO_TIMESTAMP > 0){ + if (DATA_DISPLAY_TO_TIMESTAMP < DATA[key][0][0]){ + start_id = stop_id = null; + }else { + start_id = find_index_sub_gte(DATA[key],DATA_FROM_TIMESTAMP,0); + stop_id = find_index_sub_lte(DATA[key],DATA_DISPLAY_TO_TIMESTAMP,0) + 1; + } + }else { + start_id = find_index_sub_gte(DATA[key],DATA_FROM_TIMESTAMP,0); + stop_id = find_index_sub_lte(DATA[key],DATA_TO_TIMESTAMP,0) + 1; + } + return DATA[key].slice(start_id, stop_id); +} //--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4202,6 +4302,15 @@ function init_pyscada_content() { CHART_VARIABLE_KEYS[id] = 0; }); + // Add control item variables with transform data (display value option) needing the whole data + document.querySelectorAll(".transformdata-config2").forEach(e => { + if (e.dataset["needHistoricalData"] == "True") { + displayvalueoption_id = get_config_from_hidden_config('displayvalueoption', 'transform-data', e.dataset["id"], 'id') + variable_id = get_config_from_hidden_config('controlitem', 'display-value-options', displayvalueoption_id, 'variable') + CHART_VARIABLE_KEYS[variable_id] = 0; + } + }); + set_loading_state(1, loading_states[1] + 10); diff --git a/pyscada/hmi/templates/gauge.html b/pyscada/hmi/templates/circular_gauge.html similarity index 59% rename from pyscada/hmi/templates/gauge.html rename to pyscada/hmi/templates/circular_gauge.html index 79ea2639..26c7b8f5 100644 --- a/pyscada/hmi/templates/gauge.html +++ b/pyscada/hmi/templates/circular_gauge.html @@ -1,8 +1,8 @@ -
+
-
+
Loading gauge...
- +
diff --git a/pyscada/hmi/templates/control_element.html b/pyscada/hmi/templates/control_element.html index 173c14e4..efa05e35 100644 --- a/pyscada/hmi/templates/control_element.html +++ b/pyscada/hmi/templates/control_element.html @@ -30,7 +30,10 @@ data-timestamp-conversion="{{ item.display_value_options.timestamp_conversion }}" {% endif %} >
- {% if item.type == 0 %} + + {% if item.display_value_options and item.display_value_options.template and item.display_value_options.template.template_name %} + {% include item.display_value_options.template.get_template_name %} + {% elif item.type == 0 %} {% if item.value_class == 'BOOL' or item.value_class == 'BOOLEAN' %} {% include "button.html" %} @@ -52,17 +55,11 @@ {% endif %} {% else %} - {% if item.display_value_options.type == 0 %} - - {% if item.value_class == 'BOOL' or item.value_class == 'BOOLEAN' or item.display_value_options.color_only == 1 %} - - {% include "button.html" %} - {% else %} - {% include "value_field.html" %} - {% endif %} - {% elif item.display_value_options.type == 3 %} - - {% include "gauge.html" with gauge=item %} + {% if item.value_class == 'BOOL' or item.value_class == 'BOOLEAN' or item.display_value_options.color_only == 1 %} + + {% include "button.html" %} + {% else %} + {% include "value_field.html" %} {% endif %} {% endif %} {% else %} diff --git a/pyscada/hmi/templates/template_not_found.html b/pyscada/hmi/templates/template_not_found.html new file mode 100644 index 00000000..9819c698 --- /dev/null +++ b/pyscada/hmi/templates/template_not_found.html @@ -0,0 +1 @@ +Template not found.